mesh/merge-gate head d3e182503c19 merged into trunk c8b07242a009 — pass: [release feat/57-config-and-provisioning, judged by its controller fcdecb436117] t…
mesh/repo-check head d3e182503c19 merged into trunk c8b07242a009 — pass: [release feat/57-config-and-provisioning, judged by its controller fcdecb436117] i…
mesh/delivery delivered
Providers, the state bus, templates and the environment collection that the feature's modules build on (feature 57, config and provisioning).
73 lines
2.9 KiB
Go
73 lines
2.9 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
)
|
|
|
|
// The provider's consumers' credentials, from the file its machine's provisioner client fills (novox/hq ADR 0373,
|
|
// issue 679).
|
|
//
|
|
// **The provider never asks the vault.** A provider in the tool runner has no account of its own, and the vault
|
|
// hands a secret only to a node's provisioner client or to a module under an account of its own (ADR 0349 clause
|
|
// 11, ADR 0370 clause 2). So the provider's template names `${consumers:<provision>}`, and its machine's client
|
|
// fills that file with one entry per consumer bound to it for that provision, each credential asked of the vault in
|
|
// the client's own name; the file is the provider's alone (mode 0600, owned). ConsumersFile is that file as the
|
|
// helper's Vault: it reads the file on each ask, so a consumer that binds or a credential the vault remakes reaches
|
|
// the provider at its next pass.
|
|
|
|
// ConsumerCredential is one entry of the file: the consumer, the vault's name of its credential, and its value.
|
|
type ConsumerCredential struct {
|
|
// Consumer and Module are the consumer's machine and module, Local its local name for the provision, if any.
|
|
Consumer string `json:"consumer"`
|
|
Module string `json:"module"`
|
|
Local string `json:"local,omitempty"`
|
|
// Credential is the vault's name of the consumer's credential, PairName's.
|
|
Credential string `json:"credential"`
|
|
// Value is the credential itself.
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
// ConsumersFile answers a credential from the files the provider machine's client fills: one per provision whose
|
|
// template names `${consumers:<provision>}`, each a JSON list of ConsumerCredential.
|
|
type ConsumersFile struct {
|
|
Paths []string
|
|
}
|
|
|
|
// Value is the credential named name, from the first file carrying it. A file not there yet is passed over; one not
|
|
// in the client's form is an error, never read as no consumers.
|
|
func (f ConsumersFile) Value(_ context.Context, name string) ([]byte, error) {
|
|
var missing []string
|
|
for _, at := range f.Paths {
|
|
raw, err := os.ReadFile(at)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
missing = append(missing, at)
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the consumers' file %s: %w", at, err)
|
|
}
|
|
var entries []ConsumerCredential
|
|
if err := json.Unmarshal(raw, &entries); err != nil {
|
|
return nil, fmt.Errorf("the consumers' file %s is not the provisioner client's list of consumers", at)
|
|
}
|
|
for _, e := range entries {
|
|
if e.Credential == name {
|
|
if e.Value == "" {
|
|
return nil, fmt.Errorf("the consumers' file %s carries %s with no value", at, name)
|
|
}
|
|
return []byte(e.Value), nil
|
|
}
|
|
}
|
|
}
|
|
if len(missing) == len(f.Paths) {
|
|
return nil, fmt.Errorf("%s: no consumers' file is filled yet (%v)", name, f.Paths)
|
|
}
|
|
return nil, fmt.Errorf("%s is in no consumers' file: the vault holds none for it yet, or its machine's client "+
|
|
"has not filled it", name)
|
|
}
|