Documentation
¶
Overview ¶
Package hubdoctor diagnoses a cloop *control plane* — the thing an operator deploys — as distinct from pkg/doctor, which diagnoses a developer's checkout.
Why it is a separate package ¶
pkg/doctor answers "can this working copy run cloop": is there a .cloop directory, is `go` on PATH, is the API key set, is state.json valid JSON. Every one of its checks predates the hub, and none of them can fail on a deployment that is thoroughly broken as a hub: an issuer that does not resolve, a certificate that expired last week, an RBAC policy under which nobody is an admin, strict mode with nothing to dispatch to. That hub passes `cloop doctor` with fourteen green lines and cannot serve a single request.
So this is a second diagnosis with a different subject, not more checks bolted onto the first. Its checks are the ones whose answers only exist once cloop is hosted — identity, transport, authorization, image trust, executors, storage and admission — and each one is written to be actionable rather than merely true: every non-pass finding carries a one-line remediation naming the file, field or command that fixes it.
The contract every check keeps ¶
- A check never fails the whole run. A hub whose issuer is unreachable must still be told about its expiring certificate, so each check contributes findings and the report is the sum of them.
- Severity means something specific. Fail: the hub is broken or unsafe in a way an operator must fix. Warn: it works, but a foreseeable event breaks it or a control is weaker than the deployment implies. Pass: verified, not merely absent.
- Absence is not a pass. A check that could not run — no network, no database — reports that as a warning, because "we did not look" and "we looked and it was fine" are different answers and only one of them is worth acting on.
Exit codes and --json ¶
Run returns a Report; the caller decides how loudly to complain. ExitCode is 0 with only passes and warnings, 1 with any failure, so `cloop hub doctor` is usable as a CI gate and as a pre-flight in a deployment pipeline. --json emits the same Report verbatim, which is why every field has a stable JSON tag and every finding has a stable Check id: those ids are the thing a pipeline greps for, and renaming one is a breaking change.
Index ¶
Constants ¶
const DefaultProbeTimeout = 10 * time.Second
DefaultProbeTimeout bounds one network probe. Ten seconds is long enough for a slow IdP behind a cold proxy and short enough that a hub with an unreachable issuer, an unreachable registry and three unreachable executors still finishes in under a minute.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Finding ¶
type Finding struct {
Check string `json:"check"`
Title string `json:"title"`
Severity Severity `json:"severity"`
Message string `json:"message"`
Remediation string `json:"remediation,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
Finding is one diagnostic result.
Check is the stable machine-readable identifier ("oidc.discovery"), Title is what a human reads, Message says what was observed, and Remediation says what to do about it. Remediation is required for anything that is not a pass — a diagnostic that reports a problem without naming its fix is the failure mode this command exists to remove, and TestEveryNonPassCarriesRemediation enforces it.
type Options ¶
type Options struct {
// Offline skips every check that would make a network request: OIDC
// discovery, the JWKS fetch, registry reachability, and executor health
// probes that dial. Those checks report a warning saying they were
// skipped rather than silently vanishing.
Offline bool
// Timeout bounds each individual network probe. Zero uses
// DefaultProbeTimeout.
Timeout time.Duration
// HTTPClient overrides the client used for probes. Tests inject one
// pointed at an httptest server; production leaves it nil so the system
// trust store (and SSL_CERT_DIR, which the compose stack sets) applies.
HTTPClient *http.Client
// Now overrides the clock, so certificate-expiry findings are testable
// without generating a certificate per case.
Now func() time.Time
// LookPath overrides executable lookup, for the cosign check. Tests
// substitute it; nil means exec.LookPath.
LookPath func(string) (string, error)
}
Options tunes a run.
type Report ¶
type Report struct {
// Dir is the control plane's working directory (where .cloop lives).
Dir string `json:"dir"`
// CheckedAt is when the run started.
CheckedAt time.Time `json:"checked_at"`
// StrictMode reports executors.allow_host_process=false — recorded
// because it changes what several findings mean.
StrictMode bool `json:"strict_mode"`
// Offline reports that network probes were skipped.
Offline bool `json:"offline"`
// Findings, in the order the checks produced them.
Findings []Finding `json:"findings"`
}
Report is the whole diagnosis.
func Run ¶
Run executes every hub check against the control plane rooted at dir and returns the report.
It never returns an error: a hub is diagnosed by *reporting* what is wrong with it, and a doctor that aborts on the first unreadable file is a doctor that cannot diagnose the case it is most needed for. cfg may be nil, which is itself a finding.
func (*Report) ExitCode ¶
ExitCode is 1 when any check failed, 0 otherwise.
Warnings deliberately do not fail the command. A warning is frequently a deployment choice rather than a defect — a hub behind a proxy has no certificate of its own — and a gate that goes red on those gets switched off, taking the failures with it.
type Severity ¶
type Severity string
Severity is how much a finding matters. The three values are ordered, and Report.ExitCode only distinguishes Fail from the rest.
const ( // SeverityPass: verified working. Not "nothing was configured". SeverityPass Severity = "pass" // SeverityWarn: works now, but is fragile, weaker than the deployment // implies, or could not be verified. SeverityWarn Severity = "warn" // SeverityFail: broken or unsafe. An operator must act. SeverityFail Severity = "fail" )