Two controllers could both act (issue 204), a reconcile's report could overtake the apply after it and the digest decided (issue 267), and a grant could make a second writer of a machine's report. - The lease (internal/lease, ADR 0229): mesh-controller_lease key `holder`, 15 s age, renewed every 5 s by compare-and-set; the epoch is the revision it was taken at. The gate is the clock (stops 3 s before expiry); a refused renewal is a loss and the process exits; a holder that stops gives it back. serve takes it before asserting the bus. Epochs kept in the store (migration 0068 controller_epoch) as a floor: a bucket raised from nothing is compacted past it. Unleased (no epoch, S12 urgent) only when nobody holds it and the bus will not let it be written. A shell command acts under the holder's epoch, or its own lease when none. - Declarations carry `epoch` inside the signed envelope, only to a machine whose latest account carried a report_sequence (mesh-host #35); would-send is composed with the epoch last sent. Allot and the send both pass the gate. - Reports: contract in internal/link/order.go (epoch, sequence, report_sequence, older_than, refused_older). Accounts kept by epoch, then sequence, then report sequence; older refused, counted; unordered reports keep the digest rule. Plans by compare-and-set on a revision, with epoch. Conditions and calls carry the epoch and are not written off the lease. - S12 and S13 (naming the writer by epoch) watched, D5 run; reset of the bucket said. Writers table compiled in and enforced in PermissionsFor; the controller no longer publishes mesh.control.>. A contract per consumed kind, and the empty-on-error lint over the repository. - mesh-host pinned to its main with the epoch in the validator (D1 validates the envelope as sent). Needs mesh-host's genesis lock with the lease grant (mesh-host PR) for TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose.
54 lines
1.7 KiB
Go
54 lines
1.7 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Signer is whatever holds the control plane's signing key.
|
|
type Signer interface {
|
|
Sign(ctx context.Context, message []byte) ([]byte, error)
|
|
}
|
|
|
|
// ActingGate is what every send passes before it is made (novox/hq to-be 45 §6): whether this process
|
|
// may act under the controller's lease. Set by the controller; nil passes every send — a test, a tool.
|
|
var ActingGate func(ctx context.Context) error
|
|
|
|
// Declare sends a node what it should be, signed.
|
|
//
|
|
// The signature is over the declaration exactly as it is published — the same bytes the node
|
|
// verifies. Anything that re-encoded between here and there would produce a signature over
|
|
// something else, and the node would refuse a declaration that was genuinely the mesh's.
|
|
//
|
|
// Published to the node's own queue, which its account alone may read.
|
|
func Declare(ctx context.Context, bus Bus, signer Signer, node string,
|
|
declaration []byte, timeout time.Duration) error {
|
|
|
|
if !json.Valid(declaration) {
|
|
return fmt.Errorf("refusing to send %s something that is not a declaration", node)
|
|
}
|
|
// **At the send, not only where it was composed**: a lease lost between the two stops this one.
|
|
if ActingGate != nil {
|
|
if err := ActingGate(ctx); err != nil {
|
|
return fmt.Errorf("%s was not sent its declaration: %w", node, err)
|
|
}
|
|
}
|
|
|
|
signature, err := signer.Sign(ctx, declaration)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot sign a declaration for %s: %w", node, err)
|
|
}
|
|
|
|
body, err := json.Marshal(Signed{Declaration: declaration, Signature: signature})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
publish, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
return bus.PublishDeclaration(publish, node, body)
|
|
}
|