mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-29 16:17:20 +09:00
The failure snapshot selected pods by their STATUS column, so a pod whose container runs while its readiness probe never passes -- STATUS Running, READY 0/1 -- was dropped from the report entirely. That is a common way for a suite to time out: the replica never joins, its owning HelmRelease sits in InProgress, and the artifact carries no trace of the pod the run died on. Select on the READY column instead, which also covers the partially ready multi-container case, and keep every phase that was collected before. On one degraded cluster that is four of the seven broken pods the old filter reported nothing about. The VMI and VM sections had the same blind spot and now share one selector that reads READY as the last field, because a Pending object prints fewer columns than a Running one and the two kinds differ only in how many columns precede READY. READY counts ready containers rather than the pod's Ready condition, so a pod held back by an unsatisfied readiness gate is out of scope; a test pins that rather than leaving it to be found in an empty directory. Logs are requested for every container of a selected pod rather than skipped for Pending ones, and the per-pod collection moved into a helper so it can be driven against a stub kubectl. kubectl's refusal is the finding, and an empty result is ambiguous on its own: a read against an unscheduled pod exits 0 having printed nothing, which is what a container that started and stayed quiet also leaves. Every site that classifies a failed read now orders its branches the same way, naming the timeout first where both a cutoff and an uncapturable stderr apply. The CRD gate in the summary had no such branch at all: it returned in silence, which is byte-identical to the CRD being absent, so every section behind that gate vanished from a report whose only real problem was that the machine writing it had nowhere to put a temp file. Collection used to stop silently. A walk that hit its cap said so in the job log and nowhere in the artifact. Now the pod walk leaves COLLECTION-TRUNCATED.txt when it stops at COZY_REPORT_PODS_MAX or COZY_REPORT_PODS_BUDGET, CONTAINER-LOGS-TRUNCATED.txt on a pod cut off part way through, COLLECTION-FAILED.txt when the pod list never came back rather than the empty tree a healthy cluster produces, and COLLECTION-UNBOUNDED.txt when timeout is missing locally, which is a fact about the machine writing the report and not about the cluster. Tenant namespaces are collected first, so a cap that only bites on a broadly degraded cluster is not spent on platform pods while the pod the suite died on falls off the end. Container logs are read with --tail=2000 where they were previously read whole, and every bounded file states the bound it was written under, because --tail drops the oldest lines and nothing else in the output would say so. COZY_REPORT_POD_TAIL=-1 restores the complete log. The bounded whole-object read no longer merges stderr into pod.yaml, vm.yaml and vmi.yaml, which are read by yq rather than by eye: a warning on an otherwise complete read left an unparseable file. stderr goes to a scratch file and is copied in only when kubectl returned no object, and the truncation markers those files carry are written as YAML comments, since a bare marker line after a complete mapping is a second top-level node. Six latent faults surfaced while making these reads report. The kamaji module was gated on the LINSTOR controller, so a cluster without LINSTOR got no kamaji evidence at all, which is what a tenant-kubernetes bootstrap timeout needs most. The Flux sources section nested one jsonpath filter inside another, which kubectl rejects outright, so it printed nothing on every report ever taken and read as "all sources Ready". The HelmRelease listing took a single field as STATUS where STATUS is the Ready condition's whole message, so every failed release rendered as one word. The services section filtered for a pending LoadBalancer address on the CLUSTER-IP column, which never carries that value. The cozystack module read its image from a Deployment the installer stopped shipping when it moved to the operator, putting kubectl's NotFound in the first file of the report. And the cozystack apps section gated all three of its kinds on a CRD in an aggregated API group, so it never ran once; two of those names were wrong, and both are collected again. The tests avoid EXIT-trap cleanup, and e2e-testing.md says why: a bats test that installs one and then fails prints no TAP line at all, so output grepped for `not ok` reads as green, while hack/cozytest.sh reports the same failure correctly. A guard enforces that over a named list of files rather than a glob with exceptions, because a list of exceptions is the same claim written so that forgetting to shorten it makes it false. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
350 lines
19 KiB
Bash
Executable File
350 lines
19 KiB
Bash
Executable File
#!/bin/sh
|
|
# Emit a human-readable summary of "what is broken" to a single file.
|
|
# Reads the live cluster (not the report dir) so it can use kubectl JSONPath.
|
|
# Usage: cozyreport-summary.sh > summary.txt
|
|
#
|
|
# Environment:
|
|
# COZY_REPORT_PODS_MAX how many not-Ready pods this summary lists before it
|
|
# reports the rest as an overflow (default 40, shared with
|
|
# the collector so the two never start from different
|
|
# numbers; the counts can still differ when the collector
|
|
# stops on its time budget, and its
|
|
# COLLECTION-TRUNCATED.txt explains that). A malformed value
|
|
# falls back to the default and says so in the section.
|
|
# Note it does not bound the HelmRelease listing, which has
|
|
# its own cap at the same default.
|
|
# COZY_REPORT_PREFER_NS namespace prefix listed first (default `tenant-`; empty
|
|
# disables the ordering). Reached through the selection
|
|
# helpers sourced from hack/cozyreport.sh rather than read
|
|
# by name here, so it appears nowhere in this file's CODE
|
|
# (only in this block) -- which is exactly why it went
|
|
# undocumented:
|
|
# a knob arriving by `.` is invisible to a guard that greps
|
|
# for its name. It changes which pods this summary lists
|
|
# first, and the collector uses the same value, so the two
|
|
# orderings stay in step.
|
|
set -eu
|
|
|
|
# Share the pod-selection helper with cozyreport.sh rather than restating it:
|
|
# the summary and the collected evidence disagreeing about which pods are broken
|
|
# is worse than either being wrong on its own. COZYREPORT_LIB makes the source
|
|
# return before it starts a report of its own.
|
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
# shellcheck disable=SC2034 # read by the sourcing guard in cozyreport.sh
|
|
COZYREPORT_LIB=1
|
|
# shellcheck source=hack/cozyreport.sh
|
|
. "$SCRIPT_DIR/cozyreport.sh"
|
|
|
|
# The wall-clock wrapper, taken from cozyreport.sh rather than resolved again so
|
|
# the two cannot hold different values. Empty when `timeout` is absent; the reads
|
|
# then run unbounded and the header says so.
|
|
BOUND=$COZYREPORT_BOUND
|
|
|
|
# How many rows any one section lists before it reports the rest as an overflow.
|
|
# Shares the collector's default so the two never START from different numbers.
|
|
# The counts can still differ, and legitimately: the collector also stops on its
|
|
# time budget, so it may hold fewer pods than this lists. Its own
|
|
# COLLECTION-TRUNCATED.txt says so; see the note on the pod listing below.
|
|
SUMMARY_CAP=$COZYREPORT_PODS_DEFAULT
|
|
|
|
# summary_read <what> <kubectl args...>
|
|
#
|
|
# One bounded read into READ_OUT. On failure it prints why the section is empty,
|
|
# because a killed read otherwise renders the section byte-identical to a healthy
|
|
# cluster -- and an empty "## HelmReleases not Ready" is the most misleading line
|
|
# this file can carry. CRD probes go through summary_crd, where a non-zero status
|
|
# has two meanings.
|
|
summary_read() {
|
|
_sr_what=$1
|
|
shift
|
|
# kubectl's own reason is kept rather than sent to /dev/null, for the reason the
|
|
# pod list below already keeps its own: "did not return (exit 1)" names a symptom
|
|
# and no cause, and the difference between an apiserver that is too slow and one
|
|
# answering "forbidden" is the whole finding. Discarding it here while the pod-list
|
|
# read below keeps its own would have two reads in one file answering the same
|
|
# question differently.
|
|
_sr_err=$(mktemp "${TMPDIR:-/tmp}/cozyreport-summary.XXXXXX" 2>/dev/null) || _sr_err=""
|
|
# The status is taken in an explicit `else`, not after the `if`: a compound `if`
|
|
# whose condition fails and which has no else leaves `$?` at 0, so reading it
|
|
# afterwards reports "exit 0" for every failure.
|
|
# shellcheck disable=SC2086 # empty BOUND must vanish, not become ""
|
|
if READ_OUT=$($BOUND "$@" 2>"${_sr_err:-/dev/null}"); then
|
|
[ -z "$_sr_err" ] || rm -f "$_sr_err"
|
|
return 0
|
|
else
|
|
_sr_rc=$?
|
|
fi
|
|
# The rows kubectl printed before it died are kept: they are the only ones it
|
|
# named, and before this refactor they flowed down the pipe and were processed.
|
|
# Discarding them turns a partial answer into no answer and then says nothing
|
|
# was readable, which is false in the most useful direction.
|
|
if [ -n "$READ_OUT" ]; then
|
|
echo " the $_sr_what read did not return (exit $_sr_rc); the rows below are the ones it printed before it stopped, and there may be others it never reached"
|
|
else
|
|
echo " the $_sr_what read did not return (exit $_sr_rc), so this section is empty because nothing could be read, NOT because there was nothing to report"
|
|
fi
|
|
# LAST line, not the first, matching the pod list: a failing read is preceded by
|
|
# one klog discovery-retry line per attempt and kubectl's own message comes after
|
|
# them. A read killed by its own timeout is killed before kubectl writes anything,
|
|
# so the file is empty and there is nothing to quote -- which is itself the
|
|
# difference between "too slow to answer" and "answered, and the answer was no".
|
|
if [ -n "$_sr_err" ] && [ -s "$_sr_err" ]; then
|
|
# printf, not echo: kubectl's text is untrusted and dash's echo expands
|
|
# backslash escapes, so a message carrying one could forge a line in a file a
|
|
# triager reads as fact.
|
|
printf ' kubectl said: %s\n' "$(tail -n 1 "$_sr_err" | cut -c1-200)"
|
|
fi
|
|
[ -z "$_sr_err" ] || rm -f "$_sr_err"
|
|
return 0
|
|
}
|
|
|
|
# summary_crd <crd>: true when the CRD is present, false otherwise. Absence is the
|
|
# only outcome that answers false in silence; a killed read, a refusal with a
|
|
# message, a refusal without one, and a message that could not be captured each
|
|
# say which of them happened before answering false.
|
|
#
|
|
# `if $BOUND kubectl get crd X` collapses those last two into one, and a slow
|
|
# aggregated discovery layer -- the state that made the bound necessary -- then
|
|
# skips the whole section as though the CRD were not installed.
|
|
summary_crd() {
|
|
# stderr kept for the same reason summary_read keeps it, but reported only on a
|
|
# killed read: a CRD that is simply absent also exits non-zero, with kubectl
|
|
# writing a NotFound to stderr, and quoting that would turn "this cluster does not
|
|
# run cert-manager" into a line that reads like a failure. The killed case is the
|
|
# one where the cause is not already implied by the outcome.
|
|
_sc_err=$(mktemp "${TMPDIR:-/tmp}/cozyreport-summary.XXXXXX" 2>/dev/null) || _sc_err=""
|
|
# shellcheck disable=SC2086
|
|
if $BOUND kubectl get crd "$1" >/dev/null 2>"${_sc_err:-/dev/null}"; then
|
|
[ -z "$_sc_err" ] || rm -f "$_sc_err"
|
|
return 0
|
|
else
|
|
_sc_rc=$?
|
|
fi
|
|
if cozyreport_timed_out "$_sc_rc"; then
|
|
echo " the $1 presence check did not return (exit $_sc_rc), so this section was skipped without knowing whether the CRD is installed"
|
|
elif [ -z "$_sc_err" ]; then
|
|
# No scratch file means no writable temp dir here, so kubectl's message went to
|
|
# /dev/null and is gone. Silence at this point would be the report's own local
|
|
# condition rendered as a cluster fact, and rendered as the most common one:
|
|
# every section this gate opens simply disappears, exactly as it does on a
|
|
# cluster that does not run the operator behind the CRD.
|
|
echo " the $1 presence check failed (exit $_sc_rc) and its message could not be captured (no writable temp dir on the machine producing this report), so this section was skipped without knowing whether the CRD is installed"
|
|
elif [ -s "$_sc_err" ]; then
|
|
# Not a timeout and not a plain NotFound: an RBAC refusal on the CRD read skips
|
|
# the section exactly like an absent CRD would, and only kubectl's message
|
|
# separates "not installed" from "not allowed to look".
|
|
if ! grep -qi 'not found' "$_sc_err"; then
|
|
printf ' the %s presence check failed, so this section was skipped without knowing whether the CRD is installed; kubectl said: %s\n' \
|
|
"$1" "$(tail -n 1 "$_sc_err" | cut -c1-200)"
|
|
fi
|
|
else
|
|
# Exited non-zero having said nothing. Not the same as NotFound, which kubectl
|
|
# is explicit about, so it cannot be filed under "the CRD is absent".
|
|
echo " the $1 presence check failed (exit $_sc_rc) without a message, so this section was skipped without knowing whether the CRD is installed"
|
|
fi
|
|
[ -z "$_sc_err" ] || rm -f "$_sc_err"
|
|
return 1
|
|
}
|
|
|
|
# Printed before the first section, not inside one: every read in this file goes
|
|
# through the same empty $BOUND, so a notice sitting under "## Pods not Ready"
|
|
# leaves the reader of "## HelmReleases not Ready" -- which is above it and is the
|
|
# first section anyone opens -- with no indication at all. The collector states the
|
|
# same fact for its whole tree in COLLECTION-UNBOUNDED.txt rather than per section.
|
|
if [ -z "$BOUND" ]; then
|
|
echo "(timeout is not on this host, so every read below is unbounded; this is a local dependency, not a cluster symptom)"
|
|
echo
|
|
fi
|
|
|
|
echo "# Cozystack E2E Diagnostic Summary"
|
|
echo "Generated: $(date -Iseconds)"
|
|
echo
|
|
|
|
echo "## HelmReleases not Ready"
|
|
echo
|
|
if summary_crd helmreleases.helm.toolkit.fluxcd.io; then
|
|
summary_read "HelmRelease" kubectl get hr -A --no-headers
|
|
# The whole STATUS field, not $5. `kubectl get hr` prints NAMESPACE NAME AGE
|
|
# READY STATUS, and STATUS is the Ready condition's *message* -- see the printer
|
|
# columns on the CRD this repo ships, internal/fluxinstall/manifests/fluxcd.yaml
|
|
# ("jsonPath: .status.conditions[?(@.type==\"Ready\")].message"). It is a whole
|
|
# sentence, so $5 rendered every failed release as "Helm" or "Reconciliation",
|
|
# dropping the part that says what broke, in the first section of the first file
|
|
# a triager opens. Take everything from the fifth field on.
|
|
# Rebuilt field by field rather than with a `{4}` interval expression: mawk
|
|
# 1.3.4, the awk on the CI image, mis-applies an exact interval to a
|
|
# variable-width group and strips one repetition instead of four. `sub()` still
|
|
# returns 1, so there is no error and no signal -- the row simply arrives with
|
|
# most of itself still in the message. Verified on ubuntu:24.04; gawk,
|
|
# one-true-awk and busybox awk all handle it, which is how it would have reached
|
|
# CI unnoticed.
|
|
HR_ROWS=$(printf '%s\n' "$READ_OUT" | awk 'NF && $4 != "True" {
|
|
msg = ""
|
|
for (i = 5; i <= NF; i++) msg = msg (i > 5 ? " " : "") $i
|
|
printf " %s/%s — %s\n", $1, $2, msg
|
|
}')
|
|
HR_TOTAL=$(printf '%s\n' "$HR_ROWS" | grep -c . || true)
|
|
if [ "$HR_TOTAL" -gt 0 ]; then
|
|
printf '%s\n' "$HR_ROWS" | head -n "$SUMMARY_CAP"
|
|
fi
|
|
# The cap is reported for the same reason the pod cap is: an install that fails
|
|
# early leaves well over 40 HelmReleases not Ready, and a listing that stops at 40
|
|
# without saying so reads as the complete set.
|
|
if [ "$HR_TOTAL" -gt "$SUMMARY_CAP" ]; then
|
|
echo " ... $((HR_TOTAL - SUMMARY_CAP)) more not-Ready HelmRelease(s) not listed here; the full set is in the report's kubernetes/helmreleases.txt"
|
|
fi
|
|
fi
|
|
echo
|
|
|
|
echo "## Pods not Ready"
|
|
echo
|
|
# AGE is read as $NF, not $6: a pod that has restarted prints RESTARTS as
|
|
# `7 (4m12s ago)`, three awk fields, which shifts every later column.
|
|
# A restarted pod is the normal case in a failed run, so $6 printed `age=(4m12s`
|
|
# for exactly the rows worth reading. AGE is last whichever form RESTARTS takes.
|
|
#
|
|
# The row cap is reported rather than applied in silence: this section is a
|
|
# reader's first answer to "what is broken", and a cap that hides the rest reads
|
|
# like there is no rest. The ordering is shared with the collector, so the rows
|
|
# this cap keeps are the rows whose evidence the collector reached for first --
|
|
# the same rows, not necessarily the same count, since the collector can also stop
|
|
# on its time budget, and then it holds fewer pods than are listed here. Its own
|
|
# COLLECTION-TRUNCATED.txt says so.
|
|
#
|
|
# The pod list is read on its own, keeping its exit status, before anything is
|
|
# selected from it. Piping it straight into the selection discards that status,
|
|
# and then a list the apiserver refused renders byte-identical to a cluster where
|
|
# every pod is Ready -- in the file a triager opens first, and on exactly the run
|
|
# where the summary must not be the thing that says nothing is wrong.
|
|
POD_LIST_ERR=$(mktemp "${TMPDIR:-/tmp}/cozyreport-summary.XXXXXX" 2>/dev/null) || POD_LIST_ERR=""
|
|
# Bounded like the collector's copy of this read. The summary runs immediately
|
|
# before the tarball is written, so a hang here costs the whole artifact rather
|
|
# than this one section, and `|| true` on the caller does not survive a hang.
|
|
#
|
|
# `timeout` is used when it is there and skipped when it is not, rather than being
|
|
# required: without the fallback every read here exits 127 and this section would
|
|
# report that the cluster did not answer, on a host where the only thing missing is
|
|
# a local coreutils binary. That is the same false claim the collector's
|
|
# COLLECTION-UNBOUNDED.txt exists to avoid, and the two must not disagree inside
|
|
# one tarball. It also keeps working where the old unbounded pipeline did.
|
|
# Unquoted on purpose: an empty BOUND must expand to no argument at all.
|
|
# shellcheck disable=SC2086
|
|
if POD_LIST=$($BOUND kubectl get pod -A --no-headers 2>"${POD_LIST_ERR:-/dev/null}"); then
|
|
POD_LIST_RC=0
|
|
else
|
|
POD_LIST_RC=$?
|
|
fi
|
|
if [ "$POD_LIST_RC" -ne 0 ]; then
|
|
# Two different failures, and the sentence has to match which one happened. A
|
|
# read that produced nothing leaves an empty section; a read that printed rows
|
|
# and then failed leaves the rows, and calling that section "empty" while the
|
|
# rows are listed three lines below puts two contradictory statements about the
|
|
# same read in one file. The partial rows are kept, as everywhere else in this
|
|
# tree, because they are the only evidence there is -- they are just not all of
|
|
# it, and the difference has to be said rather than implied.
|
|
if [ -n "$POD_LIST" ]; then
|
|
echo " the cluster-wide pod list did not return (kubectl exit $POD_LIST_RC); the pods listed below are the ones it printed before it stopped, and there may be others it never reached"
|
|
else
|
|
echo " the cluster-wide pod list did not return (kubectl exit $POD_LIST_RC), so this section is empty because nothing could be read, NOT because every pod was Ready"
|
|
fi
|
|
if [ -n "$POD_LIST_ERR" ] && [ -s "$POD_LIST_ERR" ]; then
|
|
# Last line: a failing cluster-wide list is preceded by one klog discovery
|
|
# retry line per attempt and kubectl's own message comes after them.
|
|
printf ' kubectl said: %s\n' "$(tail -n 1 "$POD_LIST_ERR" | cut -c1-200)"
|
|
fi
|
|
fi
|
|
[ -z "$POD_LIST_ERR" ] || rm -f "$POD_LIST_ERR"
|
|
POD_ROWS=$(printf '%s\n' "$POD_LIST" | cozyreport_pods_not_ready | cozyreport_pods_prioritize)
|
|
POD_TOTAL=$(printf '%s\n' "$POD_ROWS" | grep -c . || true)
|
|
POD_CAP=${COZY_REPORT_PODS_MAX:-$COZYREPORT_PODS_DEFAULT}
|
|
if ! cozyreport_is_count "$POD_CAP"; then
|
|
echo " (ignored a malformed COZY_REPORT_PODS_MAX='${COZY_REPORT_PODS_MAX:-}'; listing at most $COZYREPORT_PODS_DEFAULT)"
|
|
POD_CAP=$COZYREPORT_PODS_DEFAULT
|
|
fi
|
|
# `NF` guard: an empty selection arrives as one empty line, and an unguarded action
|
|
# would print a row of blanks -- a nameless broken pod on every green run.
|
|
POD_LISTING=$(printf '%s\n' "$POD_ROWS" \
|
|
| awk 'NF {printf " %s/%s — %s ready=%s (restarts=%s, age=%s)\n", $1, $2, $4, $3, $5, $NF}')
|
|
# A zero cap is branched on rather than written as `A && B || C`, which would send
|
|
# `head -n 0` the value this guard keeps away from it (GNU head returns nothing,
|
|
# BSD/macOS head errors out).
|
|
if [ "$POD_CAP" -gt 0 ] && [ -n "$POD_LISTING" ]; then
|
|
printf '%s\n' "$POD_LISTING" | head -n "$POD_CAP"
|
|
fi
|
|
if [ "${POD_TOTAL:-0}" -gt "$POD_CAP" ]; then
|
|
echo " ... $((POD_TOTAL - POD_CAP)) more not-Ready pod(s) not listed here; the full set is in kubernetes/pods.txt"
|
|
fi
|
|
echo
|
|
|
|
echo "## ImagePullBackOff / ErrImagePull"
|
|
echo
|
|
# Reuses the list already read above rather than calling the apiserver a second
|
|
# time for the same rows: one fewer cluster-wide read on the path that runs
|
|
# immediately before the tarball is written, and the two sections cannot disagree
|
|
# about what the cluster looked like.
|
|
#
|
|
# Which means it inherits that read's failure too. Without this guard the section
|
|
# is byte-identical between a healthy cluster and one whose pod list was refused,
|
|
# and a triager who jumps straight to this heading reads "nothing is failing to
|
|
# pull" off a list that never arrived.
|
|
if [ "$POD_LIST_RC" -ne 0 ]; then
|
|
echo " not known: this section is derived from the cluster-wide pod list above, which did not return"
|
|
else
|
|
printf '%s\n' "$POD_LIST" \
|
|
| awk 'NF && $4 ~ /ImagePullBackOff|ErrImagePull/ {printf " %s/%s — %s\n", $1, $2, $4}'
|
|
fi
|
|
echo
|
|
|
|
echo "## Recent OOMKilled events (last 20)"
|
|
echo
|
|
summary_read "OOMKilling event" kubectl get events -A --field-selector reason=OOMKilling --sort-by=.lastTimestamp
|
|
printf '%s\n' "$READ_OUT" | awk 'NF' | tail -20
|
|
echo
|
|
|
|
echo "## Recent Warning events (last 30)"
|
|
echo
|
|
summary_read "Warning event" kubectl get events -A --field-selector type=Warning --sort-by=.lastTimestamp
|
|
printf '%s\n' "$READ_OUT" | awk 'NF' | tail -30
|
|
echo
|
|
|
|
echo "## cert-manager: Certificates not Ready"
|
|
echo
|
|
if summary_crd certificates.cert-manager.io; then
|
|
summary_read "Certificate" kubectl get certificates.cert-manager.io -A --no-headers
|
|
printf '%s\n' "$READ_OUT" | awk 'NF && $3 != "True" {printf " %s/%s — Ready=%s\n", $1, $2, $3}'
|
|
fi
|
|
echo
|
|
|
|
echo "## Flux Sources not Ready"
|
|
echo
|
|
for kind in helmrepositories.source.toolkit.fluxcd.io ocirepositories.source.toolkit.fluxcd.io gitrepositories.source.toolkit.fluxcd.io externalartifacts.source.toolkit.fluxcd.io; do
|
|
summary_crd "$kind" || continue
|
|
short=${kind%%.*}
|
|
# custom-columns, not jsonpath. The jsonpath this replaces nested one filter
|
|
# inside another -- `.items[?(...conditions[?(@.type=="Ready")]...)]` -- which
|
|
# kubectl rejects outright with "unterminated filter", so this section printed
|
|
# nothing on every report ever taken and read as "all Flux sources are Ready".
|
|
# A form that sends stderr to /dev/null and drops the status hides that
|
|
# completely: the section renders empty and reads as "all sources Ready".
|
|
#
|
|
# Not the printer columns either: READY sits at a different index per kind, and
|
|
# STATUS carries spaces, so column counting is wrong for at least one of the four.
|
|
# custom-columns takes a single filter, the same shape the node section below uses.
|
|
summary_read "$short" kubectl get "$kind" -A --no-headers \
|
|
-o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,READY:.status.conditions[?(@.type=="Ready")].status'
|
|
printf '%s\n' "$READ_OUT" | awk -v k="$short" 'NF && $3 != "True" {printf " %s %s/%s — Ready=%s\n", k, $1, $2, $3}'
|
|
done
|
|
echo
|
|
|
|
echo "## Storage: PVCs not Bound, PVs not Bound"
|
|
echo
|
|
summary_read "PVC" kubectl get pvc -A --no-headers
|
|
printf '%s\n' "$READ_OUT" | awk 'NF && $3 != "Bound" {printf " PVC %s/%s — %s\n", $1, $2, $3}'
|
|
summary_read "PV" kubectl get pv --no-headers
|
|
printf '%s\n' "$READ_OUT" | awk 'NF && $5 != "Bound" {printf " PV %s — %s\n", $1, $5}'
|
|
echo
|
|
|
|
echo "## Node Conditions"
|
|
summary_read "node condition" kubectl get nodes -o 'custom-columns=NAME:.metadata.name,READY:.status.conditions[?(@.type=="Ready")].status,DISK:.status.conditions[?(@.type=="DiskPressure")].status,MEM:.status.conditions[?(@.type=="MemoryPressure")].status'
|
|
[ -z "$READ_OUT" ] || printf '%s\n' "$READ_OUT"
|