From 8815f950b0a48b49d363e239ee1818b14e126e78 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 10:27:01 +0200 Subject: [PATCH] Add the buildah module and the check that only it provides rootless-image-build (issues 1702, 1704) --- checks/imagebuild/go.mod | 3 ++ checks/imagebuild/main.go | 92 ++++++++++++++++++++++++++++++++++ checks/imagebuild/main_test.go | 74 +++++++++++++++++++++++++++ checks/toolchains/main.go | 11 ++-- checks/toolchains/main_test.go | 22 +++++++- modules/buildah/README.md | 13 +++++ modules/buildah/module.json | 21 ++++++++ 7 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 checks/imagebuild/go.mod create mode 100644 checks/imagebuild/main.go create mode 100644 checks/imagebuild/main_test.go create mode 100644 modules/buildah/README.md create mode 100644 modules/buildah/module.json diff --git a/checks/imagebuild/go.mod b/checks/imagebuild/go.mod new file mode 100644 index 00000000..05a7de3a --- /dev/null +++ b/checks/imagebuild/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-catalog/checks/imagebuild + +go 1.22 diff --git a/checks/imagebuild/main.go b/checks/imagebuild/main.go new file mode 100644 index 00000000..3d61fcd3 --- /dev/null +++ b/checks/imagebuild/main.go @@ -0,0 +1,92 @@ +// Command imagebuild refuses any manifest but modules/buildah providing the rootless-image-build provision +// (novox/hq ADR 0487 §3, issue 1704): the image builder's tool is buildah alone, and docker, whose daemon holds +// the machine's root, never stands in for it. A second provider would let a node compose mesh-build-image over +// something that is not the rootless builder it was designed against. +// +// go run . +// +// Exits 1 on a finding. +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" +) + +// Provision is the provision only Provider may provide. +const Provision = "rootless-image-build" + +// Provider is the one module allowed to provide it. +const Provider = "buildah" + +// Finding is one manifest refused: a module other than buildah providing the provision. +type Finding struct { + Module string +} + +// Check reads every manifest under root/modules and answers which ones it refuses, and whether buildah provides. +func Check(root string) ([]Finding, error) { + manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) + if err != nil { + return nil, err + } + sort.Strings(manifests) + var out []Finding + provided := false + for _, path := range manifests { + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var m struct { + Module string `json:"module"` + Provides []struct { + Name string `json:"name"` + } `json:"provides"` + } + if err := json.Unmarshal(raw, &m); err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + for _, p := range m.Provides { + if p.Name != Provision { + continue + } + if m.Module == Provider { + provided = true + } else { + out = append(out, Finding{Module: m.Module}) + } + break + } + } + if !provided { + out = append(out, Finding{Module: Provider}) + } + return out, nil +} + +func main() { + root := "." + if len(os.Args) > 1 { + root = os.Args[1] + } + found, err := Check(root) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + for _, f := range found { + if f.Module == Provider { + fmt.Printf("%s does not provide %s (novox/hq ADR 0487 §3)\n", Provider, Provision) + continue + } + fmt.Printf("%s provides %s: only %s may, and docker never (novox/hq ADR 0487 §3)\n", f.Module, Provision, Provider) + } + if len(found) > 0 { + os.Exit(1) + } + fmt.Println("imagebuild: rootless-image-build is provided only by buildah") +} diff --git a/checks/imagebuild/main_test.go b/checks/imagebuild/main_test.go new file mode 100644 index 00000000..3918a600 --- /dev/null +++ b/checks/imagebuild/main_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "os" + "path/filepath" + "reflect" + "testing" +) + +func catalogue(t *testing.T, manifests map[string]string) string { + t.Helper() + root := t.TempDir() + for name, body := range manifests { + dir := filepath.Join(root, "modules", name) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + return root +} + +const buildah = `{"module":"buildah","provides":[{"name":"rootless-image-build","reach":"machine"}]}` + +func TestOnlyBuildahProvidesTheRootlessImageBuild(t *testing.T) { + root := catalogue(t, map[string]string{"buildah": buildah, "go": `{"module":"go","provides":[{"name":"go-toolchain"}]}`}) + got, err := Check(root) + if err != nil { + t.Fatal(err) + } + if len(got) != 0 { + t.Errorf("found %v, want none", got) + } +} + +func TestDockerAndAnySecondProviderAreRefused(t *testing.T) { + root := catalogue(t, map[string]string{ + "buildah": buildah, + "docker": `{"module":"docker","provides":[{"name":"rootless-image-build"}]}`, + "other": `{"module":"other","provides":[{"name":"x"},{"name":"rootless-image-build"}]}`, + }) + got, err := Check(root) + if err != nil { + t.Fatal(err) + } + want := []Finding{{Module: "docker"}, {Module: "other"}} + if !reflect.DeepEqual(got, want) { + t.Errorf("found %v, want %v", got, want) + } +} + +func TestBuildahMustProvideIt(t *testing.T) { + root := catalogue(t, map[string]string{"buildah": `{"module":"buildah"}`}) + got, err := Check(root) + if err != nil { + t.Fatal(err) + } + if want := []Finding{{Module: "buildah"}}; !reflect.DeepEqual(got, want) { + t.Errorf("found %v, want %v", got, want) + } +} + +// The catalogue as it stands: this is the check itself, run here too. +func TestTheCatalogueGivesTheProvisionToBuildahAlone(t *testing.T) { + got, err := Check("../..") + if err != nil { + t.Fatal(err) + } + if len(got) != 0 { + t.Errorf("the catalogue's providers of rootless-image-build are wrong: %v", got) + } +} diff --git a/checks/toolchains/main.go b/checks/toolchains/main.go index 8954a5c6..9a3eb851 100644 --- a/checks/toolchains/main.go +++ b/checks/toolchains/main.go @@ -1,5 +1,5 @@ // Command toolchains refuses any manifest but the owner's declaring a language's toolchain package (novox/hq -// ADR 0477 clause 4, issue 1573): the package go only in modules/go, and nodejs and npm only in modules/nodejs. +// ADR 0477 clause 4, issue 1573): the package go only in modules/go, nodejs and npm only in modules/nodejs, and buildah (issue 1702) only in modules/buildah. // Everything that compiles or runs that language requires the owner's provision instead (go-toolchain, // node-toolchain), so the package is declared once, and a node without the owner refuses the module that // needs it at resolution rather than failing at its first build. The owners must declare their packages. @@ -19,9 +19,10 @@ import ( // Owners names, for each toolchain package, the one module that declares it. var Owners = map[string]string{ - "go": "go", - "nodejs": "nodejs", - "npm": "nodejs", + "buildah": "buildah", + "go": "go", + "nodejs": "nodejs", + "npm": "nodejs", } // Finding is one refusal: Module declares Package, which Owner alone may, or (Missing) Owner does not declare @@ -100,5 +101,5 @@ func main() { if len(found) > 0 { os.Exit(1) } - fmt.Println("toolchains: go, nodejs and npm are declared only by their owners") + fmt.Println("toolchains: go, nodejs, npm and buildah are declared only by their owners") } diff --git a/checks/toolchains/main_test.go b/checks/toolchains/main_test.go index b80cd58e..77a4e8c8 100644 --- a/checks/toolchains/main_test.go +++ b/checks/toolchains/main_test.go @@ -26,6 +26,7 @@ func TestOnlyTheOwnerDeclaresAToolchainPackage(t *testing.T) { root := catalogue(t, map[string]string{ "go": `{"module":"go","resources":[{"id":"go","type":"package","package":"go"}]}`, "nodejs": `{"module":"nodejs","resources":[{"id":"nodejs","type":"package","package":"nodejs"},{"id":"npm","type":"package","package":"npm"}]}`, + "buildah": `{"module":"buildah","resources":[{"id":"b","type":"package","package":"buildah"}]}`, "lab": `{"module":"lab","resources":[{"id":"git","type":"package","package":"git"},{"id":"go","type":"package","package":"go"}]}`, "builder": `{"module":"builder","resources":[{"id":"n","type":"package","package":"nodejs"},{"id":"dir","type":"directory","path":"/go"}]}`, }) @@ -39,6 +40,23 @@ func TestOnlyTheOwnerDeclaresAToolchainPackage(t *testing.T) { } } +func TestASecondDeclarerOfBuildahIsRefused(t *testing.T) { + root := catalogue(t, map[string]string{ + "go": `{"module":"go","resources":[{"id":"go","type":"package","package":"go"}]}`, + "nodejs": `{"module":"nodejs","resources":[{"id":"nodejs","type":"package","package":"nodejs"},{"id":"npm","type":"package","package":"npm"}]}`, + "buildah": `{"module":"buildah","resources":[{"id":"b","type":"package","package":"buildah"}]}`, + "imagery": `{"module":"imagery","resources":[{"id":"b","type":"package","package":"buildah"}]}`, + }) + got, err := Check(root) + if err != nil { + t.Fatal(err) + } + want := []Finding{{Module: "imagery", Package: "buildah", Owner: "buildah"}} + if !reflect.DeepEqual(got, want) { + t.Errorf("found %v, want %v", got, want) + } +} + func TestTheOwnersMustExistAndDeclareTheirPackages(t *testing.T) { root := catalogue(t, map[string]string{ "go": `{"module":"go","resources":[]}`, @@ -47,8 +65,8 @@ func TestTheOwnersMustExistAndDeclareTheirPackages(t *testing.T) { if err != nil { t.Fatal(err) } - if len(got) != 3 { - t.Errorf("an owner missing its packages gave %v, want three findings (go, nodejs, npm)", got) + if len(got) != 4 { + t.Errorf("an owner missing its packages gave %v, want four findings (buildah, go, nodejs, npm)", got) } } diff --git a/modules/buildah/README.md b/modules/buildah/README.md new file mode 100644 index 00000000..7aac2bb6 --- /dev/null +++ b/modules/buildah/README.md @@ -0,0 +1,13 @@ +# buildah + +The rootless image builder as a module (novox/hq ADR 0487, issue 1702). + +- **The only module that declares the package `buildah`** (`checks/toolchains`), and the only one that provides + `rootless-image-build` (`checks/imagebuild`; docker never does). +- **Provides `rootless-image-build` with the machine's reach**: mesh-build-image requires it, so it composes only on a + machine where this module is assigned, and a node without it is refused at resolution naming `buildah`. +- **No account and no subordinate-id word.** The node-engine makes every `user` account with `useradd`, which + allocates the account 65536 subordinate uids and gids; mesh-build-image's own account gets its ranges that way and + its health proves them (it reads /etc/subuid and /etc/subgid). +- The helpers (an OCI runtime, passt, containers-common) come as buildah's package dependencies. +- Holds no long-running resource, so it adds nothing to `health-undeclared`. diff --git a/modules/buildah/module.json b/modules/buildah/module.json new file mode 100644 index 00000000..70ddd0a4 --- /dev/null +++ b/modules/buildah/module.json @@ -0,0 +1,21 @@ +{ + "module": "buildah", + "version": "1", + "capabilities": [ + "package-manager", + "user-namespaces" + ], + "provides": [ + { + "name": "rootless-image-build", + "reach": "machine" + } + ], + "resources": [ + { + "id": "buildah", + "type": "package", + "package": "buildah" + } + ] +}