package bootstrap import ( "context" "fmt" "os" "path/filepath" "strings" ) // placeholderDigest is what the catalogue writes where a built image's digest will go. // // Sixty-four zeros. A manifest in the catalogue names an image the mesh builds and pushes, and // until that has happened there is no digest to name — so the convention is a digest that is // obviously not one, replaced by the pipeline when it publishes. The installer meets it twice: it // must NOT be there in the registry's manifest, whose image is upstream and never built // (novox/hq 04-ISSUES/029), and it MUST be there in the control plane's, which is the image // step 8 has just pushed. const placeholderDigest = "sha256:" + "0000000000000000000000000000000000000000000000000000000000000000" // catalogueDir is where a mesh-catalog checkout keeps its manifests. const catalogueDir = "modules" // manifestFile is the path a module's manifest is read from, given a catalogue checkout. func manifestFile(catalogue, module string) string { return filepath.Join(catalogue, catalogueDir, module, "module.json") } // readManifest reads one module's manifest out of a mesh-catalog checkout. // // **From a checkout rather than from anything the mesh serves**, and that is the ordering the whole // pivot exists to respect: at this point the mesh has no build machine, no forge and — until step 7 // finishes — no registry. What a manifest is, is a file; the installer is handed the directory it // is in and reads it. func readManifest(catalogue, module string) ([]byte, error) { path := manifestFile(catalogue, module) raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf( "the %s module's manifest could not be read: %w\n"+ "--catalog is a checkout of the mesh's catalogue repository, and this is read from "+ "%s inside it", module, err, filepath.Join(catalogueDir, module, "module.json")) } return raw, nil } // Installed is what installing one module did. type Installed struct { // Module is its name. Module string // Known is true when the mesh already had this module in its catalogue. The manifest is // registered either way — a re-run with a changed manifest must land — so this reports what // was found rather than what was skipped. Known bool // Assigned is true when this node was given the module during this run. Assigned bool // Pushed is what the mesh said when it sent this node its declaration. Pushed string } // installModule registers a manifest, gives it to this node, and sends it. // // The three verbs a person types, in the order they type them, through the same commands. There is // no installer-only path into the mesh: everything here is `module add`, `assign` and `push`, so // what the installer does on a bare machine and what an operator does on a running mesh are the // same act (novox/hq ADR 0035, one refusal per act however it is asked for). func installModule(ctx context.Context, o Options, control controlPlane, module string, manifest []byte, say func(string)) (Installed, error) { out, err := registerAndAssign(ctx, o, control, module, manifest, say) if err != nil { return out, err } out.Pushed, err = pushNode(ctx, o, control, say) return out, err } // registerAndAssign is the half of installing that happens before anything is sent. // // Split out because one module needs something in between: the control plane's own store // connections have to be accepted before its declaration is composed, or the mesh would seal // thirty-two random bytes into the file it expects a connection string in and the container would // come up unable to open anything (mesh-control's `secret accept`, and what it exists for). // // **`module add` is run every time and is not skipped when the module is already known.** It is an // upsert on the manifest, and the manifest is exactly what changes between runs — step 9 registers // the control plane with a digest that did not exist the first time. Skipping it because the name // was already in the catalogue would silently pin the mesh to the previous image. func registerAndAssign(ctx context.Context, o Options, control controlPlane, module string, manifest []byte, say func(string)) (Installed, error) { out := Installed{Module: module} known, err := control.tell(ctx, "module", "list") if err != nil { return out, err } out.Known = mentions(known, module) remote := "/" + module + "-module.json" if err := control.carrying(ctx, module+"-module.json", manifest, remote); err != nil { return out, err } if _, err := control.tell(ctx, "module", "add", remote); err != nil { return out, err } if out.Known { say(" registered " + module + " — the mesh already knew it; the manifest is now this one") } else { say(" registered " + module) } assigned, err := control.tell(ctx, "assign", o.Node, module) if err != nil { return out, err } out.Assigned = true say(" assigned " + module + " to " + o.Node) if refusal := strings.TrimSpace(assigned); strings.Contains(refusal, "but ") { // `assign` records what a person meant and says at once when the machine cannot host it. // Repeated rather than swallowed: the push below will apply everything else and this is // the only place the reason appears. say(indent(refusal)) } return out, nil } // pushNode sends this node everything it should be. // // Given the long wait rather than the probe timeout: a push composes every declaration this node // should hold, seals every secret in them and publishes them, and on a first node that is the // slowest thing the mesh does. func pushNode(ctx context.Context, o Options, control controlPlane, say func(string)) (string, error) { said, err := control.within(o.Wait).tell(ctx, "push", o.Node) if err != nil { return "", fmt.Errorf( "%w\n\nWhat was registered and assigned is registered and assigned, and this node has "+ "not been sent it. Nothing is half-applied — a push the mesh refused sent nothing "+ "at all. Fix what it named and run this installer again: it will find the module "+ "already registered and try the push again", err) } say(" pushed " + o.Node) return strings.TrimSpace(said), nil }