package infra import ( "go/ast" "go/parser" "go/token" "strconv" "strings" "testing" ) // TestImagesCoversEveryPin is the anti-drift gate for the golden bake. // // Images() feeds `felhom-controller --print-infra-images`, which build-golden.sh uses to decide what // to pre-pull into the appliance image. If someone adds a fifth infra stack — a new `FooImage` const // — and forgets to add it to Images(), the golden silently bakes 4 of 5 and enabling that stack on a // fresh box goes back to being a multi-minute silent registry pull. That is exactly how felhom-samba // was missed, so this test reads the CONST BLOCK OUT OF THE SOURCE rather than restating the list: // a hand-written expected list would need the same edit and would rot the same way. func TestImagesCoversEveryPin(t *testing.T) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "infra.go", nil, 0) if err != nil { t.Fatalf("parsing infra.go: %v", err) } pins := map[string]string{} // const name -> image ref for _, decl := range f.Decls { gd, ok := decl.(*ast.GenDecl) if !ok || gd.Tok != token.CONST { continue } for _, spec := range gd.Specs { vs, ok := spec.(*ast.ValueSpec) if !ok || len(vs.Names) != 1 || len(vs.Values) != 1 { continue } name := vs.Names[0].Name if !strings.HasSuffix(name, "Image") { continue } lit, ok := vs.Values[0].(*ast.BasicLit) if !ok || lit.Kind != token.STRING { continue } val, err := strconv.Unquote(lit.Value) if err != nil { t.Fatalf("unquoting %s: %v", name, err) } pins[name] = val } } if len(pins) == 0 { t.Fatal("found no *Image consts in infra.go — the parser walk is broken, not the pins") } got := map[string]bool{} for _, img := range Images() { got[img] = true } for name, img := range pins { if !got[img] { t.Errorf("const %s = %q is not in Images() — the golden bake would not pre-pull it, so "+ "enabling that stack on a fresh box would block on a silent registry pull", name, img) } } if len(Images()) != len(pins) { t.Errorf("Images() has %d entries but infra.go declares %d *Image consts (%v) — they must "+ "correspond one-to-one", len(Images()), len(pins), pins) } } // TestImagesArePinned guards the other half of the contract: a floating tag would make the golden // bake unreproducible (the bake and a later deploy could resolve the same name to different digests). func TestImagesArePinned(t *testing.T) { for _, img := range Images() { if !strings.Contains(img, ":") { t.Errorf("infra image %q has no tag — implicitly :latest", img) continue } if strings.HasSuffix(img, ":latest") { t.Errorf("infra image %q is :latest — a floating tag breaks reproducible golden bakes", img) } } }