package resolution import ( "encoding/json" "fmt" "regexp" "sort" "strconv" "strings" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) // 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 mcat.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 mcat.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 mcat.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 }