OpenAPI에서 Kubernetes API Delphi 클라이언트
모든 Kubernetes 클러스터는 자신의 API를 스스로 설명하고, sgcOpenAPI는 그 설명을 Delphi에서 호출하는 Pascal 유닛으로 바꿔요. 이 페이지들에서 다루는 공개 사양 가운데 가장 크고, 소스 문서 선택이 실제로 결과를 바꾸는 유일한 사례이기도 해요. 그래서 이 페이지는 어느 엔드포인트에서 생성해야 하는지, 알려진 두 가지 한계가 무엇인지를 짚어요.
모든 Kubernetes 클러스터는 자신의 API를 스스로 설명하고, sgcOpenAPI는 그 설명을 Delphi에서 호출하는 Pascal 유닛으로 바꿔요. 이 페이지들에서 다루는 공개 사양 가운데 가장 크고, 소스 문서 선택이 실제로 결과를 바꾸는 유일한 사례이기도 해요. 그래서 이 페이지는 어느 엔드포인트에서 생성해야 하는지, 알려진 두 가지 한계가 무엇인지를 짚어요.
아래 내용은 모두 Kubernetes 사양에 생성기를 실행하고 결과를 컴파일해서 측정한 값이에요. 추정치가 아니에요.
/openapi/v3 아래에 있는 그룹별 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 이름을 그대로 담고 있어요. 그래서 파드는 Metadata, Spec, Status를 가진 TsgcOpenAPI_io_k8s_api_core_v1_Pod_Class이고, 목록은 타입이 지정된 Items를 가진 TsgcOpenAPI_io_k8s_api_core_v1_PodList_Class예요. Kubernetes가 선택 항목으로 선언한 불리언 쿼리 매개변수는 TsgcOpenAPIBoolean이 되고, 그 oapiBoolNull 값은 매개변수를 false로 보내는 대신 URL에서 아예 빼라는 뜻이에요.
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는 namespace와 pretty를 path item에 한 번 선언하고 $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짜리 유닛이에요. 읽기와 생성은 1초도 걸리지 않지만 그 크기의 파일에서는 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을 다시 할당하세요. 클라이언트는 마지막으로 준 값을 그대로 들고 있어요.