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:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"dynamic": "/services/traefik/dynamic",
|
||||
"entrypoint": "websecure",
|
||||
"certificate-resolver": "le",
|
||||
"machine": "host.docker.internal"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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=
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user