The packages port given at genesis is a module's setting, like every other
Every foundation port given at genesis became a per-node setting of the module
that binds it, except the package registry's: that one was fixed by rewriting
the builder's manifest when the installer registered it. Registering the builder
again from the catalogue undid it, and the forge's own module, when it took the
bootstrap forge over, came up on the catalogue's port — which on a machine where
a predecessor holds 3000 points the builder at the predecessor's forge.
So the rewrite is gone, and the port is recorded twice as a setting, both from
the one input:
- the forge's module is registered at genesis — not assigned, nothing of it runs
— so the controller has something to hold `{"ports": {"3000": <given>}}`
against. Assigning the forge later raises it on the port this machine was
given, and its container, its filter rule, its opening, what it serves and
what consumers are told all read it from there.
- the builder is given `{"serves": {"port": <given>}}`, which merges into the
binding it carries in place of one nothing can resolve yet.
A genesis on the catalogue's port records nothing and registers nothing, so it
does exactly what it did before.
novox/hq 04-ISSUES/085, ADR 0100
This commit is contained in:
@@ -98,8 +98,8 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh
|
||||
everything this mesh will ever run
|
||||
--source-path the module's directory inside that repository, if not its root
|
||||
--catalog a checkout of the mesh's catalogue, holding the registry's, the
|
||||
control plane's and the builder's manifests. Without it this stops
|
||||
after step 6
|
||||
control plane's, the builder's and — when a packages port is given
|
||||
— the forge's manifests. Without it this stops after step 6
|
||||
--node the name this machine is known by (default: its hostname)
|
||||
--registry where this mesh keeps its own images (default ` + defaultRegistry + `)
|
||||
every node pulls the control plane from this, so on a mesh of more
|
||||
@@ -279,7 +279,7 @@ func newFlagSet(opts *bootstrap.Options, jsonOut *bool) *flag.FlagSet {
|
||||
set.StringVar(&opts.Out, "out", opts.Out, "where the produced bundle is written")
|
||||
set.StringVar(&opts.State, "state", opts.State, "where this node records what it has applied")
|
||||
set.StringVar(&opts.Catalogue, "catalog", opts.Catalogue,
|
||||
"a checkout of the mesh's catalogue, for the registry's and the builder's manifests and what phase two installs; without it this stops after the foundation")
|
||||
"a checkout of the mesh's catalogue, for the registry's, the builder's and the forge's manifests and what phase two installs; without it this stops after the foundation")
|
||||
set.StringVar(&opts.Source.Repository, "source", opts.Source.Repository,
|
||||
"the repository the control plane is built from, on a mesh that already exists")
|
||||
set.StringVar(&opts.Source.Ref, "source-ref", opts.Source.Ref,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -55,9 +53,6 @@ func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane
|
||||
"This is the manifest that makes the builder an ordinary module. Without it the mesh "+
|
||||
"has the image and no way to run it, so nothing can be built here", err)
|
||||
}
|
||||
if manifest, err = followPackagesPort(manifest, o.Ports.orDefaults().Packages); err != nil {
|
||||
return out, err
|
||||
}
|
||||
pinned, places, err := pinPlaceholder(manifest, published.Reference, BuilderModule)
|
||||
if err != nil {
|
||||
return out, err
|
||||
@@ -89,25 +84,3 @@ func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane
|
||||
out.Installed.Pushed, err = pushNode(ctx, o, control, say)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// packagesPortInBinding is the package registry's port as the builder's manifest names it, in the
|
||||
// binding file it carries — JSON inside a JSON string, so its quotes are escaped.
|
||||
const packagesPortInBinding = `\"port\": 3000`
|
||||
|
||||
// followPackagesPort points the builder's package binding at the port the node gave the package
|
||||
// registry (novox/hq ADR 0100). Genesis raises the registry by hand before gitea is a module, so no
|
||||
// binding the controller resolves can say where it is; the builder carries the address in its own
|
||||
// manifest, and a port given at genesis must reach it there or the base build dials a port nothing
|
||||
// answers on. At the default it is left byte for byte as the catalogue has it.
|
||||
func followPackagesPort(manifest []byte, port int) ([]byte, error) {
|
||||
if port == defaultGiteaPort {
|
||||
return manifest, nil
|
||||
}
|
||||
if n := bytes.Count(manifest, []byte(packagesPortInBinding)); n != 1 {
|
||||
return nil, fmt.Errorf("the builder's manifest names the package registry's port %d time(s) where "+
|
||||
"this installer looks for it once (%s), so the port given with --packages-port cannot reach "+
|
||||
"it; nothing was changed", n, packagesPortInBinding)
|
||||
}
|
||||
return bytes.Replace(manifest, []byte(packagesPortInBinding),
|
||||
[]byte(`\"port\": `+strconv.Itoa(port)), 1), nil
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0100: a port given for the package registry at genesis reaches the one
|
||||
// thing that dials it by a fixed number, the builder's package binding.
|
||||
// Defends novox/hq 04-ISSUES/085: the port given for the package registry at genesis is a setting
|
||||
// of a module and not text in a manifest, so registering that manifest again does not put the
|
||||
// catalogue's number back.
|
||||
|
||||
// The builder's package binding exactly as the catalogue's manifest carries it.
|
||||
const builderManifest = `{
|
||||
// theBuildersBinding is the resource the builder carries in place of a binding nothing can resolve
|
||||
// yet, exactly as the catalogue's manifest has it — settable, with the port as its only default.
|
||||
const theBuildersBinding = `{
|
||||
"module": "builder",
|
||||
"resources": [
|
||||
{
|
||||
@@ -18,52 +23,113 @@ const builderManifest = `{
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/builder/package-registry.json",
|
||||
"mode": "0600",
|
||||
"merge": "json",
|
||||
"protected": ["provision", "from", "as"],
|
||||
"content": "{\"provision\": \"package-registry\", \"from\": \"gitea\", \"at\": \"127.0.0.1\", \"as\": \"mesh-builder\", \"serves\": {\"scheme\": \"http\", \"port\": 3000, \"npm-path\": \"/api/packages/novox/npm/\"}}\n"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
func bindingPort(t *testing.T, manifest []byte) float64 {
|
||||
func recording(t *testing.T) (*controlRecorder, controlPlane) {
|
||||
t.Helper()
|
||||
var m struct {
|
||||
Resources []struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"resources"`
|
||||
}
|
||||
if err := json.Unmarshal(manifest, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var binding struct {
|
||||
Serves struct {
|
||||
Port float64 `json:"port"`
|
||||
} `json:"serves"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(m.Resources[0].Content), &binding); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return binding.Serves.Port
|
||||
t.Setenv("TMPDIR", t.TempDir())
|
||||
c := &controlRecorder{settings: map[string]string{}}
|
||||
return c, controlPlane{container: "temp-mesh-controller", run: c.run, timeout: time.Second}
|
||||
}
|
||||
|
||||
func TestTheBuilderFollowsThePackageRegistrysGivenPort(t *testing.T) {
|
||||
got, err := followPackagesPort([]byte(builderManifest), 3100)
|
||||
if err != nil {
|
||||
func TestTheBuildersPackageRegistryPortIsASettingAndNotTheManifest(t *testing.T) {
|
||||
c, control := recording(t)
|
||||
o := Options{Node: "anchor", Ports: FoundationPorts{Packages: 3100}, Wait: time.Second}
|
||||
if _, err := installModule(context.Background(), o, control, BuilderModule,
|
||||
[]byte(theBuildersBinding), quietly); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p := bindingPort(t, got); p != 3100 {
|
||||
t.Errorf("the builder's binding dials %v, not the port given", p)
|
||||
if got := c.settings["builder-settings.json"]; got != `{"serves":{"port":3100}}` {
|
||||
t.Errorf("the builder was told %q about the package registry's port", got)
|
||||
}
|
||||
// The manifest reaches the mesh as the catalogue wrote it. A port rewritten into it here is a
|
||||
// port the next registration from the catalogue silently takes back.
|
||||
if got := c.settings["builder-module.json"]; got != theBuildersBinding {
|
||||
t.Errorf("the installer changed the builder's manifest:\n%s", got)
|
||||
}
|
||||
set, push := c.index("settings set builder"), c.index("push anchor")
|
||||
if set < 0 || push < 0 || set > push {
|
||||
t.Fatalf("the port was not set before the push that raises the builder: %v", c.told)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheBuilderOnTheDefaultPortIsUnchanged(t *testing.T) {
|
||||
got, err := followPackagesPort([]byte(builderManifest), 3000)
|
||||
if err != nil || string(got) != builderManifest {
|
||||
t.Errorf("the default port changed the manifest: %v", err)
|
||||
func TestTheBuilderOnTheDefaultPackagesPortIsToldNothing(t *testing.T) {
|
||||
c, control := recording(t)
|
||||
o := Options{Node: "anchor", Wait: time.Second}
|
||||
if _, err := installModule(context.Background(), o, control, BuilderModule,
|
||||
[]byte(theBuildersBinding), quietly); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.index("settings") >= 0 {
|
||||
t.Errorf("a genesis on the catalogue's packages port set a setting: %v", c.told)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuilderManifestThatNoLongerNamesThePortIsRefused(t *testing.T) {
|
||||
moved := strings.Replace(builderManifest, `\"port\": 3000`, `\"port\": 3001`, 1)
|
||||
if _, err := followPackagesPort([]byte(moved), 3100); err == nil || !strings.Contains(err.Error(), "--packages-port") {
|
||||
t.Errorf("a manifest the port cannot reach was accepted: %v", err)
|
||||
// aCatalogueWith writes a checkout holding one module's manifest, which is all the installer reads
|
||||
// a catalogue for.
|
||||
func aCatalogueWith(t *testing.T, module, manifest string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
at := filepath.Join(dir, catalogueDir, module)
|
||||
if err := os.MkdirAll(at, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(at, "module.json"), []byte(manifest), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
const theForgesManifest = `{"module": "gitea", "version": "1"}`
|
||||
|
||||
func TestThePackagesPortIsTheForgeModulesSettingOnThisNode(t *testing.T) {
|
||||
c, control := recording(t)
|
||||
o := Options{Node: "anchor", Catalogue: aCatalogueWith(t, ForgeModule, theForgesManifest),
|
||||
Ports: FoundationPorts{Packages: 3100}, Wait: time.Second}
|
||||
if err := recordTheForgesPort(context.Background(), o, control, quietly); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := c.settings["gitea-settings.json"]; got != `{"ports":{"3000":3100}}` {
|
||||
t.Errorf("the forge module was told %q about its port", got)
|
||||
}
|
||||
// Registered so there is something to hold the setting against, and never assigned: the forge
|
||||
// module cannot run until the base it stands on has been built.
|
||||
add, set := c.index("module add"), c.index("settings set gitea")
|
||||
if add < 0 || set < 0 || add > set {
|
||||
t.Fatalf("the forge's setting was recorded against a module the mesh does not know: %v", c.told)
|
||||
}
|
||||
if c.index("assign") >= 0 {
|
||||
t.Errorf("genesis assigned the forge module: %v", c.told)
|
||||
}
|
||||
if !strings.Contains(c.told[set], "--node anchor") {
|
||||
t.Errorf("the forge's port was set for the whole mesh, not this machine: %s", c.told[set])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAGenesisOnTheCataloguesPackagesPortRegistersNoForge(t *testing.T) {
|
||||
c, control := recording(t)
|
||||
o := Options{Node: "anchor", Catalogue: aCatalogueWith(t, ForgeModule, theForgesManifest),
|
||||
Wait: time.Second}
|
||||
if err := recordTheForgesPort(context.Background(), o, control, quietly); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(c.told) != 0 {
|
||||
t.Errorf("a genesis on the defaults told the mesh something new: %v", c.told)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAForgeManifestTheCatalogueDoesNotHaveIsNamed(t *testing.T) {
|
||||
_, control := recording(t)
|
||||
o := Options{Node: "anchor", Catalogue: t.TempDir(),
|
||||
Ports: FoundationPorts{Packages: 3100}, Wait: time.Second}
|
||||
err := recordTheForgesPort(context.Background(), o, control, quietly)
|
||||
if err == nil || !strings.Contains(err.Error(), ForgeModule) ||
|
||||
!strings.Contains(err.Error(), "package registry") {
|
||||
t.Errorf("a missing forge manifest was not explained: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,16 @@ const (
|
||||
defaultGiteaPort = 3000
|
||||
)
|
||||
|
||||
// ForgeModule is the module that owns the package registry — the one the bootstrap forge above is
|
||||
// a stand-in for, and which takes it over once the base exists.
|
||||
//
|
||||
// Named here because the port given for the package registry is that module's setting on this node
|
||||
// and not this installer's private number (novox/hq 04-ISSUES/085): the forge's own container, its
|
||||
// filter rule, its opening, what it says it serves and what consumers are told all read it from
|
||||
// there, so taking the bootstrap forge over does not move the registry back to the catalogue's
|
||||
// port.
|
||||
const ForgeModule = "gitea"
|
||||
|
||||
// RaisePackageRegistry puts a working npm registry in front of the base build. It is idempotent:
|
||||
// every step tolerates having been done, because genesis is safe to run again.
|
||||
func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control controlPlane,
|
||||
@@ -100,6 +110,11 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro
|
||||
return err
|
||||
}
|
||||
|
||||
say(" recording the port it was given as the forge module's own")
|
||||
if err := recordTheForgesPort(ctx, o, control, say); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
say(" delivering the builder its registry credential")
|
||||
if err := deliverBuilderNpm(ctx, o, control, builderPassword, say); err != nil {
|
||||
return err
|
||||
@@ -108,6 +123,43 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordTheForgesPort makes the port given for the package registry a setting of the module that
|
||||
// serves it, on this node (novox/hq 04-ISSUES/085, ADR 0100).
|
||||
//
|
||||
// **The forge is the one foundation port that was not a setting.** The store's, the bus's, the
|
||||
// broker's management port and the registry's each become a `ports` setting of the module that
|
||||
// binds them, set where that module is registered and assigned; the forge is raised by hand here,
|
||||
// long before its module can be built, so there was no registration to hang it on and the number
|
||||
// lived in rewritten manifest text instead. So the manifest is registered here — not assigned, and
|
||||
// nothing of it runs — for the one thing registering buys: the controller will hold a setting
|
||||
// against it. Whenever somebody later assigns the forge on this node, it comes up on the port this
|
||||
// machine was given rather than on the catalogue's, and so does the address its consumers are told.
|
||||
//
|
||||
// A node given the catalogue's own port records nothing and registers nothing, so a genesis on the
|
||||
// defaults does exactly what it did before.
|
||||
func recordTheForgesPort(ctx context.Context, o Options, control controlPlane,
|
||||
say func(string)) error {
|
||||
if o.Ports.orDefaults().Packages == DefaultPorts().Packages {
|
||||
return nil
|
||||
}
|
||||
manifest, err := readManifest(o.Catalogue, ForgeModule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w\n"+
|
||||
"This is the module that owns the forge genesis just raised. Without it the port this "+
|
||||
"machine was given for the package registry is nobody's setting, and taking the forge "+
|
||||
"over would put it back on the catalogue's port", err)
|
||||
}
|
||||
remote := "/" + ForgeModule + "-module.json"
|
||||
if err := control.carrying(ctx, ForgeModule+"-module.json", manifest, remote); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := control.tell(ctx, "module", "add", remote); err != nil {
|
||||
return err
|
||||
}
|
||||
say(" registered " + ForgeModule + " — registered, not assigned: nothing of it runs yet")
|
||||
return setFoundationSettings(ctx, o, control, ForgeModule, say)
|
||||
}
|
||||
|
||||
// seedGiteaDatabase creates gitea's role and database inside the foundation postgres, the same way
|
||||
// the foundation creates its own — psql run through the store container (the map's Route B). The role
|
||||
// is created before the database because the database is owned by it. Both are tolerant of already
|
||||
|
||||
@@ -105,12 +105,25 @@ func (p FoundationPorts) moduleSettings() map[string]map[string]int {
|
||||
add("lavinmq", d.AMQP, p.AMQP)
|
||||
add("lavinmq", d.Management, p.Management)
|
||||
add(RegistryModule, d.Registry, p.Registry)
|
||||
add(ForgeModule, d.Packages, p.Packages)
|
||||
return out
|
||||
}
|
||||
|
||||
// PortsSetting is the controller's settings key for a module's given ports.
|
||||
const PortsSetting = "ports"
|
||||
|
||||
// ServesSetting is the settings key that carries what a provider serves, inside the one binding a
|
||||
// module carries rather than resolves — the builder's package binding (novox/hq 04-ISSUES/085).
|
||||
//
|
||||
// **A port given at genesis has to be a setting wherever it is read, or something puts it back.**
|
||||
// The builder reaches the package registry through a binding written in its own manifest, because
|
||||
// at genesis no module yet provides `package-registry` and so there is nothing for the controller
|
||||
// to resolve it from. That binding used to be rewritten, as text, when the installer registered the
|
||||
// builder — which a later registration from the catalogue undid silently. Set as a node's setting
|
||||
// instead, it is held by the controller rather than by the manifest, so re-registering the builder
|
||||
// leaves it where it was.
|
||||
const ServesSetting = "serves"
|
||||
|
||||
// setFoundationSettings tells the controller the ports this node gave a foundation module — and,
|
||||
// on an adopted node, that the registry is reached from anywhere, as a node pulls from it before it
|
||||
// has a private-network address (novox/hq ADR 0100). Done after the module is registered and before
|
||||
@@ -124,6 +137,13 @@ func setFoundationSettings(ctx context.Context, o Options, control controlPlane,
|
||||
if o.Adopted && module == RegistryModule {
|
||||
values["expose"] = map[string]string{strconv.Itoa(DefaultPorts().Registry): "anywhere"}
|
||||
}
|
||||
if packages := o.Ports.orDefaults().Packages; module == BuilderModule &&
|
||||
packages != DefaultPorts().Packages {
|
||||
// The one binding nothing resolves: the builder dials the forge by a number it carries.
|
||||
// The rest of the binding — the provision, the forge it is from, the account it presents —
|
||||
// is the manifest's and stays there.
|
||||
values[ServesSetting] = map[string]int{"port": packages}
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ func TestAPredecessorsContainerUnderTheMeshsNameIsRefused(t *testing.T) {
|
||||
}
|
||||
|
||||
// controlRecorder is a control plane that answers everything and writes down what it was told,
|
||||
// with the content of every settings file carried to it.
|
||||
// with the content of every file carried to it, by the name it was carried under.
|
||||
type controlRecorder struct {
|
||||
told []string
|
||||
settings map[string]string
|
||||
@@ -229,9 +229,7 @@ type controlRecorder struct {
|
||||
func (c *controlRecorder) run(_ context.Context, name string, args ...string) (string, error) {
|
||||
if name == "docker" && args[0] == "cp" {
|
||||
raw, _ := os.ReadFile(args[1])
|
||||
if strings.HasSuffix(args[2], "-settings.json") {
|
||||
c.settings[filepath.Base(args[2])] = string(raw)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
if name == "docker" && args[0] == "exec" {
|
||||
|
||||
Reference in New Issue
Block a user