package catalogue import ( "fmt" "sort" "strings" ) // The node's tool runtime, as the catalogue knows it (novox/hq ADR 0175, to-be 38). // // **One module is the runtime.** Where it is assigned, one process per machine serves every assigned // module's tools and every held seat's verbs, on the host side, from the bundles each module's build // produced — and no module needs a container to reach the bus with its tools. The name is a constant // rather than a manifest field because a rule turns on it: the composer places the runtime's process // where this module is, and registration refuses the old pattern once this module exists. // RuntimeModule is the module that is the node's tool runtime. Mirrored in the broker package, // which composes a principal of its own for it; the agreement test there holds the two to one string. const RuntimeModule = "node-tools" // BundleRoot is where a machine keeps the tools bundles the mesh delivers to it: under the mesh's // own directory, beside the daemons the host unpacks there, and never where a package manager also // writes. One directory per module, one per bundle beneath it, at a path that does not move with // the version — so the runtime's process names each entrypoint once and is restarted, not // recomposed, when a bundle changes. const BundleRoot = "/var/lib/mesh/bundles" // BundleID names the archive resource that delivers one of a module's bundles; prefixed with the // module like every resource of its own. func BundleID(bundle string) string { return "bundle-" + bundle } // BundlePath is where one module's bundle is unpacked on a machine. func BundlePath(module, bundle string) string { return BundleRoot + "/" + module + "/" + bundle } // runtimeHere says whether this node's set includes the runtime module, which is what decides // whether anything about tools changes on the machine (to-be 38 WP2): until the runtime is assigned, // a node is sent exactly what it was sent before, bundles included, because a bundle nothing loads // is bytes nobody reads. func (r Resolution) runtimeHere() bool { for _, m := range r.Modules { if m.Module == RuntimeModule { return true } } return false } // bundleArchives is one archive per tools bundle of a module — a bundle the runtime LOADS something // from — as the host fetches and unpacks any artifact (novox/hq ADR 0175 §3: a module brings its // tools as a bundle, delivered by the host like any artifact, never an image). A bundle it loads // nothing from is run rather than loaded: a daemon, a step, the runtime itself — delivered by the // process that runs it, and not again here. // // The source is the kept reference; the per-resource pass that follows routes it through the // artifact store as this network reaches it now, as it does every image and archive the mesh built. func bundleArchives(m Manifest) []map[string]any { var out []map[string]any for _, b := range m.Bundles { if len(b.Loads) == 0 { continue } out = append(out, map[string]any{ "id": BundleID(b.Name), "type": "archive", "source": b.Source, "digest": b.Digest, "path": BundlePath(m.Module, b.Name), }) } return out } // RuntimeProcessID names the one process the mesh composes for a machine's runtime; prefixed with // the runtime module like a resource of its own, because that module is what the host sees it as. func RuntimeProcessID() string { return "runtime" } // RuntimeToolModules is the variable the runtime reads the modules it serves from: one // `=` per file it loads, comma-separated — several entries may name one module. // RuntimeBrokerFile is where it reads the node's credential; RuntimeOperatorAccount and // RuntimeOperatorHome are the machine's operator account and home, handed to every tool's // environment (to-be 38 WP1), and absent on a machine with no account. const ( RuntimeToolModules = "MESH_TOOL_MODULES" RuntimeBrokerFile = "MESH_BROKER_FILE" RuntimeOperatorAccount = "MESH_OPERATOR_ACCOUNT" RuntimeOperatorHome = "MESH_OPERATOR_HOME" ) // interpreterFor is how a bundle in a language is run: the program the host's unit starts, with the // bundle's entrypoint after it. The one thing the composer takes from a language, and said here // rather than in a manifest because the runtime's process is the mesh's to compose (to-be 38 WP3). func interpreterFor(language string) (string, error) { switch language { case "typescript": return "node", nil } return "", fmt.Errorf( "%s is written in %q, and the mesh knows no interpreter to run a %q bundle with", RuntimeModule, language, language) } // runtimeProcess is the one process a machine runs the node's tool runtime as (novox/hq ADR 0175, // to-be 38 WP2.3): the runtime module's own bundle, run by its language's interpreter, told which // modules it serves and from which files, where its credential is, and who the machine's operator // is — and restarted when any bundle it loads or the credential it holds changes. // // Composed from the placed manifests, so the credential's path is where this node puts it. The // runtime runs as the operator's account when the machine has one, which is what lets a tool that // needs root escalate as the operator would (ADR 0175 §4); on a machine with no account it runs as // root, and the two operator words are not set. func (r Resolution) runtimeProcess(with Rendering) (map[string]any, error) { var runtime *Manifest for i := range r.Modules { if r.Modules[i].Module == RuntimeModule { runtime = &r.Modules[i] } } if runtime == nil { return nil, nil } if len(runtime.Bundles) != 1 { return nil, fmt.Errorf( "%s is assigned to %s and its build produced %d bundle(s); the runtime is one bundle "+ "the mesh runs, so the module declares exactly one (novox/hq to-be 38)", RuntimeModule, r.Node, len(runtime.Bundles)) } bundle := runtime.Bundles[0] if len(bundle.Entrypoints) != 1 { return nil, fmt.Errorf( "%s's bundle %q names %d entrypoint(s); the runtime is run from one, so the module "+ "declares exactly one (novox/hq to-be 38)", RuntimeModule, bundle.Name, len(bundle.Entrypoints)) } interpreter, err := interpreterFor(bundle.Language) if err != nil { return nil, err } credential, declared := runtime.OwnSecrets["broker"] if !declared { return nil, fmt.Errorf( "%s declares no own secret named broker, and the node's credential is delivered there: "+ "a module that speaks on the bus declares \"own-secrets\": {\"broker\": }", RuntimeModule) } // What it serves, and from which files: every module on this machine that composes here, in // name order, each bundle it loads from in the order the manifest gave. A module left out of // the declaration — a filter on an adopted machine — is left out of this too, or the runtime // would be told to load files that were never delivered. var served []string var restartOn []string for _, m := range r.Modules { if with.Adopted && m.Filtering != nil { continue } for _, b := range m.Bundles { if len(b.Loads) == 0 { continue } for _, load := range b.Loads { served = append(served, m.Module+"="+BundlePath(m.Module, b.Name)+"/"+load) } restartOn = append(restartOn, m.Module+"."+BundleID(b.Name)) } } sort.Strings(served) restartOn = append(restartOn, RuntimeModule+"."+NeedID("broker")) sort.Strings(restartOn) env := map[string]string{ RuntimeToolModules: strings.Join(served, ","), RuntimeBrokerFile: credential.Path, } process := map[string]any{ "id": RuntimeModule + "." + RuntimeProcessID(), "type": "process", "name": RuntimeModule, "source": bundle.Source, "digest": bundle.Digest, "run": []any{interpreter, bundle.Entrypoints[0]}, "env": env, "restart-on": toAny(restartOn), } if r.Account != "" { env[RuntimeOperatorAccount] = r.Account env[RuntimeOperatorHome] = accountHomeOf(r.Account, r.AccountHome) process["user"] = r.Account } // Routed through the artifact store as this network reaches it now, like everything the mesh // built; refused with the same words when there is no store to route through. if err := artifactsInto(process, RuntimeModule, with); err != nil { return nil, err } return process, nil } func toAny(in []string) []any { out := make([]any, 0, len(in)) for _, s := range in { out = append(out, s) } return out } // RuntimeImageModule and RuntimeImageArtifact name the image every per-module tool container was // built on: the tool runtime's own runtime image. With the runtime a module of its own, that image // stays the way a module's SERVICE may be built and stops being the way tools reach a node (ADR 0175). const ( RuntimeImageModule = "mesh-tools" RuntimeImageArtifact = "runtime" ) // ToolContainerOnTheRuntime says why a manifest is the pattern ADR 0175 retires — a module whose tools // are served from a container built on the tool runtime's image — or nothing when it is not. Judged // from the manifest's own `build.on` when it is a repository manifest, and from what its build stood // on when it is a built one, because a resolved manifest carries no build. The gate itself is // registration's (to-be 38 WP2.4): once the runtime module is in the catalogue, this is refused. // // Three things must hold, and each alone is fine: declaring tools (a bundle does that); a container // (a module's service may well be one); building on the runtime's image (a service written against // the SDK may). All three is a container whose purpose is tools, which the runtime now serves. func ToolContainerOnTheRuntime(m Manifest, against []string) string { if len(m.Tools) == 0 { return "" } container := false for _, r := range m.Resources { if fmt.Sprint(r["type"]) == "container" { container = true } } if !container { return "" } onTheRuntime := false if m.Build != nil { for _, on := range m.Build.On { if on.Module == RuntimeImageModule && on.Artifact == RuntimeImageArtifact { onTheRuntime = true } } } for _, ref := range against { path, kept := InArtifactStore(Recorded(ref)) if kept && strings.HasPrefix(path, RuntimeImageModule+"/"+RuntimeImageArtifact+"@") { onTheRuntime = true } } if !onTheRuntime { return "" } return fmt.Sprintf( "%s declares tools and a container built on %s's %s image — a container whose purpose is "+ "serving tools. The node's tool runtime (%s) serves every module's tools from its bundle "+ "now (novox/hq ADR 0175, to-be 38); declare the tools as a bundle and drop the container", m.Module, RuntimeImageModule, RuntimeImageArtifact, RuntimeModule) }