125 lines
4.7 KiB
Go
125 lines
4.7 KiB
Go
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:<seat name>`. `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:<seat name> (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
|
|
}
|