Unify trunk on main: initialization → main #3

Merged
jschoubben merged 58 commits from initialization into main 2026-09-05 01:13:33 +00:00
6 changed files with 825 additions and 39 deletions
Showing only changes of commit 337126603e - Show all commits
+6 -1
View File
@@ -153,7 +153,12 @@ func run(ctx context.Context, command string, opts options) error {
if err != nil {
return fmt.Errorf("reading the declaration: %w", err)
}
d, err := declaration.Parse(raw)
// ParseTrusted: a file handed to the host by someone already running it as root is
// not the link. novox/hq ADR 0047 bounds what a REMOTE party may push; someone who
// can write this file and run this binary can do anything the binary can, so refusing
// them an action would buy nothing and would make an action untestable except by
// rebuilding the bundle.
d, err := declaration.ParseTrusted(raw)
if err != nil {
return err
}
+292 -14
View File
@@ -12,11 +12,13 @@ package apply
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
@@ -95,15 +97,16 @@ func Apply(
}
for _, orphan := range known.Orphans(declared) {
if err := remove(ctx, orphan, run); err != nil {
action, detail, err := remove(ctx, orphan, run)
if err != nil {
return report, known, &Error{Resource: orphan.ID, Err: err, Done: report}
}
known.Forget(orphan.ID)
report.Outcomes = append(report.Outcomes, Outcome{
ID: orphan.ID, Type: orphan.Type, Target: orphan.Target, Action: "removed",
Detail: "no longer declared",
ID: orphan.ID, Type: orphan.Type, Target: orphan.Target,
Action: action, Detail: detail,
})
log(fmt.Sprintf(" removed %s (%s)", orphan.ID, orphan.Target))
log(fmt.Sprintf(" %s %s (%s)", action, orphan.ID, orphan.Target))
}
for _, resource := range d.Resources {
@@ -133,6 +136,12 @@ func applyOne(ctx context.Context, r declaration.Resource, run Runner) (Outcome,
return applyFile(r)
case declaration.TypeService:
return applyService(ctx, r, run)
case declaration.TypePackage:
return applyPackage(ctx, r, run)
case declaration.TypeContainer:
return applyContainer(ctx, r, run)
case declaration.TypeAction:
return applyAction(ctx, r, run)
default:
// Unreachable: the declaration refused this already. Present because "unreachable"
// stops being true the moment someone adds a type and forgets this switch.
@@ -390,20 +399,25 @@ func serviceState(ctx context.Context, unit string, run Runner) (string, error)
}
}
// remove undoes one resource the host applied and the declaration no longer names.
// remove undoes one resource the host applied and the declaration no longer names, and reports
// what it actually did.
//
// Only ever called for something in the store, which is what bounds it: the host is
// authoritative over its own footprint and inert everywhere else (novox/hq ADR 0043).
func remove(ctx context.Context, a store.Applied, run Runner) error {
//
// It returns the action rather than assuming "removed", because for half the vocabulary the
// honest word is "forgotten". A host that reported a package removed when it left the package
// installed would be describing an effect it declined to have.
func remove(ctx context.Context, a store.Applied, run Runner) (string, string, error) {
switch declaration.Type(a.Type) {
case declaration.TypeFile, declaration.TypeDirectory:
if err := os.RemoveAll(a.Target); err != nil {
return err
return "", "", err
}
if _, err := os.Stat(a.Target); !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("%s is still there after removing it", a.Target)
return "", "", fmt.Errorf("%s is still there after removing it", a.Target)
}
return nil
return "removed", "no longer declared", nil
case declaration.TypeService:
// A unit that is no longer declared is stopped, not deleted. The host did not install
@@ -416,17 +430,46 @@ func remove(ctx context.Context, a store.Applied, run Runner) error {
// same reason `os.RemoveAll` is.
if _, err := serviceState(ctx, a.Target, run); err != nil {
if strings.Contains(err.Error(), "does not exist on this machine") {
return nil
return "forgotten", "the unit no longer exists", nil
}
return err
return "", "", err
}
if _, err := run(ctx, "systemctl", "stop", a.Target); err != nil {
return fmt.Errorf("stopping %s: %w", a.Target, err)
return "", "", fmt.Errorf("stopping %s: %w", a.Target, err)
}
return nil
return "removed", "stopped; the unit file is not the host's to delete", nil
case declaration.TypeContainer:
// The host CREATED this one, so the host removes it. That is the line: it removes what
// it made and leaves what it merely configured.
if _, err := run(ctx, "docker", "rm", "-f", a.Target); err != nil {
// Already gone is the state removal wants. Anything else is a real failure.
if _, alive := containerState(ctx, a.Target, run); alive == nil {
return "", "", fmt.Errorf("removing container %s: %w", a.Target, err)
}
}
if _, err := containerState(ctx, a.Target, run); err == nil {
return "", "", fmt.Errorf("container %s is still there after removing it", a.Target)
}
return "removed", "no longer declared", nil
case declaration.TypePackage:
// Deliberately not uninstalled, and this is a decision rather than an omission.
//
// The host cannot know what else on this machine needs the package. Uninstalling a
// container runtime because a declaration changed would stop every container on the
// node, and the machine may have had the package before the mesh ever saw it
// (novox/hq research 012: adopted, not installed). Undeclaring says "the mesh no
// longer requires this", which is not the same as "remove it".
return "forgotten", "left installed; the host does not uninstall what it cannot know is unused", nil
case declaration.TypeAction:
// An action has no footprint the host can undo — it ran, and whatever it did belongs
// to whatever it acted on.
return "forgotten", "an action leaves nothing the host owns", nil
default:
return fmt.Errorf("no way to remove a %q", a.Type)
return "", "", fmt.Errorf("no way to remove a %q", a.Type)
}
}
@@ -445,3 +488,238 @@ func ExecRunner(ctx context.Context, name string, args ...string) (string, error
}
return string(out), nil
}
// applyPackage installs a package the machine does not have.
//
// It never upgrades and never removes. "Present" is the whole of what a package resource
// asserts, because version is the package manager's business and the mesh does not have a
// second opinion about it (novox/hq ADR 0041 — the host depends on nothing, and that includes
// not becoming a second package manager).
func applyPackage(ctx context.Context, r declaration.Resource, run Runner) (Outcome, error) {
out := Outcome{ID: r.ID, Type: string(r.Type), Target: r.Package}
installed, err := packageInstalled(ctx, r.Package, run)
if err != nil {
return out, err
}
if installed {
out.Action = "unchanged"
out.Detail = "already installed"
return out, nil
}
if _, err := run(ctx, "pacman", "-S", "--noconfirm", "--needed", r.Package); err != nil {
return out, fmt.Errorf("installing %s: %w", r.Package, err)
}
// Read back. A package manager exiting zero says the transaction was accepted.
installed, err = packageInstalled(ctx, r.Package, run)
if err != nil {
return out, err
}
if !installed {
return out, fmt.Errorf(
"%s was installed without error and the package database does not have it", r.Package)
}
out.Action = "created"
return out, nil
}
// packageInstalled asks the package database, having first established that it answers.
//
// The two-step is the same trap `serviceState` documents. `pacman -Q name` exits non-zero for
// a package that is not installed AND for a package database that cannot be read, so believing
// the first answer would report a broken package manager as "nothing is installed" — absence
// read as fact. Proving the tool answers about something that certainly exists separates them.
func packageInstalled(ctx context.Context, name string, run Runner) (bool, error) {
if _, err := run(ctx, "pacman", "-Q", "pacman"); err != nil {
return false, fmt.Errorf(
"the package database does not answer on this machine, so nothing can be said "+
"about %q: %w", name, err)
}
if _, err := run(ctx, "pacman", "-Q", name); err != nil {
return false, nil
}
return true, nil
}
// Labels the host puts on every container it creates.
//
// specLabel carries a digest of the declaration that made the container. It is what lets a
// reconcile answer "is this container the one the current declaration describes" without
// comparing every field the runtime reports — which cannot be done reliably, because a runtime
// normalises, defaults and reorders what it is given, and the differences that produces are
// indistinguishable from real drift.
const (
specLabel = "mesh-host.spec"
idLabel = "mesh-host.id"
)
// containerSpec is the identity of a declared container: everything that, if changed, means
// the running container is no longer what was asked for.
func containerSpec(r declaration.Resource) string {
keys := make([]string, 0, len(r.Env))
for k := range r.Env {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
b.WriteString(r.Image + "\n" + r.Name + "\n")
for _, k := range keys {
b.WriteString("env " + k + "=" + r.Env[k] + "\n")
}
for _, p := range r.Ports {
b.WriteString("port " + p + "\n")
}
for _, v := range r.Volumes {
b.WriteString("volume " + v + "\n")
}
for _, a := range r.Args {
b.WriteString("arg " + a + "\n")
}
return fmt.Sprintf("%x", sha256.Sum256([]byte(b.String())))
}
// containerState reports whether a container is running and which spec made it.
// The error means the container does not exist.
func containerState(ctx context.Context, name string, run Runner) (state struct {
Running bool
Spec string
}, err error) {
out, err := run(ctx, "docker", "inspect", "--format",
"{{.State.Running}}\t{{index .Config.Labels \""+specLabel+"\"}}", name)
if err != nil {
return state, fmt.Errorf("no container named %s", name)
}
running, spec, _ := strings.Cut(strings.TrimSpace(out), "\t")
state.Running = running == "true"
state.Spec = strings.TrimSpace(spec)
return state, nil
}
// applyContainer makes the declared container the one that is running.
//
// There is no "update" for a container: a container's configuration is fixed when it is
// created, so any change is a replacement. Saying that plainly is better than a partial
// in-place update that leaves the running thing half-declared.
func applyContainer(ctx context.Context, r declaration.Resource, run Runner) (Outcome, error) {
out := Outcome{ID: r.ID, Type: string(r.Type), Target: r.Name}
want := containerSpec(r)
if _, err := run(ctx, "docker", "version", "--format", "{{.Server.Version}}"); err != nil {
return out, fmt.Errorf(
"the container runtime does not answer on this machine, so nothing can be said "+
"about %q: %w", r.Name, err)
}
before, err := containerState(ctx, r.Name, run)
existed := err == nil
switch {
case existed && before.Spec == want && before.Running:
out.Action = "unchanged"
return out, nil
case existed:
if _, err := run(ctx, "docker", "rm", "-f", r.Name); err != nil {
return out, fmt.Errorf("replacing container %s: %w", r.Name, err)
}
}
args := []string{"run", "--detach", "--name", r.Name, "--restart", "unless-stopped",
"--label", specLabel + "=" + want, "--label", idLabel + "=" + r.ID}
for _, k := range sortedKeys(r.Env) {
args = append(args, "--env", k+"="+r.Env[k])
}
for _, p := range r.Ports {
args = append(args, "--publish", p)
}
for _, v := range r.Volumes {
args = append(args, "--volume", v)
}
args = append(args, r.Image)
args = append(args, r.Args...)
if _, err := run(ctx, "docker", args...); err != nil {
return out, fmt.Errorf("starting container %s: %w", r.Name, err)
}
// Read back. `docker run --detach` returning an id says the container was created, not
// that it is still running — a container whose entrypoint exits immediately satisfies the
// command exactly as one that came up does.
after, err := containerState(ctx, r.Name, run)
if err != nil {
return out, fmt.Errorf("started container %s and it is not there: %w", r.Name, err)
}
if !after.Running {
return out, fmt.Errorf(
"container %s was started and is not running. It exited; ask the runtime for its "+
"logs", r.Name)
}
if after.Spec != want {
return out, fmt.Errorf("container %s is not the one that was declared after creating it", r.Name)
}
out.Action = "created"
if existed {
out.Action = "updated"
out.Detail = "replaced; a container's configuration is fixed when it is created"
}
return out, nil
}
func sortedKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// applyAction runs something the bundle declared, and never learns what it means.
//
// Verify does double duty, and that is the design rather than a convenience: it is both the
// idempotency check and the read-back. Running it first is how the host knows whether there is
// anything to do — it does not know what a database is, so "is the database there" is a
// question only the declaration can ask. Running it again afterwards is how the host knows the
// command had the effect it claimed (novox/hq ADR 0047).
func applyAction(ctx context.Context, r declaration.Resource, run Runner) (Outcome, error) {
out := Outcome{ID: r.ID, Type: string(r.Type), Target: strings.Join(r.Command, " ")}
if r.In != "" {
out.Target = "in " + r.In + ": " + out.Target
}
if _, err := runAction(ctx, r, r.Verify, run); err == nil {
out.Action = "unchanged"
out.Detail = "already true"
return out, nil
}
if _, err := runAction(ctx, r, r.Command, run); err != nil {
return out, fmt.Errorf("running the action: %w", err)
}
if _, err := runAction(ctx, r, r.Verify, run); err != nil {
return out, fmt.Errorf(
"the action ran without error and its own verify still fails: %w\n\n"+
"The command reported success and the thing it was for did not happen, which "+
"is exactly what verify exists to catch", err)
}
out.Action = "created"
out.Detail = "verify was false and is now true"
return out, nil
}
// runAction runs one of an action's command lines, on the machine or inside a container.
func runAction(ctx context.Context, r declaration.Resource, argv []string, run Runner) (string, error) {
if len(argv) == 0 {
return "", errors.New("no command")
}
if r.In != "" {
return run(ctx, "docker", append([]string{"exec", r.In}, argv...)...)
}
return run(ctx, argv[0], argv[1:]...)
}
+276 -2
View File
@@ -407,7 +407,281 @@ func TestForgettingAUnitThatIsGoneDoesNotStrandTheNode(t *testing.T) {
if _, still := state.Find("gone"); still {
t.Error("the host still believes it owns a unit that is gone")
}
if report.Outcomes[0].Action != "removed" {
t.Errorf("the vanished unit was not reported as removed: %+v", report.Outcomes)
// "forgotten", not "removed": the host stopped believing it owns the unit, and did not
// remove anything, because there was nothing there to remove. Reporting an effect it did
// not have would be the same class of untruth as reporting a package uninstalled.
if report.Outcomes[0].Action != "forgotten" {
t.Errorf("the vanished unit was not reported as forgotten: %+v", report.Outcomes)
}
}
// --- package, container and action (novox/hq 07-the-substrate.md, ADR 0046, ADR 0047) ---
func parseTrusted(t *testing.T, raw string) *declaration.Declaration {
t.Helper()
d, err := declaration.ParseTrusted([]byte(raw))
if err != nil {
t.Fatalf("fixture is not a valid declaration: %v", err)
}
return d
}
const pinned = "docker.io/library/postgres@sha256:" +
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
func TestABrokenPackageDatabaseIsNotReadAsNotInstalled(t *testing.T) {
// The same trap serviceState documents. `pacman -Q x` exits non-zero both for a package
// that is not installed and for a database that cannot be read — so believing the first
// answer would silently reinstall on a machine whose package manager is broken, or report
// "installed nothing" as success. The apply must fail instead.
run := func(ctx context.Context, name string, args ...string) (string, error) {
return "", errors.New("pacman: error: could not lock database")
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"package","package":"docker"}
]}`)
_, _, err := Apply(context.Background(), d, store.State{}, run, nil)
if err == nil {
t.Fatal("a broken package database was read as 'not installed'")
}
if !strings.Contains(err.Error(), "does not answer") {
t.Errorf("failed for the wrong reason: %v", err)
}
}
func TestAnInstalledPackageIsNotReinstalled(t *testing.T) {
var installed bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
if args[0] == "-S" {
installed = true
}
return "docker 27.0-1\n", nil // -Q succeeds for everything
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"package","package":"docker"}
]}`)
report, _, err := Apply(context.Background(), d, store.State{}, run, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if installed {
t.Error("a package that was already present was installed again")
}
if report.Changed() {
t.Errorf("an already-installed package reported a change: %+v", report.Outcomes)
}
}
func TestAPackageIsNeverUninstalled(t *testing.T) {
// Deliberate: the host cannot know what else needs the package. Uninstalling a container
// runtime because a declaration changed would stop every container on the node, and the
// machine may have had it before the mesh ever saw it. Undeclaring is not "remove it".
var uninstalled bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
if len(args) > 0 && (args[0] == "-R" || args[0] == "-Rs") {
uninstalled = true
}
return "", nil
}
known := store.State{Resources: []store.Applied{
{ID: "rt", Type: "package", Target: "docker"},
}}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"f","type":"file","path":"`+filepath.Join(t.TempDir(), "a")+`","content":"a\n"}
]}`)
report, state, err := Apply(context.Background(), d, known, run, nil)
if err != nil {
t.Fatalf("dropping a package stranded the apply: %v", err)
}
if uninstalled {
t.Fatal("the host uninstalled a package")
}
if _, still := state.Find("rt"); still {
t.Error("the host still believes it owns the package")
}
// "forgotten", not "removed" — the host must not claim an effect it declined to have.
if report.Outcomes[0].Action != "forgotten" {
t.Errorf("dropping a package was not reported as forgotten: %+v", report.Outcomes[0])
}
}
func TestAnActionThatIsAlreadyTrueDoesNotRun(t *testing.T) {
// Verify is the idempotency check as well as the read-back. The host does not know what a
// database is, so "is it already there" is a question only the declaration can ask.
var ran bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
if name == "create-db" {
ran = true
}
return "", nil // verify passes
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"db","type":"action","command":["create-db","mesh"],"verify":["has-db","mesh"]}
]}`)
report, _, err := Apply(context.Background(), d, store.State{}, run, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if ran {
t.Error("an action whose verify already passed was run anyway")
}
if report.Changed() {
t.Errorf("an already-satisfied action reported a change: %+v", report.Outcomes)
}
}
func TestAnActionThatSucceedsAndDoesNothingFails(t *testing.T) {
// The whole reason verify is mandatory: a command that exits zero and has no effect is
// this repository's most expensive failure shape. Here the command "succeeds" every time
// and verify never passes.
run := func(ctx context.Context, name string, args ...string) (string, error) {
if name == "has-db" {
return "", errors.New("no such database")
}
return "", nil
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"db","type":"action","command":["create-db","mesh"],"verify":["has-db","mesh"]}
]}`)
_, state, err := Apply(context.Background(), d, store.State{}, run, nil)
if err == nil {
t.Fatal("an action that reported success and did nothing was accepted")
}
if !strings.Contains(err.Error(), "verify still fails") {
t.Errorf("failed for the wrong reason: %v", err)
}
if _, recorded := state.Find("db"); recorded {
t.Error("an action that did not work was recorded as applied")
}
}
func TestAnActionRunsInsideTheContainerItNames(t *testing.T) {
// Steps 2 and 3 of the bootstrap act on something inside the store's container, before
// there is any mesh to ask.
var sawExec bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
if name == "docker" && args[0] == "exec" && args[1] == "store" {
sawExec = true
return "", nil
}
return "", errors.New("not run in the container")
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"db","type":"action","in":"store","command":["createdb","mesh"],"verify":["psql","-lqt"]}
]}`)
if _, _, err := Apply(context.Background(), d, store.State{}, run, nil); err != nil {
t.Fatalf("apply failed: %v", err)
}
if !sawExec {
t.Error("an action naming a container did not run inside it")
}
}
func TestAContainerThatExitsImmediatelyFailsTheApply(t *testing.T) {
// `docker run --detach` returning an id says the container was created, not that it is
// still running. A container whose entrypoint dies satisfies the command exactly as one
// that came up does — which is the read-back rule, in the place it matters most.
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch {
case args[0] == "version":
return "27.0\n", nil
case args[0] == "inspect":
return "false\t" + "", nil // exists, not running
case args[0] == "run":
return "deadbeef\n", nil
}
return "", nil
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"`+pinned+`"}
]}`)
_, state, err := Apply(context.Background(), d, store.State{}, run, nil)
if err == nil {
t.Fatal("a container that exited immediately was reported as applied")
}
if !strings.Contains(err.Error(), "is not running") {
t.Errorf("failed for the wrong reason: %v", err)
}
if _, recorded := state.Find("store"); recorded {
t.Error("a container that is not running was recorded as applied")
}
}
func TestAContainerWhoseDeclarationChangedIsReplaced(t *testing.T) {
// A container's configuration is fixed when it is created, so any change is a replacement.
// The spec label is what makes the difference visible without diffing everything the
// runtime reports — which cannot be done reliably, because a runtime normalises what it is
// given and that is indistinguishable from drift.
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"`+pinned+`","env":{"PGDATA":"/data"}}
]}`)
want := containerSpec(d.Resources[0])
var removed, created bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "version":
return "27.0\n", nil
case "inspect":
if created {
return "true\t" + want, nil
}
return "true\tsome-older-spec", nil
case "rm":
removed = true
return "", nil
case "run":
created = true
return "deadbeef\n", nil
}
return "", nil
}
report, _, err := Apply(context.Background(), d, store.State{}, run, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if !removed || !created {
t.Fatalf("a changed container was not replaced (removed=%v created=%v)", removed, created)
}
if report.Outcomes[0].Action != "updated" {
t.Errorf("a replacement was not reported as an update: %+v", report.Outcomes[0])
}
}
func TestAContainerThatMatchesIsLeftAlone(t *testing.T) {
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"`+pinned+`","env":{"PGDATA":"/data"}}
]}`)
spec := containerSpec(d.Resources[0])
var touched bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "version":
return "27.0\n", nil
case "inspect":
return "true\t" + spec, nil
}
touched = true
return "", nil
}
report, _, err := Apply(context.Background(), d, store.State{}, run, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if touched {
t.Error("a container that already matched was restarted")
}
if report.Changed() {
t.Errorf("a matching container reported a change: %+v", report.Outcomes)
}
}
+4 -1
View File
@@ -56,7 +56,10 @@ func Load() (*declaration.Declaration, error) {
if IsEmpty() {
return nil, ErrEmpty
}
return declaration.Parse(stripComments(substrate))
// ParseTrusted: the bundle arrives with the binary, so it may carry actions the link may
// not (novox/hq ADR 0047). The bootstrap needs them — creating the control plane's database
// happens before there is any mesh to ask for one.
return declaration.ParseTrusted(stripComments(substrate))
}
// stripComments removes whole-line `//` comments so a bundle can be annotated.
+147 -21
View File
@@ -26,13 +26,25 @@ const (
TypeDirectory Type = "directory"
TypeFile Type = "file"
TypeService Type = "service"
TypePackage Type = "package"
TypeContainer Type = "container"
TypeAction Type = "action"
)
// known is the whole vocabulary. Anything else is refused.
var known = map[Type]bool{
TypeDirectory: true,
TypeFile: true,
TypeService: true,
// uses names the fields each type consumes. A field set on a type that is not listed here as
// using it is refused.
//
// Stated as what each type USES rather than as what it ignores. The negative form needs every
// type revisited whenever a field is added, and the one nobody revisits is the one that
// silently accepts a field it will never read — which is the whole fault this package exists
// to prevent.
var uses = map[Type]map[string]bool{
TypeDirectory: {"path": true, "mode": true},
TypeFile: {"path": true, "content": true, "mode": true},
TypeService: {"unit": true, "state": true},
TypePackage: {"package": true},
TypeContainer: {"image": true, "name": true, "env": true, "ports": true, "volumes": true, "args": true},
TypeAction: {"command": true, "verify": true, "in": true},
}
// Resource is one thing that should be true of the machine.
@@ -54,6 +66,30 @@ type Resource struct {
// Unit and State, for a service. State is "running" or "stopped".
Unit string `json:"unit,omitempty"`
State string `json:"state,omitempty"`
// Package, for a package: the name this machine's own package manager knows it by.
Package string `json:"package,omitempty"`
// Image and Name, for a container. Image is pinned by digest (novox/hq ADR 0046) — a tag
// moves and a digest does not, and a bundle that pinned a tag would not be pinned.
Image string `json:"image,omitempty"`
Name string `json:"name,omitempty"`
// Env, Ports, Volumes and Args, for a container. Literal; the host renders nothing.
Env map[string]string `json:"env,omitempty"`
Ports []string `json:"ports,omitempty"`
Volumes []string `json:"volumes,omitempty"`
Args []string `json:"args,omitempty"`
// Command, Verify and In, for an action.
//
// Verify is not optional and is not a courtesy. An action that runs and reports success
// without reading anything back is the fault this repository exists to name, and an action
// is the easiest place in the vocabulary to reintroduce it (novox/hq ADR 0047).
Command []string `json:"command,omitempty"`
Verify []string `json:"verify,omitempty"`
// In names a container to run the action inside, when the thing being acted on lives
// there. Empty means the machine itself.
In string `json:"in,omitempty"`
}
// Declaration is what a machine should be, in the order it should be made so.
@@ -85,8 +121,20 @@ func (e *RefusalError) Error() string {
strings.Join(e.Problems, "\n - "))
}
// Parse reads a declaration and refuses anything it does not fully understand.
func Parse(raw []byte) (*Declaration, error) {
// Parse reads a declaration that arrived over the link, and refuses anything it does not fully
// understand — including any action, which the link may not carry (novox/hq ADR 0047).
func Parse(raw []byte) (*Declaration, error) { return parse(raw, false) }
// ParseTrusted reads a declaration from a source already as privileged as the host itself: the
// bundle it carries, or a file handed to it by someone who is running it as root.
//
// Actions are permitted here and nowhere else. The asymmetry is deliberate and is the entire
// content of ADR 0047: refusing actions from the bundle buys nothing, because whoever built the
// bundle built the binary; refusing them from the link buys the bound on what a compromised
// control plane can express.
func ParseTrusted(raw []byte) (*Declaration, error) { return parse(raw, true) }
func parse(raw []byte, allowActions bool) (*Declaration, error) {
// DisallowUnknownFields is the whole point rather than strictness for its own sake: a
// field the host does not know is a thing the control plane believes it asked for.
dec := json.NewDecoder(bytes.NewReader(raw))
@@ -97,13 +145,13 @@ func Parse(raw []byte) (*Declaration, error) {
return nil, &RefusalError{Problems: []string{"not a declaration: " + err.Error()}}
}
if problems := validate(&d); len(problems) > 0 {
if problems := validate(&d, allowActions); len(problems) > 0 {
return nil, &RefusalError{Problems: problems}
}
return &d, nil
}
func validate(d *Declaration) []string {
func validate(d *Declaration, allowActions bool) []string {
var problems []string
if d.Version != Version {
@@ -138,32 +186,31 @@ func validate(d *Declaration) []string {
seen[r.ID] = i
}
if !known[r.Type] {
if _, ok := uses[r.Type]; !ok {
problems = append(problems, fmt.Sprintf(
"%s: unknown type %q. This host understands %s", where, r.Type, vocabulary()))
continue
}
problems = append(problems, validateResource(where, r)...)
problems = append(problems, validateResource(where, r, allowActions)...)
}
return problems
}
func validateResource(where string, r Resource) []string {
var problems []string
func validateResource(where string, r Resource, allowActions bool) []string {
problems := unusedBy(where, r)
switch r.Type {
case TypeDirectory:
if r.Path == "" {
problems = append(problems, where+": a directory needs a path")
}
problems = append(problems, checkMode(where, r.Mode)...)
problems = append(problems, unusedBy(where, r, "unit", r.Unit, "state", r.State, "content", r.Content)...)
case TypeFile:
if r.Path == "" {
problems = append(problems, where+": a file needs a path")
}
problems = append(problems, checkMode(where, r.Mode)...)
problems = append(problems, unusedBy(where, r, "unit", r.Unit, "state", r.State)...)
case TypeService:
if r.Unit == "" {
@@ -173,22 +220,101 @@ func validateResource(where string, r Resource) []string {
problems = append(problems, fmt.Sprintf(
"%s: state %q; a service is \"running\" or \"stopped\"", where, r.State))
}
problems = append(problems, unusedBy(where, r, "path", r.Path, "content", r.Content, "mode", r.Mode)...)
case TypePackage:
if r.Package == "" {
problems = append(problems, where+": a package needs a package name")
}
case TypeContainer:
if r.Name == "" {
problems = append(problems, where+": a container needs a name")
}
problems = append(problems, checkImage(where, r.Image)...)
case TypeAction:
// The whole reason an action is bounded rather than forbidden (novox/hq ADR 0047).
if !allowActions {
problems = append(problems, where+
": an action arrived over the link, and the link may not carry one. The host "+
"applies declarations of known shape; a command to run is not one. A bundle "+
"may carry an action because it arrives with the binary — anyone able to put "+
"a hostile action there could have put it in the host itself")
break
}
if len(r.Command) == 0 {
problems = append(problems, where+": an action needs a command")
}
if len(r.Verify) == 0 {
problems = append(problems, where+
": an action needs a verify. An action that runs and reports success without "+
"reading anything back is the fault this host exists to prevent, and verify is "+
"also how the host knows whether the action is already done")
}
}
return problems
}
// checkImage insists on a digest.
//
// A tag moves and a digest does not. The bundle's whole claim is that what it names is exact
// (novox/hq ADR 0046), and a bundle pinning `postgres:17` pins nothing — it names whatever
// that tag points at on the day the host happens to run.
func checkImage(where, image string) []string {
if image == "" {
return []string{where + ": a container needs an image"}
}
name, digest, found := strings.Cut(image, "@")
if !found || name == "" {
return []string{fmt.Sprintf(
"%s: image %q is not pinned. Write it as name@sha256:... — a tag moves, and a "+
"bundle that pinned a tag would not be pinned", where, image)}
}
if !strings.HasPrefix(digest, "sha256:") || len(digest) != len("sha256:")+64 {
return []string{fmt.Sprintf(
"%s: image digest %q is not a sha256 digest", where, digest)}
}
return nil
}
// setFields names every field carried on this resource, other than its identity and type.
func setFields(r Resource) []string {
var set []string
add := func(name string, populated bool) {
if populated {
set = append(set, name)
}
}
add("path", r.Path != "")
add("content", r.Content != "")
add("mode", r.Mode != "")
add("unit", r.Unit != "")
add("state", r.State != "")
add("package", r.Package != "")
add("image", r.Image != "")
add("name", r.Name != "")
add("env", len(r.Env) > 0)
add("ports", len(r.Ports) > 0)
add("volumes", len(r.Volumes) > 0)
add("args", len(r.Args) > 0)
add("command", len(r.Command) > 0)
add("verify", len(r.Verify) > 0)
add("in", r.In != "")
sort.Strings(set)
return set
}
// unusedBy refuses a field this type does not use.
//
// A field set and ignored is the fault this package exists to prevent, in miniature: the
// control plane believes it asked for something the host will never do.
func unusedBy(where string, r Resource, pairs ...string) []string {
func unusedBy(where string, r Resource) []string {
var problems []string
for i := 0; i+1 < len(pairs); i += 2 {
if pairs[i+1] != "" {
for _, name := range setFields(r) {
if !uses[r.Type][name] {
problems = append(problems, fmt.Sprintf(
"%s: a %s does not use %q, and it is set. Refused rather than ignored",
where, r.Type, pairs[i]))
where, r.Type, name))
}
}
return problems
@@ -213,7 +339,7 @@ func checkMode(where, mode string) []string {
func vocabulary() string {
var names []string
for t := range known {
for t := range uses {
names = append(names, string(t))
}
sort.Strings(names)
+100
View File
@@ -156,3 +156,103 @@ func TestARefusalSaysNothingWasApplied(t *testing.T) {
func TestAnEmptyDeclarationIsAMistake(t *testing.T) {
refusalFor(t, `{"declaration":1,"resources":[]}`)
}
// --- the vocabulary the substrate bootstrap needs (novox/hq 07-the-substrate.md) ---
func TestAnActionOverTheLinkIsRefused(t *testing.T) {
// novox/hq ADR 0047. The link may push declarations of known shape and never a command to
// run. This is the boundary the whole security argument rests on, so it is asserted
// directly rather than inferred from the type list.
raw := []byte(`{"declaration":1,"resources":[
{"id":"schema","type":"action","command":["psql","-f","x.sql"],"verify":["psql","-c","select 1"]}
]}`)
if _, err := Parse(raw); err == nil {
t.Fatal("an action arriving over the link was accepted")
} else if !strings.Contains(err.Error(), "the link may not carry one") {
t.Errorf("refused for the wrong reason: %v", err)
}
// And the same bytes from the bundle are fine — the asymmetry IS the decision.
if _, err := ParseTrusted(raw); err != nil {
t.Errorf("the bundle may carry an action, and this one was refused: %v", err)
}
}
func TestAnActionWithoutVerifyIsRefused(t *testing.T) {
// An action that runs and reports success without reading anything back is the fault this
// host exists to prevent. Verify is also the idempotency check, so an action without one
// cannot be applied twice safely either.
_, err := ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"schema","type":"action","command":["psql","-f","x.sql"]}
]}`))
if err == nil {
t.Fatal("an action with no verify was accepted")
}
if !strings.Contains(err.Error(), "needs a verify") {
t.Errorf("refused for the wrong reason: %v", err)
}
}
func TestAnImageMustBePinnedByDigest(t *testing.T) {
// novox/hq ADR 0046: reproducibility comes from pinning the identity of a thing. A bundle
// naming a tag pins nothing — it names whatever that tag points at on the day it runs.
for _, image := range []string{
"postgres:17",
"postgres",
"postgres@sha256:short",
"@sha256:0000000000000000000000000000000000000000000000000000000000000000",
} {
_, err := ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"` + image + `"}
]}`))
if err == nil {
t.Errorf("image %q was accepted and is not pinned", image)
}
}
good := "postgres@sha256:" + strings.Repeat("a", 64)
if _, err := ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"` + good + `"}
]}`)); err != nil {
t.Errorf("a properly pinned image was refused: %v", err)
}
}
func TestAFieldTheNewTypesDoNotUseIsRefused(t *testing.T) {
// The field-set check must cover the types added last, not only the three it was written
// for. A package that carries a `content` is a control plane believing it asked for
// something that will never happen.
for _, body := range []string{
`{"id":"p","type":"package","package":"docker","content":"x"}`,
`{"id":"p","type":"package","package":"docker","image":"x"}`,
`{"id":"c","type":"container","name":"n","image":"i@sha256:` + strings.Repeat("a", 64) + `","unit":"x.service"}`,
`{"id":"a","type":"action","command":["x"],"verify":["y"],"path":"/tmp/x"}`,
} {
_, err := ParseTrusted([]byte(`{"declaration":1,"resources":[` + body + `]}`))
if err == nil {
t.Errorf("a resource carrying a field its type does not use was accepted: %s", body)
continue
}
if !strings.Contains(err.Error(), "Refused rather than ignored") {
t.Errorf("refused for the wrong reason: %v", err)
}
}
}
func TestTheVocabularyIsTheSixShapesTheBootstrapNeeds(t *testing.T) {
// novox/hq 07-the-substrate.md names six shapes and the bootstrap uses all of them.
// Asserted so that removing one is a failing test rather than a discovery during a
// first-node install.
for _, want := range []Type{
TypeDirectory, TypeFile, TypeService, TypePackage, TypeContainer, TypeAction,
} {
if _, ok := uses[want]; !ok {
t.Errorf("the host no longer speaks %q", want)
}
}
if len(uses) != 6 {
t.Errorf("the vocabulary is %d shapes; every addition widens what a compromised "+
"control plane can express, so a change here is a decision: %s", len(uses), vocabulary())
}
}