从 OpenAPI 生成 Kubernetes API Delphi 客户端
每个 Kubernetes 集群都会描述自己的 API,sgcOpenAPI 把这份描述变成一个可以从 Delphi 调用的 Pascal 单元。这是这些页面上最大的公开规范,也是唯一一个源文档的选择会真正改变结果的规范,因此本页会说明该用哪个端点生成,以及两个已知限制是什么。
每个 Kubernetes 集群都会描述自己的 API,sgcOpenAPI 把这份描述变成一个可以从 Delphi 调用的 Pascal 单元。这是这些页面上最大的公开规范,也是唯一一个源文档的选择会真正改变结果的规范,因此本页会说明该用哪个端点生成,以及两个已知限制是什么。
下面的一切都来自对 Kubernetes 规范运行生成器并编译其结果的实测,不是估算。
/openapi/v3 下按 API 组划分的 OpenAPI 3 文档,一次一个。这条路线才能产出正确的客户端。
聚合的 /openapi/v2 文档。它是 Swagger 2.0,能够转换也能够生成,但有两处结果是错的。两者都在下文说明。
使用 -a 2 生成,并设置 Authentication.Token.BearerToken,它会把 ServiceAccount 令牌作为 Authorization: Bearer 发送。
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/ 目录下,因此您可以从已入库的文件生成,而不是从运行中的集群生成,并把结果纳入版本控制。
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 发送。
凡是 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。
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 文档,本页之所以建议按组生成,原因正在于此。
Kubernetes 在 path item 上只声明一次 namespace 和 pretty,再用 $ref 引用它们。从 /openapi/v2 生成时,这两个参数根本到不了方法,因此 listCoreV1NamespacedPod 没有 aNamespace 参数,URL 里还留着 {namespace} 占位符。改用同一个组的 /openapi/v3 文档生成,参数就在。请使用 v3 文档。
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,因此在您指明信任什么之前,TLS 握手会失败。生成的客户端公开了 OnSSLVerifyPeer、OnSSLGetHandler 和 OnSSLAfterCreateHandler,您可以在这些事件里安装来自 kubeconfig 的集群 CA 包。
watch=true 以及带 follow=true 的容器日志都是长期存在的分块响应。生成的方法是一次普通请求,响应结束时就返回,因此实时 watch 需要您在 TsgcHTTP1Client 之上自行实现,生成的客户端不会为您做流式处理。
自 Kubernetes 1.21 起,它们就带有时限。请从集群内重新读取 /var/run/secrets/kubernetes.io/serviceaccount/token,或从外部调用 TokenRequest API,然后再次给 Authentication.Token.BearerToken 赋值。客户端只会保留您最后一次给它的值。