mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-05-14 19:40:41 +00:00
Switching `SuggestedProtocol` to a pointer, so we can pass nil whenever both QUIC and HTTP2 fail. We should not be suggesting anything when all our protocols have failed.
236 lines
6.4 KiB
Go
236 lines
6.4 KiB
Go
package prechecks
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
const (
|
|
// tableWidth is the total character width of the separator lines.
|
|
tableWidth = 80
|
|
|
|
// Status names.
|
|
passStatus = "PASS"
|
|
failStatus = "FAIL"
|
|
skipStatus = "SKIP"
|
|
unknownStatus = "UNKNOWN"
|
|
|
|
// Section separators.
|
|
sectionChar = "-"
|
|
headerTitle = "CONNECTIVITY PRE-CHECKS"
|
|
|
|
// Log message constants.
|
|
logMsgPrecheck = "precheck"
|
|
logMsgPrecheckComplete = "precheck complete"
|
|
|
|
// Log field names.
|
|
logFieldRunID = "run_id"
|
|
logFieldComponent = "component"
|
|
logFieldTarget = "target"
|
|
logFieldStatus = "status"
|
|
logFieldDetails = "details"
|
|
logFieldHardFail = "hard_fail"
|
|
logFieldSuggestedProtocol = "suggested_protocol"
|
|
|
|
sep = " "
|
|
)
|
|
|
|
// statusLabel returns the display label for a given Status.
|
|
func (s Status) statusLabel() string {
|
|
switch s {
|
|
case Pass:
|
|
return passStatus
|
|
case Fail:
|
|
return failStatus
|
|
case Skip:
|
|
return skipStatus
|
|
default:
|
|
return unknownStatus
|
|
}
|
|
}
|
|
|
|
// logString returns the lowercase string used in structured log fields.
|
|
func (s Status) logString() string {
|
|
return strings.ToLower(s.String())
|
|
}
|
|
|
|
// separator returns a full-width horizontal line.
|
|
func separator() string {
|
|
return strings.Repeat(sectionChar, tableWidth)
|
|
}
|
|
|
|
// header returns the top section title line.
|
|
func header() string {
|
|
leftDashes := strings.Repeat(sectionChar, 3)
|
|
rightLen := tableWidth - len(leftDashes) - len(headerTitle) - len(sep)*2
|
|
return leftDashes + sep + headerTitle + sep + strings.Repeat(sectionChar, rightLen)
|
|
}
|
|
|
|
// renderTable uses text/tabwriter to format the results rows with
|
|
// automatically aligned columns, returning the rendered string.
|
|
func renderTable(results []CheckResult) string {
|
|
var buf bytes.Buffer
|
|
// minwidth=0, tabwidth=8, padding=2, padchar=' ', flags=0
|
|
w := tabwriter.NewWriter(&buf, 0, 8, 2, ' ', 0)
|
|
_, _ = fmt.Fprintln(w, "COMPONENT\tTARGET\tSTATUS\tDETAILS")
|
|
for _, r := range results {
|
|
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Component, r.Target, r.ProbeStatus.statusLabel(), r.Details)
|
|
}
|
|
_ = w.Flush()
|
|
return buf.String()
|
|
}
|
|
|
|
// renderActions collects all non-empty Action strings from results and returns
|
|
// the formatted warning/error block that appears between the table and SUMMARY.
|
|
// A Fail result is rendered as ERROR when the report is a hard fail, and as
|
|
// WARNING otherwise (degraded but tunnel can still run).
|
|
func renderActions(r Report) string {
|
|
hardFail := r.hasHardFail()
|
|
var sb strings.Builder
|
|
for _, res := range r.Results {
|
|
if res.Action == "" || res.ProbeStatus != Fail {
|
|
continue
|
|
}
|
|
if hardFail {
|
|
_, _ = fmt.Fprintf(&sb, "ERROR: %s\n", res.Action)
|
|
} else {
|
|
_, _ = fmt.Fprintf(&sb, "WARNING: %s\n", res.Action)
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// summaryLine builds the SUMMARY: line based on the Report state.
|
|
func summaryLine(r Report) string {
|
|
switch {
|
|
case r.hasHardFail():
|
|
return "SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel."
|
|
case r.hasWarn():
|
|
if r.SuggestedProtocol == nil {
|
|
return "SUMMARY: Environment ready with degraded transport."
|
|
}
|
|
|
|
protocol := r.SuggestedProtocol.String()
|
|
return fmt.Sprintf("SUMMARY: Environment ready with degraded transport. cloudflared will proceed using '%s'.", protocol)
|
|
default:
|
|
if r.SuggestedProtocol == nil {
|
|
return "SUMMARY: Environment is healthy."
|
|
}
|
|
|
|
protocol := r.SuggestedProtocol.String()
|
|
return fmt.Sprintf("SUMMARY: Environment is healthy. cloudflared will use '%s' as primary protocol.", protocol)
|
|
}
|
|
}
|
|
|
|
// hasHardFail returns true when the environment cannot establish a tunnel:
|
|
// - Any DNS probe failed, OR
|
|
// - Both the QUIC and HTTP/2 transport probes failed.
|
|
//
|
|
// A single transport failing is not a hard fail — the other transport can be
|
|
// used as a fallback (degraded state, reported via hasWarn).
|
|
func (r Report) hasHardFail() bool {
|
|
var quicFail, http2Fail bool
|
|
for _, res := range r.Results {
|
|
if res.ProbeStatus != Fail {
|
|
continue
|
|
}
|
|
switch res.Type {
|
|
case ProbeTypeDNS:
|
|
return true
|
|
case ProbeTypeQUIC:
|
|
quicFail = true
|
|
case ProbeTypeHTTP2:
|
|
http2Fail = true
|
|
case ProbeTypeManagementAPI:
|
|
// Management API failure is not a hard fail
|
|
}
|
|
}
|
|
return quicFail && http2Fail
|
|
}
|
|
|
|
// hasWarn returns true when the environment is degraded but functional:
|
|
// - Exactly one transport (QUIC or HTTP/2) failed, OR
|
|
// - The Management API is unreachable (auto-updates unavailable).
|
|
//
|
|
// Hard-fail conditions (DNS down, both transports blocked) take precedence
|
|
// and will cause hasWarn to return false.
|
|
func (r Report) hasWarn() bool {
|
|
if r.hasHardFail() {
|
|
return false
|
|
}
|
|
var quicFail, http2Fail, apiFail bool
|
|
for _, res := range r.Results {
|
|
if res.ProbeStatus != Fail {
|
|
continue
|
|
}
|
|
switch res.Type {
|
|
case ProbeTypeQUIC:
|
|
quicFail = true
|
|
case ProbeTypeHTTP2:
|
|
http2Fail = true
|
|
case ProbeTypeManagementAPI:
|
|
apiFail = true
|
|
case ProbeTypeDNS:
|
|
// DNS failures are only relevant for hard failures
|
|
}
|
|
}
|
|
return (quicFail != http2Fail) || apiFail
|
|
}
|
|
|
|
// String renders the Report as a human-readable table suitable for os.Stdout.
|
|
func (r Report) String() string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(header())
|
|
sb.WriteString("\n")
|
|
|
|
sb.WriteString(renderTable(r.Results))
|
|
|
|
actions := renderActions(r)
|
|
if actions != "" {
|
|
sb.WriteString(actions)
|
|
}
|
|
|
|
sb.WriteString("\n")
|
|
sb.WriteString(summaryLine(r))
|
|
sb.WriteString("\n")
|
|
|
|
sb.WriteString(separator())
|
|
sb.WriteString("\n")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// LogEvent emits each CheckResult as a structured zerolog log line, followed by
|
|
// a final summary event. This is the JSON-logging equivalent of String().
|
|
// Every line carries run_id so all results from a single invocation can be correlated.
|
|
func (r Report) LogEvent(logger *zerolog.Logger) {
|
|
runID := r.RunID.String()
|
|
for _, res := range r.Results {
|
|
logger.Info().
|
|
Str(logFieldRunID, runID).
|
|
Str(logFieldComponent, res.Component).
|
|
Str(logFieldTarget, res.Target).
|
|
Str(logFieldStatus, res.ProbeStatus.logString()).
|
|
Str(logFieldDetails, res.Details).
|
|
Msg(logMsgPrecheck)
|
|
}
|
|
|
|
if r.SuggestedProtocol != nil {
|
|
logger.Info().
|
|
Str(logFieldRunID, runID).
|
|
Bool(logFieldHardFail, r.hasHardFail()).
|
|
Str(logFieldSuggestedProtocol, r.SuggestedProtocol.String()).
|
|
Msg(logMsgPrecheckComplete)
|
|
} else {
|
|
logger.Info().
|
|
Str(logFieldRunID, runID).
|
|
Bool(logFieldHardFail, r.hasHardFail()).
|
|
Msg(logMsgPrecheckComplete)
|
|
}
|
|
}
|