catalogue: a module may name the machine it was assigned to
An authority inside the mesh is reached at <machine>.internal, so its own
certificate must be issued for that name — and it is the one module that cannot
be told its name by a binding, because it provides rather than requires. Written
as a literal it would be one deployment's machine name in a manifest, which is
what ADR 0056 exists to remove.
${machine:name} and ${machine:at}, beside the bound values and refused the same
way. An address the machine does not have is named here rather than discovered
later as a certificate nobody can verify.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -421,6 +421,8 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
|
|||||||
}
|
}
|
||||||
// And what its bindings say, for the half of a connection that is not secret.
|
// And what its bindings say, for the half of a connection that is not secret.
|
||||||
known := knownFor(m, r.Needs, r.Node)
|
known := knownFor(m, r.Needs, r.Node)
|
||||||
|
// And the machine underneath, which no binding of its own can tell it.
|
||||||
|
thisMachine := machineFacts(r)
|
||||||
|
|
||||||
for _, unsettled := range resources {
|
for _, unsettled := range resources {
|
||||||
resource, err := ApplySettings(unsettled, with.Settings[m.Module])
|
resource, err := ApplySettings(unsettled, with.Settings[m.Module])
|
||||||
@@ -444,6 +446,13 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
|
|||||||
if err := boundInto(copied, known, m.Module); err != nil {
|
if err := boundInto(copied, known, m.Module); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// And what the module could not have written: the machine it turned out to be
|
||||||
|
// assigned to. Beside the bound values because it is the same kind of fact — the
|
||||||
|
// mesh's own, held in the clear — and because a module that must name itself to
|
||||||
|
// something else has no binding to learn it from (novox/hq ADR 0056).
|
||||||
|
if err := machineInto(copied, thisMachine, m.Module); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if err := pinned(copied, m.Module); err != nil {
|
if err := pinned(copied, m.Module); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// What a module may say about the machine it is running on.
|
||||||
|
//
|
||||||
|
// **A module cannot know where it will be assigned, and sometimes it must say so anyway.** Every
|
||||||
|
// other name in a declaration is either the module's own — which it wrote — or something it
|
||||||
|
// requires, which arrives as a binding. The machine underneath is neither: it is chosen when the
|
||||||
|
// module is assigned, long after the manifest was written, and until now nothing carried it into a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// The case that found this is a certificate authority inside the mesh (novox/hq ADR 0056). A proxy
|
||||||
|
// reaches it at the address the mesh handed over, `<machine>.internal` — so the authority's own
|
||||||
|
// certificate has to be issued for that name, or the first thing that happens is the proxy refusing
|
||||||
|
// to talk to it. The authority is the one thing that cannot be told its name by a binding: it
|
||||||
|
// provides, it does not require. Written as a literal it would be a manifest carrying one
|
||||||
|
// deployment's machine name, which is the shape [ADR 0056] exists to remove.
|
||||||
|
//
|
||||||
|
// Two facts, both the mesh's own vocabulary — the same `node` and `at` a contribution already
|
||||||
|
// carries. Nothing about what a machine is *for*: that would be the mesh learning what a module
|
||||||
|
// means, which it does not do.
|
||||||
|
|
||||||
|
// ofMachine is where a module says a fact about the machine underneath it belongs:
|
||||||
|
// ${machine:<key>}.
|
||||||
|
var ofMachine = regexp.MustCompile(`\$\{machine:([a-z0-9][a-z0-9_-]*)\}`)
|
||||||
|
|
||||||
|
// machineUsed are the keys a file's content asks for, first appearance first.
|
||||||
|
func machineUsed(content string) []string {
|
||||||
|
var used []string
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, m := range ofMachine.FindAllStringSubmatch(content, -1) {
|
||||||
|
if key := m[1]; !seen[key] {
|
||||||
|
seen[key] = true
|
||||||
|
used = append(used, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return used
|
||||||
|
}
|
||||||
|
|
||||||
|
// machineFacts is what a module may name about the machine it was assigned to.
|
||||||
|
//
|
||||||
|
// `at` is absent rather than empty when the machine is not on the private network. A module asking
|
||||||
|
// to be reached at an address that does not exist is a misconfiguration, and it is said here —
|
||||||
|
// where the module and the machine are both named — rather than discovered later as a certificate
|
||||||
|
// nobody can verify.
|
||||||
|
func machineFacts(r Resolution) map[string]string {
|
||||||
|
out := map[string]string{"name": r.Node}
|
||||||
|
if r.At != "" {
|
||||||
|
out["at"] = r.At
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// machineInto replaces a file's ${machine:…} placeholders with what the mesh knows about the
|
||||||
|
// machine the module was assigned to.
|
||||||
|
//
|
||||||
|
// A key the mesh does not hold is refused, for the same reason a binding's is: left alone, the
|
||||||
|
// literal would be written into a configuration file and read as a value.
|
||||||
|
func machineInto(resource map[string]any, facts map[string]string, module string) error {
|
||||||
|
if fmt.Sprint(resource["type"]) != "file" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
content, ok := resource["content"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, key := range machineUsed(content) {
|
||||||
|
value, has := facts[key]
|
||||||
|
if !has {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%s has a file that says ${machine:%s}, and this machine says %s",
|
||||||
|
module, key, orNothing(namesOfFacts(facts)))
|
||||||
|
}
|
||||||
|
resource["content"] = strings.ReplaceAll(
|
||||||
|
content, fmt.Sprintf("${machine:%s}", key), value)
|
||||||
|
content = resource["content"].(string)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func namesOfFacts(facts map[string]string) []string {
|
||||||
|
out := make([]string, 0, len(facts))
|
||||||
|
for k := range facts {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// novox/hq ADR 0056 — a module that must name ITSELF to something else.
|
||||||
|
//
|
||||||
|
// The authority inside the mesh is the case. A proxy reaches it at the address the mesh handed
|
||||||
|
// over, so its certificate has to be issued for that name — and it has no binding to learn the name
|
||||||
|
// from, because it provides rather than requires. Written as a literal it would be one deployment's
|
||||||
|
// machine name living in a manifest, which is the shape the decision exists to remove.
|
||||||
|
|
||||||
|
// anchored is a machine on the private network, which is where a name worth certifying comes from.
|
||||||
|
func anchored() Node {
|
||||||
|
n := workstation()
|
||||||
|
n.At = "workstation.internal"
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// authority is a module that must put its own machine's name into a file it writes.
|
||||||
|
func authority() Manifest {
|
||||||
|
return Manifest{
|
||||||
|
Module: "authority", Version: "1",
|
||||||
|
Provides: FromAnywhere("acme-ca"),
|
||||||
|
Serves: map[string]map[string]any{"acme-ca": {}},
|
||||||
|
Resources: []map[string]any{{
|
||||||
|
"id": "init", "type": "file", "path": "/var/lib/authority/init.env",
|
||||||
|
"content": "NAMES=${machine:at},${machine:name},localhost\n",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAModuleNamesTheMachineItWasAssignedTo(t *testing.T) {
|
||||||
|
got, err := Resolve(shelf(authority()), []string{"authority"}, anchored(), World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := ""
|
||||||
|
for _, r := range mustDeclare(t, got) {
|
||||||
|
if strings.HasSuffix(plainly(r["id"]), "init") {
|
||||||
|
content = plainly(r["content"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if content == "" {
|
||||||
|
t.Fatal("the authority's file was not declared")
|
||||||
|
}
|
||||||
|
if strings.Contains(content, "${machine:") {
|
||||||
|
t.Fatalf("the placeholder was written into the file as a value: %q", content)
|
||||||
|
}
|
||||||
|
// The machine's own name on the private network — the one a consumer is handed as `at`, and so
|
||||||
|
// the one a certificate has to carry.
|
||||||
|
if !strings.Contains(content, "workstation.internal") {
|
||||||
|
t.Errorf("the file does not name the address consumers reach it at: %q", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A machine not on the private network has no such address, and a module asking to be reached at
|
||||||
|
// one is named here rather than left to fail later as a certificate nobody can verify.
|
||||||
|
func TestAnAddressAMachineDoesNotHaveIsRefused(t *testing.T) {
|
||||||
|
got, err := Resolve(shelf(authority()), []string{"authority"}, workstation(), World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := got.Declaration(Rendering{}); err == nil {
|
||||||
|
t.Fatal("an address the machine does not have was substituted rather than refused")
|
||||||
|
} else if !strings.Contains(err.Error(), "at") {
|
||||||
|
t.Errorf("the refusal does not name what was asked for: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user