Compose stackek kezelése egy helyről: deployer

A werkstatt sorozat negyedik része – egy eszköz ami YAML konfigból kezeli az összes compose stacket.

Compose stackek kezelése egy helyről: deployer

Öt-hat compose stack fut a szerveremen. Vaultwarden, Gitea, Immich, és a többiek. Mindegyiknek saját mappája van, saját docker-compose.yml-je. Ha az összeset le akarom állítani, öt mappába kell belépnem és öt podman compose down-t futtatnom.

A deployer ezt oldja meg. Egy YAML fájlban definiálom az összes stacket, és egyetlen paranccsal kezelhetem mindet – vagy egyet kiválasztva.

deployer status              # mi fut, mi nem?
deployer up vaultwarden      # egy stacket indít
deployer restart             # mindet újraindít

A konfig

A deployer.yaml egyszerű:

runtime: podman
stacks:
  - name: vaultwarden
    path: /opt/stacks/vaultwarden
  - name: gitea
    path: /opt/stacks/gitea
  - name: immich
    path: /opt/stacks/immich

A runtime mező mondja meg hogy podman vagy docker – a parancsok ugyanazok, csak a bináris neve más. Ez fontos, mert a homelab-em Podman-t futtat, de a munkahelyi szerverek Docker-t.

YAML és Go

A Go-ban a YAML kezelés a gopkg.in/yaml.v3 csomaggal történik. Ez az első külső dependency a werkstatt sorozatban – az eddigi projektek mind a standard library-vel dolgoztak.

A struct tag-ek mondják meg a YAML csomagnak hogyan párosítsa a mezőket:

type Stack struct {
    Name string `yaml:"name"`
    Path string `yaml:"path"`
}

Az yaml.Unmarshal beolvassa a bájtokat és feltölti a struct-ot. Ha a YAML hibás, hibát ad vissza – nem csendben eltűnik mint bash-ben egy rosszul parse-olt konfig.

A runner interface

Az eredeti verzióban az exec.Command közvetlenül a main() függvényben hívódott. Ez működik, de tesztelhetetlen – nem akarok valódi compose parancsokat futtatni a tesztekben.

A megoldás egy interface:

type runner interface {
    run(runtime, dir string, args ...string) (string, error)
    runAttached(runtime, dir string, args ...string) error
}

Két implementáció van. Az execRunner a valódi, ami ténylegesen futtatja a parancsokat. A mockRunner a tesztekben használt, ami előre megadott kimenetet ad vissza:

type mockRunner struct {
    output string
    err    error
}

Ez a Go módja a dependency injection-nek. Nincs framework, nincs annotation, nincs DI container. Egy interface, két implementáció, és a run() függvény megkapja paraméterként hogy melyiket használja.

Konfig validáció

Az eredeti verzió nem ellenőrizte a konfigurációt – ha hiányzott egy mező, futásidőben kapott hibát. A production ready verzióban a validateConfig függvény induláskor ellenőriz mindent:

Van-e legalább egy stack? Van-e minden stacknek neve? Van-e minden stacknek path-ja? Ha bármi hibás, értelmesebb hibaüzenet jön mint egy “nil pointer dereference”.

Ez apróságnak tűnik, de a valódi eszközöknél a felhasználói élmény nagy része az, hogy a hibaüzenetek segítenek a problémát megtalálni.

A subcommand minta

Az envcheck-ben és a netmapper-ben a flag-ek az egyetlen bemeneti mód. A deployer-ben subcommand-ok is vannak: status, up, down, restart. A Go flag csomag közvetlenül nem támogatja a subcommand-okat, de a flag.Args() visszaadja a flag-ek utáni argumentumokat:

flag.Parse()
args := flag.Args()
action := args[0]   // "status", "up", "down", "restart"
target := args[1]   // opcionális stack név

A switch action pedig az egyes akcióknak megfelelő kódot futtatja. Egyszerűbb mint egy cobra vagy urfave/cli framework – és egy négy parancsot kezelő eszköznél tökéletesen elég.

Tesztelés mock-kal

A deployer tesztjei nem indítanak konténereket. A mock runner előre beállított kimeneteket ad, és a tesztek azt ellenőrzik hogy a logika helyesen dolgozza-e fel:

A futó stack [{"Name":"test","State":"running"}] kimenetet ad – a stackStatus ezt “running”-ként azonosítja. Az üres kimenet vagy [] “stopped”. A parancs hiba “error”.

Ez 12 teszt ami lefed: konfig betöltés, validáció, szűrés, státusz ellenőrzés, és exit code-ok. Az egész 0.008 másodperc alatt fut.

Mit tanultam?

A deployer a négy production ready projekt közül a legösszetettebb, és három fontos Go mintát tanított meg:

Az interface-alapú dependency injection – hogyan válaszd szét a logikát a végrehajtástól, hogy tesztelhető legyen. A YAML konfiguráció kezelés – hogyan használj külső csomagot és hogyan validáld a bemenetet. A subcommand minta – hogyan építs egy CLI eszközt ami több akciót is tud kezelni.

Hogyan próbáld ki?

git clone https://github.com/brtkcs/werkstatt-tools.git
cd werkstatt-tools/deployer
go mod tidy
go build -o deployer

Hozz létre egy deployer.yaml-t a saját stackjeidhez és:

./deployer status
./deployer up vaultwarden
./deployer restart

A teljes forráskód a werkstatt-tools repóban.

Ez a werkstatt sorozat negyedik része. Az envcheck fájlokat validált, a stackctl HTTP-t szolgált ki, a netmapper hálózatot scannelt. A deployer mindezek fölé emelkedik – YAML konfiguráció, interface-ek és mock tesztelés.