package catalogue import ( "bytes" "encoding/json" "fmt" "sort" "strings" ) // A requirement a module can run without (novox/hq ADR 0330, issue 166). // // **Hard stays the default.** A requirement written as a bare name is hard, exactly as before: it // resolves, or the assignment is refused, and a single catalogue candidate is installed for it. One // written `{"name": "influxdb-api", "optional": true}` resolves the same way when a provider is // already assigned in the mesh or reachable from the consumer (§2), and is otherwise **absent**: not // refused, and nothing is installed for it, on this machine or elsewhere (§3). // // **An absent requirement is never rendered as an empty value** (§4–§6). Its binding, its pair // credential and its grant are not composed, so a file that names one of them is delivered only while // the requirement is bound (`only-when-bound`), and a `${bound:…}` placeholder of it outside such a // file names the value it takes while absent: `${bound::|default:}`. The // module check refuses anything else, so the gap is found when the manifest is written, not on a // machine where it reads as a blank. // OnlyWhenBound is a resource's field naming the optional requirement it is delivered only while // bound (ADR 0330 §5). Said in the catalogue and dropped before the declaration is sent: the // node-engine parses strictly and knows no such field. const OnlyWhenBound = "only-when-bound" // requirementForm is one entry of `requires` in its long form. type requirementForm struct { Name string `json:"name"` Optional bool `json:"optional,omitempty"` // Shares is who else may use the consumer's bucket (BucketShare, novox/hq ADR 0468 §8), on s3-bucket only. Shares []BucketShare `json:"shares,omitempty"` } // readRequires reads `requires` in both of its forms — a bare name, or {name, optional} — into the // names and which of them are optional. func readRequires(raw json.RawMessage) ([]string, map[string]bool, map[string][]BucketShare, error) { var entries []json.RawMessage if err := json.Unmarshal(raw, &entries); err != nil { return nil, nil, nil, fmt.Errorf("requires: a list of names, or of {name, optional}: %w", err) } var names []string optional := map[string]bool{} var shares map[string][]BucketShare for i, e := range entries { var plain string if err := json.Unmarshal(e, &plain); err == nil { names = append(names, plain) continue } var full requirementForm dec := json.NewDecoder(bytes.NewReader(e)) dec.DisallowUnknownFields() if err := dec.Decode(&full); err != nil { return nil, nil, nil, fmt.Errorf("requires[%d]: a name, or {name, optional} (novox/hq ADR 0330): %w", i, err) } if strings.TrimSpace(full.Name) == "" { return nil, nil, nil, fmt.Errorf("requires[%d] names no requirement: {name, optional}", i) } names = append(names, full.Name) if err := checkBucketShares(full.Name, full.Shares); err != nil { return nil, nil, nil, fmt.Errorf("requires[%d]: %w", i, err) } if len(full.Shares) > 0 { if shares == nil { shares = map[string][]BucketShare{} } shares[full.Name] = full.Shares } if full.Optional { optional[full.Name] = true } } if len(optional) == 0 { optional = nil } return names, optional, shares, nil } // OptionalRequiresKey is where a stored manifest keeps which of its requirements are optional: beside // `requires`, which stays a plain list of names (mesh-controller #281's review). A controller older than // optional requirements reads `requires` as names; the long form inside it failed that read, and with it // the whole catalogue, so a rollback below this controller would have composed no machine. An older // controller sets this unknown key aside instead and leaves only the module that carries it out. A // manifest may be written either way; it is stored this way. const OptionalRequiresKey = "optional-requires" // readOptionalRequires reads the stored form's list of optional requirements. func readOptionalRequires(raw json.RawMessage) (map[string]bool, error) { var names []string if err := json.Unmarshal(raw, &names); err != nil { return nil, fmt.Errorf("%s: a list of the names in `requires` that are optional: %w", OptionalRequiresKey, err) } out := map[string]bool{} for _, n := range names { out[n] = true } return out, nil } // optionalRequiresList is the stored form's list: the optional requirements, sorted. func optionalRequiresList(m Manifest) []string { var out []string for name, optional := range m.OptionalRequires { if optional { out = append(out, name) } } sort.Strings(out) return out } // IsOptional is whether this module requires the name optionally (ADR 0330 §1). A name it contributes // to without requiring, or requires as a bare name, is hard. func (m Manifest) IsOptional(requirement string) bool { return m.OptionalRequires[requirement] } // wantsHard is whether this module wants the name as a hard requirement: required without // `optional`, or contributed to. func (m Manifest) wantsHard(name string) bool { for _, w := range m.Wants() { if w == name { return !m.IsOptional(name) } } return false } // Absent is one optional requirement of one consumer that nothing assigned answers (ADR 0330 §3): // left unbound rather than refused, and listed by `plan` and `node` (§7). type Absent struct { // Consumer is the module that requires it optionally. Consumer string `json:"consumer"` // Requirement is what it requires. Requirement string `json:"requirement"` } // AbsentFor is the optional requirements of one module this resolution leaves unbound. func (r Resolution) AbsentFor(module string) map[string]bool { out := map[string]bool{} for _, a := range r.Absent { if a.Consumer == module { out[a.Requirement] = true } } return out } // absentOf is, for each requirement left absent, every module of the set that requires it // optionally, sorted by consumer then requirement. func absentOf(names map[string]bool, order []string, catalogue map[string]Manifest) []Absent { var out []Absent for _, module := range order { m := catalogue[module] for _, r := range m.Requires { if names[r] && m.IsOptional(r) { out = append(out, Absent{Consumer: module, Requirement: r}) } } } sort.Slice(out, func(i, j int) bool { if out[i].Consumer != out[j].Consumer { return out[i].Consumer < out[j].Consumer } return out[i].Requirement < out[j].Requirement }) return out } // withoutAbsent drops the resources delivered only while a requirement is bound whose requirement is // absent here (ADR 0330 §5), and the field itself from every resource that keeps it. func withoutAbsent(resources []map[string]any, absent map[string]bool) []map[string]any { out := make([]map[string]any, 0, len(resources)) for _, r := range resources { when, has := r[OnlyWhenBound] if !has { out = append(out, r) continue } if absent[fmt.Sprint(when)] { continue } kept := make(map[string]any, len(r)) for k, v := range r { if k != OnlyWhenBound { kept[k] = v } } out = append(out, kept) } return out }