258 lines
9.7 KiB
Go
258 lines
9.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// A provider's consumers' credentials, and a consumer's values from its provider (novox/hq ADR 0349 clause 12, ADR
|
|
// 0373, issue 679).
|
|
//
|
|
// **`${consumers:<provision>}`: each consumer bound to this provider, with its credential.** A provider in the tool
|
|
// runner may not ask the vault (ADR 0349 clause 11, ADR 0370 clause 2), so its machine's client fills its file. A
|
|
// consumer binds the provider where its plan, on any machine, names this machine and this module as the provider
|
|
// of the provision (issue 760); so the client reads every machine's plans (the whole assignments mirror, which
|
|
// carries no secret's value) for who binds, and asks the vault for each consumer's own credential,
|
|
// `pair/<provider>/<provision>/<node>/<module>[/<local>]`, which the vault hands the provider's machine while the
|
|
// provider is handed there (mesh-vault pair.go). The file lists one entry per consumer, in the order of the
|
|
// assignments' keys and, within one plan, of its provisions entries: a JSON list, each naming the consumer and its
|
|
// credential's name and carrying its value, which the provider's helper reads (mesh-sdk go/provider ConsumersFile).
|
|
// A consumer whose credential the vault does not hold yet, or will not hand, is left out and said, so the others are
|
|
// served; the provider's helper waits for it. An entry with a name is a credential the provider's software issued
|
|
// per holder (issue 757), which the provider holds already: not listed; nor is a bind whose offer names another
|
|
// credential form than a login per consumer (issue 768): a shared value is the provider's own secret, and an issued
|
|
// or given one is not the vault's to make. The file is the provider's alone: the
|
|
// catalogue holds it to mode 0600 and owned (mesh-controller internal/catalogue consumers.go). It is filled again
|
|
// when a consumer binds or unbinds, and when the vault says one of the credentials changed.
|
|
//
|
|
// **`${bound:<provision>:<key>}`: the consumer's entry in its provider's bucket.** The provider's helper writes one
|
|
// entry per consumer to its own bucket, `<provider module>_bound`, keyed `<node>.<module>.<provision>[.<local>]`
|
|
// (mesh-sdk go/provider, issue 678), as the registry says. The client watches its own machine's keys of the bucket
|
|
// of each provider its plans name, and fills a key from the entry: `as`, `user`, or one of its served values. An
|
|
// entry the provider refused fails the module with the provider's reason; one not written yet is waited for.
|
|
|
|
// ConsumersKind is the namespace of a provider's consumers' credentials, `${consumers:<provision>}`.
|
|
const ConsumersKind = "consumers"
|
|
|
|
// BoundKind is the namespace of a consumer's value from its provider, `${bound:<provision>:<key>}`.
|
|
const BoundKind = "bound"
|
|
|
|
// boundSuffix ends a provider's bucket of entries: `<provider module>_bound`.
|
|
const boundSuffix = "_bound"
|
|
|
|
// bindsOf is as much of any machine's assignment as the client reads for who binds a provider: lenient, since
|
|
// another machine's plan is that machine's client's to act on.
|
|
type bindsOf struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Provisioning *struct {
|
|
Provisions []PlannedProvision `json:"provisions"`
|
|
} `json:"provisioning"`
|
|
}
|
|
|
|
// binding is one assignment's binds: its machine and module, and its plan's provisions in order.
|
|
type binding struct {
|
|
node, module string
|
|
provisions []PlannedProvision
|
|
}
|
|
|
|
// takeBinds folds one assignment, of any machine, into who binds what; it answers whether that changed.
|
|
func takeBinds(s *state, key string, value []byte, gone bool) bool {
|
|
var was *binding
|
|
if b, ok := s.binds[key]; ok {
|
|
was = &b
|
|
}
|
|
if gone {
|
|
delete(s.binds, key)
|
|
return was != nil
|
|
}
|
|
var a bindsOf
|
|
if err := json.Unmarshal(value, &a); err != nil || a.Node+"."+a.Module != key {
|
|
logf("[provisioner-client] the assignment %s is not the controller's, so whom it binds is not read", key)
|
|
delete(s.binds, key)
|
|
return was != nil
|
|
}
|
|
b := binding{node: a.Node, module: a.Module}
|
|
if a.Provisioning != nil {
|
|
b.provisions = a.Provisioning.Provisions
|
|
}
|
|
s.binds[key] = b
|
|
return was == nil || was.node != b.node || was.module != b.module || !reflect.DeepEqual(was.provisions, b.provisions)
|
|
}
|
|
|
|
// PairName is one consumer's own credential as the vault names it (mesh-vault pair.go, mesh-sdk go/provider
|
|
// PairName).
|
|
func PairName(provider, provision, node, module, local string) string {
|
|
n := "pair/" + provider + "/" + provision + "/" + node + "/" + module
|
|
if local != "" {
|
|
n += "/" + local
|
|
}
|
|
return n
|
|
}
|
|
|
|
// consumerListed is one entry of a provider's consumers' file (mesh-sdk go/provider ConsumerCredential).
|
|
type consumerListed struct {
|
|
Consumer string `json:"consumer"`
|
|
Module string `json:"module"`
|
|
Local string `json:"local,omitempty"`
|
|
Credential string `json:"credential"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
// consumersOf is the file text of a provider's consumers for one provision, or why it cannot be filled.
|
|
func (c *Client) consumersOf(ctx context.Context, s *state, module string, n Need) (value, wait, fail string) {
|
|
s.providing[module] = true
|
|
list := []consumerListed{}
|
|
var left []string
|
|
for _, key := range sortedKeys(s.binds) {
|
|
b := s.binds[key]
|
|
for _, pr := range b.provisions {
|
|
if pr.issuedForHolder() || pr.Provider != c.Node || pr.ProviderModule != module || pr.Provision != n.Name ||
|
|
(pr.CredentialForm != "" && pr.CredentialForm != "pair") {
|
|
continue
|
|
}
|
|
name := PairName(c.Node, n.Name, b.node, b.module, pr.Local)
|
|
v, w, f := c.vaultValue(ctx, s, n, name)
|
|
switch {
|
|
case f != "":
|
|
left = append(left, f)
|
|
continue
|
|
case w != "":
|
|
left = append(left, w)
|
|
continue
|
|
}
|
|
list = append(list, consumerListed{Consumer: b.node, Module: b.module, Local: pr.Local, Credential: name,
|
|
Value: v})
|
|
}
|
|
}
|
|
said := strings.Join(left, "; ")
|
|
if s.leftOut[module+" "+n.Name] != said {
|
|
s.leftOut[module+" "+n.Name] = said
|
|
if said != "" {
|
|
logf("[provisioner-client] %s: consumers left out of %s until the vault hands their credential: %s",
|
|
module, n, said)
|
|
}
|
|
}
|
|
text, err := json.Marshal(list)
|
|
if err != nil {
|
|
return "", "", fmt.Sprintf("%s: %v", n, err)
|
|
}
|
|
return string(text), "", ""
|
|
}
|
|
|
|
// boundEntry is as much of a provider's entry as a consumer's file reads (mesh-sdk go/provider Entry).
|
|
type boundEntry struct {
|
|
As string `json:"as"`
|
|
User string `json:"user"`
|
|
Values map[string]any `json:"values"`
|
|
Refused string `json:"refused"`
|
|
}
|
|
|
|
// boundOf is a consumer's value from its provider's entry, or what it waits for, or why it cannot be filled.
|
|
func (c *Client) boundOf(s *state, module string, p Plan, n Need, answer Answer) (value, wait, fail string) {
|
|
provision, key, ok := strings.Cut(n.Name, ":")
|
|
if !ok || provision == "" || key == "" {
|
|
return "", "", n.String() + ": a bound value is ${bound:<provision>:<key>}"
|
|
}
|
|
key, fallback, defaulted := strings.Cut(key, "|default:")
|
|
var pr *PlannedProvision
|
|
for i := range p.Provisions {
|
|
q := p.Provisions[i]
|
|
if q.issuedForHolder() || q.Provision != provision {
|
|
continue
|
|
}
|
|
if pr == nil || q.Local == "" {
|
|
pr = &p.Provisions[i]
|
|
}
|
|
}
|
|
switch {
|
|
case pr == nil && defaulted:
|
|
return fallback, "", ""
|
|
case pr == nil:
|
|
return "", "", fmt.Sprintf("%s: the plan names no provider of %s for %s here", n, provision, module)
|
|
case pr.ProviderModule == "":
|
|
return "", "", fmt.Sprintf("%s: the plan names no provider module of %s", n, provision)
|
|
}
|
|
bucket := strings.Replace(answer.Bucket, "<provider-module>", pr.ProviderModule, 1)
|
|
if bucket != pr.ProviderModule+boundSuffix {
|
|
return "", "", fmt.Sprintf("%s: the registry answers bound from %q, which this client does not read", n,
|
|
answer.Bucket)
|
|
}
|
|
if s.boundUsers[bucket] == nil {
|
|
s.boundUsers[bucket] = map[string]bool{}
|
|
}
|
|
s.boundUsers[bucket][module] = true
|
|
if c.watchBound == nil {
|
|
return "", "", n.String() + ": this client watches no provider's bucket"
|
|
}
|
|
if err := c.watchBound(bucket); err != nil {
|
|
return "", fmt.Sprintf("%s (the provider's bucket %s cannot be watched yet: %v)", n, bucket, err), ""
|
|
}
|
|
if !s.boundReady[bucket] {
|
|
return "", fmt.Sprintf("%s (the entries of %s are not read yet)", n, bucket), ""
|
|
}
|
|
entryKey := c.Node + "." + module + "." + provision
|
|
if pr.Local != "" {
|
|
entryKey += "." + pr.Local
|
|
}
|
|
raw, ok := s.bound[bucket][entryKey]
|
|
if !ok {
|
|
return "", fmt.Sprintf("%s (%s on %s has not answered %s on %s yet)", n, pr.ProviderModule, pr.Provider,
|
|
module, c.Node), ""
|
|
}
|
|
var e boundEntry
|
|
if err := json.Unmarshal(raw, &e); err != nil {
|
|
return "", "", fmt.Sprintf("%s: the entry %s of %s is not the provider's", n, entryKey, bucket)
|
|
}
|
|
if e.Refused != "" {
|
|
return "", "", fmt.Sprintf("%s: %s on %s refused it: %s", n, pr.ProviderModule, pr.Provider, e.Refused)
|
|
}
|
|
switch key {
|
|
case "as":
|
|
if e.As != "" {
|
|
return e.As, "", ""
|
|
}
|
|
case "user":
|
|
if e.User != "" {
|
|
return e.User, "", ""
|
|
}
|
|
}
|
|
if v, ok := e.Values[key]; ok {
|
|
return plainly(v), "", ""
|
|
}
|
|
have := []string{}
|
|
if e.As != "" {
|
|
have = append(have, "as")
|
|
}
|
|
if e.User != "" {
|
|
have = append(have, "user")
|
|
}
|
|
have = append(have, sortedKeys(e.Values)...)
|
|
return "", "", fmt.Sprintf("%s: %s on %s serves %s no %q; it serves %s", n, pr.ProviderModule, pr.Provider, module,
|
|
key, strings.Join(have, ", "))
|
|
}
|
|
|
|
// plainly is a served value as a file takes it: a string as it is, a whole number without a fraction, anything else
|
|
// as its JSON (mesh-controller internal/catalogue plainly).
|
|
func plainly(v any) string {
|
|
switch x := v.(type) {
|
|
case string:
|
|
return x
|
|
case float64:
|
|
if x == float64(int64(x)) {
|
|
return strconv.FormatInt(int64(x), 10)
|
|
}
|
|
return strconv.FormatFloat(x, 'f', -1, 64)
|
|
case bool:
|
|
return strconv.FormatBool(x)
|
|
case nil:
|
|
return ""
|
|
}
|
|
raw, _ := json.Marshal(v)
|
|
return string(raw)
|
|
}
|