Skip to content

feat(hack): improve check-readiness.sh — wait/core/parallel modes and broader coverage - #2740

Closed
myasnikovdaniil wants to merge 1 commit into
mainfrom
feat/improve-check-readiness
Closed

myasnikovdaniil wants to merge 1 commit into
mainfrom
feat/improve-check-readiness

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Rewrite of hack/check-readiness.sh for a more accurate readiness signal during
installs/upgrades, while being lighter on the apiserver.

Correctness fixes

  • Reads .status.conditions[?(@.type=="Ready")].status via jsonpath instead of
    grepping for "True" anywhere in the row. Distinguishes False, Unknown, and
    missing conditions.
  • Detects .spec.suspend separately — suspended Flux resources show as
    SUSPENDED (cyan), not as failures.
  • Fixes a column-shift bug where cluster-scoped kinds (Packages, Nodes,
    APIServices, CRDs) had every field misaligned because bash read -d $'\t'
    strips leading empty fields. Switched to ASCII Unit Separator (\x1f).
  • Color auto-disabled on non-TTY; --no-color explicit toggle.

Expanded coverage

Original: Packages, ArtifactGenerators, ExternalArtifacts, HelmReleases.

Now adds: Kustomizations, GitRepository, OCIRepository, HelmRepository,
HelmChart, Bucket, Nodes (Ready), APIServices (Available),
CRDs (Established), PersistentVolumeClaims (Bound).

New modes

flag behavior
--wait + --timeout 30m Block until everything is ready (exit 0) or timeout (exit 1) — usable as a CI gate
--core Limit to the 5 cozystack/Flux primitives that actually drive an install. Minimizes apiserver load during upgrades.
--parallel Opt-in concurrent fetches. Faster wall-clock (3-4×), but bursts N concurrent LISTs at the apiserver.
-v / --verbose Show condition reason + first-line message, plus N/M not ready footer per kind
-n / -l Scope queries to a namespace or label selector

Apiserver load

Default is now sequential fetch. This script is most often used while
watching long-running upgrades, when reconcilers are already hammering the
apiserver and a burst of 14 concurrent LISTs every interval is the wrong
shape. Sequential spreads the same requests over more wall-clock and avoids
the burst. --parallel is opt-in for one-shot runs against a healthy cluster.

Default watch interval bumped 5s → 10s for the same reason.

Measured on a populated dev cluster (~60 Packages, ~126 HRs, ~38 PVCs):

mode wall-clock
--core 10s, no burst
--core --parallel 3s, 5-wide burst
full (default) 26s, no burst
full --parallel 6s, 14-wide burst

Backward compatibility

-w [INTERVAL] preserved (default interval bumped). No-arg invocation still
works (covers more kinds, same exit-code semantics).

Release note

hack/check-readiness.sh: more accurate readiness via jsonpath status parsing, broader kind coverage (Kustomizations, all Flux sources, Nodes, APIServices, CRDs, PVCs), new --wait/--core/--parallel/--verbose modes, sequential-by-default fetches to avoid apiserver bursts during upgrades.

Summary by CodeRabbit

  • New Features
    • Added namespace and selector scoping options for targeted readiness checks.
    • Introduced wait mode with timeout support, watch mode for continuous monitoring, and parallel execution option.
    • Added verbose and color output control flags.
    • Auto-detects terminal capabilities for intelligent ANSI color handling.

Review Change Stack

… broader coverage

Replace the "True"-anywhere column heuristic with structured status-condition
parsing via jsonpath, and expand coverage to all Flux sources (GitRepository,
OCIRepository, HelmRepository, HelmChart, Bucket), Kustomizations, Nodes
(Ready), APIServices (Available), CRDs (Established), and PVCs (phase=Bound).

New modes:
  --wait + --timeout   block until ready (exit 0) or timeout (exit 1) — CI gate
  --core               limit to the 5 cozystack/Flux primitives that drive an
                       install; minimizes apiserver load during upgrades
  --parallel           opt-in concurrent fetches (default is sequential to
                       avoid bursts of N concurrent LISTs while reconcilers
                       are already hammering the apiserver mid-upgrade)
  --verbose            show condition reason+message and N/M-not-ready footer
  -n / -l              scope queries to a namespace or label selector
  --no-color           explicit toggle (auto-disabled on non-TTY)

Also:
  - Detect .spec.suspend separately from failure (rendered as SUSPENDED, not
    a hard error)
  - Cluster-scoped kinds (Packages, Nodes, APIServices, CRDs) parse correctly
    — previously empty leading namespace shifted every column
  - Default watch interval 5s → 10s (less aggressive during long upgrades)
  - "CRD not installed, skipping" instead of silently scanning empty output

Assisted-By: Claude <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the hack/check-readiness.sh script to provide a more robust and flexible readiness signal for Cozystack and Flux resources. By moving away from brittle text-based parsing to structured JSONPath queries and introducing configurable execution modes, the script becomes both more accurate and better suited for high-load environments like cluster upgrades.

Highlights

  • Improved Readiness Logic: Replaced heuristic-based column grepping with precise JSONPath parsing of status conditions, allowing for accurate detection of False, Unknown, and missing states.
  • Expanded Resource Coverage: Added support for Kustomizations, Git/OCI/Helm repositories, HelmCharts, Buckets, Nodes, APIServices, CRDs, and PVCs.
  • New Operational Modes: Introduced --wait (for CI gates), --core (for minimal apiserver load), --parallel (for faster one-shot runs), and --verbose modes.
  • Performance and Stability: Switched to sequential fetching by default to prevent apiserver bursts during upgrades and implemented ASCII Unit Separator delimiters to fix column-alignment bugs.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@coderabbitai

coderabbitai Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

hack/check-readiness.sh was rewritten from a fixed 4-kind checker into a catalog-driven readiness monitor. The script now supports flexible scoping (--namespace, --selector), output control (--verbose, --no-color), and execution modes (--wait, --parallel, --core). Helper functions cache kubectl queries, detect CRDs, fetch readiness rows via JSONPath, and render results. Main flow orchestrates two phases: fetch rows for each kind and process in catalog order.

Changes

Catalog-driven readiness monitoring

Layer / File(s) Summary
Resource catalog and metadata
hack/check-readiness.sh
Defines resource catalogs (core cozystack/Flux kinds, additional Flux sources, cluster kinds, PVC kind) with condition types and suspend metadata that drive the script.
CLI argument parsing and configuration
hack/check-readiness.sh
Adds --wait, --timeout, --verbose, --namespace, --selector, --no-color, --parallel, --core, and help flags; includes TTY-based color auto-disabling and timeout-string parsing.
kubectl infrastructure and CRD detection
hack/check-readiness.sh
Reusable kubectl scoping helpers for namespace/cluster queries with optional label selectors; cached api-resources fetch and kind_exists check for API server support.
Readiness data fetching
hack/check-readiness.sh
Fetches condition/phase rows via JSONPath (conditions or phase based on kind); emits unit-separator-delimited fields with optional suspend status and reason/message suffixes.
Output formatting and result processing
hack/check-readiness.sh
Processes rows with per-kind labeling, categorizes suspended vs non-ready conditions (including PVC phase handling), prints headers and details, tracks counts and failure flags.
Main orchestration and kind query coordination
hack/check-readiness.sh
Builds entry list from selected kind catalogs, fetches rows for each kind (sequential by default, concurrent with --parallel), processes in catalog order, and handles missing CRDs.
Terminal UI and execution modes
hack/check-readiness.sh
Smooth screen clearing, --wait polling with timeout and exit codes, --watch loop with terminal refresh, Ctrl+C cleanup, and final exit logic.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 A checker once simple, now thrives on catalogs bright,
With parallel queries dancing through the readiness night,
Suspended or waiting, each kind finds its place,
While smooth screens refresh with a rabbity grace! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically summarizes the main change: rewriting check-readiness.sh with new wait/core/parallel modes and expanded coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/improve-check-readiness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot Bot added the kind/feature Categorizes issue or PR as related to a new feature label May 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hack/check-readiness.sh`:
- Around line 193-209: The script currently swallows kubectl failures (via
constructs like || true and ignoring empty caches) which causes unreachable API
server or forbidden LIST to be treated as "ready"; update
ensure_api_resources_cached to return a non-zero failure when the kubectl
api-resources call fails (don't use "|| true"), propagate that error to callers,
and change kind_exists so it distinguishes between an empty result and a real
kubectl failure (return non-zero on failure instead of assuming the kind
exists); likewise, in the per-kind get and the parallel wait logic (the other
places noted) stop discarding kubectl exit codes, check and propagate those
errors up so the script exits non-zero on API errors rather than printing "All
resources are ready."
- Around line 242-275: The jsonpath currently terminates rows with a literal
newline {"\n"}, which breaks rows when .status.conditions[].message contains
newlines; change the record terminator to a non-printing separator (e.g., ASCII
Record Separator \x1e) in the jsonpath construction inside fetch_rows (replace
{"\n"} with {"\x1e"}), then after the kubectl call convert that separator back
to newlines (pipe the KUBECTL get ... output through tr $'\x1e' '\n' or
equivalent) so multiline message fields stay contained in a single logical row;
update the jsonpath strings and the "${KUBECTL[@]}" get "$kind" ... invocation
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a601d6c4-b9a2-45d4-985f-9329f29a8c76

📥 Commits

Reviewing files that changed from the base of the PR and between 2fa15ce and 17b2caa.

📒 Files selected for processing (1)
  • hack/check-readiness.sh

Comment thread hack/check-readiness.sh
Comment on lines +193 to +209
ensure_api_resources_cached() {
if [[ $__API_RESOURCES_CACHED -eq 0 ]]; then
__API_RESOURCES_CACHE=$("${KUBECTL[@]}" api-resources --no-headers 2>/dev/null || true)
__API_RESOURCES_CACHED=1
fi
}

# Does the API server know about this CRD/kind?
# `kind` may be the bare resource name ("nodes") or "name.group" ("packages.cozystack.io").
# api-resources columns: NAME [SHORTNAMES] APIVERSION NAMESPACED KIND
# (SHORTNAMES is omitted entirely when there are none, so column index shifts.)
kind_exists() {
local kind="$1"
local header_printed=0
ensure_api_resources_cached
# If we couldn't list resources at all (e.g. API server unreachable), assume the
# kind exists and let the actual fetch fail with a real error.
[[ -z "$__API_RESOURCES_CACHE" ]] && return 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Don’t treat kubectl failures as “ready.”

api-resources, per-kind get, and parallel wait errors are all swallowed here, so an unreachable apiserver or forbidden LIST can degrade into empty row files and a final All resources are ready. That’s a false green on the script’s main path.

Also applies to: 275-275, 367-367

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/check-readiness.sh` around lines 193 - 209, The script currently
swallows kubectl failures (via constructs like || true and ignoring empty
caches) which causes unreachable API server or forbidden LIST to be treated as
"ready"; update ensure_api_resources_cached to return a non-zero failure when
the kubectl api-resources call fails (don't use "|| true"), propagate that error
to callers, and change kind_exists so it distinguishes between an empty result
and a real kubectl failure (return non-zero on failure instead of assuming the
kind exists); likewise, in the per-kind get and the parallel wait logic (the
other places noted) stop discarding kubectl exit codes, check and propagate
those errors up so the script exits non-zero on API errors rather than printing
"All resources are ready."

Comment thread hack/check-readiness.sh
Comment on lines +242 to +275
# Returns one row per object, fields separated by ASCII Unit Separator (0x1f):
# namespace<US>name<US>status<US>reason<US>message<US>suspended
#
# Why \x1f and not \t: bash `read -d ... IFS=$'\t'` treats tab as whitespace IFS
# and silently strips leading empty fields — so a cluster-scoped row that starts
# with an empty namespace would shift every subsequent column. The Unit Separator
# is non-whitespace, so leading empty fields survive intact.
#
# `status` is the literal condition value ("True"/"False"/"Unknown"/"") or the
# phase value for PVCs.
fetch_rows() {
local kind="$1" scope="$2" cond_type="$3" supports_suspend="$4"
local args=()
build_kubectl_scope_args "$scope" args

local jsonpath
if [[ "$cond_type" == "_phase" ]]; then
jsonpath='{range .items[*]}{.metadata.namespace}{"\x1f"}{.metadata.name}{"\x1f"}{.status.phase}{"\x1f"}{""}{"\x1f"}{""}{"\x1f"}{"false"}{"\n"}{end}'
else
local suspend_field='{"false"}'
if [[ "$supports_suspend" == "1" ]]; then
suspend_field='{.spec.suspend}'
fi
if ! echo "$line" | awk '{for(i=1;i<=NF;i++) if($i=="True") exit 0; exit 1}'; then
if [[ $header_printed -eq 0 ]]; then
echo -e "${BOLD}${YELLOW}=== ${kind} (not ready) ===${RESET}"
header_printed=1
jsonpath='{range .items[*]}'
jsonpath+='{.metadata.namespace}{"\x1f"}'
jsonpath+='{.metadata.name}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].status}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].reason}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].message}{"\x1f"}'
jsonpath+="${suspend_field}"'{"\n"}'
jsonpath+='{end}'
fi

"${KUBECTL[@]}" get "$kind" "${args[@]}" -o jsonpath="$jsonpath" 2>/dev/null || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rows="$(mktemp)"
printf 'default\037helmrelease-a\037False\037InstallFailed\037first line\nsecond line\037false\n' > "$rows"

while IFS=$'\x1f' read -r ns name status reason message suspended; do
  printf 'ns=%q name=%q status=%q reason=%q message=%q suspended=%q\n' \
    "$ns" "$name" "$status" "$reason" "$message" "$suspended"
done < "$rows"

Repository: cozystack/cozystack

Length of output: 231


Fix row framing for multiline message values in hack/check-readiness.sh
fetch_rows currently terminates each row with a newline ({"\n"}) while .status.conditions[..].message is emitted verbatim; a multiline message will be split into multiple records and corrupt the parsed columns (e.g., first line becomes one row and second line shifts into subsequent fields).

Suggested change
-# Returns one row per object, fields separated by ASCII Unit Separator (0x1f):
-#   namespace&lt;US&gt;name&lt;US&gt;status&lt;US&gt;reason&lt;US&gt;message&lt;US&gt;suspended
+# Returns one row per object, fields separated by ASCII Unit Separator (0x1f)
+# and records separated by ASCII Record Separator (0x1e):
+#   namespace&lt;US&gt;name&lt;US&gt;status&lt;US&gt;reason&lt;US&gt;message&lt;US&gt;suspended&lt;RS&gt;
@@
-    jsonpath='{range .items[*]}{.metadata.namespace}{"\x1f"}{.metadata.name}{"\x1f"}{.status.phase}{"\x1f"}{""}{"\x1f"}{""}{"\x1f"}{"false"}{"\n"}{end}'
+    jsonpath='{range .items[*]}{.metadata.namespace}{"\x1f"}{.metadata.name}{"\x1f"}{.status.phase}{"\x1f"}{""}{"\x1f"}{""}{"\x1f"}{"false"}{"\x1e"}{end}'
@@
-    jsonpath+="${suspend_field}"'{"\n"}'
+    jsonpath+="${suspend_field}"'{"\x1e"}'
@@
-  while IFS=$'\x1f' read -r ns name status reason message suspended; do
+  while IFS=$'\x1f' read -r -d $'\x1e' ns name status reason message suspended; do
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/check-readiness.sh` around lines 242 - 275, The jsonpath currently
terminates rows with a literal newline {"\n"}, which breaks rows when
.status.conditions[].message contains newlines; change the record terminator to
a non-printing separator (e.g., ASCII Record Separator \x1e) in the jsonpath
construction inside fetch_rows (replace {"\n"} with {"\x1e"}), then after the
kubectl call convert that separator back to newlines (pipe the KUBECTL get ...
output through tr $'\x1e' '\n' or equivalent) so multiline message fields stay
contained in a single logical row; update the jsonpath strings and the
"${KUBECTL[@]}" get "$kind" ... invocation accordingly.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly refactors and enhances the hack/check-readiness.sh script, introducing options for wait/watch modes, timeouts, parallel fetching, namespace/selector scoping, and verbose output, while parsing status conditions directly via JSONPath. The reviewer feedback focuses on improving the robustness of the script, including propagating errors from kubectl get to avoid false positives, returning explicit exit statuses from run_once instead of relying on fragile stdout grepping, dynamically parsing the help text in usage, ensuring the cursor is restored via an EXIT trap in watch mode, and handling potential tput failures gracefully.

Comment thread hack/check-readiness.sh
jsonpath+='{end}'
fi

"${KUBECTL[@]}" get "$kind" "${args[@]}" -o jsonpath="$jsonpath" 2>/dev/null || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Appending `|| true` to the `kubectl get` command in `fetch_rows` silently ignores any actual errors (such as authentication failures, network timeouts, or API server unavailability). If `kubectl` fails, it outputs nothing, which causes `process_rows` to see an empty file and assume all resources of that kind are ready. This can lead to dangerous false positives where the script reports "All resources are ready" when the cluster is actually unreachable or experiencing issues. Since `kind_exists` is already checked beforehand, any failure here is a genuine error and should be propagated.

Suggested change
"${KUBECTL[@]}" get "$kind" "${args[@]}" -o jsonpath="$jsonpath" 2>/dev/null || true
"${KUBECTL[@]}" get "$kind" "${args[@]}" -o jsonpath="$jsonpath" 2>/dev/null

Comment thread hack/check-readiness.sh
Comment on lines 393 to 396
if [[ $found_issues -eq 0 ]]; then
echo -e "${GREEN}${BOLD}All resources are ready.${RESET}"
fi
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Currently, `run_once` does not return an explicit exit status, meaning it always returns `0` (success) because the last evaluated statement (the `if` block when `found_issues` is non-zero) evaluates to `0` in Bash. This forces the `WAIT_MODE` loop to use a fragile `grep` on the stdout (`grep -q "All resources are ready."`). If any resource's status message or namespace happens to contain that string, the script will falsely report success and exit prematurely. Explicitly returning `0` or `1` from `run_once` allows us to check the exit status directly and safely.

Suggested change
if [[ $found_issues -eq 0 ]]; then
echo -e "${GREEN}${BOLD}All resources are ready.${RESET}"
fi
}
if [[ $found_issues -eq 0 ]]; then
echo -e "${GREEN}${BOLD}All resources are ready.${RESET}"
return 0
fi
return 1
}

Comment thread hack/check-readiness.sh
Comment on lines +411 to +418
output=$(run_once)
now=$(date +%s)
elapsed=$((now - start))
if echo "$output" | grep -q "All resources are ready."; then
echo -e "$output"
echo -e "${GREEN}Ready after ${elapsed}s.${RESET}"
exit 0
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Instead of grepping the stdout of `run_once` for a magic string (which is prone to false positives if any resource name, namespace, or error message contains `"All resources are ready."`), we should check the exit status of `run_once` directly. This is much more robust and idiomatic.

Suggested change
output=$(run_once)
now=$(date +%s)
elapsed=$((now - start))
if echo "$output" | grep -q "All resources are ready."; then
echo -e "$output"
echo -e "${GREEN}Ready after ${elapsed}s.${RESET}"
exit 0
fi
now=$(date +%s)
elapsed=$((now - start))
if output=$(run_once); then
echo -e "$output"
echo -e "${GREEN}Ready after ${elapsed}s.${RESET}"
exit 0
fi

Comment thread hack/check-readiness.sh
Comment on lines +43 to +45
usage() {
sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The `usage()` function relies on hardcoded line numbers (`sed -n '2,24p'`). This is fragile because any future changes to the script's header comments, adding/removing lines, or modifying the script's structure will silently break the help output. We can make this robust by using pattern matching to extract the help text dynamically.

Suggested change
usage() {
sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'
}
usage() {
sed -n '/^# Check readiness/,/^# -h, --help/p' "$0" | sed 's/^# \\{0,1\\}//'
}

Comment thread hack/check-readiness.sh
Comment on lines +427 to +428
trap 'tput cnorm 2>/dev/null || true; exit 0' INT TERM
command -v tput >/dev/null 2>&1 && tput civis 2>/dev/null || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In `WATCH` mode, the cursor is hidden using `tput civis`. If the script exits due to an error (e.g., a command fails and `set -e` triggers) or is terminated by another signal, the cursor will remain hidden in the user's terminal. Using an `EXIT` trap ensures that the cursor is always restored (`tput cnorm`) regardless of how the script exits.

Suggested change
trap 'tput cnorm 2>/dev/null || true; exit 0' INT TERM
command -v tput >/dev/null 2>&1 && tput civis 2>/dev/null || true
trap 'tput cnorm 2>/dev/null || true' EXIT
trap 'exit 0' INT TERM
command -v tput >/dev/null 2>&1 && tput civis 2>/dev/null || true

Comment thread hack/check-readiness.sh
Comment on lines +399 to +406
clear_screen_smooth() {
if [[ $USE_COLOR -eq 1 ]] && command -v tput >/dev/null 2>&1; then
tput cup 0 0
tput ed
else
clear
fi
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

If `tput` is installed but the terminal capability database is missing or `TERM` is invalid, `tput cup` or `tput ed` can fail and print error messages to stderr without falling back to `clear`. We should redirect stderr to `/dev/null` and fall back to `clear` if the `tput` commands fail.

Suggested change
clear_screen_smooth() {
if [[ $USE_COLOR -eq 1 ]] && command -v tput >/dev/null 2>&1; then
tput cup 0 0
tput ed
else
clear
fi
}
clear_screen_smooth() {
if [[ $USE_COLOR -eq 1 ]] && command -v tput >/dev/null 2>&1; then
tput cup 0 0 2>/dev/null && tput ed 2>/dev/null || clear
else
clear
fi
}

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly refactors the hack/check-readiness.sh script, adding support for advanced options such as wait mode with timeouts, parallel fetching, core-only checks, verbose logging, namespace/selector scoping, and color toggling. The review feedback highlights several critical robustness issues, including potential parsing failures caused by newlines in condition messages, silenced kubectl errors, a stale API resources cache during upgrades, script crashes under set -e if tput fails, and temporary directory leaks on interrupt or failure.

Comment thread hack/check-readiness.sh
Comment on lines +252 to +276
fetch_rows() {
local kind="$1" scope="$2" cond_type="$3" supports_suspend="$4"
local args=()
build_kubectl_scope_args "$scope" args

local jsonpath
if [[ "$cond_type" == "_phase" ]]; then
jsonpath='{range .items[*]}{.metadata.namespace}{"\x1f"}{.metadata.name}{"\x1f"}{.status.phase}{"\x1f"}{""}{"\x1f"}{""}{"\x1f"}{"false"}{"\n"}{end}'
else
local suspend_field='{"false"}'
if [[ "$supports_suspend" == "1" ]]; then
suspend_field='{.spec.suspend}'
fi
if ! echo "$line" | awk '{for(i=1;i<=NF;i++) if($i=="True") exit 0; exit 1}'; then
if [[ $header_printed -eq 0 ]]; then
echo -e "${BOLD}${YELLOW}=== ${kind} (not ready) ===${RESET}"
header_printed=1
jsonpath='{range .items[*]}'
jsonpath+='{.metadata.namespace}{"\x1f"}'
jsonpath+='{.metadata.name}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].status}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].reason}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].message}{"\x1f"}'
jsonpath+="${suspend_field}"'{"\n"}'
jsonpath+='{end}'
fi

"${KUBECTL[@]}" get "$kind" "${args[@]}" -o jsonpath="$jsonpath" 2>/dev/null || true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Newline in condition messages breaks parsing & Silenced kubectl errors

  1. Newline Issue: Kubernetes condition messages (especially for failing HelmReleases or Kustomizations) frequently contain newlines. Since fetch_rows uses jsonpath to output raw fields and process_rows reads line-by-line, any newline inside a message will break the row parsing. This leads to corrupted output (e.g., printing parts of the message as non-existent resource names with status Unknown).
  2. Silenced Errors: Appending || true and redirecting stderr to /dev/null in fetch_rows causes any real kubectl errors (such as authentication failure, network timeout, or permission denied) to be silently ignored. This results in empty output files, which process_rows treats as "no issues", ultimately leading the script to falsely report "All resources are ready." and exit with 0.

Solution

Using jq (which is already a project dependency) to parse the output of kubectl get -o json is much more robust. It allows us to safely replace newlines in messages with spaces using gsub("\n"; " "), handle missing fields gracefully, and propagate real connection/auth errors correctly.

fetch_rows() {
  local kind="$1" scope="$2" cond_type="$3" supports_suspend="$4"
  local args=()
  build_kubectl_scope_args "$scope" args

  if [[ "$cond_type" == "_phase" ]]; then
    "${KUBECTL[@]}" get "$kind" "${args[@]}" -o json | jq -r '.items[]? | [
      (.metadata.namespace // ""),
      .metadata.name,
      (.status.phase // ""),
      "",
      "",
      "false"
    ] | join("\u001f")'
  else
    "${KUBECTL[@]}" get "$kind" "${args[@]}" -o json | jq -r --arg cond "$cond_type" --arg susp "$supports_suspend" '.items[]? | [
      (.metadata.namespace // ""),
      .metadata.name,
      ([.status.conditions[]? | select(.type == $cond)][0] | .status // ""),
      ([.status.conditions[]? | select(.type == $cond)][0] | .reason // ""),
      ([.status.conditions[]? | select(.type == $cond)][0] | .message // "" | gsub("\n"; " ")),
      (if $susp == "1" then .spec.suspend // false else false end | tostring)
    ] | join("\u001f")'
  fi
}

Comment thread hack/check-readiness.sh
Comment on lines 338 to +341
run_once() {
found_issues=0
local tmpdir
tmpdir=$(mktemp -d -t check-readiness.XXXXXX)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Stale API Resources Cache during Upgrades

__API_RESOURCES_CACHED is a global variable and is never reset to 0. During a watch or wait loop, if a new CRD is installed (which is common during upgrades), the script will never detect it and will continue to skip it. Resetting __API_RESOURCES_CACHED=0 at the start of run_once ensures the cache is refreshed per iteration.

Suggested change
run_once() {
found_issues=0
local tmpdir
tmpdir=$(mktemp -d -t check-readiness.XXXXXX)
run_once() {
found_issues=0
__API_RESOURCES_CACHED=0
local tmpdir
tmpdir=$(mktemp -d -t check-readiness.XXXXXX)
CURRENT_TMPDIR="$tmpdir"

Comment thread hack/check-readiness.sh
Comment on lines +399 to +406
clear_screen_smooth() {
if [[ $USE_COLOR -eq 1 ]] && command -v tput >/dev/null 2>&1; then
tput cup 0 0
tput ed
else
clear
fi
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

tput failure under set -e crashes the script

Under set -e, if tput cup 0 0 or tput ed fails (which happens in environments with a dumb or missing TERM variable, such as some CI/CD runners or cron jobs), the script will immediately crash. Wrapping them in a safe && chain or appending || true / || clear prevents this.

Suggested change
clear_screen_smooth() {
if [[ $USE_COLOR -eq 1 ]] && command -v tput >/dev/null 2>&1; then
tput cup 0 0
tput ed
else
clear
fi
}
clear_screen_smooth() {
if [[ $USE_COLOR -eq 1 ]] && command -v tput >/dev/null 2>&1 && tput cup 0 0 2>/dev/null; then
tput ed 2>/dev/null || clear
else
clear
fi
}

Comment thread hack/check-readiness.sh
SELECTOR=""
USE_COLOR=1
PARALLEL=0
CORE_ONLY=0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Temporary directory leak on interrupt / failure

If the script is interrupted (e.g., via Ctrl+C in watch mode) or fails due to set -e while run_once is running, the temporary directory created by mktemp will be leaked in /tmp. Registering a global cleanup trap on EXIT and tracking the current temp directory ensures it is always cleaned up.

Suggested change
CORE_ONLY=0
CORE_ONLY=0
CURRENT_TMPDIR=""
cleanup() {
if [[ -n "${CURRENT_TMPDIR:-}" ]]; then
rm -rf "$CURRENT_TMPDIR"
fi
}
trap cleanup EXIT

Comment thread hack/check-readiness.sh
process_rows "$entry" "$tmpdir/$i"
done

rm -rf "$tmpdir"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Reset CURRENT_TMPDIR after successful cleanup at the end of run_once.

Suggested change
rm -rf "$tmpdir"
rm -rf "$tmpdir"
CURRENT_TMPDIR=""

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors hack/check-readiness.sh to robustly check the readiness of cozystack, Flux, and Kubernetes resources. It replaces column heuristics with direct JSONPath parsing of status conditions, introduces sequential fetching by default (with a --parallel option) to reduce API server load, and adds options for namespace/selector filtering, verbose output, and wait timeouts. The review feedback highlights several critical robustness improvements: using the ASCII Record Separator (\x1e) instead of newlines to prevent multiline status messages from breaking the parsing loop, dynamically extracting help comments in usage() rather than hardcoding line numbers, and utilizing EXIT traps to guarantee cleanup of temporary directories and restoration of the terminal cursor during early exits.

Comment thread hack/check-readiness.sh
Comment on lines +258 to +273
if [[ "$cond_type" == "_phase" ]]; then
jsonpath='{range .items[*]}{.metadata.namespace}{"\x1f"}{.metadata.name}{"\x1f"}{.status.phase}{"\x1f"}{""}{"\x1f"}{""}{"\x1f"}{"false"}{"\n"}{end}'
else
local suspend_field='{"false"}'
if [[ "$supports_suspend" == "1" ]]; then
suspend_field='{.spec.suspend}'
fi
if ! echo "$line" | awk '{for(i=1;i<=NF;i++) if($i=="True") exit 0; exit 1}'; then
if [[ $header_printed -eq 0 ]]; then
echo -e "${BOLD}${YELLOW}=== ${kind} (not ready) ===${RESET}"
header_printed=1
jsonpath='{range .items[*]}'
jsonpath+='{.metadata.namespace}{"\x1f"}'
jsonpath+='{.metadata.name}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].status}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].reason}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].message}{"\x1f"}'
jsonpath+="${suspend_field}"'{"\n"}'
jsonpath+='{end}'
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Kubernetes, status condition messages (especially for failing HelmReleases or Kustomizations) often contain newlines. Since the script currently uses \n as the record separator, any newline inside a condition message will prematurely terminate the read command, causing subsequent fields and rows to be misaligned or skipped entirely.

We can solve this robustly by using the ASCII Record Separator (\x1e) as the record delimiter instead of \n.

Suggested change
if [[ "$cond_type" == "_phase" ]]; then
jsonpath='{range .items[*]}{.metadata.namespace}{"\x1f"}{.metadata.name}{"\x1f"}{.status.phase}{"\x1f"}{""}{"\x1f"}{""}{"\x1f"}{"false"}{"\n"}{end}'
else
local suspend_field='{"false"}'
if [[ "$supports_suspend" == "1" ]]; then
suspend_field='{.spec.suspend}'
fi
if ! echo "$line" | awk '{for(i=1;i<=NF;i++) if($i=="True") exit 0; exit 1}'; then
if [[ $header_printed -eq 0 ]]; then
echo -e "${BOLD}${YELLOW}=== ${kind} (not ready) ===${RESET}"
header_printed=1
jsonpath='{range .items[*]}'
jsonpath+='{.metadata.namespace}{"\x1f"}'
jsonpath+='{.metadata.name}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].status}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].reason}{"\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].message}{"\x1f"}'
jsonpath+="${suspend_field}"'{"\n"}'
jsonpath+='{end}'
fi
if [[ "$cond_type" == "_phase" ]]; then
jsonpath='{range .items[*]}{.metadata.namespace}{"\\x1f"}{.metadata.name}{"\\x1f"}{.status.phase}{"\\x1f"}{""}{"\\x1f"}{""}{"\\x1f"}{"false"}{"\\x1e"}{end}'
else
local suspend_field='{"false"}'
if [[ "$supports_suspend" == "1" ]]; then
suspend_field='{.spec.suspend}'
fi
jsonpath='{range .items[*]}'
jsonpath+='{.metadata.namespace}{"\\x1f"}'
jsonpath+='{.metadata.name}{"\\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].status}{"\\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].reason}{"\\x1f"}'
jsonpath+='{.status.conditions[?(@.type=="'"$cond_type"'")].message}{"\\x1f"}'
jsonpath+="${suspend_field}"'{"\\x1e"}'
jsonpath+='{end}'
fi

Comment thread hack/check-readiness.sh
Comment on lines +287 to +288
while IFS=$'\x1f' read -r ns name status reason message suspended; do
[[ -z "$name" ]] && continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use read -d $'\x1e' to read records delimited by the ASCII Record Separator (\x1e), ensuring that multiline status messages do not break the parsing loop.

Suggested change
while IFS=$'\x1f' read -r ns name status reason message suspended; do
[[ -z "$name" ]] && continue
while IFS=$'\\x1f' read -d $'\\x1e' -r ns name status reason message suspended; do
[[ -z "$name" ]] && continue

Comment thread hack/check-readiness.sh
Comment on lines +43 to +45
usage() {
sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The usage() function hardcodes the line numbers (2,24p) to extract the help comment block. This is fragile and will break if any comments or lines are added or removed at the top of the file. We can make this dynamic and robust by using awk to print all comment lines starting from line 2 until the first non-comment line.

Suggested change
usage() {
sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'
}
usage() {
awk 'NR>1 && /^#/ { sub(/^# ?/, ""); print; next } NR>1 { exit }' "$0"
}

Comment thread hack/check-readiness.sh
Comment on lines +340 to +341
local tmpdir
tmpdir=$(mktemp -d -t check-readiness.XXXXXX)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The temporary directory tmpdir is created in run_once() but is only cleaned up at the very end of the function. If the script exits early (e.g., due to set -e on a command failure or user interruption), the temporary directory will be left behind in /tmp.

Using an EXIT trap with double quotes ensures that the temporary directory is always cleaned up when the shell/subshell exits, even if the local variable goes out of scope.

Suggested change
local tmpdir
tmpdir=$(mktemp -d -t check-readiness.XXXXXX)
local tmpdir
tmpdir=$(mktemp -d -t check-readiness.XXXXXX)
trap "rm -rf '${tmpdir}'" EXIT

Comment thread hack/check-readiness.sh
Comment on lines +427 to +428
trap 'tput cnorm 2>/dev/null || true; exit 0' INT TERM
command -v tput >/dev/null 2>&1 && tput civis 2>/dev/null || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In watch mode, the terminal cursor is hidden using tput civis. The current trap only restores the cursor (tput cnorm) on INT and TERM signals. If the script exits due to an error (e.g., a command failure under set -e), the cursor will remain hidden, which degrades the user's terminal experience.

Using an EXIT trap guarantees that the cursor is always restored when the script exits for any reason.

Suggested change
trap 'tput cnorm 2>/dev/null || true; exit 0' INT TERM
command -v tput >/dev/null 2>&1 && tput civis 2>/dev/null || true
trap 'tput cnorm 2>/dev/null || true' EXIT
trap 'exit 0' INT TERM
command -v tput >/dev/null 2>&1 && tput civis 2>/dev/null || true

@myasnikovdaniil myasnikovdaniil self-assigned this May 28, 2026
Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Jun 9, 2026
…2755)

## What this PR does

Replaces the `hack/check-readiness.sh` diagnostic with a Go command at
`cmd/check-readiness`, integrated into the build the same way as
`cozypkg`:

- `make check-readiness` builds `_out/bin/check-readiness`, with
`Version`
  injected via `-ldflags`.
- The tool shells out to `kubectl` and reports only **non-ready**
cozystack /
  Flux / Kubernetes resources, parsed from status conditions (not column
heuristics). Flags: `--wait`/`--timeout`, `--watch`, `--core`,
`--parallel`,
  `--verbose`, `-n`/`-l` scoping, `--no-color`, `--version`.

Adds a black-box golden test suite under `test/check-readiness/`:

- A mock `kubectl` serves canned fixtures; the suite diffs
stdout/stderr/exit
against golden files across 30 scenarios (arg parsing,
condition/phase/suspend
handling, CRD-not-installed, kubectl arg propagation, wait/timeout, edge
  cases) plus a PTY test for color output.
- `make test-check-readiness` runs it; wired into `make unit-tests`.

Supersedes #2740, which added the same feature set to the bash script.

### Release note
```release-note
feat(hack): replace check-readiness.sh with a `check-readiness` Go command (build with `make check-readiness`); reports non-ready cozystack/Flux/Kubernetes resources with --wait/--watch/--core/--parallel/--verbose and namespace/label scoping.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added `check-readiness` command for verifying Kubernetes resource
readiness status across namespaces
* Supports multiple execution modes: one-shot, wait with timeout, and
watch with configurable intervals
* Includes namespace filtering, label selectors, parallel resource
fetching, and color output control

* **Tests**
* Added comprehensive test suite validating command behavior across
various scenarios

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2755?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
pull Bot pushed a commit to medampudi/cozystack that referenced this pull request Jun 9, 2026
Replace hack/check-readiness.sh with a Go command at cmd/check-readiness,
wired into the build like cozypkg (`make check-readiness`, Version via
-ldflags). The tool shells out to kubectl and reports only non-ready
cozystack / Flux / Kubernetes resources parsed from status conditions,
supporting --wait/--timeout, --watch, --core, --parallel, --verbose,
-n/-l scoping, --no-color and --version.

Add a black-box golden test suite under test/check-readiness driven by a
mock kubectl over fixtures; `make test-check-readiness` runs it and it is
wired into `make unit-tests`.

Supersedes cozystack#2740, which added the same feature set to the bash script.

Assisted-By: Claude <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

superseeded by #2755

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Categorizes issue or PR as related to a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant