Documentation
¶
Overview ¶
Package support builds the diagnostic archive `unarr support-bundle` writes.
The command exists to collapse a support thread — "run doctor", "paste your config", "now the log", "and the daemon state" — into one attachable file. That only works if the user can attach it WITHOUT auditing it first, which is why this package is written around one rule:
Nothing reaches the bundle unless something here explicitly decided to put it there.
Concretely: the configuration is projected onto publishedConfig (an allowlist type, not a filtered copy — see redact_config.go), free text is run through the Scrubber, and every section is enumerated below. A field added to config.Config tomorrow is absent from the bundle by construction, and TestEveryConfigFieldIsClassified fails until someone says what it is.
The bundle is written to a local file and is never uploaded, offered for upload, or transmitted anywhere. There is no network call in this package.
Index ¶
Constants ¶
const DefaultLogLines = 500
DefaultLogLines is how much daemon log the bundle carries by default.
500 lines is the compromise between "enough to see the failure and what led to it" and "small enough to attach to a GitHub issue". Raise it with --log-lines when the interesting event is further back; the byte cap below still applies.
Variables ¶
This section is empty.
Functions ¶
func DefaultName ¶
DefaultName is the archive name when the user gave no --out. The timestamp is UTC and sortable so a second bundle never silently overwrites the first — support threads routinely collect a "before" and an "after".
Types ¶
type Bundle ¶
type Bundle struct {
GeneratedAt time.Time `json:"generatedAt"`
Version string `json:"version"`
Platform string `json:"platform"`
LogLines int `json:"logLines"`
Sections []Section `json:"sections"`
}
Bundle is the collected, redacted content, before it is written anywhere. Collect never touches the filesystem for output; WriteTarGz does.
func Collect ¶
Collect gathers and redacts everything. It performs no output I/O and makes no network call of its own — the only thing that can reach the network is the injected Doctor, which is the connectivity check the user asked for.
func (*Bundle) Body ¶
Body returns a collected section's bytes, or nil when it was absent. Exists for --print and for tests; the archive writer reads the field directly.
func (*Bundle) Listing ¶
Listing renders the human-readable table of what the bundle holds — what --print shows, and what the command prints after writing. An absent section is listed with its reason rather than hidden, because "no daemon log, because the file does not exist" is itself the answer often enough.
func (*Bundle) Manifest ¶
Manifest is the bundle's own index, written into the archive as manifest.json and printed by --print. Absent sections appear here WITH their reason, which is the only place that information survives.
func (*Bundle) WriteTarGz ¶
WriteTarGz writes the bundle to path.
Sections that were not collected write no file — their absence and its reason live in manifest.json, which is written first so a reader who unpacks and looks at one file looks at the index.
The file is created 0600 BEFORE anything is written to it (O_CREATE with the mode, not a chmod afterwards), so there is no window in which the archive exists world-readable.
type Inputs ¶
type Inputs struct {
Config config.Config
Version string
LogLines int
// Doctor runs the diagnostics and returns the report `unarr doctor --json`
// would print. nil records the doctor section as absent.
Doctor func() (doctor.Report, error)
// Journal reads the last n lines of daemon output from the systemd journal.
// Non-nil ONLY on a host where the daemon has no log file of its own —
// under systemd there is no unarr.log to read, and a stale one left by an
// earlier `unarr up` would be worse than nothing.
Journal func(w io.Writer, n int) error
// Logs names the daemon's log files. See LogPaths.
Logs LogPaths
// FFmpegPath is the resolved ffmpeg binary, or "" when there is none.
FFmpegPath string
// BenchCachePath is where `unarr bench` caches its encode measurement.
// Passed in rather than imported so this package keeps no dependency on
// internal/engine.
BenchCachePath string
}
Inputs is everything Collect needs that this package must not resolve for itself.
Doctor and Journal are injected because their implementations live in internal/cmd (the doctor specs need its config/client helpers; the journal reader is `unarr logs`'s own). Injecting them is what makes the bundle embed the SAME report the user sees rather than a second, subtly different implementation of the same checks.
type LogPaths ¶
type LogPaths struct {
// Daemon is unarr.log, the running daemon's own output.
Daemon string
// Err is unarr.err.log — on the macOS installs whose plist split stderr,
// this is where most of the log actually ends up.
Err string
// Boot is unarr.boot.log, held by the SERVICE LAUNCHER rather than the
// daemon. It is the only place a daemon that never started leaves a trace,
// which makes it the first file to read when there is no daemon log at all.
Boot string
// MaxFiles is how many rotated siblings (unarr.log.1 …) to walk back
// through, mirroring [daemon] log_max_files. Rotation means "the last 500
// lines" legitimately spans two files.
MaxFiles int
}
LogPaths names the daemon's three log files. They are passed in rather than resolved here because internal/cmd owns both the names and the data-dir lookup (see daemon_logfiles.go) — duplicating the literals would let the two drift, and a bundle reading the wrong file is worse than one reading none. An empty entry is reported as absent.
type Scrubber ¶
type Scrubber struct {
// contains filtered or unexported fields
}
Scrubber erases credentials from free text.
Two passes, and they are not redundant:
- Literals: the actual values of the Secret-classified config fields. This is the exact pass — it catches a credential wherever it appears, in any shape, including a bare occurrence in a log line that no pattern would recognise. It works because we hold the values.
- Patterns: secrets we do NOT hold. Stream tokens are minted at runtime, Authorization headers are echoed by HTTP debug lines, and a WireGuard key may be quoted by an error from a file we never parsed. Nothing enumerable exists for these, so shape matching is the only tool.
The pattern pass is a denylist and is treated as one: it is a second net under the allowlist, never the thing the design rests on. The config file — the one place credentials are guaranteed to live — is protected by publishedConfig, which cannot miss a field it does not know about.
func NewScrubber ¶
NewScrubber builds the scrubber for one bundle run. It reads the values of every field classified Secret — by path, through reflection — so a newly classified credential starts being erased from logs with no further wiring.
type Section ¶
type Section struct {
Name string `json:"name"`
Bytes int `json:"bytes"`
Absent string `json:"absent,omitempty"`
Note string `json:"note,omitempty"`
// contains filtered or unexported fields
}
Section is one file inside the bundle.
A Section with a non-empty Absent carries no body and writes no file: the section is reported as missing, WITH the reason, instead of taking the whole command down. That is deliberate — a box with no daemon log is exactly the box whose bundle matters most, and "unarr.log: absent (no such file)" is itself a diagnosis.
type Sensitivity ¶
type Sensitivity int
Sensitivity is the publishing verdict for one leaf field of config.Config.
Every field gets one, and the verdict is recorded in configFields below — not inferred from the field's name. A name-based rule is a denylist wearing a costume: it works until someone adds `WebdavPasswd` or `Cookie` and the pattern misses it, and the miss ships the user's credential to a public GitHub issue. An exhaustive map cannot miss; it can only be *incomplete*, and TestEveryConfigFieldIsClassified turns incompleteness into a red build.
const ( // Publishable means the field's SHAPE may appear in the bundle — see // redact_config.go, which decides how. It is not a licence to print the // value verbatim: a Publishable free-form string is still published as // "set"/"unset" or as a vocabulary match, never as the raw bytes. Publishable Sensitivity = iota // Secret means the value is a credential. It never appears in the bundle, // AND its literal value is scrubbed out of every free-text section, because // the daemon log or a doctor message may quote it. Secret )