Three guards, each silent when nothing is at stake: - settings: show reads a layer; set says what it adds, changes and removes, refuses a removal without --replace, and keeps the layer it replaced (settings_history, migration 0057). - push: plan --diff compares with what the machine was last sent, now kept as a summary that holds no file content; push with no machine needs --all. - a running container whose mount would point at another directory holds that machine's push until --move names the module; the other machines go ahead.
121 lines
4.0 KiB
Go
121 lines
4.0 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
|
|
}
|
|
|
|
// 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 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
|
|
if err := rows.Scan(&raw, &p.SetAt, &p.ReplacedAt, &p.ReplacedBy); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(raw, &p.Values); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// keepReplaced writes down the layer about to be replaced or cleared, if there is one.
|
|
func (i *Inventory) keepReplaced(ctx context.Context, nodeID *string, module, how string) error {
|
|
_, err := i.store.Pool().Exec(ctx, keepReplacedSQL, module, nodeID, how)
|
|
return 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)
|
|
select node, module, values, set_at, $3 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 0057): 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
|
|
}
|