// mesh-builder — the thing a build machine runs. // // It takes work from the mesh, turns a repository into artifacts, publishes them, and says what // came out. It is **not** the control plane and it is **not** the host: // // - the control plane decides and never touches a machine. Building runs commands on one, and // what the control plane may send a machine is bounded by the declaration language // (novox/hq ADR 0005). "Run this build" is not in it, and widening the language so it could // be would make the control plane able to run anything anywhere. // - the host applies declarations and holds no opinion about what they contain. A host that // also built things would need a container runtime and git, on every machine, to do something // almost none of them will ever do. // // So it is a module: a program a machine runs because the mesh told it to, holding its own broker // credential and nothing else. Compromise of a build machine is compromise of a build machine. package main import ( "context" "encoding/json" "fmt" "net/url" "os" "os/signal" "strings" "syscall" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/builder" "github.com/novox/mesh-controller/internal/link" ) // version is set at build time. var version = "development" func main() { if err := run(); err != nil { fmt.Fprintf(os.Stderr, "mesh-builder: %v\n", err) os.Exit(1) } } const usage = `mesh-builder — builds modules for the mesh It consumes build requests and answers with what it made. Nothing is listened on and nothing is dialled except the broker. MESH_BROKER_FILE a file the mesh sealed to this machine holding the same MESH_REGISTRY host:port to publish artifacts to, when the mesh has not said MESH_BINDING a file the mesh wrote saying where the artifact store is MESH_PACKAGE_BINDING a file the mesh wrote saying where the package registry is MESH_NPM_TOKEN_FILE a file the mesh sealed holding the token for it MESH_NPM_REGISTRY a package registry URL, when the mesh has not said (a person, the bootstrap) MESH_NPM_TOKEN the token for it, likewise MESH_NPM_SCOPE the scope it answers for (default: @novox) MESH_WORKSPACE where to clone and build (default: a temporary directory) It also builds one module and stops, which is how a mesh is raised — before there is a broker to take work from or a registry to publish into: mesh-builder build [--path P] [--ref COMMIT] [--registry HOST:PORT] Without --registry the artifacts stay in this machine's container runtime, named by the digest of their own configuration. The result is printed as JSON. ` func run() error { if len(os.Args) > 1 { switch os.Args[1] { case "version": fmt.Println(version) return nil case "build": return buildOnce(context.Background(), os.Args[2:]) default: fmt.Print(usage) return nil } } credential, err := brokerFrom() if err != nil { return err } registry, err := whereToPublish() if err != nil { return err } workspace := os.Getenv("MESH_WORKSPACE") if workspace == "" { workspace = os.TempDir() + "/mesh-builder" } // **The mesh's name for this machine, not the container's.** A build is reported to the rest // of the mesh, and a report whose origin reads `104cb10e105b` names something no other module // can look up. The mesh already knows the answer and has a way to say it — `${machine:name}` // in the environment file this module is handed — so the hostname is only what is left when // nobody said. on := os.Getenv("MESH_NODE") if on == "" { hostname, err := os.Hostname() if err != nil { return fmt.Errorf("this build machine has no name: nothing said MESH_NODE and the host would not say either: %w", err) } on = hostname } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() machine, err := takeWorkFrom(credential, on) if err != nil { return err } defer machine.Close() fmt.Fprintf(os.Stderr, "building for the mesh, publishing to %s\n", registry) publisher := builder.Registry{Address: registry, Run: builder.Command} return machine.Take(ctx, func(ctx context.Context, work link.Build) { answer(ctx, publisher, on, workspace, work) }) } // takeWorkFrom opens this machine's link to whichever bus the mesh is on. // // **One place chooses**, as everywhere else the bus change went (novox/hq ADR 0116 step 5): a build // machine told about both would take work from one and answer on the other, and every log line would // say it was fine. func takeWorkFrom(credential Credential, on string) (link.BuildMachine, error) { // **The credential names the bus, and there is one** (novox/hq ADR 0131, design 28 task 5.5). // A credential for the mesh's bus carries user, password and fingerprint beside the address, // and that is enough to dial it, pinned. if !credential.onTheNewBus() { return nil, fmt.Errorf("the credential at hand names %q, which is not the mesh's bus", credential.URL) } js, err := broker.DialPinned(credential.natsURL(), credential.Fingerprint) if err != nil { return nil, err } return link.MachineOverNATS(js, on), nil } // answer does one build and says what happened, whichever way it went. func answer(ctx context.Context, publisher builder.Publisher, on, workspace string, work link.Build) { request := work.Request() // **First thing, and to stdout.** A build request that arrives and produces no visible line until // it either finishes or fails is indistinguishable from one that never arrived — which cost a long // diagnosis against a running mesh, chasing "the handler never fired" when the truth was only that // the handler said nothing until the end. fmt.Fprintf(os.Stderr, "a build request arrived for %s\n", request.Repository) result := link.BuildResult{ ID: request.ID, Repository: request.Repository, Path: request.Path, Ref: request.Ref, On: on, } fmt.Fprintf(os.Stderr, "building %s", request.Repository) if request.Path != "" { fmt.Fprintf(os.Stderr, " at %s", request.Path) } if request.Ref != "" { fmt.Fprintf(os.Stderr, " at %s", request.Ref) } fmt.Fprintln(os.Stderr) npmrc, err := packagesFrom() var built builder.Result if err == nil { // The package-registry credential is a build input, so it is resolved before the clone: a // build that could not have resolved its dependencies is refused in front of the reason, not // after a clone that then fails at npm ci. built, err = builder.Build(ctx, builder.Command, publisher, request.Repository, request.Path, request.Ref, workspace, request.Held, npmrc, forgeFrom(), func(step, message string) { fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message) }) } if err != nil { // A failure is a result. A build that fails and says nothing is indistinguishable from a // builder that is not running, and those want completely different responses. result.Failed = err.Error() fmt.Fprintf(os.Stderr, " failed: %v\n", err) } else { manifest, marshalErr := json.Marshal(built.Manifest) if marshalErr != nil { result.Failed = marshalErr.Error() } else { result.Commit = built.Commit result.Manifest = manifest for _, made := range built.Built { result.Made = append(result.Made, link.MadeArtifact{ Name: made.Name, Kind: made.Kind, Reference: made.Reference, }) } result.Against = built.Against for _, r := range built.Read { result.Read = append(result.Read, link.ReadRepository{Repository: r.Repository, Ref: r.Ref}) } fmt.Fprintf(os.Stderr, " built %s from %s\n", built.Manifest.Module, short(built.Commit)) } } if err := work.Announce(ctx, result); err != nil { // Said, not fatal: the build happened. A build reported as failed because announcing it // failed is a lie about work that was done — and the request stays unsettled below only if // nothing was said at all, so another machine can try. fmt.Fprintf(os.Stderr, "cannot say what came of a build: %v\n", err) return } // Settled only once the outcome is away, so a machine that dies before answering leaves the work // for another rather than losing it. if err := work.Done(); err != nil { fmt.Fprintf(os.Stderr, "the outcome is away and the request could not be settled: %v\n", err) } } // packagesFrom is where a build resolves the mesh's own published packages — the SDK above all // (novox/hq ADR 0076, issue 053). // // Preferably from the mesh: a package-registry binding names the endpoint the way the artifact // store's binding does, and a sealed token file the credential the way the broker's does. The // environment variables remain for a builder run by a person, and for the bootstrap, where there is // no registry yet — there the result is disabled and a build that needs no mesh-published dependency // builds anyway. func packagesFrom() (builder.Npmrc, error) { scope := strings.TrimSpace(os.Getenv("MESH_NPM_SCOPE")) if scope == "" { scope = "@novox" } registry := strings.TrimSpace(os.Getenv("MESH_NPM_REGISTRY")) var username string if path := strings.TrimSpace(os.Getenv("MESH_PACKAGE_BINDING")); path != "" { raw, err := os.ReadFile(path) if err != nil { return builder.Npmrc{}, fmt.Errorf("cannot read what the mesh said about the package registry: %w", err) } var told struct { From string `json:"from"` At string `json:"at"` As string `json:"as"` Serves map[string]any `json:"serves"` } if err := json.Unmarshal(raw, &told); err != nil { return builder.Npmrc{}, fmt.Errorf("%s is not a binding: %w", path, err) } if told.At == "" { return builder.Npmrc{}, fmt.Errorf( "%s says the package registry is on %q and gives no address for it", path, told.From) } // Composed from what the provider serves, so nothing here knows gitea's URL shape from // another registry's: it states its port, the path its registry answers on, and the scheme. scheme := "https" if s, ok := told.Serves["scheme"]; ok { scheme = fmt.Sprintf("%v", s) } port, ok := told.Serves["port"] if !ok { return builder.Npmrc{}, fmt.Errorf("%s says nothing about which port the package registry answers on", path) } npmPath, ok := told.Serves["npm-path"] if !ok { return builder.Npmrc{}, fmt.Errorf("%s says nothing about the path the package registry answers on", path) } registry = fmt.Sprintf("%s://%s:%v%v", scheme, told.At, port, npmPath) username = told.As } // The credential the mesh sealed to this machine. The mesh authenticates the ordinary way — a // generated password the provider only applies (novox/hq ADR 0048) — so with a username this is // a password (basic auth); without one it is a bearer token a provider minted. secret := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN")) if path := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN_FILE")); path != "" { raw, err := os.ReadFile(path) if err != nil { return builder.Npmrc{}, fmt.Errorf("cannot read this builder's package-registry credential: %w", err) } secret = strings.TrimSpace(string(raw)) } if u := strings.TrimSpace(os.Getenv("MESH_NPM_USER")); u != "" { username = u } if registry == "" && secret == "" { return builder.Npmrc{}, nil } if username != "" { return builder.Npmrc{Scope: scope, Registry: registry, Username: username, Password: secret}, nil } return builder.Npmrc{Scope: scope, Registry: registry, Token: secret}, nil } // forgeFrom is the git credential this builder may offer a clone, composed from the same binding // and sealed secret its package-registry half already reads: the forge that answers npm is the // forge that hosts the repositories, and its provisioner applies one password to one user for // both. Anything missing means no credential, and every clone stays anonymous — which is all a // mesh of public repositories ever needs. // // The URL names the binding's own address — the machine the mesh says the forge is on — so a // private repository is registered and built by that address, and a clone of anything else is // never shown this credential (git's credential store matches the whole origin). func forgeFrom() builder.GitCredential { path := strings.TrimSpace(os.Getenv("MESH_PACKAGE_BINDING")) if path == "" { return builder.GitCredential{} } raw, err := os.ReadFile(path) if err != nil { return builder.GitCredential{} } var told struct { At string `json:"at"` As string `json:"as"` Serves map[string]any `json:"serves"` } if err := json.Unmarshal(raw, &told); err != nil || told.At == "" || told.As == "" { return builder.GitCredential{} } secret := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN")) if file := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN_FILE")); file != "" { if raw, err := os.ReadFile(file); err == nil { secret = strings.TrimSpace(string(raw)) } } if secret == "" { return builder.GitCredential{} } scheme := "https" if s, ok := told.Serves["scheme"]; ok { scheme = fmt.Sprintf("%v", s) } host := told.At if port, ok := told.Serves["port"]; ok { host = fmt.Sprintf("%s:%v", told.At, port) } made := url.URL{Scheme: scheme, User: url.UserPassword(told.As, secret), Host: host} return builder.GitCredential{URL: made.String()} } func short(commit string) string { if len(commit) > 8 { return commit[:8] } return commit } // whereToPublish is the artifact store this builder uses. // // **Preferably from the mesh.** A builder that is a module requires an artifact store, and the // mesh writes it a file saying which machine answers that and on what port — the same binding any // consumer of any provision gets. Reading it means the address is not a setting somebody keeps in // step by hand, and moving the store is an ordinary reassignment rather than an edit on every // build machine. // // The environment variable remains for a builder run by a person, which is how this started and // how it is still run while being developed. func whereToPublish() (string, error) { binding := strings.TrimSpace(os.Getenv("MESH_BINDING")) if binding == "" { registry := strings.TrimSpace(os.Getenv("MESH_REGISTRY")) if registry == "" { return "", fmt.Errorf("neither MESH_BINDING nor MESH_REGISTRY: a built artifact " + "nobody can fetch is not built") } return registry, nil } raw, err := os.ReadFile(binding) if err != nil { return "", fmt.Errorf("cannot read what the mesh said about the artifact store: %w", err) } var told struct { From string `json:"from"` At string `json:"at"` Serves map[string]any `json:"serves"` } if err := json.Unmarshal(raw, &told); err != nil { return "", fmt.Errorf("%s is not a binding: %w", binding, err) } if told.At == "" { // The provider is not on the private network, so there is no name to reach it by. Said // rather than falling back to the machine's own name, which would publish to a store on // the wrong machine and be found out much later. return "", fmt.Errorf( "%s says the artifact store is on %q and gives no address for it", binding, told.From) } port, ok := told.Serves["port"] if !ok { return "", fmt.Errorf("%s says nothing about which port the artifact store answers on", binding) } return fmt.Sprintf("%s:%v", told.At, port), nil } // brokerFrom is where this builder connects, and with what. // // **Preferably from a file the mesh sealed to this machine.** A builder that is a module is given // its credential the way every other module is given one: generated or accepted centrally, sealed // to the machine, written by the host. Putting it in an environment variable instead would mean // the one copy that matters passing through a terminal and a process listing. // // The variable remains for a builder run by a person. func brokerFrom() (Credential, error) { if path := strings.TrimSpace(os.Getenv("MESH_BROKER_FILE")); path != "" { raw, err := os.ReadFile(path) if err != nil { return Credential{}, fmt.Errorf("cannot read this builder's credential: %w", err) } said := strings.TrimSpace(string(raw)) if said == "" { // An empty credential file is a machine that will connect as nobody and be refused, // with the reason three layers away. return Credential{}, fmt.Errorf("%s is empty, so this builder has no credential", path) } var held Credential if err := json.Unmarshal([]byte(said), &held); err == nil && held.URL != "" { return held, nil } // A file holding only a URL, which is what a person writing one by hand produces. The // broker is then verified against whatever this machine already trusts. return Credential{URL: said}, nil } return Credential{}, fmt.Errorf( "no MESH_BROKER_FILE: a build machine with no credential for the bus has nothing to build") } // Credential is what a build machine is given so it can reach the broker. // // Two things, because reaching a broker over TLS needs both: who to connect as, and what to check // the certificate against. A mesh's broker presents a certificate of the mesh's own, which is in // no public trust store, so a URL alone can only connect to a broker somebody else vouches for. // // **The same shape a node gets, for the same reason** (novox/hq ADR 0004): the fingerprint travels // out of band — here, sealed with the credential — and the endpoint is verified once at connect. type Credential struct { URL string `json:"url"` Fingerprint string `json:"fingerprint,omitempty"` // User and Password ride beside the address on the bus being built (design 25): a credential // embedded in a URL leaks into every log line that prints a connection, so the mesh seals them // as two fields and this machine joins them once, here, to dial. User string `json:"user,omitempty"` Password string `json:"password,omitempty"` } // onTheNewBus is whether a credential is for the bus being built: its address says so, and the // mesh only ever seals such a credential with the user and password beside it. func (c Credential) onTheNewBus() bool { return strings.HasPrefix(strings.TrimSpace(c.URL), "nats://") } // natsURL is the address with this machine's credential in it, for the one dial that needs it. func (c Credential) natsURL() string { rest := strings.TrimPrefix(strings.TrimSpace(c.URL), "nats://") if c.User == "" { return "nats://" + rest } return "nats://" + c.User + ":" + c.Password + "@" + rest }