// Package catalogue is what modules are, and what a node gets when it is assigned some. // // novox/hq ADR 0009: everything is a module, a module declares what it provides and requires, // and a module declares what it claims. This turns a set of assignments into the one declaration // a node is sent — which is the first thing the control plane decides rather than relays. package catalogue import ( "bytes" "encoding/json" "fmt" "regexp" "sort" "strconv" "strings" ) // Scopes a claim can have. // // Not everything singular is singular per machine: a seat is one per node, a DHCP server is one // per segment, and the hub is one per mesh. Scope says which, and it is the same idea the mesh // already enforces by hand for the hub. const ( ScopeNode = "node" ScopeSite = "site" ScopeMesh = "mesh" ) // name is what a module, a provision or a claim may be called. // // Constrained because these become resource identities, permission patterns and error messages, // and a name that is valid in one and not the others is a fault found late. // renamed is what a field used to be called, and what it is now. // // Kept rather than dropped once the rename is done: a manifest written against the old name is // refused either way, and the difference is whether whoever wrote it has to go and find out why. var renamed = map[string]string{ // `needs` and `secrets` were both name-to-path and differed only in whose secret it was, so // reaching for the wrong one parsed cleanly and failed somewhere else entirely. "needs": "own-secrets", } var name = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)*$`) // Claim is a singular resource a module takes over. type Claim struct { Name string `json:"name"` // Scope defaults to the node, which is where nearly everything singular is singular. Scope string `json:"scope,omitempty"` } // At is this claim's scope, with the default applied. func (c Claim) At() string { if c.Scope == "" { return ScopeNode } return c.Scope } // Offer is something a module provides, and where the answer to it may live. // // **The distinction this exists for:** a shell, a display server and a private network have to be // on the machine that needs them. A database, an object store and an identity provider do not — // they run somewhere in the mesh and are reached over it. Treating the second as the first // installs PostgreSQL on every machine that runs a web application, which is what happened until // this field existed. // // Written as a bare string in the ordinary case, because nearly everything is node-scoped and // making every manifest say so would bury the few that are not: // // "provides": ["shell"] // "provides": [{"name": "postgres-database", "scope": "mesh"}] type Offer struct { Name string `json:"name"` // Scope defaults to the node, which is where most things must be to be usable. Scope string `json:"scope,omitempty"` } // At is this offer's scope, with the default applied. func (o Offer) At() string { if o.Scope == "" { return ScopeNode } return o.Scope } // UnmarshalJSON accepts a plain name as well as an object. func (o *Offer) UnmarshalJSON(raw []byte) error { var plain string if err := json.Unmarshal(raw, &plain); err == nil { o.Name, o.Scope = plain, "" return nil } var full struct { Name string `json:"name"` Scope string `json:"scope,omitempty"` } if err := json.Unmarshal(raw, &full); err != nil { return fmt.Errorf("a provided name is either a string or {name, scope}: %w", err) } o.Name, o.Scope = full.Name, full.Scope return nil } // MarshalJSON writes back the short form when there is nothing else to say, so a manifest that // went through the mesh comes out looking like the one that went in. func (o Offer) MarshalJSON() ([]byte, error) { if o.Scope == "" { return json.Marshal(o.Name) } return json.Marshal(struct { Name string `json:"name"` Scope string `json:"scope"` }{o.Name, o.Scope}) } // Manifest is everything a module says about itself. type Manifest struct { Module string `json:"module"` Version string `json:"version,omitempty"` // Provides are the names other modules may require. A module always provides its own name; // this is for the rest — `zsh` provides `shell`, `xorg` provides `display-server`. Provides []Offer `json:"provides,omitempty"` // Requires are names that must be provided by something assigned to the same node. Requires []string `json:"requires,omitempty"` // Claims are singular resources. Two modules claiming one thing within a scope cannot both // be assigned there — which is how exclusivity is expressed, rather than as a list of rivals // that every new module would force its predecessors to update. Claims []Claim `json:"claims,omitempty"` // Capabilities the machine must have. A different field from Requires because the remedy // differs: a missing module can be assigned, and a missing capability means the wrong // machine. Capabilities []string `json:"capabilities,omitempty"` // Resources are what this module puts on a node, in the host's own vocabulary. Resources []map[string]any `json:"resources,omitempty"` // Computed names something in the control plane that works this module's resources out per // node, instead of them being fixed here. // // Because some files cannot be written in advance. A machine's peer list on the private // network is derived from every other machine, so it differs on each one and changes when any // of them changes — there is nothing to put in a manifest. // // Being a module anyway is the point: it is assigned like anything else, so a machine that // should not be on the private network simply is not given it, and the network is worked out // over the machines that have it. Before this, connectivity was code beside the module system // doing the same job, and every machine with an address was on the network whether or not // anybody wanted it there. Computed string `json:"computed,omitempty"` // Contributes is what this module tells whatever answers a requirement. // // The other half of an edge. `requires` says a thing must be there; this says what to do with // it — a web application requiring a reverse proxy has to say *which name, which port*, and // until now there was nowhere to put that. Every module that needed it was reduced to // reaching into the control plane's database directly, which is how two of them came to hold // a credential to it permanently. // // Keyed by the requirement, because that is what the contribution is *about*. Contributing to // something is requiring it: asking to be published means a publisher must exist, and a // module that had to say both would eventually say one. Contributes map[string]map[string]any `json:"contributes,omitempty"` // Receives is where this module wants its consumers' contributions written, per requirement // it provides. // // A file, in the mesh's own shape, replaced whenever the set changes. **The control plane // does not know what a reverse proxy is** and does not write one's configuration — it // delivers the facts, and the module turns them into whatever it runs. That boundary is why // swapping the proxy does not touch a single module that publishes through it. Receives map[string]string `json:"receives,omitempty"` // Serves is what a consumer needs to know in order to use something this module provides — a // port, a path, a realm. The module's half of the answer; the mesh adds the other half, which // is *which machine* and *where it is on the private network*. // // It does not carry a credential and cannot: a manifest is the same on every mesh, and a // secret is the one thing that must not be. Serves map[string]map[string]any `json:"serves,omitempty"` // Build says how this module's artifacts are produced from its source. // // The manifest in a repository names artifacts; the manifest the mesh holds names digests. // **They are not the same document**, and that is deliberate: a digest is not knowable until // something is built, and a repository that carried one would be a repository whose file is // wrong the moment anybody edits anything. Build *Build `json:"build,omitempty"` // Binds is where this module wants to be told about something it requires, per requirement. // // Because "this machine needs a database from the anchor" is useless to the program that // needs it unless the program is told. A file, like everything else — the host writes files // and knows nothing about provisions, which is what keeps this from needing anything new // down there. Binds map[string]string `json:"binds,omitempty"` // Secrets is where this module wants the credential for something it requires, per // requirement. The file holds the value and nothing else, so a program can read it without // parsing anything. // // **Its own file, separate from Binds, because the mesh cannot compose a document containing // it.** The value was sealed to this node when it was made and the plaintext discarded — so // there is nothing to interpolate into a larger file, and that is the property worth keeping // rather than an inconvenience to work around. It also means the readable half stays readable // in the declaration, and the secret half changes only when the secret does, which is what // makes `restart-on` precise. Secrets map[string]string `json:"secrets,omitempty"` // OwnSecrets are secrets this module needs in order to be itself, and where to put them. // // **Named for whose they are, not how secret they are.** `secrets` above is a credential for // reaching something else, keyed by the provision it belongs to. These are keyed by a name the // module chose and belong to nobody else. Both were `map[string]string` of name to path, and // the field was called `needs` — so reaching for the wrong one parsed cleanly and failed // somewhere else entirely, which is the shape of fault this whole design exists to prevent. // // Not tied to a consumer. A database has a superuser password, a broker has an administrator, // a registry has an account — each is a secret the module needs in order to be itself, and // none of them is *for* anybody. Keyed by a name of the module's choosing, valued by the file // it lands in. // // **Generated per node and sealed to it**, like everything else the mesh hands out, so a // module running on three machines has three passwords and the mesh can read none of them. A // manifest carrying one instead would put the same secret on every machine that ever runs the // module, in a file anybody can read, for ever. OwnSecrets map[string]string `json:"own-secrets,omitempty"` // Listens is what this module accepts connections on, and from where. // // **A rule names its source** ([ADR 0007](novox/hq)). A port with no source is open to // everything that can reach the machine, and saying so is the difference between a manifest // that restricts something and one that appears to — which is the fault // [04-ISSUES/003](novox/hq) records, where five manifests carried a `scope:` nothing read. // // **Derived, not kept in step by hand.** A machine's open ports are a consequence of what runs // on it; the mesh gathers these and hands the whole set to whatever enforces them. Listens []Listening `json:"listens,omitempty"` // Filtering is where this module wants the node's whole computed rule set written. // // One module per node asks for it, and what it receives is derived from every module's // `listens` rather than from its own — a firewall is a property of the machine, and a module // that could only see its own ports would write a rule set that closed everything else. Filtering *Filtering `json:"filtering,omitempty"` // Certificate is where this module wants a certificate for its machine's name inside the // mesh, and where the key that goes with it can be found. // // **The key is named, not delivered.** The node generated it at enrolment and keeps it; the // mesh only ever signs the public half. So what arrives is a certificate, which is public, // and a path to a file the machine already has. // // Two authorities are kept apart on purpose (novox/hq 08-connectivity): this is the mesh's, // for names only the mesh knows. A name the outside world reaches is a different authority // and a different problem. Certificate *Certificate `json:"certificate,omitempty"` // Grants is a directory this module wants the credentials of its consumers written into, per // provision it offers — one file per consumer, named for it, holding the value alone. // // A directory rather than one document for the same reason as above: each value is sealed // separately and the mesh cannot open any of them to build a list. Grants map[string]string `json:"grants,omitempty"` } // Build says how to produce this module's artifacts from its source. // // **Absent means nothing is built.** A module can be entirely configuration — a shell's rc file, // a set of firewall rules — and having to declare an empty build for it would be a field that // exists to be left blank. type Build struct { // Artifacts are what the source produces, each named so a resource can refer to it before // anybody knows its digest. Artifacts []Artifact `json:"artifacts,omitempty"` } // Artifact is one thing built from a module's source. type Artifact struct { // Name is how resources refer to it. Local to the module. Name string `json:"name"` // Kind is "image" or "archive". Kind string `json:"kind"` // From is what it is built from, relative to the repository root: a Dockerfile for an image, // a directory for an archive. From string `json:"from"` } // Kinds an artifact may be. const ( // ArtifactImage is built from a Dockerfile in this repository. ArtifactImage = "image" // ArtifactArchive is a directory in this repository, packed. ArtifactArchive = "archive" // ArtifactUpstream is an image somebody else built, mirrored into the mesh's own registry and // pinned by the digest it lands with. // // **Because a module usually runs software it did not write.** A database module ships // configuration and a provisioner and does not build a database. It could name the upstream // reference directly, and then every machine needs a route to a public registry and the // reference is a tag somebody else can move — which is what pinning exists to prevent // (novox/hq ADR 0006). // // Mirroring is what the bootstrap already does by hand: the lab stocks upstream images into // the registry a first node pulls from. This makes that a thing a module can say. ArtifactUpstream = "upstream" ) // Listening is one port a module accepts connections on. type Listening struct { Port int `json:"port"` // Protocol is "tcp" or "udp". Absent means tcp, which is what almost everything is — and a // field that had to be written every time would be written wrongly some of the time. Protocol string `json:"protocol,omitempty"` // From is who may reach it. Required, because a rule with no source is open and must say so // rather than appear to restrict something. From string `json:"from"` // Fixed means the protocol chose this number, so the machine must use it too. // // **The exception, and it is a real one** (novox/hq ADR 0038). Mail is 25, submission is 587, // IMAP over TLS is 993 — a mail system on a port the mesh picked is a mail system nothing can // deliver to. Everything else the mesh assigns, because a module cannot know what else is on // the machine it lands on. // // A fixed port is a **claim**: one holder per machine, and the second is refused by name when // it is assigned rather than by a container runtime when it is applied. Fixed bool `json:"fixed,omitempty"` // Why this port is open, for somebody reading a generated rule set and wondering. Why string `json:"why,omitempty"` } // Where a listening port may be reached from. const ( // FromMesh is any machine on the private network. What almost everything wants. FromMesh = "mesh" // FromEverywhere is the public internet. Deliberately spelled out: a port open to everything // should be legible as such in the manifest, not the consequence of an omission. FromEverywhere = "anywhere" // FromMachine is this machine only — a port bound for something else on the same host. FromMachine = "machine" ) // At is this port's protocol, with the default applied. func (l Listening) At() string { if l.Protocol == "" { return "tcp" } return l.Protocol } // Filtering says where a module wants the computed rule set. type Filtering struct { // Into is the path to write it to. Whatever loads it is this module's own business — an // action beside this field, ordinarily — because how a machine enforces rules is a fact about // the machine and the mesh has no business knowing it. Into string `json:"into"` } // Certificate says where a module wants what the mesh issued for its machine. type Certificate struct { // Into is where the certificate is written. Into string `json:"into"` // Authority is where the mesh's own certificate is written, so something connecting to this // machine can be told what to believe. Optional: a module that only serves does not need it. Authority string `json:"authority,omitempty"` } // CertificateID and AuthorityID are the resource identities of what the mesh issued. func CertificateID() string { return "certificate" } func AuthorityID() string { return "certificate-authority" } // FilteringID names the computed rule set, so it is the same resource across every declaration // and a change to it is an update rather than an addition beside the old one. func FilteringID() string { return "filtering" } // NeedID is the resource identity of the file a module's own secret lands in. func NeedID(name string) string { return "needs-" + name } // SecretID is the resource identity of the file a module is given a credential in. func SecretID(requirement string) string { return "secret-" + requirement } // GrantID is the resource identity of one consumer's credential on the providing machine. func GrantID(provision, consumer string) string { return "grant-" + provision + "-" + consumer } // BoundID is the resource identity of the file a module is told about a provision in. func BoundID(requirement string) string { return "bound-" + requirement } // Wants is everything that must be provided on the same node: what this module requires, and what // it contributes to. func (m Manifest) Wants() []string { out := append([]string{}, m.Requires...) for to := range m.Contributes { var already bool for _, r := range m.Requires { if r == to { already = true } } if !already { out = append(out, to) } } sort.Strings(out) return out } // ReceivedID is the resource identity of the file a provider is given its contributions in. // // Named rather than positional so a module can point `restart-on` at it: a proxy that got a new // route and did not reload is a route that silently does not work, which is the same fault the // overlay had when a peer list changed under a running interface. func ReceivedID(requirement string) string { return "received-" + requirement } // ParseManifest reads a module manifest, refusing anything it cannot act on. // // Every problem is reported rather than the first, because somebody writing a manifest fixes // them in one pass or in four. func ParseManifest(raw []byte) (Manifest, error) { var m Manifest // Strictly. **An unknown key is refused**, which is the discipline the host's declaration // parser has and manifests lacked (novox/hq 04-ISSUES/003): a `scope:` key survived in five // manifests, read by nothing, making them appear to restrict a port and restrict nothing. // // "An unenforced rule is indistinguishable from a wrong one, and costs more, because people // believe it" — and a silently-accepted key is worse than unenforced, because a reviewer // checking whether something is restricted will find that it is, and be wrong. decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if err := decoder.Decode(&m); err != nil { // A key that used to mean something says what it became. Refusing a renamed field with // "unknown field" is correct and unhelpful: whoever wrote it knew what they meant, and // the mesh knows what it is called now. for was, is := range renamed { if strings.Contains(err.Error(), `"`+was+`"`) { return Manifest{}, fmt.Errorf( "this manifest says %q, which is now called %q: %w", was, is, err) } } return Manifest{}, fmt.Errorf("this is not a module manifest: %w", err) } var problems []string if !name.MatchString(m.Module) { problems = append(problems, fmt.Sprintf( "%q is not a usable module name: lower-case letters, digits, dashes and dots", m.Module)) } for _, offer := range m.Provides { p := offer.Name if !name.MatchString(p) { problems = append(problems, fmt.Sprintf("%q is not a usable name to provide", p)) } if instead, generic := engineGeneric[p]; generic { // A consumer is written against an engine, not a role (novox/hq ADR 0027). Providing // the role means a requirement for it matches any engine, resolves as satisfied, and // fails on the first query — with nothing pointing back at the match. problems = append(problems, fmt.Sprintf( "%s provides %q, which hides which engine it is: a requirement for %q would match "+ "any of them and fail on the first query. Name the engine — %s", m.Module, p, p, instead)) } if s := offer.At(); s != ScopeNode && s != ScopeMesh { // Site scope is meaningful for a claim — one DHCP server per segment — and is not // yet meaningful for a provision, because nothing knows how to reach "the one at my // site". Refused rather than silently treated as mesh-wide. problems = append(problems, fmt.Sprintf( "%s provides %q at scope %q; a provision is %q or %q", m.Module, p, s, ScopeNode, ScopeMesh)) } if p == m.Module { // Harmless and worth saying: a module always provides its own name, so writing it // suggests the author expected it not to. problems = append(problems, fmt.Sprintf( "%s provides its own name already; listing it says nothing", m.Module)) } } for _, r := range m.Requires { if !name.MatchString(r) { problems = append(problems, fmt.Sprintf("%q is not a usable name to require", r)) } if r == m.Module { problems = append(problems, fmt.Sprintf("%s requires itself", m.Module)) } } for _, c := range m.Claims { if !name.MatchString(c.Name) { problems = append(problems, fmt.Sprintf("%q is not a usable claim name", c.Name)) } switch c.At() { case ScopeNode, ScopeSite, ScopeMesh: default: problems = append(problems, fmt.Sprintf( "%s claims %s at scope %q; a claim is held per node, per site or per mesh", m.Module, c.Name, c.Scope)) } } if m.Computed != "" && len(m.Resources) > 0 { // One or the other. A module that both ships files and has them computed would leave // nobody able to say where a given file came from. problems = append(problems, fmt.Sprintf( "%s has resources of its own and says they are computed by %q; it is one or the other", m.Module, m.Computed)) } for to, values := range m.Contributes { if !name.MatchString(to) { problems = append(problems, fmt.Sprintf("%q is not a usable name to contribute to", to)) } if len(values) == 0 { // An empty contribution is either a mistake or a requirement written the long way // round, and both are better said plainly. problems = append(problems, fmt.Sprintf( "%s contributes nothing to %q; if it only needs one, require it", m.Module, to)) } } problems = append(problems, m.Build.problems(m.Module)...) for to := range m.Serves { var offered bool for _, o := range m.Offers() { if o == to { offered = true } } if !offered { problems = append(problems, fmt.Sprintf( "%s serves %q to whoever requires it, and does not provide it", m.Module, to)) } } for to, where := range m.Binds { if !strings.HasPrefix(where, "/") { problems = append(problems, fmt.Sprintf( "%s binds %q at %q, which is not an absolute path", m.Module, to, where)) } var wanted bool for _, w := range m.Wants() { if w == to { wanted = true } } if !wanted { // Being told about something you never asked for would write a file describing a // machine this one has no business talking to. problems = append(problems, fmt.Sprintf( "%s binds %q and does not require it", m.Module, to)) } } if f := m.Filtering; f != nil && strings.TrimSpace(f.Into) == "" { problems = append(problems, fmt.Sprintf( "%s asks for the computed rule set and does not say where to put it", m.Module)) } for _, l := range m.Listens { if l.Port < 1 || l.Port > 65535 { problems = append(problems, fmt.Sprintf( "%s listens on port %d, which is not a port", m.Module, l.Port)) } switch l.From { case FromMesh, FromEverywhere, FromMachine: case "": // The fault this field exists to prevent. A rule with no source is open, and a // manifest that omitted it would read as a restriction and be none. problems = append(problems, fmt.Sprintf( "%s listens on %d and does not say from where; it is %q, %q or %q", m.Module, l.Port, FromMesh, FromEverywhere, FromMachine)) default: problems = append(problems, fmt.Sprintf( "%s listens on %d from %q; it is %q, %q or %q", m.Module, l.Port, l.From, FromMesh, FromEverywhere, FromMachine)) } if p := l.At(); p != "tcp" && p != "udp" { problems = append(problems, fmt.Sprintf( "%s listens on %d over %q, which is tcp or udp", m.Module, l.Port, p)) } } if c := m.Certificate; c != nil { if !strings.HasPrefix(c.Into, "/") { problems = append(problems, fmt.Sprintf( "%s wants its certificate at %q, which is not an absolute path", m.Module, c.Into)) } if c.Authority != "" && !strings.HasPrefix(c.Authority, "/") { problems = append(problems, fmt.Sprintf( "%s wants the authority at %q, which is not an absolute path", m.Module, c.Authority)) } } // **A module may not declare an action, and this is where it is said** (novox/hq ADR 0005). // // The host already refuses one, correctly and for the right reason: the link may not carry a // command to run, and that bound is what limits a compromised control plane to shapes it // cannot turn into arbitrary code. But a module's resources reach a machine over the link, so // a manifest carrying an action was accepted here, stored, resolved, planned and pushed — and // refused on the machine, in the host's log, with nothing connecting it back to the manifest // that caused it. // // That is the same failure as the network shape earlier today: the refusal was right, arrived // far from its cause, and nobody was reading the log. A rule enforced only at the far end is // enforced; it is just not usable. for _, r := range m.Resources { if fmt.Sprint(r["type"]) != "action" { continue } problems = append(problems, fmt.Sprintf( "%s declares %v, which is an action, and a module may not: the link may not carry a "+ "command to run (novox/hq ADR 0005). A module that needs something done ships a "+ "program that reads what the mesh delivered and reconciles", m.Module, r["id"])) } // **A container may not mount a path the module never declared** (novox/hq 04-ISSUES/026). // // A bind mount whose source does not exist is created by the container runtime, as root, with // whatever mode it picks. So `owner` and `mode` — which exist precisely so a module can say who // its data belongs to — are silently not applied to the only directories that hold data. // // And the protection written for exactly this case does not reach them. A directory the mesh // declared and no longer wants is kept, not removed, when it holds anything the mesh did not // put there (novox/hq ADR 0030). That rule is the answer to *what happens to my data when a // module goes away*, and it is written in terms of declared directories. An undeclared one is // outside it, because the mesh does not know it is there. // // Checked here rather than on the machine because the machine cannot tell the difference: by // the time it sees the mount it is being asked to create the directory, which is a thing it is // perfectly able to do. The fault is in the manifest, so it is named at the manifest. declared := map[string]bool{} for _, r := range m.Resources { switch fmt.Sprint(r["type"]) { case "directory", "file": if p := fmt.Sprint(r["path"]); strings.HasPrefix(p, "/") { declared[strings.TrimRight(p, "/")] = true } } } // A secret the mesh seals onto the machine is a file this module owns; it is declared by // `own-secrets` rather than by a resource, and mounting it is the ordinary way a container // reads it. for _, where := range m.OwnSecrets { declared[strings.TrimRight(where, "/")] = true } for _, where := range m.Secrets { declared[strings.TrimRight(where, "/")] = true } for _, where := range m.Receives { declared[strings.TrimRight(where, "/")] = true } for _, where := range m.Grants { declared[strings.TrimRight(where, "/")] = true } // Under a declared directory is declared: a module that says where its data lives has said so // for what it puts inside. covers := func(path string) bool { for at := path; strings.HasPrefix(at, "/"); { if declared[at] { return true } cut := strings.LastIndex(at, "/") if cut <= 0 { return false } at = at[:cut] } return false } for _, r := range m.Resources { if fmt.Sprint(r["type"]) != "container" { continue } mounts, _ := r["volumes"].([]any) for _, v := range mounts { from, _, _ := strings.Cut(fmt.Sprint(v), ":") if !strings.HasPrefix(from, "/") || covers(strings.TrimRight(from, "/")) { continue } problems = append(problems, fmt.Sprintf( "%s mounts %q into %v, and nothing in %s declares it: a bind mount the module did "+ "not declare is created by the container runtime as root, so the module's own "+ "owner and mode do not reach the directory that holds its data, and the rule "+ "that keeps data when a module goes away (novox/hq ADR 0030) does not cover it", m.Module, from, r["id"], m.Module)) } } for name, where := range m.OwnSecrets { if !strings.HasPrefix(where, "/") { problems = append(problems, fmt.Sprintf( "%s needs %q at %q, which is not an absolute path", m.Module, name, where)) } if name == "" { problems = append(problems, m.Module+" needs a secret with no name") } } for to, where := range m.Secrets { if !strings.HasPrefix(where, "/") { problems = append(problems, fmt.Sprintf( "%s keeps the credential for %q at %q, which is not an absolute path", m.Module, to, where)) } var wanted bool for _, w := range m.Wants() { if w == to { wanted = true } } if !wanted { problems = append(problems, fmt.Sprintf( "%s wants the credential for %q and does not require it", m.Module, to)) } } for to, where := range m.Grants { if !strings.HasPrefix(where, "/") { problems = append(problems, fmt.Sprintf( "%s grants %q into %q, which is not an absolute path", m.Module, to, where)) } var offered bool for _, o := range m.Offers() { if o == to { offered = true } } if !offered { problems = append(problems, fmt.Sprintf( "%s grants %q to its consumers and does not provide it", m.Module, to)) } } for to, where := range m.Receives { if !name.MatchString(to) { problems = append(problems, fmt.Sprintf("%q is not a usable name to receive", to)) } if !strings.HasPrefix(where, "/") { problems = append(problems, fmt.Sprintf( "%s receives %q at %q, which is not an absolute path", m.Module, to, where)) } var offered bool for _, o := range m.Offers() { if o == to { offered = true } } if !offered { // Receiving contributions to something you do not provide would create a file nobody // ever writes to, on a machine where nothing asked for it. problems = append(problems, fmt.Sprintf( "%s receives contributions to %q and does not provide it", m.Module, to)) } } for i, r := range m.Resources { id, _ := r["id"].(string) if id == "" { problems = append(problems, fmt.Sprintf("resource %d has no id", i)) } if _, ok := r["type"].(string); !ok { problems = append(problems, fmt.Sprintf("resource %q has no type", id)) } } if len(problems) > 0 { sort.Strings(problems) return Manifest{}, fmt.Errorf("this manifest cannot be used:\n - %s", strings.Join(problems, "\n - ")) } return m, nil } // Offers is everything this module can satisfy: its own name, and what it provides. func (m Manifest) Offers() []string { out := []string{m.Module} for _, p := range m.Provides { out = append(out, p.Name) } sort.Strings(out) return out } // OffersAt is what this module provides at one scope, with its own name counted as node-scoped: // a module is only ever itself on the machine it is installed on. func (m Manifest) OffersAt(scope string) []string { var out []string if scope == ScopeNode { out = append(out, m.Module) } for _, p := range m.Provides { if p.At() == scope { out = append(out, p.Name) } } sort.Strings(out) return out } // MachineSide says where a module's declared port reaches this machine, and whether the mesh is // free to choose it. // // **The mesh may only move a port it actually publishes** (novox/hq ADR 0038). A container's // mapping is the thing that translates, so where there is one the mesh can put the machine side // anywhere it likes. Where there is not, the software binds what it binds: assigning a port then // does not move the service, it just opens the wrong number in the rule set and leaves the real // one shut — a firewall that reports success and blocks the thing it was asked to admit. // // Three cases, and only the first belongs to the mesh: // // - a container publishes it in short form — the mesh chooses // - a container publishes it as host:container — the manifest already chose // - nothing publishes it — whatever binds it, binds it // // Either side of a long mapping counts as naming it, and the host side is what comes back. A // module may reasonably read `listens` as the port its software uses or as the port the machine // exposes, and both readings have the same right answer here. func (m Manifest) MachineSide(port int) (at int, mayAssign bool) { for _, r := range m.Resources { if fmt.Sprint(r["type"]) != "container" { continue } listed, ok := r["ports"].([]any) if !ok { continue } for _, entry := range listed { written := strings.TrimSpace(fmt.Sprint(entry)) host, inside, long := strings.Cut(written, ":") if !long { if n, err := strconv.Atoi(written); err == nil && n == port { return port, true } continue } outer, err := strconv.Atoi(strings.TrimSpace(host)) if err != nil { continue } inner, err := strconv.Atoi(strings.TrimSpace(inside)) if err == nil && (outer == port || inner == port) { return outer, false } } } return port, false }