API Machinery je knižnica, ktorá poskytuje základné abstrakcie a nástroje na prácu s Kubernetes API objektami (resources). Je základným komponentom pri programovaní vlastných nástrojov pre Kubernetes, operátorov atď.
Jej úlohou je pomocť s definovaním, validovaním, verzovaním a serializáciou API objektov. Je nápomocná ako na strane klientov, tak aj na strane Kubernetes API servra.
API Machinery nemusíme použivať len v súvislosti s Kubernetesom. Môže poslúžiť aj ako základ pre tvorbu vlastného deklarativného API. Či už budeme chcieť vytvoriť vlastný server s deklaratívnym API, alebo nástroj, ktorý bude objekty načítavať zo súborov.
Povedzme si aj to, čo API Machinery nieje. Táto knižnica nerieši samotnú
komunikáciu s Kubernetes API serverom. K tomu slúží iná knižnica
k8s.io/client-go
. Taktiež API Machinery neposkytuje žiadne konkrétne
objekty ako Pod
, Deployment
alebo Node
. Samotné objekty sa nachádzajú v
samostatnej knižnici Kubernetes API k8s.io/api
. Tieto knižnice ale majú
priamu závislosť na API Machinery.
Kubernetes objekt
Aby sme lepšie pochopili význam tejto knižnice, tak si musime najprv
zodpovedať: čo je Kubernetes objekt. Objekty vo svete Kubernetes sú
semištrukturované dáta, napríklad vo formáte YAML. Sú to práve tie chronický
známe objekty ako Pod
, Deployment
atď. Poďme sa ale bližšie pozrieť na to,
z akých základných častí sa taký taký objekt sklada. Predstavme si teda Pod
:
apiVersion: v1
kind: Pod
metadata:
name: coredns-5d78c9869d-5qq6x
namespace: kube-system
labels:
...
annotations:
...
spec:
containers:
...
status:
phase: Running
conditions:
...
Každý objekt môžem rozdeliť na 4 základné časti:
- Kind a verzia
- Metadata
- Spec
- Status
Každý objekt teda začína tzv. Kind a verziou. Objekt je sám nositelom
informácie o svojom type a verzii. Vďaka tomu, dokážeme s objektami pracovať
v rôznej forme a niesme tak limitovaní len na REST API. Objekty môžu mať
napríklad formu súborov - manifestov. Kind je niečo ako typ. Preto ak ďalej v
texte budem hovoriť o type objektu, budem tym myslieť práve Kind. Hoci pri
ukážke Podu je len v1
, tak verzia nieje len číslo. Vo verzii je aj informácia,
do ktorej skupiny Kind patrí. Niečo ako package.
Poďme ale ďalej. Nasleduje časť metadata
. Tu sú informácie ako meno objektu
(Pozor! Nie typ), namespace
, do ktorého objekt patrí, ale aj ďalšie meta
informácie ako labels
, či annotations
. Táto časť je pre všetky typy rovnaká.
Asi najzaujimavejšiou časťou je spec
. Ide o akésy telo objektu, kde sa
nachádzajú samotné dáta objektu.
Posledný je status
. Treba si uvedomiť, že Kubernetes API je silne deklaratívne.
To znamená, že spec
je len akýsi požadovaný stav, v akom by som rád objekt
videl. Požadovaný však neznamená reálny. Napríklad replicas
v Deployment
neznamená, že okamžite máme toľko replík Podov. Kubernetes totiž tento
požadovaný stav môže aj odmietnúť. A to z z rôznych dôvodov. Skrz status
nam dáva Kubernetes vlastne akúsi spätnú väzbu o spracovaní požadovaného stavu.
Táto časť je podobne ako spec
pre každý typ objektu iná, podobne ako spec
.
Môj prvý objekt
Takže už vieme, čo je objekt. Predstavme si, že si v Go chcem vytvoriť knižnicu,
ktorá bude vedieť pracovať s objektom - Starfighter
. Objekt by mohol vyzerať
následovne:
apiVersion: starwars.okontajneroch.sk/v1
kind: Starfighter
metadata:
name: x-wing-1
spec:
faction: rebellion
type: x-wing
pilot: Luke Skywalker
status:
phases:
- "ready to flight"
- "flies in space"
- "wings to attack position"
Ako prvé si vytvorím nový Go projekt a adresár /api/v1
. Budem používať
zaužívanú štrukturu adresárov, a názvy súborov, aby sa v mojej knižnici
vedeli zorientovať aj iní ľudia z Kubernetes sveta.
$ go mod init github.com/okontajneroch/starwars
$ mkdir -p api/v1
$ go get k8s.io/apimachinery/pkg/runtime
Vytvorím si súbor api/v1/types.go
, kde budem dávať všetky potrebné typy. Ako
prvé si vytvorím typy pre spec
a status
:
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// príklad enumerácie
type Faction string
const (
Rebellion Faction = "rebellion"
Empire Faction = "empire"
)
// dátova štruktúra pre `spec`
type StarfighterSpec struct {
Faction Faction `json:"faction"`
Type string `json:"type"`
Pilot string `json:"pilot"`
}
// dátova štruktúra pre `status`
type StarfighterStatus struct {
Phases []string `json:"phases"`
}
Kód zatiaľ nieje komplikovaný a ide o klasické dátove štruktúry, ktoré vieme serializovať na JSON.
Okrem spec
a status
budem potrebovať ešte Metadata
. Kedže ide o časť,
ktorá je všade rovnaká, tak API Machinery už poskytuje hotovú štruktúru
ObjectMeta
. Táto štruktura disponuje parametrami ako Name
, Namespace
,
UID
, ale aj Labels
či Annotations
.
Poďme to teda zlepiť do finálnej štruktúry Starfighter
:
type Starfighter struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec StarfighterSpec `json:"spec,omitempty"`
Status StarfighterStatus `json:"status,omitempty"`
}
Za zmienku ešte stojí TypeMeta
. Ten reprezentuje dvojicu version
a kind
.
Aby ale Starfighster
bola štruktúra, ktorá bude plnohodnotne reprezentovať
API objekt, tak musí ešte implementovať rozhranie runtime.Object
, ktoré
vyzerá asi takto:
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}
Metóda GetObjectKind
je už implementovaná v TypeMeta
, čiže o tú sa nemusím
starať. Čo potrebujem ale doimplementovať je DeepCopyObject()
. Jej úlohou je
vytvoriť kópiu objektu v pamäti. Túto metódu ale umiestním do súboru
api/v1/deepcopy.go
.
package v1
import "k8s.io/apimachinery/pkg/runtime"
func (in *Starfighter) DeepCopyObject() runtime.Object {
out := new(Starfighter)
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec.Faction = in.Spec.Faction
out.Spec.Type = in.Spec.Type
out.Spec.Pilot = in.Spec.Pilot
// malá mágia s kopírovaním polí, keďže
// potrebujem plnú kópiu celého objektu a nesmiem
// referencovať na existujúce dáta
phases_in, phases_out := &in.Status.Phases, &out.Status.Phases
*phases_out = make([]string, len(*phases_in))
copy(*phases_out, *phases_in)
return out
}
Ide o pomerne nudné a pracné kopírovanie dát hore-dole. Najzložitejšia časť
kódu sa týka phases
a ide o kopírovanie poľa. ObjectMeta
disponuje svojou
vlastnou metódou DeepCopyInto()
, ktorou si viem uľahčiť prácu.
Samozrejme aby toto nebolo také pracné, tak neskôr si celé implementovanie
DeepCopyObject()
zjednoduším pomocou generátora. Zatiaľ si však všetko
vytváram ručne, aby som pochopil presne čo objekt je.
Teraz mám už pripravené všetko na to, aby som s objetom Starfighter
vedel
pracovať. Teda - skoro všetko.
GroupVersionKind a GroupVersionResource
Skôr ako pojdeme ďalej, si musíme ozrejmiť čo je GroupVersionKind
(GVK) a
GroupVersionResource
(GVR). Ide tak trochu o neštastnú komplikáciu v celom
Kubernetes API.
O tom, čo je skupina (Group) a verzia (Version) sa asi netreba veľmi rozpisovať.
Ako som už spomínal vyžsie, Group a Version v YAML manifestoch tvori jeden celok
ako apiVersion: starwars.okontajneroch.sk/v1
Čo je ale Kind a čo Resource?
Už vieme, že Kind je vlastne akýsi typ. Je vo forme jednotného čísla,
napríklad Pod
alebo Starfighter
.
Aký je ale význam Resource? Resource sa používa v súvislosti z mapovaním
objektu na REST API endpoint. Predstavme si, že mám YAML manifest s objektom.
Ak chcem použiť kubectl apply
, tak kubectl
potrebuje vedieť, aký endpoint
patrí tomuto objektu. V minulom článku o Kubernetes API
som písal o tom, ako sú tieto endpointy organizované. Enpoint teda vyzerá asi
takto:
/apis/{group}/{version}/namespaces/{namespace}/{resource}
Tu už ale neviem použiť Kind - napríklad Pod
. Resource má totiž formu
množného čísla - napríklad pods
. Musím preto vedieť aký Resource patrí akému
Kind. Treba mi vedieť mapovať GroupVersionKind
(GVK) na GroupVersionResource
(GVR).
Za toto mapovanie je zodpovedný tzv. RESTMapper
. Jeho interface je súčaštou
API Machinery, no implementácia je skôr záležitosťou inej knižnice - client-go
.
Ale o tom si presne povieme nabudúce, keď sa budeme baviť o tejto knižnici.
Schéma a SchemeBuilder
Keď chceme deserializovať objekt z YAML na Go štruktúru, tak potrebujeme vedieť,
že aká štrutúra zodpovedá ktorej skupine, verzii a Kind. API Machinery to rieši
pomocou registrácie v Scheme
. K tomu ale potrebujeme pre našu knižnicu
vytvoriť SchemeBuilder
. Zaregistrujem si moju novú štruktúru Starfighter
do skupiny starwars.okontajneroch.sk
, s verziou v1
. Vytvorím si api/v1/register.go
:
package v1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(
schema.GroupVersion{
Group: "starwars.okontajneroch.sk",
Version: "v1",
},
&Starfighter{},
)
return nil
}
Pre nás je najdôležitejšia funckia addKnownTypes()
. Tu sa odohráva samotná
registrácia štruktúry Starfighter
do Scheme
. Tu povieme, že Starfighter
patrí do skupiny starwars.okontajneroch.sk
a verzie v1
. Tu by som mohol
registrovať všetký dátove štruktúry skupiny starwars.okontajneroch.sk
.
Funkcia je následne použitá pri inicializácii tzv SchemeBuilder
. Dôvodom,
prečo je registrácia riešená cez SchemeBuilder
je, akási kompozícia. Aby
sme vedeli vytvoriť schému, ktorá bude vedieť serializovať a deserializovať
rôzne objekty rôznych knižníc.
Okrem toho je tu ešte globálna premenna AddToScheme
, ktorá len referuje na
SchemeBuilder
. Toto je taký štandard v rámci všetkých knižníc. Pridal som ho
hlavne z dôvodu konzistencie s inými knižnicami.
Serializácia a Deserializácia
API Machinery rieši aj serializáciu a deserializáciu. Poskytuje serialízery,
pre rôzné formáty ako YAML, JSON alebo Protobuf. Ako taká serializácia bude
vyzerať? Povedzme že chcem si vypísať na štandardný výstup nejaký môj
Starfighter
objekt v YAML formáte.
V existujúcom projekte si vytvorím main.go
:
package main
import starwarsv1 "github.com/sn3d/starwars/api/v1"
func createXWing() *starwarsv1.Starfighter {
xwing := &starwarsv1.Starfighter{}
xwing.APIVersion = "starwars.okontajneroch.sk/v1"
xwing.Kind = "Starfighter"
xwing.ObjectMeta.Name = "x-wing-1"
xwing.Spec = starwarsv1.StarfighterSpec{
Faction: starwarsv1.Rebellion,
Type: "x-wing",
Pilot: "Luke Skywalker",
}
return xwing
}
Zatial tu mám len funkciu createXWing()
, ktorá vytvorí a naplní celú dátovú
štrukturu Starfighter
. Tieto dáta budem chcieť serializovať na YAML.
Začnem teda s vytvorením schémy v main()
:
func main() {
scheme := runtime.NewScheme()
starwarsv1.AddToScheme(scheme)
}
Takto som si do svojej schémy pridal celú skupinu starwars.okontajneroch.sk
,
čiže táto moja schéma bude vedieť rozpoznať Starfighter
. Pamätáte si
AddToScheme
v registry.go
a celé to okolo SchemeBuilder
? Tak
starwarsv1.AddToScheme(scheme)
je jej praktické použitie. Takto si viem
vytvoriť schému, ktorá bude podporovať nielen rôzne skupiny, ale aj skupiny v
rôznych verziach.
Keď už mám schému, tak si vytvorím serializér pomocou NewSerializerWithOptions()
:
func main() {
scheme := runtime.NewScheme()
starwarsv1.AddToScheme(scheme)
serializer := json.NewSerializerWithOptions(
json.SimpleMetaFactory{},
scheme,
scheme,
json.SerializerOptions{
Yaml: true,
},
)
xwing := createXWing()
serializer.Encode(xwing, os.Stdout)
}
Ide o spoločný serialízer pre JSON a YAML. To, čo z neho robí YAML serialízer,
je Yaml: true
. Serializer potrebuje k svojej činnosti schému. Serializer je
pripravený. Funkcia serializera Encode()
prevedie môj xwing
na YAML.
Keď teraz spustím môj kód, tak na výstupe dostanem YAML formu môjho objektu, čo nieje až také prekvapivé:
$ go run main.go
apiVersion: starwars/v1
kind: Starfighter
metadata:
creationTimestamp: null
name: x-wing-1
spec:
faction: rebellion
pilot: Luke Skywalker
type: x-wing
status:
phases: null
Zoberme si ale opačný prípad. Chcem napísať program, ktorý zoberie YAML zo štandardného vstupu a vytvorí jeho dátovú štrukturu. V podstate chcem dosiahnuť deserializáciu. Vytvorenie schémy a serializéra si ponechám, a doplnim kód, ktorý načíta dáta zo vstupu:
func main() {
scheme := runtime.NewScheme()
starwarsv1.AddToScheme(scheme)
serializer := json.NewYAMLSerializer(
json.SimpleMetaFactory{},
scheme,
scheme,
)
data, err := io.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
//...
}
Ďalej budem pokračovať deserializáciou pomocou Decode()
:
o, gvk, err := serializer.Decode(data, nil, nil)
if err != nil {
panic(err)
}
Funkcia vracia niekoľko hodnôt: všeobecnú inštanciu o
typu runtime.Object
.
Tá sa dá pretypovať na konkrétny typ Starfighter
. Potom je to informácia gvk
,
ktorá hovori o aký objekt ide, a samozrejme v Go už klasicky - hodnotu chyby.
Ako prvé si necham vypísať gvk
:
fmt.Printf("Group: %s\n", gvk.Group)
fmt.Printf("Version: %s\n", gvk.Version)
fmt.Printf("Kind: %s\n\n", gvk.Kind)
Ako ďalšie si nechám vypísať spec
.
xwing := o.(*starwarsv1.Starfighter)
fmt.Printf("Faction: %s\n", xwing.Spec.Faction)
fmt.Printf("Type: %s\n", xwing.Spec.Type)
fmt.Printf("Pilot: %s\n", xwing.Spec.Pilot)
Najpr som musel inštanciu o
pretypovať zo všeobecného typu runtime.Object
na konkrétny starwars1.Starfighter
. Následne som mohol vypisať jednotlivé
hodnoty spec
časti.
Kód vyskúšam tak, že pošlem na štandardný vstup validný YAML:
$ go run main.go << EOF
apiVersion: starwars.okontajneroch.sk/v1
kind: Starfighter
metadata:
name: x-wing-1
spec:
faction: rebellion
type: x-wing
pilot: Luke Skywalker
status:
phases:
- "ready to flight"
- "flies in space"
- "wings to attack position"
EOF
Kód spracuje YAML, vyparsuje dáta do starwarsv1.Starfighter
a nakoniec
informácie vypíše na výstup:
Group: okontajneroch.io
Version: v1
Kind: Starfighter
Faction: rebellion
Type: x-wing
Pilot: Luke Skywalker
DeepCopy generator
Na záver si ešte poďme ukázať, ako si uľahčiť implementovanie DeepCopyObject()
.
Kubernetes komunita poskytuje aj sadu tzv. code-generator
nástrojov, ktoré
ako názov naznačuje, slúžia na generovanie nudnejších repetitívnych časti.
Jednym z takých nástrojov je aj deepcopy-gen
.
Poďme si teda vygenerovať DeepCopyObject()
pre môj Starfighter
. Ako prvé
si nainštalujem nástoj deepcopy-gen
:
go install k8s.io/code-generator/cmd/deepcopy-gen@latest
Potom si vytvorím api/v1/doc.go
súbor, ktorého obsah bude len jeden komentár
pre package
.
// +k8s:deepcopy-gen=package
package v1
Nejde o obyčajný komentár. Ak v jazyku Go nejaký komentár začína znakom +
,
tak hovoríme že ide o tzv. marker. Je to spôsob, ako rozným nástrojom podhodiť
akési metadáta. Javisti si to môžu predstaviť ako anotácie. Konkrétne tento
marker zapína generovanie DeepCopyXXX()
funkcie pre všetký typy v tomto balíčku.
V types.go
ešte doplním ďalší marker. Teraz pre typ Starfighter
. Tento
marker zabezpečí, že pre práve pre Starfighter
bude vygenerovaná metóda
DeepCopyObject()
.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Starfighter struct {
metav1.TypeMeta `json:",inline"`
...
A teraz môžem spustiť generátor:
$ deepcopy-gen ./api/v1 --output-file zz_generated.deepcopy.go
Generator v /api/v1
vytvori súbor zz_generated.deepcopy.go
, kde budú
vygenerované DeepCopyObject()
metódy pre všetky typy. A nielen tie. Okrem
toho vygeneruje aj DeepCopy()
a DeepCopyInto()
ktoré síce niesu potrebné
pre implementáciu rozhrania, ale sú celkom prakticke pre ďalšie použitie.
Aby som odstránil kompilačné chyby, tak zmažem môj ručne implementovaný
deepcopy.go
rm api/v1/deepcopy.go
Ja ale nechcem vždy spúštať príkaz deepcopy-gen
. Možem si to zjednodušiť
pomocou go generate
. Príkaz vložim opäť ako špecialný komentár //go:generate
do api/v1/doc.go
:
// +k8s:deepcopy-gen=package
//
//go:generate deepcopy-gen . --output-file zz_generated.deepcopy.go
package v1
Teraz bude stačiť spustiť:
$ go generate ./api/v1
Toto však nieje jediný spôsob ako si zjednodušiť prácu s deepcopy-gen
.
Niektoré projekty maju deepcopy-gen
príkaz zakomponovaný priamo v Makefile
.
V mojom projekte si môžem vytvoriť veľmi primitívny ./Makefile
:
.PHONY: generate
generate:
deepcopy-gen ./api/v1 --output-file zz_generated.deepcopy.go
Ten potom viem spustiť ako:
make generate
Záver
Výsledkom tohto celého snaženia je, že už viem pracovať s vlastným Kubernetes objektom v kóde.Viem ako ho definovať, viem ako ho vytvoriť a používať. Dotkol som sa aj problému GVK (GroupVersionKind) a GVR (GroupVersionResource), ku ktorému sa ešte vrátim v inom článku. Teraz ale potrebujem, aby tento môj objekt spoznal aj Kubernetes klaster pomocou Custom Resource Definition - CRD. To si už ale necham na ďalší článok.
Na úplný záver už len dotaz, že kompletný kód a projekt z tohto článku je dostupny na GitHube github.com/okontajneroch
Ak sa ti článok páčil a chceš ma podporiť, tak budem rád za lajk, zdielanie alebo followovanie na LinkedIn. Môj LinkedIn je tiež otvorený každému, kto má akukoľvek otázku k tejto téme.