Feature 57 (config and provisioning): release mesh-controller, the reviewed release branch merged with main, on top of main
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// Each of the four secret commands honours the switch at its own call site (the review of issue 675): run as the
|
||||
// terminal runs them, against the real store and a real bus with a stand-in vault serving the vault's seat, a
|
||||
// module filled the old way takes today's path and the vault is never asked, and one filled the new way reaches
|
||||
// the vault. Sending every module to the vault at any one call site — or none — fails one of these. Needs the
|
||||
// store (MESH_TEST_POSTGRES), and is named in testdata/store-tests.
|
||||
|
||||
// vaultOnTheBus serves the vault's four intake verbs on the test's bus with a stand-in vault, and says which verbs
|
||||
// were asked, in order.
|
||||
type vaultOnTheBus struct {
|
||||
vault *standInVault
|
||||
mu sync.Mutex
|
||||
asked []string
|
||||
}
|
||||
|
||||
func (v *vaultOnTheBus) verbs() string {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
return strings.Join(v.asked, ",")
|
||||
}
|
||||
|
||||
func (v *vaultOnTheBus) reset() {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
v.asked = nil
|
||||
}
|
||||
|
||||
// aMeshWithAVault is aMesh with the controller's bus pointed at a test bus where a stand-in vault serves its seat,
|
||||
// and the switch saying that only the modules named are filled the new way.
|
||||
func aMeshWithAVault(t *testing.T) (*stores, *vaultOnTheBus, func(moved ...string)) {
|
||||
t.Helper()
|
||||
open := aMesh(t)
|
||||
conn, err := nats.Connect(testbus.URL(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(conn.Close)
|
||||
before := handActConn
|
||||
handActConn = conn
|
||||
t.Cleanup(func() { handActConn = before })
|
||||
|
||||
served := &vaultOnTheBus{vault: newStandInVault(t)}
|
||||
for _, verb := range []string{"accept", "secret-ask", "issued", "rotate"} {
|
||||
sub, err := conn.Subscribe(broker.CalledPattern(link.SeatToolSubject("mesh-vault", verb)), func(m *nats.Msg) {
|
||||
served.mu.Lock()
|
||||
served.asked = append(served.asked, verb)
|
||||
served.mu.Unlock()
|
||||
var args map[string]any
|
||||
_ = json.Unmarshal(m.Data, &args)
|
||||
result, err := served.vault.ask(verb, args)
|
||||
answer := link.Answer{Result: result}
|
||||
if err != nil {
|
||||
answer = link.Answer{Error: err.Error()}
|
||||
}
|
||||
raw, _ := json.Marshal(answer)
|
||||
_ = m.Respond(raw)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = sub.Unsubscribe() })
|
||||
}
|
||||
if err := conn.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
switchWas := filledTheNewWay
|
||||
t.Cleanup(func() { filledTheNewWay = switchWas })
|
||||
moveOnly := func(moved ...string) {
|
||||
filledTheNewWay = switchOf(func(module string) bool {
|
||||
for _, m := range moved {
|
||||
if m == module {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
return open, served, moveOnly
|
||||
}
|
||||
|
||||
// aValueFile is a file holding a value, for `--from`.
|
||||
func aValueFile(t *testing.T, value string) string {
|
||||
t.Helper()
|
||||
file := filepath.Join(t.TempDir(), "value")
|
||||
if err := os.WriteFile(file, []byte(value+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
// `secret accept` of a module's own secret.
|
||||
func TestAcceptOfAnOwnSecretTakesThePathOfItsModulesForm(t *testing.T) {
|
||||
open, vault, moveOnly := aMeshWithAVault(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
|
||||
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/dunst/token"}}})
|
||||
if _, err := assign(ctx, open, "anchor", "dunst"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
accept := []string{"accept", "anchor", "dunst", "token", "--from", aValueFile(t, "the-bots-token")}
|
||||
|
||||
moveOnly("some-other-module")
|
||||
if err := secretCommand(ctx, accept); err != nil {
|
||||
t.Fatalf("today's path: %v", err)
|
||||
}
|
||||
if asked := vault.verbs(); asked != "" {
|
||||
t.Fatalf("the vault was asked %s for a module filled the old way", asked)
|
||||
}
|
||||
if origin, _, err := open.inventory.OwnSecretOrigin(ctx, "anchor", "dunst", "token"); err != nil || origin == "" {
|
||||
t.Fatalf("today's store does not hold it: %q %v", origin, err)
|
||||
}
|
||||
|
||||
moveOnly("dunst")
|
||||
if err := secretCommand(ctx, accept); err != nil {
|
||||
t.Fatalf("the vault's path: %v", err)
|
||||
}
|
||||
if asked := vault.verbs(); asked != "accept,accept" {
|
||||
t.Fatalf("the vault was asked %q for a module filled the new way", asked)
|
||||
}
|
||||
if vault.vault.taken["accept"] != "the-bots-token" {
|
||||
t.Fatalf("the vault opened %q", vault.vault.taken["accept"])
|
||||
}
|
||||
}
|
||||
|
||||
// `secret accept --provider`: the provider's module decides, not the consumer's.
|
||||
func TestAcceptForAProviderTakesThePathOfTheProvidersModule(t *testing.T) {
|
||||
open, vault, moveOnly := aMeshWithAVault(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, catalogue.Manifest{Module: "tv", Version: "1",
|
||||
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh}}})
|
||||
register(t, open, catalogue.Manifest{Module: "requests", Version: "1", Requires: []string{"tv-api"},
|
||||
Secrets: map[string]string{"tv-api": "/var/lib/mesh/requests/tv-api.secret"}})
|
||||
for _, a := range [][2]string{{"anchor", "tv"}, {"laptop", "requests"}} {
|
||||
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
accept := []string{"accept", "laptop", "requests", "tv-api", "--provider", "anchor", "--from",
|
||||
aValueFile(t, "the-pairs-key")}
|
||||
|
||||
// The consumer filled the new way, the provider the old: the provider's module decides, today's path.
|
||||
moveOnly("requests")
|
||||
if err := secretCommand(ctx, accept); err != nil {
|
||||
t.Fatalf("today's path: %v", err)
|
||||
}
|
||||
if asked := vault.verbs(); asked != "" {
|
||||
t.Fatalf("the vault was asked %s for a provider filled the old way", asked)
|
||||
}
|
||||
|
||||
moveOnly("tv")
|
||||
if err := secretCommand(ctx, accept); err != nil {
|
||||
t.Fatalf("the vault's path: %v", err)
|
||||
}
|
||||
if asked := vault.verbs(); asked != "accept,accept" {
|
||||
t.Fatalf("the vault was asked %q for a provider filled the new way", asked)
|
||||
}
|
||||
if said := vault.vault.said(); !strings.Contains(said, `"provider":"anchor"`) || !strings.Contains(said, `"provision":"tv-api"`) {
|
||||
t.Errorf("the vault was told %s", said)
|
||||
}
|
||||
}
|
||||
|
||||
// `secret accept` of a provision's credential issued outside the mesh, given once for its provider.
|
||||
func TestAcceptOfAnOutsideOfferTakesThePathOfTheProvidersModule(t *testing.T) {
|
||||
open, vault, moveOnly := aMeshWithAVault(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, catalogue.Manifest{Module: "tv", Version: "1",
|
||||
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh,
|
||||
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}})
|
||||
register(t, open, catalogue.Manifest{Module: "requests", Version: "1", Requires: []string{"tv-api"},
|
||||
Secrets: map[string]string{"tv-api": "/var/lib/mesh/requests/tv-api.secret"}})
|
||||
for _, a := range [][2]string{{"anchor", "tv"}, {"laptop", "requests"}} {
|
||||
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
accept := []string{"accept", "anchor", "tv", "tv-api", "--from", aValueFile(t, "the-apps-key")}
|
||||
|
||||
moveOnly("requests")
|
||||
// Today's path seals it to every consumer; whether it then sends is not this test's.
|
||||
_ = secretCommand(ctx, accept)
|
||||
if asked := vault.verbs(); asked != "" {
|
||||
t.Fatalf("the vault was asked %s for a provider filled the old way", asked)
|
||||
}
|
||||
|
||||
moveOnly("tv")
|
||||
if err := secretCommand(ctx, accept); err != nil {
|
||||
t.Fatalf("the vault's path: %v", err)
|
||||
}
|
||||
if asked := vault.verbs(); asked != "accept,accept" {
|
||||
t.Fatalf("the vault was asked %q for a provider filled the new way", asked)
|
||||
}
|
||||
if vault.vault.taken["accept"] != "the-apps-key" {
|
||||
t.Fatalf("the vault opened %q", vault.vault.taken["accept"])
|
||||
}
|
||||
}
|
||||
|
||||
// `secret ask` (and `secret accept --at-desk`): the desk is never answered on this bus, so today's path ends at the
|
||||
// prompt; the vault's path is one ask of the vault, which asks the desk itself (issue 723), so it is done without
|
||||
// the controller reaching the desk at all.
|
||||
func TestAnAskAtTheDeskTakesThePathOfItsModulesForm(t *testing.T) {
|
||||
open, vault, moveOnly := aMeshWithAVault(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
|
||||
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/dunst/token"}}})
|
||||
if _, err := assign(ctx, open, "anchor", "dunst"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, line := range [][]string{
|
||||
{"ask", "anchor", "dunst", "token", "--at", "laptop"},
|
||||
{"accept", "anchor", "dunst", "token", "--at-desk", "laptop"},
|
||||
} {
|
||||
vault.reset()
|
||||
moveOnly("some-other-module")
|
||||
err := secretCommand(ctx, line)
|
||||
if err == nil || !strings.Contains(err.Error(), "could not be asked") {
|
||||
t.Fatalf("%v: today's path did not reach the desk: %v", line, err)
|
||||
}
|
||||
if asked := vault.verbs(); asked != "" {
|
||||
t.Fatalf("%v: the vault was asked %s for a module filled the old way", line, asked)
|
||||
}
|
||||
|
||||
vault.reset()
|
||||
moveOnly("dunst")
|
||||
if err := secretCommand(ctx, line); err != nil {
|
||||
t.Fatalf("%v: the vault's path reached the desk itself, or failed: %v", line, err)
|
||||
}
|
||||
// One ask of the vault, which asks the desk in its own name: the controller carries nothing to it.
|
||||
if asked := vault.verbs(); asked != "secret-ask" {
|
||||
t.Fatalf("%v: the vault was asked %q for a module filled the new way", line, asked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `secret issued`: the provider's tool is never answered on this bus, so today's path ends at it; the vault's path
|
||||
// is one ask of the vault, which asks the tool itself (issue 723).
|
||||
func TestAnIssuedValueTakesThePathOfItsProvidersModule(t *testing.T) {
|
||||
open, vault, moveOnly := aMeshWithAVault(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, catalogue.Manifest{Module: "plex", Version: "1", Tools: []string{"plex_token"},
|
||||
Provides: []catalogue.Offer{{Name: "plex-api", Scope: catalogue.ScopeMesh,
|
||||
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedBySoftware, Tool: "plex_token"}}}})
|
||||
register(t, open, catalogue.Manifest{Module: "kometa", Version: "1", Requires: []string{"plex-api"},
|
||||
Secrets: map[string]string{"plex-api": "/var/lib/mesh/kometa/plex-api.secret"}})
|
||||
for _, a := range [][2]string{{"anchor", "plex"}, {"laptop", "kometa"}} {
|
||||
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// The consumer's send makes the pair, so the provision has a holder to take it for.
|
||||
plan, settings, err := planFor(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := declarationFor(ctx, open, "laptop", plan, settings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
moveOnly("kometa")
|
||||
err = secretCommand(ctx, []string{"issued", "plex-api"})
|
||||
if err == nil || !strings.Contains(err.Error(), "could not be asked") {
|
||||
t.Fatalf("today's path did not reach the provider's tool: %v", err)
|
||||
}
|
||||
if asked := vault.verbs(); asked != "" {
|
||||
t.Fatalf("the vault was asked %s for a provider filled the old way", asked)
|
||||
}
|
||||
|
||||
moveOnly("plex")
|
||||
if err := secretCommand(ctx, []string{"issued", "plex-api"}); err != nil {
|
||||
t.Fatalf("the vault's path reached the provider's tool itself, or failed: %v", err)
|
||||
}
|
||||
if asked := vault.verbs(); asked != "issued" {
|
||||
t.Fatalf("the vault was asked %q for a provider filled the new way", asked)
|
||||
}
|
||||
}
|
||||
|
||||
// `secret rotate` of a module's own secret.
|
||||
func TestARotationTakesThePathOfItsModulesForm(t *testing.T) {
|
||||
open, vault, moveOnly := aMeshWithAVault(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
|
||||
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/dunst/token"}}})
|
||||
if _, err := assign(ctx, open, "anchor", "dunst"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
moveOnly()
|
||||
if err := secretCommand(ctx, []string{"accept", "anchor", "dunst", "token", "--from", aValueFile(t, "tok")}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rotate := []string{"rotate", "anchor", "dunst", "token", "--why", "the test rotates it"}
|
||||
|
||||
moveOnly("some-other-module")
|
||||
_ = secretCommand(ctx, rotate) // today's path: rotated in the store, or refused there
|
||||
if asked := vault.verbs(); asked != "" {
|
||||
t.Fatalf("the vault was asked %s for a module filled the old way", asked)
|
||||
}
|
||||
|
||||
moveOnly("dunst")
|
||||
if err := secretCommand(ctx, []string{"rotate", "anchor", "dunst", "token"}); err == nil ||
|
||||
!strings.Contains(err.Error(), "--why") || vault.verbs() != "" {
|
||||
t.Fatalf("a rotation in the vault without a why: %v, the vault asked %q", err, vault.verbs())
|
||||
}
|
||||
if err := secretCommand(ctx, rotate); err != nil {
|
||||
t.Fatalf("the vault's path: %v", err)
|
||||
}
|
||||
if asked := vault.verbs(); asked != "rotate" {
|
||||
t.Fatalf("the vault was asked %q for a module filled the new way", asked)
|
||||
}
|
||||
if said := vault.vault.said(); !strings.Contains(said, `"why":"the test rotates it"`) {
|
||||
t.Errorf("the vault was not told why: %s", said)
|
||||
}
|
||||
}
|
||||
|
||||
// A moved module that runs as an account of its own is announced on the operator's channels before the vault is
|
||||
// asked, and with the announcement failing the vault is never called (issue 836, the N1-give rule).
|
||||
func TestAcceptForAMovedTrustedModuleAnnouncesBeforeTheVaultIsAsked(t *testing.T) {
|
||||
open, vault, moveOnly := aMeshWithAVault(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, catalogue.Manifest{Module: "telegram", Version: "1", RunsAs: "telegram", SecretsOwner: "telegram",
|
||||
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/telegram/token"}}})
|
||||
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
|
||||
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/dunst/token"}}})
|
||||
for _, m := range []string{"telegram", "dunst"} {
|
||||
if _, err := assign(ctx, open, "anchor", m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
moveOnly("telegram", "dunst")
|
||||
was := announceTheGiven
|
||||
t.Cleanup(func() { announceTheGiven = was })
|
||||
var order []string
|
||||
announceTheGiven = func(_ context.Context, node, module, name, how, askedBy string) error {
|
||||
order = append(order, "announce:"+module+":"+vault.verbs())
|
||||
return nil
|
||||
}
|
||||
accept := func(module string) error {
|
||||
return secretCommand(ctx, []string{"accept", "anchor", module, "token", "--from", aValueFile(t, "the-bots-token")})
|
||||
}
|
||||
if err := accept("telegram"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(order, ",") != "announce:telegram:" || vault.verbs() != "accept,accept" {
|
||||
t.Fatalf("announced %v, the vault was asked %q", order, vault.verbs())
|
||||
}
|
||||
|
||||
vault.reset()
|
||||
announceTheGiven = func(context.Context, string, string, string, string, string) error {
|
||||
return errors.New("no channel took it")
|
||||
}
|
||||
err := accept("telegram")
|
||||
if err == nil || !strings.Contains(err.Error(), "nothing was sealed") || vault.verbs() != "" {
|
||||
t.Fatalf("a failed announcement: %v, the vault was asked %q", err, vault.verbs())
|
||||
}
|
||||
// Any other module is kept first and announced after; a failed announcement is said, not undone.
|
||||
if err := accept("dunst"); err != nil || vault.verbs() != "accept,accept" {
|
||||
t.Fatalf("an untrusted module: %v, asked %q", err, vault.verbs())
|
||||
}
|
||||
}
|
||||
|
||||
// issue 851 (hand-act): a moved module's terminal give is recorded in the hand-act log like acceptKept's
|
||||
// (novox/hq ADR 0380 §6), naming where, and never carrying the value.
|
||||
func TestAcceptForAMovedModuleRecordsAHandActAndNeverTheValue(t *testing.T) {
|
||||
open, _, moveOnly := aMeshWithAVault(t)
|
||||
ctx := t.Context()
|
||||
api, err := jetstream.New(handActConn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := api.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: broker.HandActsBucket}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
|
||||
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "/var/lib/mesh/dunst/token"}}})
|
||||
if _, err := assign(ctx, open, "anchor", "dunst"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
moveOnly("dunst")
|
||||
was := announceTheGiven
|
||||
t.Cleanup(func() { announceTheGiven = was })
|
||||
announceTheGiven = func(context.Context, string, string, string, string, string) error { return nil }
|
||||
if err := secretCommand(ctx, []string{"accept", "anchor", "dunst", "token", "--from", aValueFile(t, "the-bots-token")}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
acts, err := link.HandActs(ctx, handActConn, time.Now().Add(-time.Minute))
|
||||
if err != nil || len(acts) != 1 || acts[0].Verb != "secret accept" || acts[0].Cause != "given-at-the-terminal" ||
|
||||
!strings.Contains(acts[0].Why, "at the controller's terminal") {
|
||||
t.Fatalf("recorded %+v (%v)", acts, err)
|
||||
}
|
||||
if strings.Contains(acts[0].Why, "the-bots-token") {
|
||||
t.Error("the act carries the value")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user