Files
bootapi/internal/catalog/catalog_test.go
T
unkinben 8f356346eb
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Address PR review: PXE gate + callback, git-sync templates, distro catalog, k8s targets, http+https
Implements the six review comments on PR #1:

- Per-host PXE-enable gate: read NetBox pxe_enabled custom field; a known host
  with it false gets the safe local-boot script (Cobbler netboot_enabled). Add a
  token-guarded POST /provisioned/{ident} callback that clears pxe_enabled in
  NetBox, plus a %post snippet in the default kickstarts that calls it.
- Templates from a git repo: bootapi clones a templates repo and re-pulls every
  BOOTAPI_TEMPLATE_GIT_INTERVAL (default 3m), atomically swapping the template
  set (last-good kept on parse failure; embedded defaults are the startup
  fallback). Metrics for syncs/failures/generation.
- Distro catalog (catalog/*.yaml): NetBox host -> boot images/kickstart, so
  adding an OS is a YAML + template change. Ships almalinux + fedora entries
  (artifactapi remotes); debian/talos path documented.
- Boot images from the artifactapi almalinux/fedora remotes via the catalog.
- Bind resolvers, puppet server/CA and PUPPETCA_URL env file now target the k8s
  services (198.18.200.7; puppet(ca).k8s.syd1.au.unkin.net).
- Boot path served over plain HTTP (installers lack CA trust) with an optional
  parallel HTTPS listener; docs say do not 301 the boot endpoints.

New packages: internal/catalog, internal/gitsync. NetBox client gains a
pxe_enabled write (token needs that scope - noted in docs). `bootapi validate`
subcommand validates a template/catalog set for the templates-repo CI.

go build/vet clean, go test -race green, golangci-lint v2 clean, pre-commit clean.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-28 22:34:44 +10:00

127 lines
3.6 KiB
Go

package catalog
import (
"strings"
"testing"
"git.unkin.net/unkin/bootapi/internal/model"
)
const almaYAML = `
name: almalinux9
match:
platforms: [almalinux9]
family: almalinux
kickstart: almalinux9
version_default: "9"
kernel_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/vmlinuz"
initrd_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/initrd.img"
kernel_args: [inst.text]
vars:
mirror: "{{.ArtifactBase}}/almalinux/{{.Version}}"
`
const fedoraYAML = `
name: fedora
match:
family: fedora
kickstart: fedora
version_default: "41"
kernel_url: "{{.ArtifactBase}}/fedora/releases/{{.Version}}/Everything/{{.Arch}}/os/images/pxeboot/vmlinuz"
initrd_url: "{{.ArtifactBase}}/fedora/releases/{{.Version}}/Everything/{{.Arch}}/os/images/pxeboot/initrd.img"
`
func testCatalog(t *testing.T) *Catalog {
t.Helper()
c, err := Parse(map[string][]byte{
"almalinux9.yaml": []byte(almaYAML),
"fedora.yaml": []byte(fedoraYAML),
})
if err != nil {
t.Fatalf("Parse: %v", err)
}
return c
}
func TestSelect(t *testing.T) {
c := testCatalog(t)
cases := []struct {
host *model.Host
want string
ok bool
}{
{&model.Host{Platform: "almalinux9", OSFamily: "almalinux"}, "almalinux9", true}, // exact platform
{&model.Host{Platform: "fedora42", OSFamily: "fedora"}, "fedora", true}, // family
{&model.Host{Platform: "almalinux9", TemplateOverride: "fedora"}, "fedora", true}, // override wins
{&model.Host{Platform: "debian12", OSFamily: "debian"}, "", false}, // no match
}
for _, tc := range cases {
d, ok := c.Select(tc.host)
if ok != tc.ok {
t.Errorf("Select(%+v) ok=%v, want %v", tc.host, ok, tc.ok)
continue
}
if ok && d.Name != tc.want {
t.Errorf("Select(%+v) = %q, want %q", tc.host, d.Name, tc.want)
}
}
}
func TestResolve(t *testing.T) {
c := testCatalog(t)
h := &model.Host{Platform: "almalinux9", OSFamily: "almalinux", OSVersion: "9", Arch: "x86_64"}
d, ok := c.Select(h)
if !ok {
t.Fatal("expected a match")
}
r, err := d.Resolve(h, "https://af/api/v1/remote")
if err != nil {
t.Fatal(err)
}
if r.KernelURL != "https://af/api/v1/remote/almalinux/9/BaseOS/x86_64/os/images/pxeboot/vmlinuz" {
t.Errorf("kernel = %q", r.KernelURL)
}
if r.Vars["mirror"] != "https://af/api/v1/remote/almalinux/9" {
t.Errorf("mirror = %q", r.Vars["mirror"])
}
if len(r.KernelArgs) != 1 || r.KernelArgs[0] != "inst.text" {
t.Errorf("kernel_args = %v", r.KernelArgs)
}
}
func TestResolveVersionDefault(t *testing.T) {
c := testCatalog(t)
// Host with no OSVersion falls back to the catalog's version_default.
h := &model.Host{Platform: "fedora", OSFamily: "fedora", Arch: "x86_64"}
d, _ := c.Select(h)
r, err := d.Resolve(h, "https://af")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(r.KernelURL, "/releases/41/") {
t.Errorf("expected version_default 41 in %q", r.KernelURL)
}
}
func TestParseValidation(t *testing.T) {
bad := map[string]string{
"no-kernel": "name: x\nmatch: {platforms: [x]}\nkickstart: x\ninitrd_url: y",
"no-match": "name: x\nkickstart: x\nkernel_url: k\ninitrd_url: i",
"no-name": "kickstart: x\nmatch: {family: x}\nkernel_url: k\ninitrd_url: i",
"bad-template": "name: x\nmatch: {family: x}\nkickstart: x\nkernel_url: \"{{ .Nope\"\ninitrd_url: i",
}
for name, y := range bad {
if _, err := Parse(map[string][]byte{name + ".yaml": []byte(y)}); err == nil {
t.Errorf("%s: expected a validation error, got nil", name)
}
}
}
func TestNames(t *testing.T) {
c := testCatalog(t)
got := strings.Join(c.Names(), ",")
if got != "almalinux9,fedora" {
t.Errorf("Names() = %q", got)
}
}