A take acts on the preview it showed; a setting is judged where it is stored; a kept network and a minted secret are said (hq ADR 0163)
take ends its preview with a digest and --yes names it, as the flip does; a changed preview or an account older than the flip allows is refused. A module the machine holds nothing for has nothing to compare, and --yes suffices. A published port's reach is said as the machine reported it. Every secret the module holds on the machine is listed with where it came from, and one the mesh minted for a service whose data was found refuses unless --mint names it. One judgement of a module's settings against its definition, in the catalogue: settings set refuses what cannot compose or reaches nothing, naming node, module, layer and key; Compose leaves out a module whose definition moved under a stored setting, the envelope says so (left_out), plan and push say it by name, and the machine is told everything else. A stray setting no longer refuses the whole machine where it is read (issue 096). The per-machine setting networks keeps a found network for a taken container, on an adopted machine only; the container's declaration carries it and the preview names it (rule 4).
This commit is contained in:
@@ -605,6 +605,12 @@ func (i *Inventory) SetSettings(ctx context.Context, nodeName, module string, va
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Judged here, against the module's current definition, before it is kept (novox/hq ADR 0163,
|
||||
// rule 6): a setting that cannot compose is refused where it is set, naming the node, the
|
||||
// module, the layer and the key — never stored to refuse the whole machine where it is read.
|
||||
if err := i.judgeSettings(ctx, nodeName, module, values); err != nil {
|
||||
return err
|
||||
}
|
||||
if nodeName == "" {
|
||||
// A port is a fact about one machine (novox/hq ADR 0100). Refused here, in composition's
|
||||
// words: stored, it refuses every node running the module at composition, and the mesh
|
||||
@@ -661,6 +667,50 @@ func (i *Inventory) SetSettings(ctx context.Context, nodeName, module string, va
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// judgeSettings composes a layer somebody is about to store against the module's definition, with
|
||||
// the mesh-wide layer under it when the layer is one node's, and refuses the first thing that
|
||||
// cannot work (ADR 0163, rule 6). The same judgement composition makes; what passes here composes.
|
||||
func (i *Inventory) judgeSettings(ctx context.Context, nodeName, module string, values map[string]any) error {
|
||||
m, err := i.declared(ctx, module)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrNoSuchModule, module)
|
||||
}
|
||||
where, from := "the mesh", catalogue.MeshWideLayer
|
||||
adopted := false
|
||||
var layers []catalogue.Layer
|
||||
if nodeName != "" {
|
||||
node, err := i.NodeByName(ctx, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
where, from, adopted = nodeName, nodeName, node.Adopted
|
||||
var meshWide []byte
|
||||
err = i.store.Pool().QueryRow(ctx,
|
||||
`select values from settings where module = $1 and node is null`, module).Scan(&meshWide)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
if len(meshWide) > 0 {
|
||||
var under map[string]any
|
||||
if err := json.Unmarshal(meshWide, &under); err != nil {
|
||||
return err
|
||||
}
|
||||
layers = append(layers, catalogue.Layer{From: catalogue.MeshWideLayer, Values: under})
|
||||
}
|
||||
}
|
||||
layers = append(layers, catalogue.Layer{From: from, Values: values})
|
||||
if err := catalogue.JudgeSettings(m, layers, adopted); err != nil {
|
||||
return fmt.Errorf("refused: %s on %s cannot compose with the layer %q — %w", module, where, from, err)
|
||||
}
|
||||
// And a key that reaches nothing, refused here where somebody can still fix the spelling:
|
||||
// stored, it would be a setting somebody believes they made.
|
||||
if stray := catalogue.UnusedSettings(m, layers[len(layers)-1:]); len(stray) > 0 {
|
||||
return fmt.Errorf("refused: %s on %s — these settings reach nothing:\n - %s", module, where,
|
||||
strings.Join(stray, "\n - "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// givenIn is the machine ports a node-level settings layer gives a module, software port →
|
||||
// machine port (novox/hq ADR 0100). Nothing when the layer gives none; what is not a port is left
|
||||
// for composition to refuse in its own words.
|
||||
|
||||
@@ -31,8 +31,11 @@ func TestRegisteringAModuleAgainKeepsWhatTheMeshHoldsForIt(t *testing.T) {
|
||||
if err := inv.RecordSealingKey(ctx, node.ID, key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A mergeable file, so any setting composes (novox/hq ADR 0163, rule 6: a setting is judged
|
||||
// where it is stored).
|
||||
m := catalogue.Manifest{Module: "step-ca", Version: "1",
|
||||
Provides: catalogue.Offers("acme-ca"), OwnSecrets: catalogue.OwnSecrets{"password": {Path: "/run/password"}}}
|
||||
Provides: catalogue.Offers("acme-ca"), OwnSecrets: catalogue.OwnSecrets{"password": {Path: "/run/password"}},
|
||||
Resources: []map[string]any{{"id": "conf", "type": "file", "path": "/etc/step-ca.json", "content": "{}", "merge": "json"}}}
|
||||
if err := inv.RegisterModule(ctx, m, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -229,5 +232,9 @@ func TestAModuleStillAssignedRefusesBeforeAnythingAboutWhatItHolds(t *testing.T)
|
||||
// declares (novox/hq 04-ISSUES/078).
|
||||
func withOwnSecret(m catalogue.Manifest, name string) catalogue.Manifest {
|
||||
m.OwnSecrets = catalogue.OwnSecrets{name: {Path: "/run/" + name}}
|
||||
// And a mergeable file, so any setting these tests store composes (novox/hq ADR 0163, rule 6:
|
||||
// a setting is judged where it is stored).
|
||||
m.Resources = append(m.Resources, map[string]any{"id": "conf", "type": "file",
|
||||
"path": "/etc/" + m.Module + ".json", "content": "{}", "merge": "json"})
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -15,9 +15,16 @@ func aNodeWithModules(t *testing.T, modules ...string) (*Inventory, string) {
|
||||
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Each publishes the port these tests give it a machine port for: a port given for one the
|
||||
// module does not publish is refused where it is stored (novox/hq ADR 0163, rule 6).
|
||||
publishes := map[string]string{"postgres": "5432", "another-database": "5432", "cache": "6379", "web": "8080"}
|
||||
for _, m := range modules {
|
||||
if err := inv.RegisterModule(ctx,
|
||||
catalogue.Manifest{Module: m, Version: "1"}, Source{}); err != nil {
|
||||
manifest := catalogue.Manifest{Module: m, Version: "1"}
|
||||
if port, known := publishes[m]; known {
|
||||
manifest.Resources = []map[string]any{{"id": "server", "type": "container", "name": m,
|
||||
"image": "x", "ports": []any{port}}}
|
||||
}
|
||||
if err := inv.RegisterModule(ctx, manifest, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,3 +820,52 @@ func (i *Inventory) SharedHolders(ctx context.Context, provider, providerModule,
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SecretState is one secret a module holds on a machine, as a take compares it (novox/hq ADR
|
||||
// 0163): its name, where it came from — made by the mesh or accepted from a person — and, for a
|
||||
// credential the module requires from a provider, which node provides it and the local name it
|
||||
// goes by where the module keeps several.
|
||||
type SecretState struct {
|
||||
Name string
|
||||
// Local is the credential's name inside the module (ADR 0094); empty for an own secret or the
|
||||
// ordinary one.
|
||||
Local string
|
||||
// Origin is OriginMade or OriginAccepted.
|
||||
Origin string
|
||||
// Provider is the node providing a required secret; empty for the module's own.
|
||||
Provider string
|
||||
}
|
||||
|
||||
// Own says the secret is the module's own rather than one it requires from a provider.
|
||||
func (s SecretState) Own() bool { return s.Provider == "" }
|
||||
|
||||
// SecretsOf is every secret a module holds on a machine: its own, and each credential it requires
|
||||
// from a provider — with where each value came from. What a take reads to refuse minting over a
|
||||
// service that already has one (ADR 0163, rule 2).
|
||||
func (i *Inventory) SecretsOf(ctx context.Context, node, module string) ([]SecretState, error) {
|
||||
record, err := i.NodeByName(ctx, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select name, '' as local, origin, '' as provider from module_secret
|
||||
where node = $1 and module = $2
|
||||
union all
|
||||
select s.name, s.local, s.origin, p.name from secret s
|
||||
join node p on p.id = s.provider
|
||||
where s.consumer = $1 and s.consumer_module = $2
|
||||
order by 4, 1, 2`, record.ID, module)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []SecretState
|
||||
for rows.Next() {
|
||||
var s SecretState
|
||||
if err := rows.Scan(&s.Name, &s.Local, &s.Origin, &s.Provider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
@@ -934,3 +934,47 @@ func TestASharedCredentialIsOneValueSealedToEveryHolder(t *testing.T) {
|
||||
t.Fatalf("a consumer binding after an acceptance must be refused with the way out: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SecretsOf is every secret a module holds on a machine with where each came from — what a take
|
||||
// reads to refuse minting over a service that already has a value (novox/hq ADR 0163, rule 2).
|
||||
func TestSecretsOfSaysEachSecretsOriginAndProvider(t *testing.T) {
|
||||
inv, ctx := twoNodesWithKeys(t)
|
||||
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "forge", Version: "1",
|
||||
Requires: []string{"secret", "postgres-database"},
|
||||
Secrets: map[string]string{"secret": "/run/secret", "postgres-database": "/run/pg"},
|
||||
OwnSecrets: catalogue.OwnSecrets{"admin": {Path: "/run/admin"}}}, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.SecretForModule(ctx, "consumer", "forge", "admin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.SecretFor(ctx, "postgres-database", "consumer", "forge", "provider", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.AcceptSecretForPair(ctx, "secret", "consumer", "forge", "provider", "", "hunter2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := inv.SecretsOf(ctx, "consumer", "forge")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []SecretState{
|
||||
{Name: "admin", Origin: OriginMade},
|
||||
{Name: "postgres-database", Origin: OriginMade, Provider: "provider"},
|
||||
{Name: "secret", Origin: OriginAccepted, Provider: "provider"},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("got %+v", got)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("secret %d: got %+v, want %+v", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
if !got[0].Own() || got[1].Own() {
|
||||
t.Error("own and required are not told apart")
|
||||
}
|
||||
if other, _ := inv.SecretsOf(ctx, "consumer", "gitea"); len(other) != 0 {
|
||||
t.Fatalf("another module's secrets: %+v", other)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// A setting is judged where it is stored (novox/hq ADR 0163, rule 6): one that cannot compose with
|
||||
// the module's definition is refused naming the node, the module, the layer and the key, and is not
|
||||
// kept; one that reaches nothing is refused the same way.
|
||||
func TestASettingIsJudgedWhereItIsStored(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := t.Context()
|
||||
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
web := catalogue.Manifest{Module: "web", Version: "1",
|
||||
Resources: []map[string]any{
|
||||
{"id": "server", "type": "container", "name": "web", "image": "x", "ports": []any{"8080"}},
|
||||
{"id": "conf", "type": "file", "path": "/etc/web.json", "content": "{}", "merge": "json"},
|
||||
}}
|
||||
plain := catalogue.Manifest{Module: "plain", Version: "1",
|
||||
Resources: []map[string]any{{"id": "server", "type": "container", "name": "plain", "image": "x"}}}
|
||||
for _, m := range []catalogue.Manifest{web, plain} {
|
||||
if err := inv.RegisterModule(ctx, m, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
err := inv.SetSettings(ctx, "anchor", "web", map[string]any{catalogue.PortsSetting: map[string]any{"9999": 10000}})
|
||||
for _, want := range []string{"refused: web on anchor", `layer "anchor"`, "9999"} {
|
||||
if err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("a port the module does not publish: %v, want %q", err, want)
|
||||
}
|
||||
}
|
||||
if layers, _ := inv.SettingsFor(ctx, "anchor", "web"); len(layers) != 0 {
|
||||
t.Fatalf("the refused layer was stored: %v", layers)
|
||||
}
|
||||
// A key that reaches nothing is refused too, where the spelling can still be fixed; a module
|
||||
// with a mergeable file takes any key.
|
||||
err = inv.SetSettings(ctx, "", "plain", map[string]any{"colour": "blue"})
|
||||
if err == nil || !strings.Contains(err.Error(), "reach nothing") || !strings.Contains(err.Error(), `"colour"`) {
|
||||
t.Fatalf("a stray key was stored: %v", err)
|
||||
}
|
||||
if err := inv.SetSettings(ctx, "", "web", map[string]any{"colour": "blue"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The mesh-wide layer is under the node's when the node's is judged.
|
||||
if err := inv.SetSettings(ctx, "anchor", "web", map[string]any{catalogue.PortsSetting: map[string]any{"8080": 10000}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A kept network is for an adopted machine only (rule 4).
|
||||
keep := map[string]any{catalogue.NetworksSetting: map[string]any{"server": []any{"predecessor_default"}}}
|
||||
err = inv.SetSettings(ctx, "anchor", "plain", keep)
|
||||
if err == nil || !strings.Contains(err.Error(), "anchor is converged") {
|
||||
t.Fatalf("a kept network on a converged machine was stored: %v", err)
|
||||
}
|
||||
if err := inv.SetAdopted(ctx, "anchor", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.SetSettings(ctx, "anchor", "plain", keep); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.SetSettings(ctx, "anchor", "nothing", keep); err == nil {
|
||||
t.Fatal("a setting for a module the mesh does not know was stored")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user