Documentation
¶
Overview ¶
Package recording provides storage and HTTP serving of asciicast session recordings produced by wardyn-rec. Store is intentionally minimal so the fs-backed implementation can later be replaced by object storage without touching callers.
Security constraints:
- All path construction goes through safeRunPath, which rejects any runID containing path separators or dot-sequences (path-traversal prevention).
- Reads use os.OpenInRoot because shared-mount writers can create symlinks; lexical validation alone cannot keep reads inside the recording directory.
- OpenCast returns (nil, ErrNotFound) for absent recordings so callers can distinguish "never recorded" from storage errors.
Index ¶
- Variables
- func AdmissionGroup(ctx context.Context, key string) string
- func CastKey(runID, suffix string) string
- func CopyOutput(ctx context.Context, dst io.Writer, src io.Reader) error
- func Handler(store Store, authorize Authorizer) http.Handler
- func LastOutputElapsed(data []byte) (float64, bool)
- func Names() []string
- func OpenJoined(ctx context.Context, s Store, runID string) (io.ReadCloser, error)
- func PartSuffix(n int) string
- func Register(name string, c Constructor)
- func StatJoined(ctx context.Context, s Store, runID string, tailBytes int64) (int64, []byte, error)
- func WithAdmissionGroup(ctx context.Context, group string) context.Context
- type Authorizer
- type CastHeader
- type CastWriter
- type Constructor
- type Deps
- type FSStore
- func (s *FSStore) DeleteRun(ctx context.Context, key string) (int, error)
- func (s *FSStore) OpenCast(ctx context.Context, key string) (io.ReadCloser, error)
- func (s *FSStore) SaveCast(ctx context.Context, key string, r io.Reader) error
- func (s *FSStore) SaveCastNamed(ctx context.Context, runID, suffix string, r io.Reader) error
- func (s *FSStore) SaveRecordingFile(ctx context.Context, name string, r io.Reader) error
- func (s *FSStore) StatAndTail(ctx context.Context, key string, tailBytes int64) (int64, []byte, error)
- func (s *FSStore) Sweep(olderThan time.Duration) (int, error)
- type PGStore
- func (s *PGStore) DeleteRun(ctx context.Context, key string) (int, error)
- func (s *PGStore) OpenCast(ctx context.Context, key string) (io.ReadCloser, error)
- func (s *PGStore) SaveCast(ctx context.Context, runID string, r io.Reader) error
- func (s *PGStore) SaveCastNamed(ctx context.Context, runID, suffix string, r io.Reader) error
- func (s *PGStore) StatAndTail(ctx context.Context, key string, tailBytes int64) (int64, []byte, error)
- func (s *PGStore) Sweep(olderThan time.Duration) (int, error)
- type RunDeleter
- type Store
Constants ¶
This section is empty.
Variables ¶
var ErrErased = errors.New("recording: erased")
ErrErased refuses access to a run whose recordings were durably erased.
var ErrInvalidCast = errors.New("recording: invalid asciicast v2")
ErrInvalidCast identifies a recording that cannot establish an output stream. Its error text never contains sandbox-controlled recording content.
var ErrNotFound = errors.New("recording: not found")
ErrNotFound is returned by OpenCast when no recording exists for the run.
Functions ¶
func AdmissionGroup ¶ added in v0.9.0
AdmissionGroup is the group a stream save of key queues in: the one ctx names, else the key's run.
func CastKey ¶
CastKey builds the composite cast key for a run + optional session suffix. An empty suffix yields the bare runID (the batch-run cast key).
func CopyOutput ¶ added in v0.8.9
CopyOutput decodes a joined asciicast v2 stream into consecutive output payloads. Mask dst as one stream: event and part boundaries may split secrets.
func Handler ¶
func Handler(store Store, authorize Authorizer) http.Handler
Handler returns an http.Handler serving GET /{runID} as an asciicast stream (Content-Type: application/x-asciicast); mount it under /api/v1/runs/{id}/recording.
The {runID} param is the CAST KEY: a bare run id (a batch run's cast) or a "<runID>~<suffix>" composite (an interactive attach session, or a future SSH session; see CastKey). SECURITY: this sub-route's {runID} is NOT the parent mount's {id} (the run this recording is fetched THROUGH) — the two are enforced equal (case-insensitive, on the run-id prefix) before authorize runs, so a caller can't request .../runs/A/recording/B to read run B's cast under an authorization scoped to run A. Both checks collapse to the same 404 (no existence oracle). Store errors produce 500. A bare run id is served joined across its tail-uploaded parts (OpenJoined).
A key servableKey or canonicalRunID refuses gets that same 404 before authorize or the store is asked anything.
func LastOutputElapsed ¶ added in v0.7.2
LastOutputElapsed returns the elapsed-seconds timestamp of the last output ("o") event in data, scanning backwards from the end. Mirrors the console's former client-side probe byte for byte, so a run's server-projected duration and a manually-fetched cast's duration read the same number.
data need not be the whole document — StatAndTail hands this only a tail slice, so the loop may run off an incomplete first line, which just fails to parse like any other malformed line. Returns (0, false) when no output event is found.
func Names ¶
func Names() []string
Names returns the registered store names (for /healthz and error messages).
func OpenJoined ¶ added in v0.8.0
OpenJoined opens runID's cast as one asciicast v2 document: part 1 as stored, then each later part with its repeated header line dropped, until a part is missing. A part whose header differs from part 1's belongs to an earlier cast of the same run (a revive starts a new cast and replaces part 1), so the join ends there rather than splicing two recordings. Parts are opened one at a time, so a store that buffers a whole cast (PGStore) holds one part, not the run.
ponytail: part 1 anchors the join, so retention sweeping part 1 of a run that outlived the retention window hides its later parts from replay; a part index would lift that if retention and year-long runs ever meet.
func PartSuffix ¶ added in v0.8.0
PartSuffix is the cast-key suffix part n (n >= 2) of a run's cast is stored under. It cannot collide with an attach session's suffix, which is a UUID.
func Register ¶
func Register(name string, c Constructor)
Register adds a recording-store implementation; call it from an init().
func StatJoined ¶ added in v0.8.0
StatJoined is StatAndTail over every part of runID's cast: the size is the bytes stored across all of them, and the tail is the last part's — event times run on from part to part because every part is cut from one cast file.
ponytail: unlike OpenJoined it does not read each part's header, so after a revive it still counts an earlier cast's later parts until they are swept; replay itself stays correct.
func WithAdmissionGroup ¶ added in v0.9.0
WithAdmissionGroup names the group ctx's stream saves queue in before they queue for the stream slot. The upload route passes the run's owner, so one person's uploads wait behind each other, not ahead of everyone else's.
Types ¶
type Authorizer ¶ added in v0.5.0
Authorizer decides whether req's caller may read the recording whose cast key names runIDPrefix (the run id, up to an optional "~<suffix>" session marker; see CastKey). It is Handler's ONLY authorization check — route auth middleware proves SOME authenticated human/admin, not WHICH run's recordings they may read. False collapses to the SAME 404 an absent recording gets: no existence oracle for "not yours" vs "never recorded".
type CastHeader ¶
type CastHeader struct {
Version int `json:"version"`
Width int `json:"width"`
Height int `json:"height"`
Timestamp int64 `json:"timestamp,omitempty"`
}
CastHeader is the asciicast v2 header (line 1 of the stream).
type CastWriter ¶
type CastWriter struct {
// contains filtered or unexported fields
}
asciicast v2 format: a header object on line 1, then one JSON event array per line. An output event is ["<elapsed seconds>", "o", "<data>"]. Reference: https://docs.asciinema.org/manual/asciicast/v2/
CastWriter incrementally serializes an asciicast v2 stream to record an interactive attach session: the header is written once by NewCastWriter, then each chunk of PTY output is appended as a timed "o" event via Write.
Safe for concurrent use by a single producer goroutine; a caller writing from one goroutine and reading the buffer from another after Close needs no extra locking beyond the internal mutex here.
func NewCastWriter ¶
NewCastWriter returns a CastWriter that serializes events into dst. width and height are the initial terminal size recorded in the header (0 values fall back to 80x24 so the replay player has a valid geometry). startedAt is the wall-clock session start; event timestamps are elapsed seconds from it.
func (*CastWriter) HadOutput ¶
func (w *CastWriter) HadOutput() bool
HadOutput reports whether any output event was recorded (beyond the header).
func (*CastWriter) Write ¶
func (w *CastWriter) Write(p []byte) (int, error)
Write appends p as a timed asciicast output event ["t","o",string(p)]. p is the (already masked) terminal output; Write satisfies io.Writer so it can sit directly behind a secretmask.MaskingWriter. A zero-length write is a no-op.
type Constructor ¶
Constructor builds a Store from Deps. It may return (nil, nil) to mean "recording disabled" (the fs store with an empty Dir), which callers treat as no-recording.
type Deps ¶
type Deps struct {
// Dir is the base directory for filesystem-backed stores. Empty => disabled
// (fs-specific; pg ignores it).
Dir string
// Pool is the shared pgxpool the pg-backed store persists through, so
// recordings are visible to every replica instead of one pod's local disk.
Pool *pgxpool.Pool
}
Deps are the platform primitives a recording.Store constructor may use. New seams keep their own typed Deps so heterogeneous construction stays type-safe.
type FSStore ¶
type FSStore struct {
// contains filtered or unexported fields
}
FSStore is a filesystem-backed Store. Each recording is stored as <root>/<runID>.cast. The root directory is created on first use.
func NewFSStore ¶
NewFSStore returns an FSStore that persists casts under root. The directory is created with mode 0o750 if it does not exist.
func (*FSStore) DeleteRun ¶ added in v0.8.6
DeleteRun installs a durable fence and removes the run's bare and suffixed casts, plus wardyn-rec's shared-volume .log fallback. Repeating it is safe.
func (*FSStore) OpenCast ¶
OpenCast opens <root>/<runID>.cast for reading. Returns ErrNotFound when the file does not exist.
func (*FSStore) SaveCast ¶
SaveCast writes the asciicast stream to <root>/<runID>.cast atomically (write to a temp file then rename). Fails closed on any path-traversal attempt.
func (*FSStore) SaveCastNamed ¶
SaveCastNamed writes the asciicast stream to <root>/<runID>~<suffix>.cast atomically. An empty suffix is equivalent to SaveCast. Both runID and the composite key are checked by safeRunPath, failing closed on any path-traversal attempt in either component.
func (*FSStore) SaveRecordingFile ¶ added in v0.8.9
SaveRecordingFile preserves wardyn-rec's shared-volume .cast/.log filename and existing-file permissions/ownership while applying the same run fence as SaveCast. Its final local copy retains the fallback's in-place overwrite.
func (*FSStore) StatAndTail ¶ added in v0.7.2
func (s *FSStore) StatAndTail(ctx context.Context, key string, tailBytes int64) (int64, []byte, error)
StatAndTail reports the cast's size and reads its last tailBytes without copying the rest of the file. tailBytes clamps down to size when the cast is smaller.
func (*FSStore) Sweep ¶ added in v0.4.4
Sweep unlinks every cast (and orphaned atomic-write temp file) directly under root whose mtime is older than olderThan, returning the count removed. Deliberately not on the Store interface: retention is an fs-storage concern, so callers type-assert for it.
Age is measured on ModTime, not birth time: the recordings directory is also mounted into agent containers for wardyn-rec's -out-dir fallback, so a cast may still be appended to and mtime keeps advancing — do not "improve" this to birth time.
type PGStore ¶ added in v0.5.0
type PGStore struct {
// contains filtered or unexported fields
}
PGStore is a Postgres-backed Store. Unlike FSStore (per-pod directory), a cast saved via one replica is immediately visible through any other. The zero value is unusable; use NewPGStore.
func NewPGStore ¶ added in v0.5.0
NewPGStore returns a Store backed by pool, the same pgxpool the rest of the control plane uses.
func (*PGStore) DeleteRun ¶ added in v0.8.6
DeleteRun durably fences the run and removes its bare and suffixed casts in one transaction. Absent casts are not an error.
func (*PGStore) OpenCast ¶ added in v0.5.0
OpenCast returns a ReadCloser over the stored bytes for key (either a bare runID or a "<runID>~<suffix>" composite). Returns ErrNotFound when absent.
func (*PGStore) SaveCast ¶ added in v0.5.0
SaveCast persists the asciicast bytestream from r under cast_key = runID, replacing any prior cast stored under that same key (upsert), bounded by maxCastBytes.
func (*PGStore) SaveCastNamed ¶ added in v0.5.0
SaveCastNamed persists r under the composite "<runID>~<suffix>" key (see Store's doc for the addressing contract). validSuffix is shared verbatim with FSStore so a suffix is accepted or rejected identically regardless of the selected store.
func (*PGStore) StatAndTail ¶ added in v0.7.2
func (s *PGStore) StatAndTail(ctx context.Context, key string, tailBytes int64) (int64, []byte, error)
StatAndTail reports the cast's byte size and its last tailBytes, computed and sliced server-side so a caller wanting only a size and duration never pulls the whole payload across the wire. tailBytes is clamped to size when the cast is smaller.
func (*PGStore) Sweep ¶ added in v0.5.0
Sweep deletes every cast row last written more than olderThan ago, returning how many rows it removed. Measured on updated_at, not created_at, so a re-saved cast is never swept from under an in-progress session. Deliberately not part of the Store interface — retention is a storage-backend concern, reached via the unexported recordingSweepable interface instead.
type RunDeleter ¶ added in v0.8.6
RunDeleter durably fences a run before deleting all of its recordings. A successful erase prevents later saves, opens and stat calls across restart. A composite runID erases that exact key and its "~" descendants only.
type Store ¶
type Store interface {
SaveCast(ctx context.Context, runID string, r io.Reader) error
SaveCastNamed(ctx context.Context, runID, suffix string, r io.Reader) error
OpenCast(ctx context.Context, key string) (io.ReadCloser, error)
StatAndTail(ctx context.Context, key string, tailBytes int64) (size int64, tail []byte, err error)
}
Store is the recording persistence contract.
SaveCast persists the asciicast bytestream from r under runID, replacing any prior recording; safe for concurrent saves of different runIDs.
SaveCastNamed persists under a composite key "<runID>~<suffix>" (e.g. an interactive attach session id) so it does not clobber the batch run's cast (keyed by bare runID). Same path guardrails as runID; an empty suffix is equivalent to SaveCast.
OpenCast returns a ReadCloser (caller closes it) or ErrNotFound. The key is either a bare runID or a "<runID>~<suffix>" composite.
StatAndTail reports a key's size and its last tailBytes without returning the whole payload; tailBytes clamps to size when the cast is smaller. ErrNotFound on the same terms as OpenCast.
Save, open and stat return ErrErased after a run is erased. A key belongs to the run before its first "~", including keys passed directly to SaveCast. Already-open readers may retain bytes; erasure does not revoke them.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package recordingtest provides a reusable conformance suite for any recording.Store implementation, holding a blessed default and any future alternate to the identical contract.
|
Package recordingtest provides a reusable conformance suite for any recording.Store implementation, holding a blessed default and any future alternate to the identical contract. |