Where node-tools is in a node's set, the declaration ends with one process: the runtime module's own bundle, run from its one entrypoint by its language's interpreter, told in MESH_TOOL_MODULES every <module>=<file> the machine's bundles load, where its credential is (the module's own broker secret as this node places it), and — on a machine with an operator account — who the operator is, running as that account so a tool that needs root can escalate as the operator would. Restarted when any bundle it loads or the credential changes. A machine with no account runs it as root without the two operator words; a machine without the runtime is sent nothing new. A bundle says which of its entrypoints the runtime LOADS (`loads`), because one bundle may carry a daemon beside its tools and importing the daemon into the runtime would start it there; absent, a module declaring tools has every entrypoint loaded. And the TypeScript toolchain is rooted at the module, so an entrypoint lands at the path it is named by — the runtime loading bundles by their declared paths is what made the compiler's common-directory default visible.
198 lines
8.2 KiB
Go
198 lines
8.2 KiB
Go
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
|
|
// `<module>=<entrypoint>` 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\": <path>}",
|
|
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
|
|
}
|