Issue 339 made every trusted setting the controller's terminal's alone, so an agent could only hand the operator a line to type at the control node. Now anyone the bus admits may PROPOSE a layer: settings propose keeps the proposal in the controller's own asks (the asked bucket, which the controller alone writes), judged as settings set judges, and asks the operator on the operator channel at the level approve with every key, its exact new value (in its shape where a path or an address may not leave the mesh), the was of a changed key, the removed keys and the layer's fingerprint. The serving controller sets the layer on the warrant alone: once, for the ask it holds, only when the record's values still digest to what the option bound and the layer is still the one shown, with the terminal's judgement and history, and keeps who approved it beside the layer, which settings says back (migration 0090). Decline, expiry, a cancel, a replacement or the router's refusal discard it; nothing is asked when no router, no grant or no channel can carry it. settings proposals lists them. The push afterwards is a separate act.
144 lines
4.7 KiB
Go
144 lines
4.7 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// What a change replaces (novox/hq ADR 0217, to-be 44): the settings layer a set or a clear replaced,
|
|
// and a summary of what a machine was last sent. Both were missing when a change took effect unseen.
|
|
|
|
// Layer is one settings layer as it stands — the whole mesh's when nodeName is empty — and whether
|
|
// there is one. A layer is replaced whole when set; reading it first is how one key is changed
|
|
// without losing the others.
|
|
func (i *Inventory) Layer(ctx context.Context, nodeName, module string) (map[string]any, bool, error) {
|
|
nodeID, err := i.layerNode(ctx, nodeName)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
var raw []byte
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select values from settings where module = $1 and node is not distinct from $2::uuid`,
|
|
module, nodeID).Scan(&raw)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, false, nil
|
|
}
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
values := map[string]any{}
|
|
if err := json.Unmarshal(raw, &values); err != nil {
|
|
return nil, false, err
|
|
}
|
|
return values, true, nil
|
|
}
|
|
|
|
// PastLayer is a layer that was replaced or cleared.
|
|
type PastLayer struct {
|
|
Values map[string]any
|
|
SetAt *time.Time
|
|
ReplacedAt time.Time
|
|
// ReplacedBy is "set" or "clear".
|
|
ReplacedBy string
|
|
// SetBy is who had set the layer, when it was kept (novox/hq ADR 0277); empty for one set before that was kept.
|
|
SetBy string
|
|
}
|
|
|
|
// LayerOrigin is who set a layer and when (novox/hq ADR 0277): empty words for a layer set before who set it was
|
|
// kept, and has false where there is no layer.
|
|
func (i *Inventory) LayerOrigin(ctx context.Context, nodeName, module string) (setBy string, setAt time.Time, has bool, err error) {
|
|
nodeID, err := i.layerNode(ctx, nodeName)
|
|
if err != nil {
|
|
return "", time.Time{}, false, err
|
|
}
|
|
var by *string
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select set_by, set_at from settings where module = $1 and node is not distinct from $2::uuid`,
|
|
module, nodeID).Scan(&by, &setAt)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", time.Time{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return "", time.Time{}, false, err
|
|
}
|
|
if by != nil {
|
|
setBy = *by
|
|
}
|
|
return setBy, setAt, true, nil
|
|
}
|
|
|
|
// SettingsHistory is every layer of one module on one machine — the whole mesh's when nodeName is
|
|
// empty — that was replaced or cleared, the latest first.
|
|
func (i *Inventory) SettingsHistory(ctx context.Context, nodeName, module string) ([]PastLayer, error) {
|
|
nodeID, err := i.layerNode(ctx, nodeName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select values, set_at, replaced_at, replaced_by, set_by from settings_history
|
|
where module = $1 and node is not distinct from $2::uuid order by replaced_at desc`,
|
|
module, nodeID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []PastLayer
|
|
for rows.Next() {
|
|
var raw []byte
|
|
var p PastLayer
|
|
var by *string
|
|
if err := rows.Scan(&raw, &p.SetAt, &p.ReplacedAt, &p.ReplacedBy, &by); err != nil {
|
|
return nil, err
|
|
}
|
|
if by != nil {
|
|
p.SetBy = *by
|
|
}
|
|
if err := json.Unmarshal(raw, &p.Values); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// keepReplacedSQL copies a layer into the history before it is replaced or cleared; run in the same
|
|
// transaction as the write where there is one, so a write that fails leaves no history of it.
|
|
const keepReplacedSQL = `insert into settings_history (node, module, values, set_at, replaced_by, set_by)
|
|
select node, module, values, set_at, $3, set_by from settings
|
|
where module = $1 and node is not distinct from $2::uuid`
|
|
|
|
// layerNode is the node id of a layer, nil for the whole mesh's.
|
|
func (i *Inventory) layerNode(ctx context.Context, nodeName string) (*string, error) {
|
|
if nodeName == "" {
|
|
return nil, nil
|
|
}
|
|
n, err := i.NodeByName(ctx, nodeName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &n.ID, nil
|
|
}
|
|
|
|
// RecordSentSummary keeps what a machine was last sent, summarised (migration 0078): read only to
|
|
// compare what would be sent with it, never as what the machine should be.
|
|
func (i *Inventory) RecordSentSummary(ctx context.Context, node string, summary []byte) error {
|
|
_, err := i.store.Pool().Exec(ctx, `update node set sent_summary = $2 where id = $1`, node, summary)
|
|
return err
|
|
}
|
|
|
|
// SentSummary is what a machine was last sent, summarised, by its name; empty for a machine sent
|
|
// nothing since the summary was first kept.
|
|
func (i *Inventory) SentSummary(ctx context.Context, name string) ([]byte, error) {
|
|
var raw []byte
|
|
err := i.store.Pool().QueryRow(ctx,
|
|
`select sent_summary from node where name = $1`, name).Scan(&raw)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
return raw, err
|
|
}
|