Kapitel 4. Benutzerdefinierte Ressourcen verwenden

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

In diesem Kapitel stellen wir dir Custom Resources (CR) vor, einen der zentralen Erweiterungsmechanismen, die im gesamten Kubernetes-Ökosystem verwendet werden.

Benutzerdefinierte Ressourcen werden für kleine, firmeninterne Konfigurationsobjekte ohne entsprechende Controller-Logik verwendet - rein deklarativ definiert. Aber benutzerdefinierte Ressourcen spielen auch eine zentrale Rolle für viele ernsthafte Entwicklungsprojekte auf Kubernetes, die ein Kubernetes-natives API-Erlebnis bieten wollen. Beispiele dafür sind Service Meshes wie Istio, Linkerd 2.0 und AWS App Mesh, die alle benutzerdefinierte Ressourcen als Herzstück haben.

Erinnerst du dich an "Ein motivierendes Beispiel" aus Kapitel 1? Im Kern hat es eine CR, die wie folgt aussieht:

apiVersion: cnat.programming-kubernetes.info/v1alpha1
kind: At
metadata:
  name: example-at
spec:
  schedule: "2019-07-03T02:00:00Z"
status:
  phase: "pending"

Benutzerdefinierte Ressourcen sind in jedem Kubernetes-Cluster seit Version 1.7 verfügbar. Sie werden in derselben etcd Instanz wie die Haupt-Kubernetes-API-Ressourcen gespeichert und von demselben Kubernetes-API-Server bereitgestellt. Wie in Abbildung 4-1 dargestellt, werden Anfragen an den apiextensions-apiserver weitergeleitet, der die über CRDs definierten Ressourcen bedient, wenn sie keine der folgenden Eigenschaften aufweisen:

  • Wird von aggregierten API-Servern verwaltet (siehe Kapitel 8).

  • Native Kubernetes-Ressourcen.

API Extensions API server inside of the Kubernetes API server
Abbildung 4-1. Der API Extensions API-Server innerhalb des Kubernetes API-Servers

Eine CustomResourceDefinition (CRD) ist selbst eine Kubernetes-Ressource. Sie beschreibt die verfügbaren CRs im Cluster. Für das vorangegangene CR-Beispiel sieht die entsprechende CRD wie folgt aus:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ats.cnat.programming-kubernetes.info
spec:
  group: cnat.programming-kubernetes.info
  names:
    kind: At
    listKind: AtList
    plural: ats
    singular: at
  scope: Namespaced
  subresources:
    status: {}
  version: v1alpha1
  versions:
  - name: v1alpha1
    served: true
    storage: true

Der Name der CRD - in diesem Fall ats.cnat.programming-kubernetes.info- muss mit dem Pluralnamen, gefolgt vom Gruppennamen, übereinstimmen. Er definiert die Art At CR in der API-Gruppe cnat.programming-kubernetes.info als Namensraum-Ressource namens ats.

Wenn diese CRD in einem Cluster erstellt wird, erkennt kubectl die Ressource automatisch und der Nutzer kann über sie zugreifen:

$ kubectl get ats
NAME                                         CREATED AT
ats.cnat.programming-kubernetes.info         2019-04-01T14:03:33Z

Informationen zur Entdeckung

Hinter den Kulissen nutzt kubectl die Discovery-Informationen des API-Servers, um die neuen Ressourcen zu finden. Schauen wir uns diesen Discovery-Mechanismus etwas genauer an.

Nachdem wir die Ausführlichkeit von kubectl erhöht haben, können wir sehen, wie sie den neuen Ressourcentyp kennenlernt:

$ kubectl get ats -v=7
... GET https://XXX.eks.amazonaws.com/apis/cnat.programming-kubernetes.info/
                                      v1alpha1/namespaces/cnat/ats?limit=500
... Request Headers:
... Accept: application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json
      User-Agent: kubectl/v1.14.0 (darwin/amd64) kubernetes/641856d
... Response Status: 200 OK in 607 milliseconds
NAME         AGE
example-at   43s

Die Entdeckungsschritte im Einzelnen sind:

  1. Zunächst weiß kubectl nichts von ats.

  2. Daher fragt kubectl den API-Server über den Endpunkt /apis discovery nach allen bestehenden API-Gruppen.

  3. Als Nächstes fragt kubectl den API-Server über die Endpunkte /apis/group version group discovery nach Ressourcen in allen bestehenden API-Gruppen.

  4. Dann übersetzt kubectl den angegebenen Typ ats in ein Triple von:

    • Gruppe (hier cnat.programming-kubernetes.info)

    • Version (hier v1alpha1)

    • Ressource (hier ats).

Die Discovery-Endpunkte liefern alle notwendigen Informationen, um die Übersetzung im letzten Schritt durchzuführen:

$ http localhost:8080/apis/
{
  "groups": [{
    "name": "at.cnat.programming-kubernetes.info",
    "preferredVersion": {
      "groupVersion": "cnat.programming-kubernetes.info/v1",
      "version": "v1alpha1“
    },
    "versions": [{
      "groupVersion": "cnat.programming-kubernetes.info/v1alpha1",
      "version": "v1alpha1"
    }]
  }, ...]
}

$ http localhost:8080/apis/cnat.programming-kubernetes.info/v1alpha1
{
  "apiVersion": "v1",
  "groupVersion": "cnat.programming-kubernetes.info/v1alpha1",
  "kind": "APIResourceList",
  "resources": [{
    "kind": "At",
    "name": "ats",
    "namespaced": true,
    "verbs": ["create", "delete", "deletecollection",
      "get", "list", "patch", "update", "watch"
    ]
  }, ...]
}

Dies alles wird durch die Discovery RESTMapper umgesetzt. Wir haben diese sehr verbreitete Art von RESTMapper auch in "REST Mapping" gesehen .

Warnung

Das kubectl CLI verwaltet außerdem einen Cache der Ressourcentypen in ~/.kubectl, damit es die Discovery-Informationen nicht bei jedem Zugriff neu abrufen muss. Dieser Cache wird alle 10 Minuten geleert. Daher kann eine Änderung in der CRD bis zu 10 Minuten später in der CLI des jeweiligen Nutzers auftauchen.

Typ Definitionen

Schauen wir uns das CRD und die angebotenen Funktionen genauer an: Wie im cnat Beispiel sind CRDs Kubernetes-Ressourcen in der apiextensions.k8s.io/v1beta1 API-Gruppe, die von der apiextensions-apiserver innerhalb des Kubernetes API Server-Prozesses bereitgestellt werden.

Das Schema der CRDs sieht folgendermaßen aus:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: name
spec:
  group: group name
  version: version name
  names:
    kind: uppercase name
    plural: lowercase plural name
    singular: lowercase singular name # defaulted to be lowercase kind
    shortNames: list of strings as short names # optional
    listKind: uppercase list kind # defaulted to be kindList
    categories: list of category membership like "all" # optional
  validation: # optional
    openAPIV3Schema: OpenAPI schema # optional
  subresources: # optional
    status: {} # to enable the status subresource (optional)
    scale: # optional
      specReplicasPath: JSON path for the replica number in the spec of the
                        custom resource
      statusReplicasPath: JSON path for the replica number in the status of
                          the custom resource
      labelSelectorPath: JSON path of the Scale.Status.Selector field in the
                         scale resource
  versions: # defaulted to the Spec.Version field
  - name: version name
    served: boolean whether the version is served by the API server # defaults to false
    storage: boolean whether this version is the version used to store object
  - ...

Viele der Felder sind optional oder voreingestellt. Wir werden die Felder in den folgenden Abschnitten genauer erklären.

Nachdem ein CRD-Objekt erstellt hat, überprüft apiextensions-apiserver innerhalb von kube-apiserver die Namen und stellt fest, ob sie mit anderen Ressourcen in Konflikt stehen oder ob sie in sich stimmig sind. Nach ein paar Augenblicken meldet es das Ergebnis im Status des CRDs, zum Beispiel:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ats.cnat.programming-kubernetes.info
spec:
  group: cnat.programming-kubernetes.info
  names:
    kind: At
    listKind: AtList
    plural: ats
    singular: at
  scope: Namespaced
  subresources:
    status: {}
  validation:
    openAPIV3Schema:
      type: object
      properties:
        apiVersion:
          type: string
        kind:
          type: string
        metadata:
          type: object
        spec:
          properties:
            schedule:
              type: string
          type: object
        status:
          type: object
  version: v1alpha1
  versions:
  - name: v1alpha1
    served: true
    storage: true
status:
    acceptedNames:
      kind: At
      listKind: AtList
      plural: ats
      singular: at
    conditions:
    - lastTransitionTime: "2019-03-17T09:44:21Z"
      message: no conflicts found
      reason: NoConflicts
      status: "True"
      type: NamesAccepted
    - lastTransitionTime: null
      message: the initial names have been accepted
      reason: InitialNamesAccepted
      status: "True"
      type: Established
    storedVersions:
    - v1alpha1

Du kannst sehen, dass die fehlenden Namensfelder in der Spezifikation voreingestellt sind und im Status als akzeptierte Namen angezeigt werden. Außerdem sind die folgenden Bedingungen festgelegt:

  • NamesAccepted beschreibt, ob die angegebenen Namen in der Spezifikation konsistent und frei von Konflikten sind.

  • Established beschreibt, dass der API-Server die angegebene Ressource unter den Namen in status.acceptedNames bereitstellt.

Beachte, dass bestimmte Felder noch lange nach der Erstellung des CRDs geändert werden können. Du kannst zum Beispiel Kurznamen oder Spalten hinzufügen. In diesem Fall kann ein CRD erstellt, d. h. mit den alten Namen bedient werden, obwohl es Konflikte mit den vorgegebenen Namen gibt. Die Bedingung NamesAccepted wäre also falsch, und die angegebenen Namen und die akzeptierten Namen würden sich unterscheiden.

Erweiterte Funktionen der benutzerdefinierten Ressourcen

In diesem Abschnitt geht es um erweiterte Funktionen von benutzerdefinierten Ressourcen, wie z. B. Validierung oder Subressourcen.

Benutzerdefinierte Ressourcen validieren

CRs können bei der Erstellung und Aktualisierung durch den API-Server validiert werden. Dies geschieht auf der Grundlage des OpenAPI v3-Schemas, das in den validation -Feldern in der CRD-Spezifikation angegeben ist.

Wenn eine Anfrage eine CR erstellt oder verändert, wird das JSON-Objekt in der Spezifikation gegen diese Spezifikation validiert und im Falle von Fehlern wird das widersprüchliche Feld in einer HTTP-Code-Antwort 400 an den Benutzer zurückgegeben. Abbildung 4-2 zeigt, wo die Validierung im Request Handler innerhalb der apiextensions-apiserver stattfindet.

Komplexere Validierungen können in Webhooks für die Validierungszulassung implementiert werden, d.h. in einer Turing-kompletten Programmiersprache. Abbildung 4-2 zeigt, dass diese Webhooks direkt nach den in diesem Abschnitt beschriebenen OpenAPI-basierten Überprüfungen aufgerufen werden. In "Zulassungs-Webhooks" werden wir sehen, wie Zulassungs-Webhooks implementiert und eingesetzt werden. Dort werden wir uns mit Validierungen beschäftigen, die andere Ressourcen berücksichtigen und damit weit über die OpenAPI v3-Validierung hinausgehen. Glücklicherweise reichen für viele Anwendungsfälle OpenAPI v3 Schemata aus.

Validation step in the handler stack of the `apiextensions-apiserver`
Abbildung 4-2. Validierungsschritt im Handler-Stack des apiextensions-apiserver

Die OpenAPI-Schemasprache basiert auf dem JSON-Schema-Standard, der JSON/YAML selbst verwendet, um ein Schema auszudrücken. Hier ist ein Beispiel:

type: object
properties:
  apiVersion:
    type: string
  kind:
    type: string
  metadata:
    type: object
  spec:
    type: object
    properties:
      schedule:
        type: string
        pattern: "^\d{4}-([0]\d|1[0-2])-([0-2]\d|3[01])..."
      command:
        type: string
    required:
    - schedule
    - command
  status:
    type: object
    properties:
      phase:
        type: string
required:
- metadata
- apiVersion
- kind
- spec

Dieses Schema legt fest, dass der Wert tatsächlich ein JSON-Objekt ist;1 das heißt, es handelt sich um eine String-Map und nicht um ein Slice oder einen Wert wie eine Zahl. Außerdem hat es (neben metadata, kind und apiVersion, die für benutzerdefinierte Ressourcen implizit definiert sind) zwei zusätzliche Eigenschaften: spec und status.

Jedes ist ebenfalls ein JSON-Objekt. spec hat die erforderlichen Felder schedule und command, die beide Strings sind. schedule muss einem Muster für ein ISO-Datum entsprechen (hier mit einigen regulären Ausdrücken skizziert). Die optionale Eigenschaft status hat ein String-Feld namens phase.

OpenAPI-Schemata manuell zu erstellen, kann mühsam sein. Zum Glück wird daran gearbeitet, dies durch Codegenerierung zu vereinfachen: Das Kubebuilder-Projekt - siehe "Kubebuilder" - hat crd-gen in sig.k8s.io/controller-tools entwickelt, das Schritt für Schritt erweitert wird, damit es auch in anderen Kontexten verwendet werden kann. Der Generator crd-schema-gen ist eine Abspaltung von crd-gen in diese Richtung.

Kurznamen und Kategorien

Wie native Ressourcen können auch benutzerdefinierte Ressourcen lange Ressourcennamen haben. Auf der API-Ebene sind sie toll, aber in der CLI mühsam einzugeben. CRs können auch kurze Namen haben, wie die native Ressource daemonsets, die mit kubectl get ds abgefragt werden kann. Diese kurzen Namen werden auch als Aliasnamen bezeichnet, und jede Ressource kann beliebig viele davon haben.

Um alle verfügbaren Kurznamen anzuzeigen, verwende den Befehl kubectl api-resources wie folgt:

$ kubectl api-resources
NAME                   SHORTNAMES  APIGROUP NAMESPACED  KIND
bindings                                    true        Binding
componentstatuses      cs                   false       ComponentStatus
configmaps             cm                   true        ConfigMap
endpoints              ep                   true        Endpoints
events                 ev                   true        Event
limitranges            limits               true        LimitRange
namespaces             ns                   false       Namespace
nodes                  no                   false       Node
persistentvolumeclaims pvc                  true       PersistentVolumeClaim
persistentvolumes      pv                   false       PersistentVolume
pods                   po                   true        Pod
statefulsets           sts         apps     true        StatefulSet
...

Auch hier erfährt kubectl die Kurznamen über die Entdeckungsinformationen (siehe "Entdeckungsinformationen"). Hier ist ein Beispiel:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ats.cnat.programming-kubernetes.info
spec:
  ...
  shortNames:
  - at

Danach werden auf kubectl get at alle cnat CRs im Namensraum aufgelistet.

Außerdem können CRs - wie jede andere Ressource auch - Teil von Kategorien sein. Am häufigsten wird die Kategorie all verwendet, wie in kubectl get all. Sie listet alle benutzerorientierten Ressourcen in einem Cluster auf, wie Pods und Services.

Die im Cluster definierten CRs können sich einer Kategorie anschließen oder über das Feld categories ihre eigene Kategorie erstellen:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ats.cnat.programming-kubernetes.info
spec:
  ...
  categories:
  - all

Damit listet kubectl get all auch die cnat CR im Namensraum auf.

Drucker-Spalten

Das Tool kubectl CLI nutzt das serverseitige Drucken, um die Ausgabe von kubectl get zu rendern. Das bedeutet, dass es den API-Server nach den anzuzeigenden Spalten und den Werten in jeder Zeile abfragt.

Benutzerdefinierte Ressourcen unterstützen auch serverseitige Druckerspalten, und zwar über additionalPrinterColumns. Sie werden "zusätzlich" genannt, weil die erste Spalte immer der Name des Objekts ist. Diese Spalten werden wie folgt definiert:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ats.cnat.programming-kubernetes.info
spec:
  additionalPrinterColumns: (optional)
  - name: kubectl column name
    type: OpenAPI type for the column
    format: OpenAPI format for the column (optional)
    description: human-readable description of the column (optional)
    priority: integer, always zero supported by kubectl
    JSONPath: JSON path inside the CR for the displayed value

Das Feld name ist der Spaltenname, type ist ein OpenAPI-Typ, wie er im Abschnitt Datentypen der Spezifikation definiert ist, und format (wie im gleichen Dokument definiert) ist optional und kann von kubectl oder anderen Clients interpretiert werden.

Außerdem ist description eine optionale, von Menschen lesbare Zeichenkette, die für Dokumentationszwecke verwendet wird. priority steuert, in welchem Ausführlichkeitsmodus von kubectl die Spalte angezeigt wird. Zum Zeitpunkt der Erstellung dieses Artikels (mit Kubernetes 1.14) wird nur der Wert Null unterstützt, und alle Spalten mit höherer Priorität werden ausgeblendet.

Schließlich legt JSONPath fest, welche Werte angezeigt werden sollen. Es ist ein einfacher JSON-Pfad innerhalb der CR. Einfach" bedeutet hier, dass er eine Objektfeldsyntax wie .spec.foo.bar unterstützt, aber keine komplexeren JSON-Pfade, die über Arrays oder Ähnliches laufen.

Damit könnte das Beispiel CRD aus der Einleitung mit additionalPrinterColumns wie folgt erweitert werden:

additionalPrinterColumns: #(optional)
- name: schedule
  type: string
  JSONPath: .spec.schedule
- name: command
  type: string
  JSONPath: .spec.command
- name: phase
  type: string
  JSONPath: .status.phase

Dann würde kubectl eine cnat Ressource wie folgt darstellen:

$ kubectl get ats
NAME  SCHEDULER             COMMAND             PHASE
foo   2019-07-03T02:00:00Z  echo "hello world"  Pending

Als Nächstes werfen wir einen Blick auf die Subressourcen.

Subressourcen

Wir kurz erwähnt Subressourcen in "Status Subressourcen: UpdateStatus". Subressourcen sind spezielle HTTP-Endpunkte, die ein Suffix verwenden, das an den HTTP-Pfad der normalen Ressource angehängt wird. Der Standard-HTTP-Pfad eines Pods lautet zum Beispiel /api/v1/namespace/namespace/pods/ name. Pods haben eine Reihe von Subressourcen, wie z. B. /logs, /portforward, /exec und /status. Die entsprechenden HTTP-Pfade der Subressourcen sind:

  • /api/v1/namespace/namespace /pods/name /logs

  • /api/v1/namespace/namespace /pods/name /portforward

  • /api/v1/namespace/namespace /pods/name /exec

  • /api/v1/namespace/namespace /pods/name /status

Die Endpunkte der Subressourcen verwenden ein anderes Protokoll als der Endpunkt der Hauptressource.

Zum Zeitpunkt der Erstellung dieses Artikels unterstützen die benutzerdefinierten Ressourcen zwei Subressourcen: /scale und /status. Beide sind opt-in, d.h. sie müssen explizit im CRD aktiviert werden.

Subressource Status

Die Subresource /status wird verwendet, um die vom Nutzer bereitgestellte Spezifikation einer CR-Instanz vom durch den Controller bereitgestellten Status zu trennen. Die Hauptmotivation dafür ist die Trennung von Privilegien:

  • Der Benutzer sollte normalerweise keine Statusfelder schreiben.

  • Der Kontrolleur sollte keine Spezifikationsfelder schreiben.

Der RBAC-Mechanismus für die Zugriffskontrolle erlaubt keine Regeln auf dieser Detailstufe. Diese Regeln gelten immer pro Ressource. Die Subressource /status löst dieses Problem, indem sie zwei Endpunkte bereitstellt, die eigenständige Ressourcen sind. Beide können unabhängig voneinander mit RBAC-Regeln kontrolliert werden. Dies wird oft als spec-status split bezeichnet. Hier ist ein Beispiel für eine solche Regel für die Ressource ats, die nur für die Subressource /status gilt (während "ats" für die Hauptressource gelten würde):

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: ...
rules:
- apiGroups: [""]
  resources: ["ats/status"]
  verbs: ["update", "patch"]

Bei Ressourcen (einschließlich benutzerdefinierter Ressourcen), die eine Subressource /status haben, hat sich die Semantik geändert, auch für den Endpunkt der Hauptressource:

  • Sie ignorieren Statusänderungen am Haupt-HTTP-Endpunkt während des Erstellens (der Status wird während des Erstellens einfach gelöscht) und der Aktualisierung.

  • Ebenso ignoriert der Subressourcen-Endpunkt /status Änderungen, die nicht den Status der Nutzlast betreffen. Ein Erstellungsvorgang für den Endpunkt /status ist nicht möglich.

  • Immer wenn sich etwas außerhalb von metadata und außerhalb von status ändert (das bedeutet vor allem Änderungen in der Spezifikation), erhöht der Hauptressourcen-Endpunkt den Wert metadata.generation. Dies kann als Auslöser für einen Controller verwendet werden, der anzeigt, dass sich der Benutzerwunsch geändert hat.

Beachte, dass normalerweise sowohl spec als auch status in Aktualisierungsanfragen gesendet werden, aber technisch gesehen könntest du den jeweils anderen Teil in einem Anfrage-Payload weglassen.

Beachte auch, dass der Endpunkt /status alles außerhalb des Status ignoriert, einschließlich Änderungen an Metadaten wie Beschriftungen oder Kommentaren.

Der Spec-Status-Split einer benutzerdefinierten Ressource wird wie folgt aktiviert:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
spec:
  subresources:
    status: {}
  ...

Beachte hier, dass dem Feld status in diesem YAML-Fragment das leere Objekt zugewiesen wird. So setzt man ein Feld, das keine anderen Eigenschaften hat. Einfach schreiben

subresources:
  status:

führt zu einem Validierungsfehler, denn in YAML ist das Ergebnis ein null Wert für status.

Warnung

Die Aktivierung des Spec-Status-Splits ist eine einschneidende Änderung für eine API. Alte Controller werden an den Hauptendpunkt schreiben. Sie werden nicht bemerken, dass der Status ab dem Zeitpunkt, an dem der Split aktiviert wird, immer ignoriert wird. Ebenso kann ein neuer Controller nicht an den neuen Endpunkt /status schreiben, bis der Split aktiviert wird.

In Kubernetes 1.13 und später können Subressourcen pro Version konfiguriert werden. So können wir die Subressource /status einführen, ohne eine Änderung vorzunehmen:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
spec:
  ...
  versions:
  - name: v1alpha1
    served: true
    storage: true
  - name: v1beta1
    served: true
    subresources:
      status: {}

Dies aktiviert die Subresource /status für v1beta1, aber nicht für v1alpha1.

Hinweis

Die optimistische Gleichzeitigkeitssemantik (siehe "Optimistische Gleichzeitigkeit") ist dieselbe wie bei den Hauptressourcen-Endpunkten; das heißt, status und spec teilen sich denselben Ressourcenversionszähler und /status-Aktualisierungen können aufgrund von Schreibvorgängen auf die Hauptressource in Konflikt geraten und umgekehrt. Mit anderen Worten: Es gibt keine Aufteilung von spec und status auf der Ebene der Speicherung.

Subressource skalieren

Die zweite Subressource, die für benutzerdefinierte Ressourcen zur Verfügung stellt, ist /scale. Die Subressource /scale ist eine (projektive)2 Ansicht auf die Ressource, mit der wir nur die Replikat-Werte sehen und ändern können. Diese Subressource ist bekannt für Ressourcen wie Deployments und Replikat-Sets in Kubernetes, die natürlich hoch und runter skaliert werden können.

Der Befehl kubectl scale verwendet die Subressource /scale; zum Beispiel ändert der folgende Befehl den angegebenen Replikat-Wert in der angegebenen Instanz:

$ kubectl scale --replicas=3 your-custom-resource -v=7
I0429 21:17:53.138353   66743 round_trippers.go:383] PUT
https://host/apis/group/v1/your-custom-resource/scale
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
spec:
  subresources:
    scale:
      specReplicasPath: .spec.replicas
      statusReplicasPath: .status.replicas
      labelSelectorPath: .status.labelSelector
  ...

Dabei wird eine Aktualisierung des Replikat-Wertes auf spec.replicas geschrieben und von dort bei einer GET zurückgegeben.

Der Label-Selektor kann über die Subresource /status nicht geändert, sondern nur gelesen werden. Sein Zweck ist es, einem Controller die Informationen zu geben, um die entsprechenden Objekte zu zählen. Der Controller ReplicaSet zum Beispiel zählt die entsprechenden Pods, die diesen Selektor erfüllen.

Der Label-Selektor ist optional. Wenn deine benutzerdefinierte Ressourcensemantik nicht zu den Label-Selektoren passt, gib den JSON-Pfad für einen solchen einfach nicht an.

In, dem vorherigen Beispiel von kubectl scale --replicas=3 ..., wird der Wert 3 in spec.replicas geschrieben. Natürlich kann auch jeder andere einfache JSON-Pfad verwendet werden; je nach Kontext wäre zum Beispiel spec.instances oder spec.size ein sinnvoller Feldname.

Die Art des Objekts, das vom Endpunkt gelesen oder in ihn geschrieben wird, ist Scale von der autoscaling/v1 API-Gruppe. So sieht es aus:

type Scale struct {
    metav1.TypeMeta `json:",inline"`
    // Standard object metadata; More info: https://git.k8s.io/
    // community/contributors/devel/api-conventions.md#metadata.
    // +optional
    metav1.ObjectMeta `json:"metadata,omitempty"`

    // defines the behavior of the scale. More info: https://git.k8s.io/community/
    // contributors/devel/api-conventions.md#spec-and-status.
    // +optional
    Spec ScaleSpec `json:"spec,omitempty"`

    // current status of the scale. More info: https://git.k8s.io/community/
    // contributors/devel/api-conventions.md#spec-and-status. Read-only.
    // +optional
    Status ScaleStatus `json:"status,omitempty"`
}

// ScaleSpec describes the attributes of a scale subresource.
type ScaleSpec struct {
    // desired number of instances for the scaled object.
    // +optional
    Replicas int32 `json:"replicas,omitempty"`
}

// ScaleStatus represents the current status of a scale subresource.
type ScaleStatus struct {
    // actual number of observed instances of the scaled object.
    Replicas int32 `json:"replicas"`

    // label query over pods that should match the replicas count. This is the
    // same as the label selector but in the string format to avoid
    // introspection by clients. The string will be in the same
    // format as the query-param syntax. More info about label selectors:
    // http://kubernetes.io/docs/user-guide/labels#label-selectors.
    // +optional
    Selector string `json:"selector,omitempty"`
}

Eine Instanz sieht dann so aus:

metadata:
  name: cr-name
  namespace: cr-namespace
  uid: cr-uid
  resourceVersion: cr-resource-version
  creationTimestamp: cr-creation-timestamp
spec:
  replicas: 3
  status:
    replicas: 2
    selector: "environment = production"

Beachte, dass die optimistische Gleichzeitigkeitssemantik für die Hauptressource und für die Subressource /scale gleich ist. Das heißt, dass Schreibvorgänge auf der Hauptressource mit Schreibvorgängen auf der /scale-Ressource kollidieren können und umgekehrt.

Die Sicht eines Entwicklers auf benutzerdefinierte Ressourcen

Auf die benutzerdefinierten Ressourcen kann von Golang aus mit einer Reihe von Clients zugegriffen werden. Wir werden uns darauf konzentrieren:

Die Wahl des zu verwendenden Clients hängt hauptsächlich vom Kontext des zu schreibenden Codes ab, insbesondere von der Komplexität der implementierten Logik und den Anforderungen (z. B. dynamisch sein und zur Kompilierzeit unbekannte GVKs unterstützen).

Die vorangehende Liste der Kunden:

  • Geringere Flexibilität bei der Handhabung unbekannter GVKs.

  • Erhöht die Typensicherheit.

  • Die Vollständigkeit der Funktionen der Kubernetes-API, die sie bereitstellen, wird erhöht.

Dynamischer Kunde

Der dynamische Client in k8s.io/client-go/dynamic ist völlig unabhängig von den bekannten GVKs. Er verwendet keine anderen Go-Typen als unstructured.Unstructured, das nur json.Unmarshal und seine Ausgabe umschließt.

Der dynamische Client macht weder von einem Schema noch von einem RESTMapper Gebrauch. Das bedeutet, dass der Entwickler das gesamte Wissen über Typen manuell bereitstellen muss, indem er eine Ressource (siehe "Ressourcen") in Form einer GVR bereitstellt:

schema.GroupVersionResource{
  Group: "apps",
  Version: "v1",
  Resource: "deployments",
}

Wenn eine REST-Client-Konfiguration verfügbar ist (siehe "Erstellen und Verwenden eines Clients"), kann der dynamische Client in einer Zeile erstellt werden:

client, err := NewForConfig(cfg)

Der REST-Zugang zu einer bestimmten GVR ist genauso einfach:

client.Resource(gvr).
   Namespace(namespace).Get("foo", metav1.GetOptions{})

So erhältst du den Einsatz foo im angegebenen Namensraum.

Hinweis

Du musst den Geltungsbereich der Ressource kennen (d. h., ob sie Namensräume oder Cluster hat). Bei Ressourcen, die in einem Cluster liegen, wird der Aufruf Namespace(namespace) einfach weggelassen.

Die Ein- und Ausgabe des dynamischen Clients ist ein *unstructured.Unstructured, d.h. ein Objekt, das dieselbe Datenstruktur enthält, die json.Unmarshal beim Unmarshaling ausgeben würde:

  • Objekte werden durch map[string]interface{} dargestellt.

  • Arrays werden durch []interface{} dargestellt.

  • Primitive Typen sind string, bool, float64, oder int64.

Die Methode UnstructuredContent() ermöglicht den Zugriff auf diese Datenstruktur innerhalb eines unstrukturierten Objekts (wir können auch einfach auf Unstructured.Object zugreifen). Im selben Paket gibt es Helfer, die das Abrufen von Feldern und die Manipulation des Objekts erleichtern - zum Beispiel:

name, found, err := unstructured.NestedString(u.Object, "metadata", "name")

der den Namen des Einsatzes zurückgibt - in diesem Fall"foo". found ist wahr, wenn das Feld tatsächlich gefunden wurde (nicht nur leer, sondern tatsächlich vorhanden). err meldet, wenn der Typ eines vorhandenen Feldes unerwartet ist (d. h. in diesem Fall kein String). Weitere Helfer sind die generischen Helfer, einmal mit einer tiefen Kopie des Ergebnisses und einmal ohne:

func NestedFieldCopy(obj map[string]interface{}, fields ...string)
  (interface{}, bool, error)
func NestedFieldNoCopy(obj map[string]interface{}, fields ...string)
  (interface{}, bool, error)

Es gibt andere typisierte Varianten, die einen Typ-Cast durchführen und einen Fehler zurückgeben, wenn dieser fehlschlägt:

func NestedBool(obj map[string]interface{}, fields ...string) (bool, bool, error)
func NestedFloat64(obj map[string]interface{}, fields ...string)
  (float64, bool, error)
func NestedInt64(obj map[string]interface{}, fields ...string) (int64, bool, error)
func NestedStringSlice(obj map[string]interface{}, fields ...string)
  ([]string, bool, error)
func NestedSlice(obj map[string]interface{}, fields ...string)
  ([]interface{}, bool, error)
func NestedStringMap(obj map[string]interface{}, fields ...string)
  (map[string]string, bool, error)

Und schließlich ein allgemeiner Setzer:

func SetNestedField(obj, value, path...)

Der dynamische Client wird in Kubernetes selbst für generische Controller verwendet, wie z.B. den Controller für die Speicherbereinigung, der Objekte löscht, deren Eltern verschwunden sind. Die Speicherbereinigung arbeitet mit jeder Ressource im System und macht daher ausgiebig Gebrauch vom dynamischen Client.

Typisierte Kunden

Typisierte Clients verwenden keine map[string]interface{}-ähnlichen generischen Datenstrukturen, sondern echte Golang-Typen, die für jeden GVK unterschiedlich und spezifisch sind. Sie sind viel einfacher zu benutzen, haben eine wesentlich höhere Typsicherheit und machen den Code viel prägnanter und lesbarer. Auf der anderen Seite sind sie weniger flexibel, da die verarbeiteten Typen zur Kompilierzeit bekannt sein müssen und diese Clients generiert werden, was die Komplexität erhöht.

Bevor wir uns zwei Implementierungen von typisierten Clients ansehen, wollen wir einen Blick auf die Darstellung von Typen im Golang-Typsystem werfen (siehe "API Machinery in Depth" für die Theorie hinter dem Kubernetes-Typsystem).

Anatomie eines Typs

Kinds werden als Golang-Strukturen dargestellt. Normalerweise wird die Struktur nach dem Typ benannt (obwohl sie das technisch gesehen nicht sein muss) und wird in einem Paket abgelegt, das der Gruppe und Version des vorliegenden GVK entspricht. Eine übliche Konvention ist es, die GVK group/version.Kind in ein Go-Paket zu packen:

pkg/apis/group/version

und definiere eine Golang-Struktur Kind in der Datei types.go.

Jeder Golang-Typ, der einem GVK entspricht, bettet das TypeMeta struct aus dem Paket k8s.io/apimachinery/pkg/apis/meta/v1 ein. TypeMeta besteht nur aus den Feldern Kind und ApiVersion:

type TypeMeta struct {
    // +optional
    APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
    // +optional
    Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
}

Darüber hinaus muss jeder Top-Level-Typ - also einer, der einen eigenen Endpunkt und damit eine (oder mehrere) entsprechende GVRs hat (siehe "REST-Mapping")- einen Namen, einen Namensraum für Namensraum-Ressourcen und eine ziemlich lange Reihe weiterer Metaebenenfelder speichern. All dies wird in einer Struktur namens ObjectMeta im Paket k8s.io/apimachinery/pkg/apis/meta/v1 gespeichert:

type ObjectMeta struct {
    Name string `json:"name,omitempty"`
    Namespace string `json:"namespace,omitempty"`
    UID types.UID `json:"uid,omitempty"`
    ResourceVersion string `json:"resourceVersion,omitempty"`
    CreationTimestamp Time `json:"creationTimestamp,omitempty"`
    DeletionTimestamp *Time `json:"deletionTimestamp,omitempty"`
    Labels map[string]string `json:"labels,omitempty"`
    Annotations map[string]string `json:"annotations,omitempty"`
    ...
}

Es gibt eine Reihe von zusätzlichen Feldern. Wir empfehlen dir, die ausführliche Inline-Dokumentation zu lesen, da sie einen guten Überblick über die Kernfunktionen von Kubernetes-Objekten gibt.

Kubernetes-Top-Level-Typen (d.h. solche, die ein eingebettetes TypeMeta und ein eingebettetes ObjectMeta haben und - in diesem Fall - in etcd persistiert werden) sehen einander sehr ähnlich in dem Sinne, dass sie normalerweise ein spec und ein status haben. Siehe dieses Beispiel eines Deployments aus k8s.io/kubernetes/apps/v1/types.go:

type Deployment struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec DeploymentSpec `json:"spec,omitempty"`
    Status DeploymentStatus `json:"status,omitempty"`
}

Während sich der tatsächliche Inhalt der Typen für spec und status zwischen den verschiedenen Typen erheblich unterscheidet, ist diese Aufteilung in spec und status ein gängiges Thema oder sogar eine Konvention in Kubernetes, obwohl sie technisch nicht erforderlich ist. Daher ist es eine gute Praxis, diese Struktur auch für CRDs zu verwenden. Einige CRD-Funktionen erfordern sogar diese Struktur; so gilt die Subressource /status für benutzerdefinierte Ressourcen (siehe "Status-Subressource")- wenn sie aktiviert ist - immer nur für die Substruktur status der benutzerdefinierten Ressourceninstanz. Sie kann nicht umbenannt werden.

Struktur der Golang-Pakete

Wie wir auf gesehen haben, werden die Golang-Typen traditionell in einer Datei namens types.go im Paket pkg/apis/group/ version abgelegt. Zusätzlich zu dieser Datei gibt es noch ein paar weitere Dateien, die wir jetzt durchgehen wollen. Einige von ihnen werden vom Entwickler manuell geschrieben, während andere mit Codegeneratoren erzeugt werden. Siehe Kapitel 5 für weitere Informationen.

Die Datei doc.go beschreibt den Zweck der API und enthält eine Reihe von paketübergreifenden Tags zur Codegenerierung:

// Package v1alpha1 contains the cnat v1alpha1 API group
//
// +k8s:deepcopy-gen=package
// +groupName=cnat.programming-kubernetes.info
package v1alpha1

Als nächstes enthält register.go Helfer, um die benutzerdefinierten Golang-Typen in einem Schema zu registrieren (siehe "Schema"):

package version

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/schema"

    group "repo/pkg/apis/group"
)

// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{
    Group: group.GroupName,
    Version: "version",
}

// Kind takes an unqualified kind and returns back a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
    return SchemeGroupVersion.WithKind(kind).GroupKind()
}

// Resource takes an unqualified resource and returns a Group
// qualified GroupResource
func Resource(resource string) schema.GroupResource {
    return SchemeGroupVersion.WithResource(resource).GroupResource()
}

var (
    SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
    AddToScheme   = SchemeBuilder.AddToScheme
)

// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
    scheme.AddKnownTypes(SchemeGroupVersion,
        &SomeKind{},
        &SomeKindList{},
    )
    metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
    return nil
}

Dann definiert zz_generated.deepcopy.go Deep-Copy-Methoden für die benutzerdefinierten Golang-Top-Level-Typen (d.h. SomeKind und SomeKindList im vorangegangenen Beispielcode). Außerdem werden alle Unterkonstruktionen (wie die für spec und status) ebenfalls deep-copy-fähig.

Da das Beispiel das Tag +k8s:deepcopy-gen=package in doc.go verwendet, erfolgt die Deep-Copy-Generierung auf Opt-Out-Basis, d.h. DeepCopy Methoden werden für jeden Typ im Paket generiert, der sich nicht mit +k8s:deepcopy-gen=false abmeldet. Siehe Kapitel 5 und insbesondere "deepcopy-gen Tags" für weitere Details.

Getippter Client, erstellt über client-gen

Wenn das API-Paket pkg/apis/group / version vorhanden ist, erstellt der Client-Generator client-gen einen typisierten Client (siehe Kapitel 5 für Details, insbesondere "client-gen Tags"), standardmäßig in pkg/generated/clientset/versioned (pkg/client/clientset/versioned in alten Versionen des Generators). Genauer gesagt, ist das generierte Top-Level-Objekt ein Client-Set. Es fasst eine Reihe von API-Gruppen, Versionen und Ressourcen zusammen.

Die Top-Level-Datei sieht wie folgt aus:

// Code generated by client-gen. DO NOT EDIT.

package versioned

import (
    discovery "k8s.io/client-go/discovery"
    rest "k8s.io/client-go/rest"
    flowcontrol "k8s.io/client-go/util/flowcontrol"

    cnatv1alpha1 ".../cnat/cnat-client-go/pkg/generated/clientset/versioned/
)

type Interface interface {
    Discovery() discovery.DiscoveryInterface
    CnatV1alpha1() cnatv1alpha1.CnatV1alpha1Interface
}

// Clientset contains the clients for groups. Each group has exactly one
// version included in a Clientset.
type Clientset struct {
    *discovery.DiscoveryClient
    cnatV1alpha1 *cnatv1alpha1.CnatV1alpha1Client
}

// CnatV1alpha1 retrieves the CnatV1alpha1Client
func (c *Clientset) CnatV1alpha1() cnatv1alpha1.CnatV1alpha1Interface {
    return c.cnatV1alpha1
}

// Discovery retrieves the DiscoveryClient
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
   ...
}

// NewForConfig creates a new Clientset for the given config.
func NewForConfig(c *rest.Config) (*Clientset, error) {
    ...
}

Das Client-Set wird durch die Schnittstelle Interface repräsentiert und ermöglicht den Zugriff auf die Client-Schnittstelle der API-Gruppe für die jeweilige Version - in diesem Beispielcode zum Beispiel CnatV1alpha1Interface:

type CnatV1alpha1Interface interface {
    RESTClient() rest.Interface
    AtsGetter
}

// AtsGetter has a method to return a AtInterface.
// A group's client should implement this interface.
type AtsGetter interface {
    Ats(namespace string) AtInterface
}

// AtInterface has methods to work with At resources.
type AtInterface interface {
    Create(*v1alpha1.At) (*v1alpha1.At, error)
    Update(*v1alpha1.At) (*v1alpha1.At, error)
    UpdateStatus(*v1alpha1.At) (*v1alpha1.At, error)
    Delete(name string, options *v1.DeleteOptions) error
    DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
    Get(name string, options v1.GetOptions) (*v1alpha1.At, error)
    List(opts v1.ListOptions) (*v1alpha1.AtList, error)
    Watch(opts v1.ListOptions) (watch.Interface, error)
    Patch(name string, pt types.PatchType, data []byte, subresources ...string)
        (result *v1alpha1.At, err error)
    AtExpansion
}

Eine Instanz eines Client-Sets kann mit der Hilfsfunktion NewForConfig erstellt werden. Dies ist analog zu den Clients für Kubernetes-Kernressourcen, die in "Erstellen und Verwenden eines Clients" beschrieben werden :

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/tools/clientcmd"

    client "github.com/.../cnat/cnat-client-go/pkg/generated/clientset/versioned"
)

kubeconfig = flag.String("kubeconfig", "~/.kube/config", "kubeconfig file")
flag.Parse()
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
clientset, err := client.NewForConfig(config)

ats := clientset.CnatV1alpha1Interface().Ats("default")
book, err := ats.Get("kubernetes-programming", metav1.GetOptions{})

Wie du siehst, können wir mit der Codegenerierung die Logik für benutzerdefinierte Ressourcen auf die gleiche Weise programmieren wie für die Kernressourcen von Kubernetes. Es gibt auch Tools auf höherer Ebene wie Informer; siehe informer-gen in Kapitel 5.

controller-runtime Client von Operator SDK und Kubebuilder

der Vollständigkeit halber wollen wir einen kurzen Blick auf den dritten Client werfen, der in "Die Sicht eines Entwicklers auf benutzerdefinierte Ressourcen" als zweite Option aufgeführt ist . Das Projekt controller-runtime bildet die Grundlage für die in Kapitel 6 vorgestellten Operator-Lösungen Operator SDK und Kubebuilder. Es enthält einen Client, der die in "Anatomie eines Typs" vorgestellten Go-Typen verwendet .

Im Gegensatz zum client-gen-generierten Client des vorherigen "Typed client created via client-gen" und ähnlich wie der "Dynamic Client" ist dieser Client eine Instanz, die mit jeder Art umgehen kann, die in einem bestimmten Schema registriert ist.

Er nutzt die Discovery-Informationen des API-Servers, um die Arten auf HTTP-Pfade abzubilden. In Kapitel 6 wird ausführlicher beschrieben, wie dieser Client als Teil der beiden Betreiberlösungen eingesetzt wird.

Hier ist ein kurzes Beispiel, wie du controller-runtime verwenden kannst:

import (
    "flag"

    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes/scheme"
    "k8s.io/client-go/tools/clientcmd"

    runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
)

kubeconfig = flag.String("kubeconfig", "~/.kube/config", "kubeconfig file path")
flag.Parse()
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)

cl, _ := runtimeclient.New(config, client.Options{
    Scheme: scheme.Scheme,
})
podList := &corev1.PodList{}
err := cl.List(context.TODO(), client.InNamespace("default"), podList)

Die Methode List() des Client-Objekts akzeptiert jede runtime.Object, die im angegebenen Schema registriert ist, das in diesem Fall von client-go übernommen wurde, wobei alle Standard-Kubernetes-Typen registriert sind. Intern verwendet der Client das angegebene Schema, um den Golang-Typ *corev1.PodList auf einen GVK abzubilden. In einem zweiten Schritt nutzt die Methode List() die Discovery-Informationen, um die GVR für Pods zu erhalten, die schema.GroupVersionResource{"", "v1", "pods"} ist, und greift daher auf /api/v1/namespace/default/pods zu, um die Liste der Pods im übergebenen Namensraum zu erhalten.

Die gleiche Logik kann mit benutzerdefinierten Ressourcen verwendet werden. Der Hauptunterschied besteht darin, ein eigenes Schema zu verwenden, das den übergebenen Go-Typ enthält:

import (
    "flag"

    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes/scheme"
    "k8s.io/client-go/tools/clientcmd"

    runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    cnatv1alpha1 "github.com/.../cnat/cnat-kubebuilder/pkg/apis/cnat/v1alpha1"
)

kubeconfig = flag.String("kubeconfig", "~/.kube/config", "kubeconfig file")
flag.Parse()
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)

crScheme := runtime.NewScheme()
cnatv1alpha1.AddToScheme(crScheme)

cl, _ := runtimeclient.New(config, client.Options{
    Scheme: crScheme,
})
list := &cnatv1alpha1.AtList{}
err := cl.List(context.TODO(), client.InNamespace("default"), list)

Beachte, dass sich der Aufruf des Befehls List() überhaupt nicht ändert.

Stell dir vor, du schreibst einen Operator, der über diesen Client auf viele verschiedene Typen zugreift. Mit dem typisierten Client von "Typed client created via client-gen" müsstest du viele verschiedene Clients an den Operator übergeben, was den Plumbing-Code ziemlich komplex macht. Im Gegensatz dazu ist der hier vorgestellte controller-runtime Client nur ein Objekt für alle Arten, vorausgesetzt, sie befinden sich alle in einem Schema.

Alle drei Arten von Clients haben ihren Nutzen, mit Vor- und Nachteilen je nach Kontext, in dem sie verwendet werden. In generischen Controllern, die mit unbekannten Objekten umgehen, kann nur der dynamische Client verwendet werden. In Controllern, bei denen die Typsicherheit sehr hilfreich ist, um die Korrektheit des Codes durchzusetzen, sind die generierten Clients eine gute Wahl. Das Kubernetes-Projekt selbst hat so viele Mitwirkende, dass die Stabilität des Codes sehr wichtig ist, auch wenn er von so vielen Leuten erweitert und neu geschrieben wird. Wenn Bequemlichkeit und hohe Geschwindigkeit bei minimalem Aufwand wichtig sind, ist der controller-runtime Client eine gute Wahl.

Zusammenfassung

In diesem Kapitel haben wir dir die benutzerdefinierten Ressourcen, die zentralen Erweiterungsmechanismen im Kubernetes-Ökosystem, vorgestellt. Inzwischen solltest du ihre Funktionen und Grenzen sowie die verfügbaren Clients gut kennen.

Kommen wir nun zur Codegenerierung für die Verwaltung dieser Ressourcen.

1 Verwechsle hier nicht Kubernetes und JSON-Objekte. Letzteres ist nur ein anderer Begriff für eine String-Map, die im Kontext von JSON und in OpenAPI verwendet wird.

2 "Projektiv" bedeutet hier, dass das Objekt scale eine Projektion der Hauptressource in dem Sinne ist, dass es nur bestimmte Felder zeigt und alles andere ausblendet.

Get Kubernetes programmieren now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.