Clientset predstavuje ďalšiu vrstvu abstrakcie pre komunikáciu s Kubernetes API. Aby sme lepšie pochopili celú myšlienku, tak si najprv clientset naprogramujeme ručne. Potom si ukážeme, ako sa používa generátor.
Čo ale vlastne je ten clientset?
Clientset je štandardný spôsob, ako sa v Kubernetes svete tvorí typovo bezpečný API klient. Ako naznačuje slovo “set” v názve, nejde len o jedného klienta, ale o celú skupinu klientov organizovaných do logickej hierarchie. Takýto klient poskytuje operácie nad konkrétnymi resources (ako napríklad náš Starfighter
). Programátor pritom už nemusí riešiť nízkoúrovňové detaily, ako serializáciu či mapovanie. Pozrime sa na konkrétny príklad:
clientset, err := kubernetes.NewForConfig(config)
clientset.CoreV1().Pods("default").Create(...)
Z príkladu sa dá vypozorovať, že clientset je rozdelený na tri úrovne:

Zoberiem to zdola. Na úplnom spodku je Object-level klient. Je to klient, ktorý poskytuje CRUD operácie pre konkrétny objekt alebo typ.
GroupVersion klient predstavuje strednú úroveň v hierarchii. Je to akýsi “manažér”, ktorý spravuje prístup k Object-level klientom pre určitú skupinu API (napríklad “starwars”) a jej verziu (napríklad “v1”). Predstavte si to ako priečinok, v ktorom máte uložené všetky súvisiace API operácie. Ak by sme neskôr vytvorili novú verziu API (povedzme “v2”), budeme potrebovať nový GroupVersion klient pre túto verziu.
Posledná a najvyššia úroveň je Globálny clientset, ktorý je globálny prístupový bod ku všetkým API skupinám. V drvivej väčšine prípadov poskytuje len jednu skupinu a verziu. Sú však aj komplexnejšie prípady, kde táto úroveň vnáša celkom poriadok. Príkladom je samotný Kubernetes, ktorý už prichádza s niekoľko skupinami a verziami.
Môj prvý Clientset
Takže teraz, keď už vieme ako je štandardný clientset organizovaný, tak môžeme začať s tvorbou vlastného pre môj fiktívny Starfighter
zo skupiny starwars.okontajneroch.sk
.
Ešte jedno malé upozornenie. V existujúcom projekte z predchádzajúcich článkov som musel spraviť zopár úprav. Typy boli premiestnené z api/v1
do api/starwars/v1
. Taktiež som doplnil ďalší typ - StarfighterList
. Ide o kolekciu, ktorá bude používaná pri listovaní viacerých Starfighter
. Všetky tieto zmeny a celý projekt je pripravený na Githube, ktorý môžeš použiť ako odrazový mostík. Stačí si teda vyklonovať:
git clone https://github.com/okontajneroch/19-clientset.git
Adresárová štruktúra
Keďže clientset je organizovaný v troch úrovniach, prislúcha tomu aj adresárová štruktúra. Najskôr teda začnem s adresármi. V projekte si vytvorím nasledujúce adresáre:
clientset/
├── scheme/
└── typed/
└── starwars/
└── v1/
Opäť - každý adresár má svoj význam.
Adresár typed
obsahuje konkrétne implementácie GroupVersion a Object-level klientov. Adresár sa volátyped
preto, lebo tieto implementácie sú pre konkrétne typy. Kód je v tomto adresári rozdelený do podadresárov podľa skupiny a verzie.
Adresár scheme
obsahuje samotnú schému, registruje jednotlivé typy a konverzie do tejto schémy, rieši tzv. “default” hodnoty, mappingy medzi GroupVersionKind a Go typmi atď. Vo všeobecnosti sa používa jedna schéma pre jeden globálny clientset.
Okrem týchto adresárov tu môže byť ešte tzv. fake
adresár, ktorého súčasťou su tzv. mock implementácie klientov. Tieto implementácie nekomunikujú s reálnym klastrom a preto sa používajú pri unit testovaní. Mocky si však ručne nebudem vytvárať.
Na úrovni clientset
adresára sa často nachádza len jeden súbor - clientset.go
, kde je samotný kód globálneho clientsetu.
Je dobre túto organizáciu dodržiavať, aby potom iní programátori vedeli kde sa čo nachádza. Pre mňa osobne je super, ak zoberiem existujúci controller alebo operátor, a viem kde čo sa nachádza.
Object-level klient
Začnem teda z dola – object-level klientom. Kód klienta sa v rámci Go projektu umiestňuje do adresára clientset/typed/{skupina}/{verzia}
, a jeho meno kopíruje meno typu. Takže pre Starfighter
to bude starfighter.go
// Interface definuje CRUD operácie pre Starfighter
type StarfighterInterface interface {
Get(ctx context.Context, name string) (*starwarsv1.Starfighter, error)
Create(ctx context.Context, starfighter *starwarsv1.Starfighter) (*starwarsv1.Starfighter, error)
}
// Implementácia interface-u
type starfighters struct {
restClient rest.Interface
ns string
}
// Ukážka jednej CRUD operácie
func (c *starfighters) Get(ctx context.Context, name string) (*starwarsv1.Starfighter, error) {
result := &starwarsv1.Starfighter{}
return result, c.restClient.Get().
Namespace(c.ns).
Resource("starfighters").
Name(name).
Do(ctx).
Into(result)
}
...
Ako prvé som si definoval StarfighterInterface
, ktorý bude poskytovať základné operácie ako Get()
, Create()
a Update()
pre Starfighter
. Potom nasleduje samotná implementácia, súčasťou ktorej je konkrétny REST klient restClient
a menný priestor ns
. No a samozrejmosťou sú implementácie operácií, čo je vlastne volanie REST klienta pre konkrétny typ. Je to volanie, ktoré som už robil v predchádzajúcich článkoch. Podobne ako Get()
bude implementovaný Create()
, respektíve ostatné operácie.
Keďže štruktúra starfighters
je privátna, tak bude vytvorená cez factory metódu newStarfighters()
func newStarfighters(c rest.Interface, namespace string) *starfighters {
return &starfighters{
restClient: c,
ns: namespace,
}
}
Celý kód klienta je tu
GroupVersion klient
Podobne ako Object-level klient, aj GroupVersion klient je súčasťou adresára clientset/typed/starwars/v1
. Kód tohto klienta sa väčšinou nachádza v súbore {skupina}_client.go
. Keďže meno mojej skupiny je pomerne dlhé, úplne postačí skratka starwars_client.go
// Interface pre prístup k API objektom v skupine starwars.v1
type StarwarsV1Interface interface {
Starfighters(namespace string) StarfighterInterface
}
// Klient pre skupinu starwars.v1
type StarwarsV1Client struct {
restClient rest.Interface
}
// Vytvorenie (Starfighter) object-level klienta pre daný namespace
func (c *StarwarsV1Client) Starfighters(namespace string) StarfighterInterface {
return newStarfighters(c.restClient, namespace)
}
// Vytvorenie nového klienta
func NewForConfig(c *rest.Config) (*StarwarsV1Client, error) {
config := *c
config.APIPath = "/apis"
config.GroupVersion = &starwarsv1.SchemeGroupVersion
client, err := rest.RESTClientFor(&config)
if err != nil {
return nil, err
}
return &StarwarsV1Client{restClient: client}, nil
}
Tento klient má omnoho jednoduchší interface. V podstate klient je len množinou akýchsi getter metód jednotlivých object-level klientov. Opäť súčasťou štruktúry bude referencia na REST klienta. Táto štruktúra je oproti Object-level štruktúre už verejná.
No a nechýba ani factory metóda NewForConfig()
. Jej implementácia je čitateľovi predchádzajúcich článkov povedomá. Ide o vytvorenie tzv. „povrchovej“ kópie konfigurácie, ktorej už nastavíme aj serializer pre skupinu. Okrem toho táto metóda vytvorí REST klienta pre túto „povrchovú“ kópiu konfigurácie. Následne tento REST klient sa použije vo výslednej štruktúre.
Globálny Clientset
Nakoniec mi už stačí vytvoriť globálny clientset. Vytvorím si súbor ./clientset/clientset.go
, kde začnem najprv s interface
:
// Interface pre prístup ku skupinám a verziam
type Interface interface {
StarwarsV1() starwarsv1.StarwarsV1Interface
}
// Implementácia interface-u
type Clientset struct {
starwarsV1 *starwarsv1.StarwarsV1Client
}
// Ukážka prístupu ku konkretnej skupine a verzii (GroupVersion klient)
func (c *Clientset) StarwarsV1() starwarsv1.StarwarsV1Interface {
return c.starwarsV1
}
func NewForConfig(c *rest.Config) (*Clientset, error) {
config := *c // shallow copy
config.APIPath = "/apis"
config.GroupVersion = &starwarsv1api.SchemeGroupVersion
config.NegotiatedSerializer = serializer.NewCodecFactory(scheme.Scheme)
client, err := rest.RESTClientFor(&config)
if err != nil {
return nil, err
}
cs := &Clientset{
starwarsV1: starwarsv1.New(client),
}
return cs, nil
}
Samotná implementácia je vlastne len akýsi kontajner referencií na všetkých GroupVersion klientov. To najdôležitejšie sa odohráva v NewForConfig()
. Tu sa najprv vytvorí plytká kópia config
a REST klient, a potom funkcia inicializuje všetkých GroupVersion klientov.
Použitie
Všetok potrebný kód bol vytvorený a ja mám v podstate použiteľnú knižnicu k môjmu API pre Kubernetes. Teraz mi nič nebráni použiť môj clientset nasledujúcim spôsobom:
// vytvorenie clientsetu
cs, _ := clientsetv1.NewForConfig(config)
// vytvorenie objektu
xwing := &starwarsv1.Starfighter{
Name: "x-wing-1",
Spec: starwarsv1.StarfighterSpec{
Pilot: "Luke Skywalker",
Type: "X-Wing",
Faction: starwarsv1.Rebellion,
},
}
// použitie clientsetu na vytvorenie objektu v klastri
starfighter, _ := cs.StarwarsV1().
Starfighters("default").
Create(context.TODO(), xwing)
Skrz inštanciu clientsetu cs
sa dostávam ku GroupVersion klientovi StarwarsV1()
a následne k object-level klientovi Starfighters("default")
, ktorý je platný pre konkrétny menný priestor default
. Poslednou je už konkrétna operácia Create()
nad k8s objektom Starfighter
.
Celý program je dostupný tu.
Ak si vytvorím klaster a aplikujem moje CRD:
$ kind create cluster
$ kubectl apply -f ./k8s/starwars.okontajneroch.sk_starfighters.yaml
A spustím program:
$ go run ./examples/clientset-demo
Created Starfighter: x-wing-1 f30f374d-3bf6-498b-b7de-e7bd794eca10
ak na výstupe môžem vidieť, že došlo k vytvoreniu objektu Starfighter
s menom x-wing-1
. To si môžem overiť aj priamo na klastri cez kubectl
:
$ kubectl get starfighter x-wing-1 -o yaml
apiVersion: starwars.okontajneroch.sk/v1
kind: Starfighter
metadata:
creationTimestamp: "2024-12-18T00:09:51Z"
generation: 1
name: x-wing-1
namespace: default
resourceVersion: "493"
uid: f30f374d-3bf6-498b-b7de-e7bd794eca10
spec:
faction: rebellion
pilot: Luke Skywalker
type: X-Wing
Generovanie Clientsetov
Napísať si vlastný clientset je dobré spraviť raz, pre štúdijné účely. V reálnom vývoji je to ale dosť otravná činnosť a množstvo tzv. „boilerplate“ kódu. Je lepšie si nechať clientset vygenerovať. Generátorov je viacero. Ja budem používať client-gen
, ktorý je súčasťou Kubernetesu.
Vráťme sa naspäť, kedy mám v Go projekte len typy, schému a vygenerované CRD. Opäť môžeš použiť GitHub repozitár:
git clone https://github.com/okontajneroch/19-clientset.git
Ako prvé je potrebné si nainštalovať samotný generátor:
go get k8s.io/code-generator
go install k8s.io/code-generator/cmd/client-gen
Potom je potrebné pridať marker +genclient
pre ten typ, ktorý chceme zahrnúť do clientsetu. To spravím v súbore ./api/starwars/v1/types.go
:
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:object:generate=true
// +genclient
type Starfighter struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec StarfighterSpec `json:"spec,omitempty"`
Status StarfighterStatus `json:"status,omitempty"`
}
Treba povedať, že ide o najjednoduchšie použitie +genclient
markera. V rámci tohto markera môžeme určiť, aké akcie sa majú generovať. Napríklad +genclient:onlyVerbs=get,create,update
vygeneruje Object-level klienta, ktorý bude mať k dispozícii len vymenované akcie. Pre pokročilejšie nastavenia odporúčam pozrieť dokumentáciu.
Teraz môžem spustiť generátor a podhodiť mu zopár parametrov
client-gen --input-base="github.com/okontajneroch/starwars/api" \
--input="starwars/v1" \
--clientset-name="generated" \
--output-dir=. \
--output-pkg="github.com/okontajneroch/starwars"
Samotný client-gen
bude hľadať typy, schémy atď. v adresári ./api/starwars/v1
. Cesta je výsledkom parametrov --input-base
a --input
. Následne clientset bude vygenerovaný v adresári ./generated
podľa kombinácie parametrov --clientset-name
a --output-dir
. Posledný parameter je --output-pkg
, ktorý je dôležitý pre správne importy v Go súboroch. Výsledkom generátora budú nasledujúce adresáre a súbory:
generated/
├── fake/
├── scheme/
| └── register.go
├── typed/
| └── starwars/
| └── v1/
| ├─ starfighter.go
| └─ starwars_client.go
└── clientset.go
Už vieme, že starfighter.go
obsahuje Object-level klienta, následne starwars_client.go
je súbor s GroupVersion klientom a na vrchole je globálny clientset v clientset.go
. Okrem klientov je tu aj registrácia schémy v ./generated/scheme/register.go
. No a zvláštnosťou je ./fake
adresár, ktorý obsahuje vygenerované mocky klientov, čo je celkom užitočné pre unit testing.
Clientset je teda vygenerovaný a ja ho môžem použiť. Opäť si vytvorím podobný program, akurát použitie bude trošičku odlišné:
result, err := client.
StarwarsV1().
Starfighters("default").
Create(context.Background(), xwing, metav1.CreateOptions{})
V tomto prípade je Create(...)
trošku pokročilejší a „ukecanejší“ o metav1.CreateOptions{}
, skrz ktorý môžeme vykonať napríklad DryRun
.
Záver
V tomto článku sme si:
- Vysvetlili, čo je clientset a jeho trojúrovňovú hierarchiu (Globálny clientset, GroupVersion klient a Object-level klient)
- Vytvorili vlastný ručne písaný clientset pre náš Starfighter resource
- Ukázali, ako sa clientset prakticky používa pri komunikácii s Kubernetes klastrom
- Naučili sa, ako si clientset vygenerovať pomocou nástroja
client-gen
Ručné písanie clientsetu je síce pracné, ale pomáha lepšie pochopiť, ako funguje komunikácia s Kubernetes API. V reálnych projektoch je však rozumnejšie použiť generovanie pomocou client-gen
, ktorý nám ušetrí množstvo rutinnej práce a zároveň vytvorí aj mocky pre testovanie.