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.
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 in607
milliseconds NAME AGE example-at 43s
Die Entdeckungsschritte im Einzelnen sind:
-
Zunächst weiß
kubectl
nichts vonats
. -
Daher fragt
kubectl
den API-Server über den Endpunkt /apis discovery nach allen bestehenden API-Gruppen. -
Als Nächstes fragt
kubectl
den API-Server über die Endpunkte /apis/group version
group discovery nach Ressourcen in allen bestehenden API-Gruppen. -
Dann übersetzt
kubectl
den angegebenen Typats
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
kind
List
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 instatus.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.
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 bindingstrue
Binding componentstatuses csfalse
ComponentStatus configmaps cmtrue
ConfigMap endpoints eptrue
Endpoints events evtrue
Event limitranges limitstrue
LimitRange namespaces nsfalse
Namespace nodes nofalse
Node persistentvolumeclaims pvctrue
PersistentVolumeClaim persistentvolumes pvfalse
PersistentVolume pods potrue
Pod statefulsets sts appstrue
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:00Zecho
"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 vonstatus
ändert (das bedeutet vor allem Änderungen in der Spezifikation), erhöht der Hauptressourcen-Endpunkt den Wertmetadata.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:
-
Verwendung des dynamischen Clients
client-go
(siehe "Dynamischer Client") -
Einen getippten Client verwenden:
-
Wie von kubernetes-sigs/controller-runtime bereitgestellt und vom Operator SDK und Kubebuilder verwendet (siehe "controller-runtime Client von Operator SDK und Kubebuilder")
-
Wie von
client-gen
generiert, z. B. in k8s.io/client-go/kubernetes (siehe "Typed client created via client-gen")
-
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
, oderint64
.
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.