Let genesis assign the build agent before the package registry exists (hq issue 121, ADR 0336)
mesh/merge-gate head e0bc919d4945 merged into trunk f88e3452f8b8 — pass: builds build-agent → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head e0bc919d4945 merged into trunk f88e3452f8b8 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery-group group fix/121-genesis-package-registry delivering: 2 of 4 delivered
mesh/delivery held for a person: its group fix/121-genesis-package-registry's composed check did not pass for the heads that merged; a person decides tha…
mesh/merge-gate head e0bc919d4945 merged into trunk f88e3452f8b8 — pass: builds build-agent → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head e0bc919d4945 merged into trunk f88e3452f8b8 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery-group group fix/121-genesis-package-registry delivering: 2 of 4 delivered
mesh/delivery held for a person: its group fix/121-genesis-package-registry's composed check did not pass for the heads that merged; a person decides tha…
build-agent's npm-package-registry becomes optional, so genesis's step 13 is no longer refused with 'nothing provides npm-package-registry'. Until gitea is assigned, the agent on genesis's node reads the genesis-package-registry own-secret the installer seals at the packages step; it is issued outside the mesh, so no other machine is given one. The binding and its credential are composed only while bound (ADR 0330 §6), and no file names them otherwise.
This commit is contained in:
@@ -7,9 +7,23 @@ machine that is off builds nothing and blocks nothing. The controller asks the r
|
||||
the outcome names the machine that built it.
|
||||
|
||||
What a holding machine needs is what the builder always needed, said here once: a container runtime
|
||||
(the socket is mounted), the artifact store and the package registry as provisions, a workspace, and
|
||||
the bus credential. The code is `cmd/mesh-builder` in the mesh-controller repository, compiled from
|
||||
(the socket is mounted), the artifact store as a provision, a workspace, and the bus credential. The code is `cmd/mesh-builder` in the mesh-controller repository, compiled from
|
||||
that repository's main (`build.artifacts[].context`); this module ships the packaging.
|
||||
|
||||
Assign it to every machine with a container runtime. It replaces `builder`, the one-holder form of the
|
||||
same thing; retire that once this is assigned where it was.
|
||||
|
||||
## The package registry (novox/hq ADR 0336)
|
||||
|
||||
The package registry (`npm-package-registry`, provided by the gitea module) is an optional requirement.
|
||||
Genesis assigns the build agent before anything provides it, so the agent is assignable on a mesh with no
|
||||
registry, and `mesh-controller.plan` and `node` list the requirement as unbound until the gitea module is
|
||||
assigned; it then binds with no further act.
|
||||
|
||||
Until it binds, the agent on genesis's node reads the `genesis-package-registry` own-secret, which the
|
||||
installer seals to it at the packages step: the forge it raised, the port, the npm path, the account
|
||||
`mesh-builder` and its password. Issued outside the mesh, so no other machine is given one. A real binding
|
||||
wins over it, and each build's log says which the build resolved from. A build that needs the registry (a
|
||||
`package` artifact, an image whose Dockerfile names `.npmrc`, a TypeScript artifact with an `@novox/`
|
||||
dependency) on an agent with neither fails before building, naming the machine. Go artifacts and images
|
||||
that do not name `.npmrc` build without it. `manifest_test.go` holds the manifest to this.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module build-agent
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,117 @@
|
||||
// The build agent's manifest (novox/hq ADR 0336, issue 121). The build agent is assigned at genesis's
|
||||
// step 13, before anything provides the package registry, so that requirement is optional
|
||||
// (ADR 0330); until it binds, the agent on genesis's node reads the genesis document the installer
|
||||
// seals to it at the packages step. The program is mesh-controller's cmd/mesh-builder; only the
|
||||
// packaging lives here, and this is its test.
|
||||
package buildagent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type manifest struct {
|
||||
Requires []json.RawMessage `json:"requires"`
|
||||
Binds map[string]string `json:"binds"`
|
||||
Secrets map[string]string `json:"secrets"`
|
||||
OwnSecrets map[string]json.RawMessage `json:"own-secrets"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
}
|
||||
|
||||
func read(t *testing.T) manifest {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("module.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m manifest
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// requirement is one entry of `requires`: its name, and whether it is optional.
|
||||
func requirements(t *testing.T, m manifest) map[string]bool {
|
||||
t.Helper()
|
||||
out := map[string]bool{}
|
||||
for _, raw := range m.Requires {
|
||||
var name string
|
||||
if json.Unmarshal(raw, &name) == nil {
|
||||
out[name] = false
|
||||
continue
|
||||
}
|
||||
var long struct {
|
||||
Name string `json:"name"`
|
||||
Optional bool `json:"optional"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &long); err != nil {
|
||||
t.Fatalf("requires: %s: %v", raw, err)
|
||||
}
|
||||
out[long.Name] = long.Optional
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestThePackageRegistryIsOptionalAndTheArtifactStoreStaysHard(t *testing.T) {
|
||||
r := requirements(t, read(t))
|
||||
optional, has := r["npm-package-registry"]
|
||||
if !has || !optional {
|
||||
t.Errorf("npm-package-registry is not an optional requirement (ADR 0336 §1): %v", r)
|
||||
}
|
||||
if optional, has := r["artifact-store"]; !has || optional {
|
||||
t.Errorf("artifact-store is not a hard requirement: %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
// The binding and the credential of the registry are delivered only while it is bound: ADR 0330 §6
|
||||
// composes neither for an absent requirement, so no file of the module may name them through a
|
||||
// placeholder unless it is itself only-when-bound.
|
||||
func TestTheRegistrysBindsAndSecretsAreOnlyWhenBound(t *testing.T) {
|
||||
m := read(t)
|
||||
if m.Binds["npm-package-registry"] == "" || m.Secrets["npm-package-registry"] == "" {
|
||||
t.Fatalf("the agent is not told where the registry is once it binds: binds %v, secrets %v", m.Binds, m.Secrets)
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
content, _ := r["content"].(string)
|
||||
if strings.Contains(content, "${bound:npm-package-registry:") || strings.Contains(content, "${secret:npm-package-registry}") {
|
||||
if r["only-when-bound"] != "npm-package-registry" {
|
||||
t.Errorf("%v names the registry's binding and is delivered while it is absent", r["id"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The genesis document is an own-secret issued outside the mesh — by the installer at genesis's packages
|
||||
// step (ADR 0336 §3) — so the mesh never makes one of its own on any other machine, and the agent is
|
||||
// told where it lands.
|
||||
func TestTheGenesisDocumentIsDeclaredAndIssuedOutside(t *testing.T) {
|
||||
m := read(t)
|
||||
raw, has := m.OwnSecrets["genesis-package-registry"]
|
||||
if !has {
|
||||
t.Fatalf("the build agent declares no genesis-package-registry own-secret: %v", m.OwnSecrets)
|
||||
}
|
||||
var own struct {
|
||||
Path string `json:"path"`
|
||||
IssuedBy string `json:"issued-by"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &own); err != nil || own.IssuedBy != "outside" {
|
||||
t.Fatalf("the genesis document is not issued outside the mesh: %s (%v)", raw, err)
|
||||
}
|
||||
if !strings.HasPrefix(own.Path, "${dir:mesh-state}/") {
|
||||
t.Fatalf("the genesis document is not where the container reads /run/mesh: %s", own.Path)
|
||||
}
|
||||
inside := "/run/mesh/" + strings.TrimPrefix(own.Path, "${dir:mesh-state}/")
|
||||
for _, r := range m.Resources {
|
||||
if r["id"] == "agent-env" {
|
||||
content, _ := r["content"].(string)
|
||||
if !strings.Contains(content, "MESH_GENESIS_PACKAGE_REGISTRY="+inside+"\n") {
|
||||
t.Errorf("the agent is not told where the genesis document is (%s):\n%s", inside, content)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("the build agent has no environment file")
|
||||
}
|
||||
@@ -14,7 +14,10 @@
|
||||
],
|
||||
"requires": [
|
||||
"artifact-store",
|
||||
"npm-package-registry"
|
||||
{
|
||||
"name": "npm-package-registry",
|
||||
"optional": true
|
||||
}
|
||||
],
|
||||
"binds": {
|
||||
"npm-package-registry": "${dir:mesh-state}/package-registry.json"
|
||||
@@ -23,7 +26,11 @@
|
||||
"npm-package-registry": "${dir:mesh-state}/package-registry.secret"
|
||||
},
|
||||
"own-secrets": {
|
||||
"broker": "${dir:mesh-state}/broker"
|
||||
"broker": "${dir:mesh-state}/broker",
|
||||
"genesis-package-registry": {
|
||||
"path": "${dir:mesh-state}/genesis-package-registry.json",
|
||||
"issued-by": "outside"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"own": [
|
||||
@@ -52,7 +59,7 @@
|
||||
"type": "file",
|
||||
"path": "${dir:mesh-state}/build-agent.env",
|
||||
"mode": "0600",
|
||||
"content": "MESH_BROKER_FILE=/run/mesh/broker\nMESH_NODE=${machine:name}\nMESH_REGISTRY=${bound:artifact-store:at}:${bound:artifact-store:port}\nMESH_PACKAGE_BINDING=/run/mesh/package-registry.json\nMESH_NPM_TOKEN_FILE=/run/mesh/package-registry.secret\nMESH_WORKSPACE=${dir:workspace}\n"
|
||||
"content": "MESH_BROKER_FILE=/run/mesh/broker\nMESH_NODE=${machine:name}\nMESH_REGISTRY=${bound:artifact-store:at}:${bound:artifact-store:port}\nMESH_PACKAGE_BINDING=/run/mesh/package-registry.json\nMESH_NPM_TOKEN_FILE=/run/mesh/package-registry.secret\nMESH_GENESIS_PACKAGE_REGISTRY=/run/mesh/genesis-package-registry.json\nMESH_WORKSPACE=${dir:workspace}\n"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
|
||||
Reference in New Issue
Block a user