Go 中的 Kubernetes GraphQL 動態查詢

語言: CN / TW / HK

C T O

   

 

Go Rust Python Istio containerd CoreDNS Envoy etcd Fluentd Harbor Helm Jaeger Kubernetes Open Policy Agent Prometheus Rook TiKV TUF Vitess Arg o Buildpacks CloudEvents CNI Contour Cortex CRI-O Falco Flux gRPC KubeEdge Linkerd NATS Notary OpenTracing Operator  Framework SPIFFE SPIRE     Thanos

Go 中的 Kubernetes GraphQL 動態查詢

Kubernetes GraphQL Query in Go , 我們使用 GraphQL 構建了伺服器程式碼來查詢叢集中的 Pod 。但是,此類查詢的靈活性和可擴充套件性較差,因為相關的定義 graphql.Feilds 都是硬編碼的,需要重構。

第二部分我們將 Dynamic Query 使用 client-go DiscoveryClient 靈活定義 GraphQL 查詢結構。

自定義資源定義 ( CRD )

大多數 Kubernetes 使用者都知道 Kubernetes 資源物件, Pod Deployment 都是 Kubernetes 原生的資源型別

Kubernetes 原生的 Operator 的形式實現的 CRD + Controller 你可以認為是就像那些使用者自定義的 Operator 一樣 ,唯一的區別是原生資源型別分散在 Kubernetes 原始碼中的不同包中。

operator如何處理

  • 使用者提交 CRD-definition-abiding YAML 到叢集。
  • Kubernetes APIServer 將資源儲存在 etcd 基於相關的 CRD 型別。
  • CRD 對應的 Controller 找到要處理的資源並執行程式碼邏輯,包括更新資源狀態。
  • 將最新的資源狀態儲存到 etcd .

很容易得出結論, CRD Kubernetes 資源型別對應的 schema ,就像 DDL 語句對資料庫表,或者 XSD 檔案對 XML 檔案一樣。

讀取架構

在構建叢集中所有資源型別對應的 GraphQL 查詢時,我們面臨的第一個難題是如何獲取所有資源型別的 schema

Kubectl 命令

kubectl command 始終是我們在 Kubernetes 相關查詢中嘗試的第一種方法。

當前叢集中的所有資源型別都可以返回 kubectl get api-resources , 如 Pod, Deployment

但執行時並非如此 kubectl get crd 我們沒有看到原生資源型別的返回 pod .

我們肯定需要尋求補充來找到原生資源型別的模式,讓我們嘗試一些。

  • 一些 YAML 驗證器工具,例如 kubeval ,它指向一些 GitHub 儲存庫,其中包含各種 Kubernetes 版本的模式,例如 yannh/kubernetes-json-schema 。這些 GitHub 架構還支援通過相應的 URL 獲取 JSON 版本架構,例如 1.22.8 版本的 pod 架構 。

yannh/kubernetes-json-schema: https://github.com/yannh/kubernetes-json-schema

例如 1.22.8 版本的 pod 架構: https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.8/_definitions.json#/definitions/io.k8s.api.core.v1.Pod

  • kubectl explain 命令,可以列印對應型別的 schema

但是以上兩種方式都存在隱患:第一種受版本限制,一旦叢集升級就需要程式碼升級;第二種只能返回搜尋型別的 schema ,不能返回巢狀型別的 schema ,並且不支援 JSON 格式。

  • Discovery API ,是我終於找到的真正可以掃除棘手問題的東西 。

發現包用於發現 Kubernetes API 伺服器支援的 API OpenAPISchema 使用 rest 客戶端獲取 open api v2 模式並解析 proto

OpenAPISchema API DiscoveryClient 返回叢集中定義的所有型別的模式定義。你不能從它的簡單介紹中完全感受到這個 API 的強大,尤其是在沒有相關示例的情況下 client-go .

但嘗試更多永遠不會讓你失望。一開始,我只是在 Kubernetes 文件 “CRD is an optional OpenAPI v3 based validation schema” 的指導下嘗試使用這個 API 來獲取 CRD 的定義。然後我很幸運地發現它可以返回叢集中的所有模式定義,包括原生型別。

Kubernetes API

讓我們深入挖掘,看看為什麼 OpenAPISchema 函式可以返回所有模式嗎?

沒什麼神奇的,只需 呼叫 Kubernetes /openapi/v2API

https://github.com/kubernetes/client-go/blob/246c5406433299079871e0cd4a73fd4928d7428e/discovery/discovery_client.go#L411
d.restClient.Get().AbsPath(“/openapi/v2”).SetHeader(“Accept”, openAPIV2mimePb).Do(context.TODO()).Raw() 

轉回上一個 kubectl explain 命令,新增 -v 標誌,然後再次執行它。

kubectl explain pods -v 9 顯示

正如所見, OpenApi explain 命令,但接收資料 protobuf 格式,併為使用者提供更高的可讀性。

OpenApi : https://kubernetes.io/id/docs/concepts/overview/kubernetes-api/#swagger-and-openapi-definition

我們也可以嘗試呼叫 OpenApi CLI 中,以下命令適用於 GKE

curl -X GET https://{ip}/openapi/v2 --header "Authorization: Bearer $(gcloud auth application-default print-access-token)" --insecure

解析模式

我們需要充分了解我們獲得的資訊,然後才能充分利用它。

匯出輸出 curl 命令到一個檔案,我們可以看到我們得到的只是對 Kubernetes 中每個 API 的分析,就像 swagger API 文件一樣。並向下滾動到底部以檢視 definition s,其中顯示了所有資源型別的所有欄位的定義。

Pod 定義會很長,所以取 resources 下面以欄位為例。

"resources":{
   "description":"Compute Resources required by this container. Cannot be updated. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/",
   "type":"object",
   "properties":{
      "limits":{
         "description":"Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/",
         "type":"object",
         "additionalProperties":{
            "type":"string"
         }
      },
      "requests":{
         "description":"Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/",
         "type":"object",
         "additionalProperties":{
            "type":"string"
         }
      }
   }

client-go 已將架構解析為 openapi_v2 Document ,已準備好應用於開發。

Document: https://pkg.go.dev/github.com/google/gnostic/openapiv2#Document

但有一個問題不容忽視:在返回所有資源型別的情況下,很難區分真正的頂級資源型別,例如,我們不希望使用者查詢 PodAffinity ,而只查詢 Pod

DiscoveryClient 還提供 API ServerGroups ServerResourcesForGroupVersion 幫助建立 GKV(GroupKindVersion) 需要。

API ServerGroups : https://pkg.go.dev/k8s.io/[email protected]/discovery#DiscoveryClient.ServerGroups

ServerResourcesForGroupVersion : https://pkg.go.dev/k8s.io/[email protected]/discovery#DiscoveryClient.ServerResourcesForGroupVersion

func resourceTypes() (resourceTypes, err) {
 apiGroups, err := discoveryClient.ServerGroups()
 resourceTypes = make(map[string]resourceType)
 for _, apiGroup := range apiGroups.Groups {
  for _, groupVersion := range apiGroup.Versions {
   resources, err := discoveryClient.ServerResourcesForGroupVersion(groupVersion.GroupVersion)
      if err != nil {
        return nil, fmt.Errorf("failed to get ServerResourcesForGroupVersion", err)
   }
   for _, resource := range resources.APIResources {
    var openApiDefinitionId string
    if apiGroup.Name == "" {
     openApiDefinitionId = groupVersion.Version + "." + resource.Kind
    } else {
     openApiDefinitionId = strings.Join(apiGroup.Name, ".") + "." + groupVersion.Version + "." + resource.Kind
    }

    resourceTypes[openApiDefinitionId] = resourceType{
     group:      apiGroup.Name,
     version:    version,
     resource:   resource.Name,
     kind:       kind,
     namespaced: resource.Namespaced,
    }
   }
  }
 }
  return
}

type resourceType struct {
 group      string
 version    string
 resource   string
 kind       string
 namespaced bool
}

現在我們終於可以開始解析模式了。

  • 找到要解析的資源型別

  • 查詢型別中的欄位。

  • 建立相應的 graphql.Fields.

func parse(resourceTypes map[string]resourceType, document *openapi_v2.Document) graphql.Fields {
 for _, resource := range document.GetDefinitions().GetAdditionalProperties() {
  // if it is a resource type we want
  rt, ok := resourceTypes[resource.Name]
  if !ok {
   continue
  }
  fields := graphql.Fields{}
  for _, property := range resource.GetValue().GetProperties().GetAdditionalProperties() {
   propertySchema := property.GetValue()
   propertyName := property.GetName()
   // Ignore certain internal or reference fields
   if strings.HasPrefix(propertyName, "x-") || strings.HasPrefix(propertyName, "$") {
    continue
   }
   possible_type := rt.Kind + strings.Title(propertyName)
      // build the match between Kubernetes types and graphql types
   graphqlType := graphqlType(document, possible_type, propertySchema)
   if graphqlType != nil {
    if stringSliceContains(objectSchema.GetRequired(), propertyName) {
     graphqlType = graphql.NewNonNull(graphqlType)
    }
    fields[propertyName] = &graphql.Field{
     Type: graphqlType,
    }
   }
  }
 }
}

構建 GraphQL 查詢

這一步的關鍵是通過以下方式在對應的 Kubernetes Go 型別和 GraphQL 型別之間建立連線 graphqlType 功能。

  • 對於普通型別,直接連線。如

// a TypeItem from the schema
switch openApiType {
case "boolean":
   return graphql.Boolean
case "integer":
   return graphql.Int
case "number":
   return graphql.Float
case "string":
   return graphql.String
  • 對於物件型別,遞迴解析後在 GraphQL 中構建物件型別。

case "object":
   objectFields := parse(document, schema)
   return graphql.NewObject(graphql.ObjectConfig{
      Name:        rt.Kind + "Props",
      Description: schema.GetDescription(),
      Fields:      objectFields,
   })
  • 對於陣列型別,解析第一個元素對應的GraphQL型別,然後組合成GraphQL List型別。

case "array":
   iType = graphqlType(docuemnt, possible_type,     schema.GetItems().GetSchema()[0])
   return graphql.NewList(iTypes)

迴圈之後,我們得到 GraphQL Fields 對應叢集中的所有資源型別,可以進一步組合成一個 GraphQL Object .

現在,讓我們在 localhost:8080/graphql . 重新執行程式,並嘗試獲取有關 PrometheusRule 資源。 GraphQL 在編輯請求時不僅會返回相關資訊,還會返回相關提示。

總結

GraphQL 已經推出了 10 年,但它並沒有像 REST 那樣被廣泛採用。很難解釋為什麼,就像你無法弄清楚為什麼許多 Java 程式仍然停留在 Java8 甚至 Java 7 上一樣。

但我對 GraphQL 有信心。它在很多方面都比 REST 有巨大的優勢,比如大大簡化了前後端 API 設計互動,提高了開發效率。並且被越來越多的網際網路公司採用,將成為未來的發展標準。

感謝你的閱讀!

https://pkg.go.dev/k8s.io/[email protected]/discovery