package catalogue import ( "encoding/json" "fmt" "regexp" "sort" ) // BucketShare is one entry of `shares` on a module's requirement of `s3-bucket` (novox/hq ADR 0468 §8): who else may // put or get in the consumer's bucket, and with which methods. Not the NFS shares of shares.go (ADR 0263). // // "requires": [{"name": "s3-bucket", "shares": [{"to": "seat:node-files", "methods": ["put"]}, // {"to": "print-and-scan", "methods": ["get"]}]}] // // `to` is a module name, or `seat:`. `methods` is a non-empty subset of get and put, without a repeat. // Shares are allowed on `s3-bucket` only, and no two entries name the same `to`. type BucketShare struct { To string `json:"to"` Methods []string `json:"methods"` } // BucketRequirement is the requirement `shares` may be written on. const BucketRequirement = "s3-bucket" // RequireSharesKey is where a stored manifest keeps the shares of its requirements: beside `requires`, which stays a // plain list of names, for the reason OptionalRequiresKey is beside it. An older controller sets the unknown key // aside instead of failing the catalogue. const RequireSharesKey = "requires-shares" // SharesValue is the key of the value a consumer gives the provider of s3-bucket in its grant: the list of // {to, methods} sorted by `to`. A consumer's own `contributes` may not name it. const SharesValue = "shares" var shareTarget = regexp.MustCompile(`^(seat:)?[a-z0-9][a-z0-9-]*$`) // checkBucketShares refuses a malformed `shares` of one requirement, naming the entry and ADR 0468 §8. func checkBucketShares(requirement string, shares []BucketShare) error { if len(shares) == 0 { return nil } if requirement != BucketRequirement { return fmt.Errorf("requires %s: `shares` is allowed on %s only (novox/hq ADR 0468 §8)", requirement, BucketRequirement) } seen := map[string]bool{} for i, s := range shares { at := fmt.Sprintf("requires %s: shares[%d] (to %q)", requirement, i, s.To) if !shareTarget.MatchString(s.To) { return fmt.Errorf("%s: `to` is a module name or seat: (novox/hq ADR 0468 §8)", at) } if seen[s.To] { return fmt.Errorf("%s: a second entry for the same `to` (novox/hq ADR 0468 §8)", at) } seen[s.To] = true if len(s.Methods) == 0 { return fmt.Errorf("%s: `methods` is empty; name get, put or both (novox/hq ADR 0468 §8)", at) } used := map[string]bool{} for _, method := range s.Methods { if method != "get" && method != "put" { return fmt.Errorf("%s: method %q is not get or put (novox/hq ADR 0468 §8)", at, method) } if used[method] { return fmt.Errorf("%s: method %q twice (novox/hq ADR 0468 §8)", at, method) } used[method] = true } } return nil } // readRequireShares reads the stored form's side key. func readRequireShares(raw json.RawMessage) (map[string][]BucketShare, error) { var out map[string][]BucketShare if err := json.Unmarshal(raw, &out); err != nil { return nil, fmt.Errorf("%s: an object of requirement to a list of {to, methods}: %w", RequireSharesKey, err) } for requirement, shares := range out { if err := checkBucketShares(requirement, shares); err != nil { return nil, err } } return out, nil } // sortedShares is a requirement's shares as they are handed to the provider: sorted by `to` (a file whose lines move // looks changed), each entry's methods sorted, and a copy so the manifest is not reordered. func sortedShares(shares []BucketShare) []BucketShare { out := make([]BucketShare, 0, len(shares)) for _, s := range shares { methods := append([]string(nil), s.Methods...) sort.Strings(methods) out = append(out, BucketShare{To: s.To, Methods: methods}) } sort.Slice(out, func(i, j int) bool { return out[i].To < out[j].To }) return out } // withShares adds this module's shares of the requirement to the values it gives the provider. A module whose own // `contributes` already names `shares` is refused: the field is the only way to say it (ADR 0468 §8). func withShares(m Manifest, requirement string, values map[string]any) (map[string]any, error) { shares := m.RequireShares[requirement] if _, named := values[SharesValue]; named && requirement == BucketRequirement { return nil, fmt.Errorf("%s contributing to %s names `shares`; write it on its requirement instead (novox/hq ADR 0468 §8)", m.Module, requirement) } if len(shares) == 0 { return values, nil } out := make(map[string]any, len(values)+1) for k, v := range values { out[k] = v } listed := []any{} for _, s := range sortedShares(shares) { methods := []any{} for _, method := range s.Methods { methods = append(methods, method) } listed = append(listed, map[string]any{"to": s.To, "methods": methods}) } out[SharesValue] = listed return out, nil }