Files
mesh-catalog/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/consumers.go
T

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)
}