f0dfa689fe
Three small follow-ups from review:
1. Restore target is now operator-editable. Default value is the
literal '\$HOME/rm-restore/<job-id>/' (agent expands \$HOME at
run time using os.UserHomeDir(); also handles \${HOME} and ~/
prefixes). Operator can replace with any absolute path.
- ui_restore.go validates the input is either absolute or starts
with one of the recognised prefixes; other env-var refs (\$PATH
etc.) are deliberately rejected so operator paths can't pick up
arbitrary agent env values.
- host_restore.html replaces the read-only mono-text display with
a real <input>; help text spells out that \$HOME resolves
agent-side and <job-id> is substituted on dispatch.
- install.sh + the systemd unit prep /root/rm-restore so the
default works under the sandbox: ReadWritePaths gains a soft
'-/root/rm-restore' entry (the '-' makes the bind-mount soft-fail
if missing, but install.sh pre-creates it root-owned 0700).
2. --no-ownership flag now gated on restic version. The flag was
added in restic 0.17 and 0.16 rejects it. Previously dropped it
wholesale — that meant new-dir restores silently preserved
ownership against design intent on 0.17+. Now the agent threads
its detected restic version (sysinfo already collects it) through
runner.Config -> restic.Env, and RunRestore appends --no-ownership
only when AtLeastVersion(0, 17) returns true. 0.16 hosts still
restore with original uid/gid; help text in the wizard explicitly
notes this. The previous 'Original ownership is preserved' copy
was wrong for new-dir mode and is corrected.
3. golangci-lint misspell locale switched US -> UK and the codebase
swept (73 corrections, mostly behaviour/serialise/recognise/honour).
Wire-format ErrorCode 'unauthorized' -> 'unauthorised' is a tiny
contract change but the agent doesn't parse those codes today and
no external API consumers exist yet. Tests passed before + after.
Tests:
- internal/restic/version_test.go covers Env.AtLeastVersion across
edge cases (empty, exact match, patch above, minor below, non-
numeric) and expandHome on \$HOME / \${HOME} / ~/, plus
pass-through for absolute paths and refusal of other env vars.
- ui_restore_test updated: TargetDir now starts '\$HOME/rm-restore/'
with the job_id substituted into the placeholder.
Live verified on the smoke env: default target restored to
/root/rm-restore/<job-id>/ as the agent's expanded \$HOME (2 files,
14 bytes); custom override '/tmp/custom-restore/<job-id>/' restored
into the agent's PrivateTmp namespace (1 file, 6 bytes); both jobs
'succeeded', exit 0.
92 lines
3.1 KiB
Go
92 lines
3.1 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// MessageType enumerates every kind of envelope that can flow over
|
|
// the agent ↔ server WebSocket. Keeping these as string constants
|
|
// (not iota ints) makes traffic readable in logs and packet captures.
|
|
type MessageType string
|
|
|
|
// Agent → server message types.
|
|
const (
|
|
MsgHello MessageType = "hello"
|
|
MsgHeartbeat MessageType = "heartbeat"
|
|
MsgJobStarted MessageType = "job.started"
|
|
MsgJobProgress MessageType = "job.progress"
|
|
MsgJobFinished MessageType = "job.finished"
|
|
MsgSnapshotsRpt MessageType = "snapshots.report"
|
|
MsgRepoStats MessageType = "repo.stats"
|
|
MsgLogStream MessageType = "log.stream"
|
|
MsgScheduleAck MessageType = "schedule.ack"
|
|
MsgScheduleFire MessageType = "schedule.fire" // agent: a local cron entry fired, please dispatch a job
|
|
MsgCommandResult MessageType = "command.result" // ack for command.run
|
|
MsgTreeListResult MessageType = "tree.list.result" // reply to a server-driven tree.list
|
|
MsgError MessageType = "error"
|
|
)
|
|
|
|
// Server → agent message types.
|
|
const (
|
|
MsgCommandRun MessageType = "command.run"
|
|
MsgCommandCancel MessageType = "command.cancel"
|
|
MsgScheduleSet MessageType = "schedule.set"
|
|
MsgConfigUpdate MessageType = "config.update"
|
|
MsgAgentUpdateAvail MessageType = "agent.update.available"
|
|
MsgTreeList MessageType = "tree.list" // sync RPC: list a snapshot's children
|
|
)
|
|
|
|
// Envelope is the framing for every WS message in either direction.
|
|
// Payload is parsed into the concrete struct chosen by Type.
|
|
//
|
|
// ID is set on RPC-style messages (command.run / command.result) so
|
|
// responses can be correlated. For one-shot pushes (heartbeat,
|
|
// job.progress) it is empty.
|
|
type Envelope struct {
|
|
Type MessageType `json:"type"`
|
|
ID string `json:"id,omitempty"`
|
|
Payload json.RawMessage `json:"payload,omitempty"`
|
|
}
|
|
|
|
// Marshal builds an envelope from a concrete payload struct.
|
|
func Marshal(t MessageType, id string, payload any) (Envelope, error) {
|
|
if payload == nil {
|
|
return Envelope{Type: t, ID: id}, nil
|
|
}
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return Envelope{}, fmt.Errorf("marshal %s payload: %w", t, err)
|
|
}
|
|
return Envelope{Type: t, ID: id, Payload: raw}, nil
|
|
}
|
|
|
|
// UnmarshalPayload decodes the envelope's payload into v.
|
|
func (e Envelope) UnmarshalPayload(v any) error {
|
|
if len(e.Payload) == 0 {
|
|
return nil
|
|
}
|
|
return json.Unmarshal(e.Payload, v)
|
|
}
|
|
|
|
// ErrorCode enumerates error reasons surfaced over the wire.
|
|
// These are stable identifiers; client code may switch on them.
|
|
type ErrorCode string
|
|
|
|
// Stable ErrorCode values surfaced over the wire. Clients switch on
|
|
// these; renaming requires a wire-version bump.
|
|
const (
|
|
ErrProtocolTooOld ErrorCode = "protocol_too_old"
|
|
ErrProtocolTooNew ErrorCode = "protocol_too_new"
|
|
ErrUnauthorized ErrorCode = "unauthorised"
|
|
ErrBadRequest ErrorCode = "bad_request"
|
|
ErrInternal ErrorCode = "internal"
|
|
)
|
|
|
|
// ErrorPayload is the body of an `error` envelope.
|
|
type ErrorPayload struct {
|
|
Code ErrorCode `json:"code"`
|
|
Message string `json:"message"`
|
|
HelpURL string `json:"help_url,omitempty"`
|
|
}
|