Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac9c2d57be | ||
|
|
473376259b | ||
|
|
e1293fb0ad | ||
|
|
82481099b7 | ||
|
|
b127f005c3 | ||
|
|
58b4fcb8c8 |
@@ -115,3 +115,14 @@ func TestACycleIsOneLastTierAndSaidSo(t *testing.T) {
|
|||||||
t.Fatalf("a cycle should be one tier of two, said: %v", tiers)
|
t.Fatalf("a cycle should be one tier of two, said: %v", tiers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// novox/hq 04-ISSUES/211: a merge moving the toolchain and a bundle compiled in it builds the
|
||||||
|
// bundle a tier after the toolchain, not beside it.
|
||||||
|
func TestABundleIsPlannedAfterTheToolchainItIsCompiledIn(t *testing.T) {
|
||||||
|
edges := []inventory.Edge{{From: "node-tools", To: "mesh-tools", Kind: inventory.EdgeStandsOn}}
|
||||||
|
p := planOfMerge(link.SourceMoved{Owner: "novox", Repo: "mesh-tools", Commit: "abc"},
|
||||||
|
[]string{"mesh-tools", "node-tools"}, edges)
|
||||||
|
if len(p.Tiers) != 2 || p.Tiers[0][0] != "mesh-tools" || p.Tiers[1][0] != "node-tools" {
|
||||||
|
t.Fatalf("the toolchain, then the bundle: %v", p.Tiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -402,19 +402,22 @@ func seatAnnouncement(handlers map[string]link.ToolHandler) micro.Info {
|
|||||||
var endpoints []micro.EndpointInfo
|
var endpoints []micro.EndpointInfo
|
||||||
for _, verb := range verbs {
|
for _, verb := range verbs {
|
||||||
schema, _ := json.Marshal(about[verb].Input)
|
schema, _ := json.Marshal(about[verb].Input)
|
||||||
|
// The same shape every tool runtime announces in (node-tools' announce package): the name is
|
||||||
|
// `<seat>__<verb>`, as the protocol's characters allow; the metadata is what identifies it.
|
||||||
endpoints = append(endpoints, micro.EndpointInfo{
|
endpoints = append(endpoints, micro.EndpointInfo{
|
||||||
Name: verb,
|
Name: catalogue.ControllerSeatName + "__" + verb,
|
||||||
Subject: link.SeatToolSubject(catalogue.ControllerSeatName, verb),
|
Subject: link.SeatToolSubject(catalogue.ControllerSeatName, verb),
|
||||||
QueueGroup: "seat." + catalogue.ControllerSeatName,
|
QueueGroup: "seat." + catalogue.ControllerSeatName,
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]string{
|
||||||
|
"kind": "seat", "module": catalogue.ControllerSeatName, "tool": verb,
|
||||||
|
"seat": catalogue.ControllerSeatName, "scope": "mesh", "interchangeable": "false",
|
||||||
"description": about[verb].Description, "schema": string(schema),
|
"description": about[verb].Description, "schema": string(schema),
|
||||||
"seat": catalogue.ControllerSeatName, "scope": "mesh",
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return micro.Info{
|
return micro.Info{
|
||||||
ServiceIdentity: micro.ServiceIdentity{
|
ServiceIdentity: micro.ServiceIdentity{
|
||||||
Name: catalogue.ControllerSeatName, ID: "controller", Version: "1.0.0",
|
Name: catalogue.ControllerSeatName, ID: "controller", Version: "0.1.0",
|
||||||
Metadata: map[string]string{"seat": catalogue.ControllerSeatName, "scope": "mesh"},
|
Metadata: map[string]string{"seat": catalogue.ControllerSeatName, "scope": "mesh"},
|
||||||
},
|
},
|
||||||
Description: "the mesh's own verbs, answered by the holder of the mesh-controller seat",
|
Description: "the mesh's own verbs, answered by the holder of the mesh-controller seat",
|
||||||
|
|||||||
@@ -281,10 +281,14 @@ func TestTheControllerAnnouncesTheVerbsItServes(t *testing.T) {
|
|||||||
t.Fatalf("%d endpoints announced for %d verbs served", len(info.Endpoints), len(handlers))
|
t.Fatalf("%d endpoints announced for %d verbs served", len(info.Endpoints), len(handlers))
|
||||||
}
|
}
|
||||||
for _, e := range info.Endpoints {
|
for _, e := range info.Endpoints {
|
||||||
if _, served := handlers[e.Name]; !served {
|
verb := e.Metadata["tool"]
|
||||||
t.Errorf("%s is announced and not served", e.Name)
|
if _, served := handlers[verb]; !served || e.Name != catalogue.ControllerSeatName+"__"+verb {
|
||||||
|
t.Errorf("%s (%s) is announced and not served under that name", e.Name, verb)
|
||||||
}
|
}
|
||||||
if e.Subject != link.SeatToolSubject(catalogue.ControllerSeatName, e.Name) || e.QueueGroup != "seat."+catalogue.ControllerSeatName {
|
if e.Metadata["kind"] != "seat" || e.Metadata["seat"] != catalogue.ControllerSeatName {
|
||||||
|
t.Errorf("%s is not announced as the seat's verb: %v", e.Name, e.Metadata)
|
||||||
|
}
|
||||||
|
if e.Subject != link.SeatToolSubject(catalogue.ControllerSeatName, verb) || e.QueueGroup != "seat."+catalogue.ControllerSeatName {
|
||||||
t.Errorf("%s is announced on %s/%s, not where it is served", e.Name, e.Subject, e.QueueGroup)
|
t.Errorf("%s is announced on %s/%s, not where it is served", e.Name, e.Subject, e.QueueGroup)
|
||||||
}
|
}
|
||||||
if e.Metadata["description"] == "" || e.Metadata["schema"] == "" || e.Metadata["scope"] != "mesh" {
|
if e.Metadata["description"] == "" || e.Metadata["schema"] == "" || e.Metadata["scope"] != "mesh" {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A backend behind the proxy learns the client used TLS and which name it asked for, so the addresses
|
||||||
|
// it writes into its own pages are the ones a client can use (2026-10-03: a forge's Go import tag
|
||||||
|
// named an http clone URL, and Go refused the module path).
|
||||||
|
func TestABackendIsToldTheRequestWasHTTPSAndForWhichName(t *testing.T) {
|
||||||
|
var proto, host, fwdHost, fwdFor string
|
||||||
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
proto, host, fwdHost, fwdFor = r.Header.Get("X-Forwarded-Proto"), r.Host, r.Header.Get("X-Forwarded-Host"), r.Header.Get("X-Forwarded-For")
|
||||||
|
}))
|
||||||
|
defer backend.Close()
|
||||||
|
where, _ := url.Parse(backend.URL)
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "https://git.example.org/novox/mesh-sdk/go?go-get=1", nil)
|
||||||
|
req.TLS = &tls.ConnectionState{}
|
||||||
|
req.Host = "git.example.org"
|
||||||
|
req.RemoteAddr = "192.0.2.7:51000"
|
||||||
|
towards(where).ServeHTTP(httptest.NewRecorder(), req)
|
||||||
|
if proto != "https" || fwdHost != "git.example.org" || host != "git.example.org" || fwdFor != "192.0.2.7" {
|
||||||
|
t.Errorf("the backend was told proto=%q host=%q forwarded-host=%q for=%q", proto, host, fwdHost, fwdFor)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -273,7 +273,7 @@ func (t *table) set(routes map[string][]rule, public map[string]bool) {
|
|||||||
log.Printf("route %s points at %q, which is not a URL: %v", host, r.target, err)
|
log.Printf("route %s points at %q, which is not a URL: %v", host, r.target, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
r.to = httputil.NewSingleHostReverseProxy(where)
|
r.to = towards(where)
|
||||||
if r.insecure {
|
if r.insecure {
|
||||||
r.to.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
r.to.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
||||||
}
|
}
|
||||||
@@ -1060,3 +1060,17 @@ func asPort(v any) (int, bool) {
|
|||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// towards proxies to one backend and tells it what the client asked: **X-Forwarded-Proto, -Host and
|
||||||
|
// -For**, set from the request this proxy received. A backend that builds its own addresses — a forge
|
||||||
|
// writing its clone URL into a page, a login redirect — otherwise sees the plain HTTP hop from this
|
||||||
|
// proxy and writes `http://`, though every client reached it over TLS: Go refused the forge's module
|
||||||
|
// path for exactly that on 2026-10-03, its import tag naming an http clone URL.
|
||||||
|
// The standard library's NewSingleHostReverseProxy sets only X-Forwarded-For.
|
||||||
|
func towards(where *url.URL) *httputil.ReverseProxy {
|
||||||
|
return &httputil.ReverseProxy{Rewrite: func(pr *httputil.ProxyRequest) {
|
||||||
|
pr.SetURL(where)
|
||||||
|
pr.Out.Host = pr.In.Host
|
||||||
|
pr.SetXForwarded()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|||||||
+25
-5
@@ -465,10 +465,28 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
pub = append(pub, invoked...)
|
pub = append(pub, invoked...)
|
||||||
// It says what it serves and may ask what answers (novox/hq ADR 0197): the runtime answers
|
// It says what it serves and may ask what answers (novox/hq ADR 0197): the runtime answers
|
||||||
// discovery for each module and seat it carries, and the console it is asks the bus.
|
// discovery for each module and seat it carries, and the console it is asks the bus.
|
||||||
sub = append(sub, announcing(serves...)...)
|
// One service per runtime process, named for the runtime: the bus lets a principal answer each
|
||||||
|
// request once, so the runtime announces everything it carries under its own name.
|
||||||
|
sub = append(sub, announcing(append([]string{RuntimeModule}, serves...)...)...)
|
||||||
pub = append(pub, discovering()...)
|
pub = append(pub, discovering()...)
|
||||||
// Nothing about consumers: it consumes nothing. A module's reactions to events are its
|
// **And it consumes for the modules it carries** (novox/hq ADR 0198, which changes ADR 0175's
|
||||||
// own long-lived process, which ADR 0175 leaves where it is; what moves here is tools.
|
// "it consumes nothing"): a module's long-running code is a bundle this runtime launches, and
|
||||||
|
// the runtime is its bus — it reads the module's own durable consumer and acknowledges what
|
||||||
|
// the module's code took. Exactly the grants the module's own principal has for that consumer,
|
||||||
|
// on its name and no other's: asking about it, pulling from it, acknowledging it. The
|
||||||
|
// consumer is still the controller's to make, from the module's own principal.
|
||||||
|
for _, d := range p.Carries {
|
||||||
|
own := Principal{Kind: KindModule, Node: p.Node, Module: d.Module, Emits: d.Emits,
|
||||||
|
Consumes: d.Consumes, Serves: d.Serves, Holds: d.Holds, Uses: d.Uses, Watches: d.Watches}
|
||||||
|
if _, consumes := ConsumerFor(own); !consumes {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stream, durable := consumerStream(own), consumerDurable(own)
|
||||||
|
pub = append(pub,
|
||||||
|
"$JS.API.CONSUMER.INFO."+stream+"."+durable,
|
||||||
|
"$JS.API.CONSUMER.MSG.NEXT."+stream+"."+durable,
|
||||||
|
"$JS.ACK."+stream+"."+durable+".>")
|
||||||
|
}
|
||||||
sub = unique(sub)
|
sub = unique(sub)
|
||||||
pub = unique(pub)
|
pub = unique(pub)
|
||||||
}
|
}
|
||||||
@@ -793,12 +811,14 @@ func invokedSubjects(invokes []string) ([]string, error) {
|
|||||||
// protocol's discovery (novox/hq ADR 0197): the questions asked of every service, and those asked of
|
// protocol's discovery (novox/hq ADR 0197): the questions asked of every service, and those asked of
|
||||||
// each name it serves — its own and no other's, so it cannot answer for a service it is not.
|
// each name it serves — its own and no other's, so it cannot answer for a service it is not.
|
||||||
func announcing(names ...string) []string {
|
func announcing(names ...string) []string {
|
||||||
out := []string{"$SRV.PING", "$SRV.INFO"}
|
out := []string{"$SRV.PING", "$SRV.INFO", "$SRV.STATS"}
|
||||||
for _, n := range names {
|
for _, n := range names {
|
||||||
if !safeSubject.MatchString(n) {
|
if !safeSubject.MatchString(n) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out = append(out, "$SRV.PING."+n, "$SRV.PING."+n+".>", "$SRV.INFO."+n, "$SRV.INFO."+n+".>")
|
for _, verb := range []string{"PING", "INFO", "STATS"} {
|
||||||
|
out = append(out, "$SRV."+verb+"."+n, "$SRV."+verb+"."+n+".>")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ func TestAModulePullsItsOwnConsumerAndNoOthers(t *testing.T) {
|
|||||||
// their tools (novox/hq ADR 0175): every carried module's tool namespace, every held seat's verbs
|
// their tools (novox/hq ADR 0175): every carried module's tool namespace, every held seat's verbs
|
||||||
// on this node, every module's membership on this node, and a call to anything. Nothing it
|
// on this node, every module's membership on this node, and a call to anything. Nothing it
|
||||||
// consumes, because it reacts to nothing.
|
// consumes, because it reacts to nothing.
|
||||||
func TestTheRuntimeServesTheUnionAndConsumesNothing(t *testing.T) {
|
func TestTheRuntimeServesTheUnionAndConsumesForItsModules(t *testing.T) {
|
||||||
filter := Seat{Name: "node-packet-filter", Scope: "node", Serves: []string{"rules", "reload"}}
|
filter := Seat{Name: "node-packet-filter", Scope: "node", Serves: []string{"rules", "reload"}}
|
||||||
p := Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule, Carries: []Declared{
|
p := Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule, Carries: []Declared{
|
||||||
{Module: "nftables", Holds: []Seat{filter}, Serves: []string{"firewall_rules"}},
|
{Module: "nftables", Holds: []Seat{filter}, Serves: []string{"firewall_rules"}},
|
||||||
@@ -408,22 +408,33 @@ func TestTheRuntimeServesTheUnionAndConsumesNothing(t *testing.T) {
|
|||||||
t.Errorf("the runtime may not publish %s: %v", want, perms.Publish)
|
t.Errorf("the runtime may not publish %s: %v", want, perms.Publish)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Nothing of what a carried module consumes, and no consumer of its own to ack.
|
// It reads the consumer of every carried module that consumes — that module's, by its name, as
|
||||||
for _, s := range perms.Subscribe {
|
// the module's own principal could (novox/hq ADR 0198) — and of no module that consumes nothing.
|
||||||
if strings.Contains(s, ".event.") || strings.HasPrefix(s, "_DELIVER.") {
|
for _, want := range []string{
|
||||||
t.Errorf("the runtime was granted a delivery it has no consumer for: %s", s)
|
"$JS.API.CONSUMER.INFO.EVENTS.anchor_zsh",
|
||||||
|
"$JS.API.CONSUMER.MSG.NEXT.EVENTS.anchor_zsh",
|
||||||
|
"$JS.ACK.EVENTS.anchor_zsh.>",
|
||||||
|
} {
|
||||||
|
if !contains(perms.Publish, want) {
|
||||||
|
t.Errorf("the runtime may not read zsh's consumer: %s missing from %v", want, perms.Publish)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, s := range perms.Publish {
|
for _, s := range perms.Publish {
|
||||||
if strings.HasPrefix(s, "$JS.ACK.") || strings.Contains(s, "CONSUMER") {
|
if (strings.HasPrefix(s, "$JS.ACK.") || strings.Contains(s, "CONSUMER")) && !strings.Contains(s, "anchor_zsh") {
|
||||||
t.Errorf("the runtime was granted a consumer's subject and has no consumer: %s", s)
|
t.Errorf("the runtime was granted a consumer no carried module of it consumes on: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// It pulls; nothing is pushed to it, and it subscribes no event subject directly.
|
||||||
|
for _, s := range perms.Subscribe {
|
||||||
|
if strings.Contains(s, ".event.") || strings.HasPrefix(s, "_DELIVER.") {
|
||||||
|
t.Errorf("the runtime was granted a delivery: %s", s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !perms.AllowResponses {
|
if !perms.AllowResponses {
|
||||||
t.Error("the runtime answers what it is asked, and may not reply")
|
t.Error("the runtime answers what it is asked, and may not reply")
|
||||||
}
|
}
|
||||||
if _, needed := ConsumerFor(p); needed {
|
if _, needed := ConsumerFor(p); needed {
|
||||||
t.Error("a consumer would be made for the runtime, which consumes nothing")
|
t.Error("a consumer would be made for the runtime itself; it reads its modules' consumers, never one of its own")
|
||||||
}
|
}
|
||||||
// Each subject once in each list: the file is read as the mesh's authority model. One subject may
|
// Each subject once in each list: the file is read as the mesh's authority model. One subject may
|
||||||
// stand in both — the runtime answers discovery on `$SRV.INFO` and, as the console, asks it
|
// stand in both — the runtime answers discovery on `$SRV.INFO` and, as the console, asks it
|
||||||
|
|||||||
+4
-4
@@ -25,7 +25,7 @@ accounts {
|
|||||||
users = [
|
users = [
|
||||||
{ user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: {
|
{ user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: {
|
||||||
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "_INBOX.enrol.>", "mesh.assignment.>", "mesh.control.>", "mesh.mod.*.tool.>", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.refused", "mesh.seat.node-build-agent.accept.>"] }
|
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "_INBOX.enrol.>", "mesh.assignment.>", "mesh.control.>", "mesh.mod.*.tool.>", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.refused", "mesh.seat.node-build-agent.accept.>"] }
|
||||||
subscribe: { allow: ["$JS.API.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.gitea.event.pull.merged", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built"] }
|
subscribe: { allow: ["$JS.API.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.gitea.event.pull.merged", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built"] }
|
||||||
allow_responses: { max: 1, ttl: "1m" }
|
allow_responses: { max: 1, ttl: "1m" }
|
||||||
} }
|
} }
|
||||||
{ user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: {
|
{ user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: {
|
||||||
@@ -38,17 +38,17 @@ accounts {
|
|||||||
} }
|
} }
|
||||||
{ user: "one.telegram", password: "$2a$11$tttttttttttttttttttttt", permissions: {
|
{ user: "one.telegram", password: "$2a$11$tttttttttttttttttttttt", permissions: {
|
||||||
publish: { allow: ["$JS.ACK.EVENTS.one_telegram.>", "$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.>", "$JS.API.CONSUMER.INFO.EVENTS.one_telegram", "$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram", "$JS.API.CONSUMER.MSG.NEXT.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.telegram", "mesh.seat.telegram-sender.event.delivered", "mesh.seat.telegram-sender.event.failed"] }
|
publish: { allow: ["$JS.ACK.EVENTS.one_telegram.>", "$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.>", "$JS.API.CONSUMER.INFO.EVENTS.one_telegram", "$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram", "$JS.API.CONSUMER.MSG.NEXT.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.telegram", "mesh.seat.telegram-sender.event.delivered", "mesh.seat.telegram-sender.event.failed"] }
|
||||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.telegram", "$SRV.INFO.telegram.>", "$SRV.PING", "$SRV.PING.telegram", "$SRV.PING.telegram.>", "_INBOX.one.telegram.>", "mesh.assignment.one.telegram", "mesh.mod.telegram.tool.>", "mesh.seat.telegram-sender.accept.send"] }
|
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.telegram", "$SRV.INFO.telegram.>", "$SRV.PING", "$SRV.PING.telegram", "$SRV.PING.telegram.>", "$SRV.STATS", "$SRV.STATS.telegram", "$SRV.STATS.telegram.>", "_INBOX.one.telegram.>", "mesh.assignment.one.telegram", "mesh.mod.telegram.tool.>", "mesh.seat.telegram-sender.accept.send"] }
|
||||||
allow_responses: { max: 1, ttl: "1m" }
|
allow_responses: { max: 1, ttl: "1m" }
|
||||||
} }
|
} }
|
||||||
{ user: "two.audit", password: "$2a$11$aaaaaaaaaaaaaaaaaaaaaa", permissions: {
|
{ user: "two.audit", password: "$2a$11$aaaaaaaaaaaaaaaaaaaaaa", permissions: {
|
||||||
publish: { allow: ["$JS.ACK.EVENTS.two_audit.>", "$JS.API.CONSUMER.INFO.EVENTS.two_audit", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_audit", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.audit"] }
|
publish: { allow: ["$JS.ACK.EVENTS.two_audit.>", "$JS.API.CONSUMER.INFO.EVENTS.two_audit", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_audit", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.audit"] }
|
||||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.audit", "$SRV.INFO.audit.>", "$SRV.PING", "$SRV.PING.audit", "$SRV.PING.audit.>", "_INBOX.two.audit.>", "mesh.assignment.two.audit", "mesh.mod.audit.tool.>", "mesh.mod.shop.event.order.placed"] }
|
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.audit", "$SRV.INFO.audit.>", "$SRV.PING", "$SRV.PING.audit", "$SRV.PING.audit.>", "$SRV.STATS", "$SRV.STATS.audit", "$SRV.STATS.audit.>", "_INBOX.two.audit.>", "mesh.assignment.two.audit", "mesh.mod.audit.tool.>", "mesh.mod.shop.event.order.placed"] }
|
||||||
allow_responses: { max: 1, ttl: "1m" }
|
allow_responses: { max: 1, ttl: "1m" }
|
||||||
} }
|
} }
|
||||||
{ user: "two.shop", password: "$2a$11$ssssssssssssssssssssss", permissions: {
|
{ user: "two.shop", password: "$2a$11$ssssssssssssssssssssss", permissions: {
|
||||||
publish: { allow: ["$JS.ACK.EVENTS.two_shop.>", "$JS.API.CONSUMER.INFO.EVENTS.two_shop", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.shop", "mesh.mod.shop.event.order.placed", "mesh.seat.telegram-sender.accept.send"] }
|
publish: { allow: ["$JS.ACK.EVENTS.two_shop.>", "$JS.API.CONSUMER.INFO.EVENTS.two_shop", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.shop", "mesh.mod.shop.event.order.placed", "mesh.seat.telegram-sender.accept.send"] }
|
||||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.shop", "$SRV.INFO.shop.>", "$SRV.PING", "$SRV.PING.shop", "$SRV.PING.shop.>", "_INBOX.two.shop.>", "mesh.assignment.two.shop", "mesh.mod.shop.tool.>"] }
|
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.shop", "$SRV.INFO.shop.>", "$SRV.PING", "$SRV.PING.shop", "$SRV.PING.shop.>", "$SRV.STATS", "$SRV.STATS.shop", "$SRV.STATS.shop.>", "_INBOX.two.shop.>", "mesh.assignment.two.shop", "mesh.mod.shop.tool.>"] }
|
||||||
allow_responses: { max: 1, ttl: "1m" }
|
allow_responses: { max: 1, ttl: "1m" }
|
||||||
} }
|
} }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/builder"
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,6 +97,21 @@ func dependenciesOf(entries []Entry, against map[string][]string, read map[strin
|
|||||||
add(name, on.Module, EdgeDeclared)
|
add(name, on.Module, EdgeDeclared)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// **A bundle stands on the toolchain it is compiled in** (novox/hq 04-ISSUES/211). A
|
||||||
|
// manifest names its toolchain by language, not in `build.on`, so the edge was implicit
|
||||||
|
// and a merge that moved the toolchain and a bundle together built both in one tier —
|
||||||
|
// the bundle against the toolchain as it was, recorded as built from the new commit. Read
|
||||||
|
// from the manifest, so it holds before any build has recorded what it stood on; and a
|
||||||
|
// toolchain that moves rebuilds every bundle compiled in it, which is what a toolchain
|
||||||
|
// carrying a bundle's dependencies requires.
|
||||||
|
for _, a := range e.Manifest.Build.Artifacts {
|
||||||
|
if a.Kind != catalogue.ArtifactBundle {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if chain, err := builder.ToolchainFor(a.Language); err == nil {
|
||||||
|
add(name, chain.Base, EdgeStandsOn)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, ref := range against[name] {
|
for _, ref := range against[name] {
|
||||||
if rest, ok := strings.CutPrefix(ref, catalogue.ArtifactStoreScheme); ok {
|
if rest, ok := strings.CutPrefix(ref, catalogue.ArtifactStoreScheme); ok {
|
||||||
|
|||||||
@@ -62,3 +62,41 @@ func TestDependenciesAreOneRelationWithTheirKinds(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// novox/hq 04-ISSUES/211: a bundle stands on the toolchain it is compiled in, so a merge moving both
|
||||||
|
// builds the toolchain first — read from the manifest, before any build recorded it.
|
||||||
|
func TestABundleStandsOnTheToolchainItIsCompiledIn(t *testing.T) {
|
||||||
|
entries := []Entry{
|
||||||
|
{Manifest: catalogue.Manifest{Module: "mesh-tools"}, Source: Source{Repository: "novox/mesh-tools"}},
|
||||||
|
{Manifest: catalogue.Manifest{Module: "mesh-tools-go"}, Source: Source{Repository: "novox/mesh-tools-go"}},
|
||||||
|
{Manifest: catalogue.Manifest{Module: "node-tools", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
|
||||||
|
{Name: "runtime", Kind: catalogue.ArtifactBundle, Language: "go", System: "arch", From: "cmd/node-tools"}}}},
|
||||||
|
Source: Source{Repository: "novox/mesh-tools"}},
|
||||||
|
{Manifest: catalogue.Manifest{Module: "nftables", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
|
||||||
|
{Name: "tools", Kind: catalogue.ArtifactBundle, Language: "typescript", Entrypoints: []string{"tools/index.js"}}}}},
|
||||||
|
Source: Source{Repository: "novox/mesh-catalog"}},
|
||||||
|
{Manifest: catalogue.Manifest{Module: "photos", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
|
||||||
|
{Name: "server", Kind: catalogue.ArtifactImage, From: "Dockerfile"}}}},
|
||||||
|
Source: Source{Repository: "novox/photos"}},
|
||||||
|
}
|
||||||
|
edges := dependenciesOf(entries, nil, nil)
|
||||||
|
has := func(from, to string) bool {
|
||||||
|
for _, e := range edges {
|
||||||
|
if e.From == from && e.To == to && e.Kind == EdgeStandsOn {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !has("nftables", "mesh-tools") {
|
||||||
|
t.Errorf("a TypeScript bundle does not stand on the TypeScript toolchain: %v", edges)
|
||||||
|
}
|
||||||
|
if !has("node-tools", "mesh-tools-go") {
|
||||||
|
t.Errorf("a Go bundle does not stand on the Go toolchain: %v", edges)
|
||||||
|
}
|
||||||
|
for _, e := range edges {
|
||||||
|
if e.From == "photos" && e.Kind == EdgeStandsOn {
|
||||||
|
t.Errorf("an image stands on a toolchain it is not compiled in: %v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
// DiscoverySubjects are where one service instance is asked to say what it is.
|
// DiscoverySubjects are where one service instance is asked to say what it is.
|
||||||
func DiscoverySubjects(name, id string) []string {
|
func DiscoverySubjects(name, id string) []string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, verb := range []string{"PING", "INFO"} {
|
for _, verb := range []string{"PING", "INFO", "STATS"} {
|
||||||
out = append(out, "$SRV."+verb, "$SRV."+verb+"."+name, "$SRV."+verb+"."+name+"."+id)
|
out = append(out, "$SRV."+verb, "$SRV."+verb+"."+name, "$SRV."+verb+"."+name+"."+id)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
@@ -35,6 +35,16 @@ func (b OverNATS) Announce(info micro.Info, logger *log.Logger) (func(), error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Statistics the protocol asks for; the controller keeps none per verb, so it answers its
|
||||||
|
// identity and its endpoints with nothing counted — an honest zero, not a refusal.
|
||||||
|
stats := micro.Stats{ServiceIdentity: info.ServiceIdentity, Type: micro.StatsResponseType}
|
||||||
|
for _, e := range info.Endpoints {
|
||||||
|
stats.Endpoints = append(stats.Endpoints, µ.EndpointStats{Name: e.Name, Subject: e.Subject, QueueGroup: e.QueueGroup})
|
||||||
|
}
|
||||||
|
statsBody, err := json.Marshal(stats)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var subs []*nats.Subscription
|
var subs []*nats.Subscription
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
stop := func() {
|
stop := func() {
|
||||||
@@ -46,8 +56,11 @@ func (b OverNATS) Announce(info micro.Info, logger *log.Logger) (func(), error)
|
|||||||
for _, subject := range DiscoverySubjects(info.Name, info.ID) {
|
for _, subject := range DiscoverySubjects(info.Name, info.ID) {
|
||||||
subject := subject
|
subject := subject
|
||||||
body := infoBody
|
body := infoBody
|
||||||
if len(subject) >= 9 && subject[:9] == "$SRV.PING" {
|
switch {
|
||||||
|
case len(subject) >= 9 && subject[:9] == "$SRV.PING":
|
||||||
body = pingBody
|
body = pingBody
|
||||||
|
case len(subject) >= 10 && subject[:10] == "$SRV.STATS":
|
||||||
|
body = statsBody
|
||||||
}
|
}
|
||||||
bind := func() (*nats.Subscription, error) {
|
bind := func() (*nats.Subscription, error) {
|
||||||
return b.Conn.Subscribe(subject, func(msg *nats.Msg) {
|
return b.Conn.Subscribe(subject, func(msg *nats.Msg) {
|
||||||
|
|||||||
Reference in New Issue
Block a user