package catalogue import ( "encoding/json" "fmt" "regexp" "sort" "strconv" "strings" ) // Telling a module where this machine put the holder of a seat. // // **The foundation's ports are the node's** (novox/hq ADR 0100): the port a foundation server was // given at genesis becomes that node's setting for the module that serves it, and every reader // follows the setting. Every consumer's binding did. The control plane's own connections did not // (04-ISSUES/102): they are written at genesis, before any module exists to bind to — full // connection strings, sealed, with the port inside — so when the node moved the store, the // control plane went on dialling where genesis had written and the mesh was headless. // // The control plane cannot open its own sealed connection to move the port, and it cannot bind // the store as a consumer would: a binding mints a credential, and what the control plane holds // is the foundation's superuser, made before the mesh. What it can do is read the node's settings // when it composes its own declaration — it is the thing that composes every other module's — and // say in its own environment which port this machine put the store at. // // So a module may ask about a **seat** (ADR 0079: a foundation seat is named after the server it // guards — `mesh-store`, `mesh-broker`). `${seat:mesh-store:5432}` is "the port this machine put // the holder of the mesh-store seat's 5432 at". Not a provision: nothing is required, nothing is // granted, no credential is minted. A seat is the mesh's own vocabulary for the store and the // broker, which is what makes this the control plane's way of naming them and not a way for a // module to reach a server it was not granted — the answer is a port number the mesh holds in the // clear, and the credential to use it is still the module's own to have. // // **The answer may be empty, and that is the one place a placeholder answers with nothing.** The // store and the broker are raised at genesis, before the mesh knows them as modules; a mesh raised // on the catalogue's own ports never gives them a setting at all. In both, the port genesis wrote // into the connection string is the right one, and the mesh has nothing to add. An empty answer // says exactly that, and what reads it — the control plane's `_PORT` twin — treats an empty value // as no value. Answering with the software's own port instead would override what genesis wrote // with a number the mesh never checked, on the one machine where that is a headless mesh. // ofSeat is where a module asks about a seat: ${seat::}. var ofSeat = regexp.MustCompile(`\$\{seat:([a-z0-9][a-z0-9-]*):([0-9]+)\}`) // **Where this machine reaches a mesh seat's holder** (novox/hq ADR 0222, issue 190). // // `${seat::reach}` is host:port — the address this machine dials to reach whatever holds a // seat the mesh holds once. The same reasoning as the port above, one step further: nothing is // required, nothing is granted, no credential is minted, and the answer is an address the mesh // already holds in the clear and composes into every reference it built. What it is for is a module // that must *state* where a mesh service is to software it owns — the container runtime trusting // the mesh's registry is the case — without becoming that service's consumer. // // Answered for the seats in reachedSeats only. Another seat is refused by name rather than answered // with nothing: a module asking where something is that the mesh does not say would otherwise be // given an empty value and never know the question was not understood. // // The answer may be empty: no machine on the private network holds the seat yet (genesis raises // the store before the network). In a file written into as JSON, an empty member is dropped from // its list, and a list left with none is dropped, so the runtime is never told to trust "". var ofSeatReach = regexp.MustCompile(`\$\{seat:([a-z0-9][a-z0-9-]*):reach\}`) // reachedSeats is every seat ${seat:…:reach} answers for. var reachedSeats = map[string]bool{"mesh-artifact-store": true} // seatInto replaces a resource's ${seat:…} placeholders with where this machine put each seat's // holder — in a file's content, and in a value of a container's or a process's environment. The // same places portInto fills, for the same reason: they are where a program reads a number from. func seatInto(resource map[string]any, module string, with Rendering) error { switch fmt.Sprint(resource["type"]) { case "file": content, ok := resource["content"].(string) if !ok || (!ofSeat.MatchString(content) && !ofSeatReach.MatchString(content)) { return nil } where := fmt.Sprintf("%s has a file that", module) filled, err := seatsFilledInto(content, where, with) if err != nil { return err } if ofSeatReach.MatchString(filled) { if filled, err = reachFilledInto(filled, where, with); err != nil { return err } if fmt.Sprint(resource["into"]) == "json" { if filled, err = withoutEmptyMembers(filled, where); err != nil { return err } } } resource["content"] = filled case "container", "process": env, ok := resource["env"].(map[string]any) if !ok { return nil } named := make([]string, 0, len(env)) for key := range env { named = append(named, key) } sort.Strings(named) // A fresh map, and only when something changes — this map is the catalogue's, shared by // every node running the module (see portInto). var filled map[string]any for _, key := range named { written, ok := env[key].(string) if !ok || (!ofSeat.MatchString(written) && !ofSeatReach.MatchString(written)) { continue } where := fmt.Sprintf("%s's %s %s sets %s to something that", module, resource["type"], resource["name"], key) value, err := seatsFilledInto(written, where, with) if err != nil { return err } if value, err = reachFilledInto(value, where, with); err != nil { return err } if filled == nil { filled = map[string]any{} for k, v := range env { filled[k] = v } } filled[key] = value } if filled != nil { resource["env"] = filled } } return nil } // seatsFilledInto answers every ${seat:…} in one written value. // // A port the seat's holder does not publish on this machine — or a seat nothing on it holds — // answers with nothing, for the reason the package comment gives. A port that is not one is // refused: it was written by a person and it is wrong. func seatsFilledInto(written, where string, with Rendering) (string, error) { for _, m := range ofSeat.FindAllStringSubmatch(written, -1) { seat, port := m[1], m[2] wanted, err := strconv.Atoi(port) if err != nil || wanted < 1 || wanted > 65535 { return "", fmt.Errorf("%s says ${seat:%s:%s}, and %s is not a port", where, seat, port, port) } answer := "" if at, known := with.Seats[seat][wanted]; known { answer = strconv.Itoa(at) } written = strings.ReplaceAll(written, m[0], answer) } return written, nil } // reachFilledInto answers every ${seat:…:reach} in one written value with where this machine // reaches the seat's holder, or with nothing when no holder is reachable yet. A seat the mesh does // not answer this for is refused by name. func reachFilledInto(written, where string, with Rendering) (string, error) { for _, m := range ofSeatReach.FindAllStringSubmatch(written, -1) { seat := m[1] if !reachedSeats[seat] { known := make([]string, 0, len(reachedSeats)) for s := range reachedSeats { known = append(known, s) } sort.Strings(known) return "", fmt.Errorf("%s says ${seat:%s:reach}, and the mesh says where a seat's holder is "+ "reached only for %s (novox/hq ADR 0222)", where, seat, strings.Join(known, ", ")) } written = strings.ReplaceAll(written, m[0], with.SeatReach[seat]) } return written, nil } // withoutEmptyMembers drops every empty string from the lists at the top of a JSON object written // into a machine's file, and a list left with no members, so an unanswered ${seat:…:reach} adds // nothing to the machine's list rather than adding "". func withoutEmptyMembers(content, where string) (string, error) { var object map[string]json.RawMessage if err := json.Unmarshal([]byte(content), &object); err != nil { return "", fmt.Errorf("%s is written into as JSON and is not a JSON object: %w", where, err) } changed := false for key, raw := range object { var members []json.RawMessage if err := json.Unmarshal(raw, &members); err != nil { continue // not a list } kept := make([]json.RawMessage, 0, len(members)) for _, member := range members { var s string if json.Unmarshal(member, &s) == nil && s == "" { continue } kept = append(kept, member) } if len(kept) == len(members) { continue } changed = true if len(kept) == 0 { delete(object, key) continue } list, err := json.Marshal(kept) if err != nil { return "", err } object[key] = list } if !changed { return content, nil } out, err := json.Marshal(object) if err != nil { return "", err } return string(out) + "\n", nil }