Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)

This commit is contained in:
2026-10-16 07:37:43 +00:00
parent 1a2c917abf
commit 9652b2d5ce
771 changed files with 52063 additions and 1774 deletions
+8
View File
@@ -171,3 +171,11 @@ rather than skipped: the predecessor already served its own registry name this w
not a whole positive number of bytes takes the route with it — written without the limit, the
predecessor would carry exactly what the module said not to carry, and this module would report
success doing it.
## Handed to the provisioner (issue 792)
The module is marked `moved`, and a small Go program beside the TypeScript adapter (`cmd/route-provider`) runs the SDK's
provider helper for `route`, with no credential. A consumer of a route whose templates name `${bound:route:name-web}`
(a handed gitea's origins file) is filled by its machine's client from this module's entry for it. The entry's values are
the names the controller composed for that consumer, carried in its plan and copied unchanged: the public domain is
placement, so only the controller can join a label to it. The adapter still writes the predecessor proxy's files.
@@ -0,0 +1,37 @@
// route-provider: route-adapter's Go half, one binary the node's runtime launches and speaks MCP to over stdio
// through the Go SDK. It serves no tools; it runs the SDK's provider helper that answers the consumers of `route`
// (issue 792). The TypeScript adapter beside it keeps writing the proxy's files.
//
// stdout is the MCP channel; everything this module says, it says on stderr.
package main
import (
"context"
"fmt"
"os"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/provider"
)
func say(format string, args ...any) {
fmt.Fprintf(os.Stderr, "[route-adapter] "+format+"\n", args...)
}
func main() {
node := os.Getenv("MESH_NODE")
if node == "" {
say("MESH_NODE is not set; answering no consumers")
} else {
p := newProvider(node, provider.StateBus(ModuleName), say)
go func() {
if err := p.Run(context.Background()); err != nil {
say("the provider helper stopped: %v", err)
}
}()
}
if err := stdio.Serve("", nil); err != nil {
say("%v", err)
os.Exit(1)
}
}
@@ -0,0 +1,36 @@
package main
// route-adapter handed to the provisioner (novox/hq ADR 0349 clause 12, issue 792): the module runs the SDK's provider
// helper for `route`, so a consumer of a route — a handed gitea reading `${bound:route:name-web}` — is told its public
// name from the entry in this module's bucket, the one its machine's client fills its files from.
//
// **It composes nothing.** A route's public name is the label its consumer chose joined to the node's public domain,
// which only the controller knows (the domain is placement), so the controller composes it and carries it in the
// consumer's plan entry (`names`); the helper copies it into the entry's values unchanged. There is no credential for
// a route and nothing to make: the traefik files stay the TypeScript adapter's, written from the contributions the
// mesh still gives it.
import (
"git.novox.be/novox/mesh-sdk/go/provider"
)
// ProvisionName is what route-adapter provides, as its manifest says.
const ProvisionName = "route"
// ModuleName is this module.
const ModuleName = "route-adapter"
// newProvider is route-adapter answering its consumers over bus on node: the route, served as the manifest serves it
// (nothing of its own), with no credential, and nothing to make or retire for a consumer.
func newProvider(node string, bus provider.Bus, logf func(string, ...any)) *provider.Provider {
return provider.New(provider.Options{
Node: node,
Module: ModuleName,
Offers: map[string]provider.Offer{ProvisionName: {
Credential: provider.NoCredential,
Serves: map[string]any{},
}},
Bus: bus,
Logf: logf,
})
}
@@ -0,0 +1,72 @@
package main
import (
"context"
"encoding/json"
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/provider"
)
// bucketBus is the helper's bus over in-memory buckets.
type bucketBus struct {
mu sync.Mutex
buckets map[string]map[string][]byte
}
func (b *bucketBus) Watch(_ context.Context, bucket string, handle func(string, []byte)) error {
b.mu.Lock()
defer b.mu.Unlock()
for k, v := range b.buckets[bucket] {
handle(k, v)
}
return nil
}
func (b *bucketBus) Put(_ context.Context, bucket, key string, value []byte) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.buckets[bucket] == nil {
b.buckets[bucket] = map[string][]byte{}
}
b.buckets[bucket][key] = append([]byte(nil), value...)
return nil
}
func (b *bucketBus) Delete(_ context.Context, bucket, key string) error {
b.mu.Lock()
defer b.mu.Unlock()
delete(b.buckets[bucket], key)
return nil
}
// A consumer of a route is answered with the names its plan's entry carries, and nothing of a credential (issue 792):
// the entry's values are the controller's composition, copied unchanged, and no vault is asked.
func TestAConsumerOfARouteIsToldItsNamesFromItsPlanEntry(t *testing.T) {
bus := &bucketBus{buckets: map[string]map[string][]byte{}}
plan, _ := json.Marshal(map[string]any{"node": "g14", "module": "gitea", "provisioning": map[string]any{
"provisions": []any{map[string]any{"provision": ProvisionName, "provider": "novox",
"provider-module": ModuleName, "at": "10.7.0.1", "from": "novox", "as": "mesh_g14_gitea",
"names": map[string]any{"name": "git.example.org", "internal-name": "git.novox.internal",
"name-web": "git.example.org"}}}}})
if err := bus.Put(context.Background(), provider.AssignmentsBucket, "g14.gitea", plan); err != nil {
t.Fatal(err)
}
p := newProvider("novox", bus, t.Logf)
out, err := p.Pass(context.Background())
if err != nil {
t.Fatal(err)
}
if len(out) != 1 || out[0].Status != provider.Answered {
t.Fatalf("the pass did %+v", out)
}
var e provider.Entry
if err := json.Unmarshal(bus.buckets[provider.BucketOf(ModuleName)]["g14.gitea.route"], &e); err != nil {
t.Fatal(err)
}
if e.Values["name-web"] != "git.example.org" || e.Values["name"] != "git.example.org" ||
e.Values["internal-name"] != "git.novox.internal" || e.Credential != "" || e.User != "" || e.Refused != "" {
t.Fatalf("the entry is %+v", e)
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"dynamic": "/services/traefik/dynamic",
"entrypoint": "websecure",
"certificate-resolver": "le",
"machine": "host.docker.internal"
}
+5
View File
@@ -0,0 +1,5 @@
module route-adapter
go 1.25.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261015210729-d3e182503c19
+2
View File
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261015210729-d3e182503c19 h1:mAMt3KFmrHzDiBv4MLL94opWcG9y73ISMrBUyisapQE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261015210729-d3e182503c19/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
+53 -2
View File
@@ -1,6 +1,7 @@
{
"module": "route-adapter",
"version": "1",
"moved": true,
"slug": "radapt",
"capabilities": [
"container-runtime"
@@ -25,6 +26,33 @@
"mode": "read-write"
}
],
"config": {
"dynamic": {
"type": "string",
"description": "The directory the predecessor proxy's file provider reads, on this machine, as an absolute path.",
"pattern": "/.*",
"trusted": true,
"default": "/services/traefik/dynamic"
},
"entrypoint": {
"type": "string",
"description": "The predecessor proxy's entry point it serves public HTTPS on, by its own name.",
"trusted": true,
"default": "websecure"
},
"certificate-resolver": {
"type": "string",
"description": "The predecessor proxy's certificate resolver, by its own name.",
"trusted": true,
"default": "le"
},
"machine": {
"type": "string",
"description": "How the predecessor's proxy reaches this machine: a host name or an address.",
"trusted": true,
"default": "host.docker.internal"
}
},
"resources": [
{
"id": "state",
@@ -42,9 +70,10 @@
"id": "config",
"type": "file",
"path": "${dir:state}/config.json",
"takes-config": ["dynamic", "entrypoint", "certificate-resolver", "machine"],
"merge": "json",
"mode": "0644",
"content": "{\n \"dynamic\": \"/services/traefik/dynamic\",\n \"entrypoint\": \"websecure\",\n \"certificate-resolver\": \"le\",\n \"machine\": \"host.docker.internal\"\n}\n"
"template": "files/config.json"
},
{
"id": "adapt",
@@ -75,7 +104,29 @@
"entrypoints": [
"index.js"
]
},
{
"name": "provider",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/route-provider",
"binary": "route-provider",
"loads": [
"route-provider"
]
},
{
"name": "files",
"kind": "archive",
"from": "files"
}
]
}
},
"state": [
"bound"
],
"reads": [
"mesh-controller.assignments"
]
}