Merge main, and close what the confirmation review found (hq ADR 0272)
- Any failure of a request the link may have delivered now says the line may have run; "nothing ran" is said only of a request the link knows it never asked (no link open, nobody listening: link.ErrNotAsked). - A test holds that a connection saying nothing is answered within its deadline, set at accept, and gives its slot back. - The README says the socket refuses a process outside the machine's own namespaces, so flatpak, toolbox and distrobox shells are refused.
This commit is contained in:
@@ -151,6 +151,19 @@ carries. The claim is not. The foundation is four container services, and:
|
||||
So `foundation.lock` here is a real bundle with a placeholder's content. Saying that plainly
|
||||
beats shipping a host that claims a foundation it has never raised.
|
||||
|
||||
## mesh-cli's socket (novox/hq ADR 0272)
|
||||
|
||||
The engine serves `/run/nox-mesh-host/cli.sock` for `mesh-cli`, the operator's command (`internal/meshcli`). It
|
||||
reads the asking account from the kernel (`SO_PEERCRED`) and asks the controller on this node's own subject; the
|
||||
controller decides what the line may do, and the engine runs nothing it receives. One line per connection, each
|
||||
connection's deadline set the moment it is accepted, two lines at once per account and eight for the machine,
|
||||
"busy" beyond that.
|
||||
|
||||
**A process outside the machine's own namespaces is refused**, whatever its user id: the peer's mount and user
|
||||
namespace must be the engine's. So `mesh-cli` run inside a flatpak, a toolbox or distrobox container, or any other
|
||||
container — even one running as the operator's own id — is refused with the reason; run it from the machine's own
|
||||
shell.
|
||||
|
||||
## A capability is detected, never assumed
|
||||
|
||||
The reason this is the first thing built rather than a detail of it.
|
||||
|
||||
@@ -230,7 +230,8 @@ func (l *natsLink) Ask(ctx context.Context, subject string, body []byte) ([]byte
|
||||
case err == nil:
|
||||
return reply, nil
|
||||
case errors.Is(err, nats.ErrNoResponders):
|
||||
return nil, fmt.Errorf("nothing on this machine answers %s", subject)
|
||||
// The server says nobody subscribes the subject: delivered to no one.
|
||||
return nil, fmt.Errorf("%w: nothing on this machine answers %s", ErrNotAsked, subject)
|
||||
}
|
||||
if refused := l.refusal(subject, before); refused != "" {
|
||||
return nil, fmt.Errorf("%s: %v%s", subject, err, refused)
|
||||
|
||||
@@ -552,15 +552,19 @@ func (q *Queue) AskController(ctx context.Context, body []byte) ([]byte, error)
|
||||
bus := q.bus
|
||||
q.mu.Unlock()
|
||||
if bus == nil {
|
||||
return nil, errors.New("this machine has no link to the bus open now")
|
||||
return nil, fmt.Errorf("%w: this machine has no link to the bus open now", ErrNotAsked)
|
||||
}
|
||||
asker, ok := bus.(ToolBus)
|
||||
if !ok {
|
||||
return nil, errors.New("the link to the bus open now cannot ask the controller")
|
||||
return nil, fmt.Errorf("%w: the link to the bus open now cannot ask the controller", ErrNotAsked)
|
||||
}
|
||||
return asker.Ask(ctx, CLISubject(q.Membership.Node), body)
|
||||
}
|
||||
|
||||
// ErrNotAsked is a request the link knows it never delivered: no link open, or nobody listening on its subject. Any
|
||||
// other failure of a request may come after it was delivered, so what came of it is not known (novox/hq ADR 0272).
|
||||
var ErrNotAsked = errors.New("not asked")
|
||||
|
||||
// ReadToolAnswer reads a tool's reply envelope — `{result, error}`, as the node tools answer every call —
|
||||
// as a health answer: an error is not healthy, and so is a result that does not say `healthy: true`.
|
||||
func ReadToolAnswer(reply []byte) (bool, string, error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/cli"
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
)
|
||||
|
||||
// The controller answers a line as it answers every call (novox/hq ADR 0272, review): in an envelope, `result` or
|
||||
@@ -15,6 +16,9 @@ import (
|
||||
// followed by the same account on the same node until it ends. Its side holds the same field names (mesh-controller
|
||||
// internal/link, the mesh-cli field names' test).
|
||||
|
||||
// ErrNotAsked is the link's: a request it knows it never delivered.
|
||||
var ErrNotAsked = link.ErrNotAsked
|
||||
|
||||
// AskFunc asks the controller one request on this node's subject and returns its reply.
|
||||
type AskFunc func(ctx context.Context, body []byte) ([]byte, error)
|
||||
|
||||
@@ -40,11 +44,12 @@ func AskTheController(ctx context.Context, ask AskFunc, asked Asked) cli.Answer
|
||||
body, _ := json.Marshal(asked)
|
||||
reply, err := ask(ctx, body)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) || ctx.Err() != nil {
|
||||
return cli.NotKnown("", "no answer from the controller in time")
|
||||
// **Only a request the link knows it never asked ran nothing**: no link open, nobody listening. Any other
|
||||
// failure may come after the request was delivered, and the line may have run (review of ADR 0272).
|
||||
if errors.Is(err, ErrNotAsked) {
|
||||
return cli.Refusal("the controller was not reached, so nothing ran: " + err.Error())
|
||||
}
|
||||
// Never delivered to a controller: no link, no subscriber, the bus refusing the publish.
|
||||
return cli.Refusal("the controller was not reached, so nothing ran: " + err.Error())
|
||||
return cli.NotKnown("", "no answer from the controller ("+err.Error()+")")
|
||||
}
|
||||
call := ""
|
||||
for {
|
||||
|
||||
@@ -83,16 +83,41 @@ func TestNoAnswerInTimeSaysTheLineMayHaveRun(t *testing.T) {
|
||||
// A request the bus never carried to a controller ran nothing, and says so.
|
||||
func TestARequestNobodyTookSaysNothingRan(t *testing.T) {
|
||||
c := &controllerSays{script: []func(Asked) ([]byte, error){func(Asked) ([]byte, error) {
|
||||
return nil, fmt.Errorf("nothing on this machine answers mesh.control.laptop.cli")
|
||||
return nil, fmt.Errorf("%w: nothing on this machine answers mesh.control.laptop.cli", ErrNotAsked)
|
||||
}}}
|
||||
a := AskTheController(context.Background(), c.ask, Asked{Line: []string{"status"}, Account: "op"})
|
||||
if !strings.Contains(a.Refused, "nothing ran") || a.Unknown != "" {
|
||||
t.Fatalf("a request nobody took reads %+v", a)
|
||||
}
|
||||
c = &controllerSays{script: []func(Asked) ([]byte, error){func(Asked) ([]byte, error) {
|
||||
return nil, errors.New("this machine has no link to the bus open now")
|
||||
return nil, fmt.Errorf("%w: this machine has no link to the bus open now", ErrNotAsked)
|
||||
}}}
|
||||
if a := AskTheController(context.Background(), c.ask, Asked{Line: []string{"status"}}); !strings.Contains(a.Refused, "nothing ran") {
|
||||
t.Fatalf("no link reads %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
// **Any error once the request may have been delivered is not known, never "nothing ran"** (review of ADR 0272):
|
||||
// only a request the link says it never asked — no link open, nobody listening — ran nothing.
|
||||
func TestOnlyARequestNeverAskedSaysNothingRan(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
err error
|
||||
nothing bool
|
||||
}{
|
||||
{fmt.Errorf("%w: this machine has no link to the bus open now", ErrNotAsked), true},
|
||||
{fmt.Errorf("%w: nothing on this machine answers mesh.control.laptop.cli", ErrNotAsked), true},
|
||||
{errors.New("nats: connection closed"), false},
|
||||
{errors.New("mesh.control.laptop.cli: permissions violation"), false},
|
||||
{context.DeadlineExceeded, false},
|
||||
} {
|
||||
err := c.err
|
||||
ctl := &controllerSays{script: []func(Asked) ([]byte, error){func(Asked) ([]byte, error) { return nil, err }}}
|
||||
a := AskTheController(context.Background(), ctl.ask, Asked{Line: []string{"push"}, Account: "op"})
|
||||
if c.nothing && (!strings.Contains(a.Refused, "nothing ran") || a.Unknown != "") {
|
||||
t.Errorf("%v reads %+v; it was never asked", err, a)
|
||||
}
|
||||
if !c.nothing && (a.Refused != "" || !strings.Contains(a.Unknown, "may have run")) {
|
||||
t.Errorf("%v reads %+v; it may have been asked", err, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package meshcli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/cli"
|
||||
)
|
||||
|
||||
// **A connection that says nothing is bounded from the moment it is accepted** (review of ADR 0272): its deadline is
|
||||
// set before any lookup, so it is answered and closed within readWithin, holds no slot after, and asks nothing.
|
||||
func TestAConnectionThatSaysNothingIsAnsweredWithinItsDeadline(t *testing.T) {
|
||||
was := readWithin
|
||||
readWithin = 200 * time.Millisecond
|
||||
t.Cleanup(func() { readWithin = was })
|
||||
asked := false
|
||||
socket := serving(t, func(context.Context, Asked) cli.Answer {
|
||||
asked = true
|
||||
return cli.Answer{}
|
||||
})
|
||||
conn, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
a, err := cli.ReadAnswer(conn)
|
||||
if err != nil {
|
||||
t.Fatalf("a silent connection was not answered within its deadline: %v", err)
|
||||
}
|
||||
if asked || a.Refused == "" || !strings.Contains(a.Refused, "nothing was asked") {
|
||||
t.Fatalf("a silent connection was asked (%v) or answered %+v", asked, a)
|
||||
}
|
||||
// Its slot was given back: the account's next line is not busy.
|
||||
b, err := cli.Ask(context.Background(), socket, []string{"status"})
|
||||
if err != nil || strings.Contains(b.Refused, "busy") || !asked {
|
||||
t.Fatalf("the next line after a silent one reads %+v (%v), asked %v", b, err, asked)
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,8 @@ type Asked struct {
|
||||
type Forward func(ctx context.Context, asked Asked) cli.Answer
|
||||
|
||||
// readWithin bounds how long a connection may take to say its line: a program that connects and says nothing
|
||||
// holds nothing for long.
|
||||
const readWithin = 30 * time.Second
|
||||
// holds nothing for long. A variable so a test need not wait.
|
||||
var readWithin = 30 * time.Second
|
||||
|
||||
// atOnce bounds how many lines are asked at the same time, and perAccount how many of them one account's: a person
|
||||
// types one at a time, so more is a program on the machine. One over either bound is answered busy at once — the
|
||||
|
||||
Reference in New Issue
Block a user