Skip to content

test(tests): pre-pull timing-sensitive platform images before install - #2556

Merged
myasnikovdaniil merged 1 commit into
mainfrom
daniil/split-prepull
May 4, 2026
Merged

myasnikovdaniil merged 1 commit into
mainfrom
daniil/split-prepull

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented May 2, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Some workloads (kube-ovn raft, LINSTOR controller / piraeus-server) fail when replicas start at materially different times due to image-pull stagger across nodes. Adds a DaemonSet-based pre-pull step that runs before helm install, ensuring every node has the images cached so replicas start within milliseconds of each other.

hack/e2e-prepull-images.sh accepts image refs on stdin and creates one container per image so pulls run in parallel — total time = max(any single image) rather than sum. The bats test sources image refs directly from the rendered platform charts via yq, walking only PodSpec-shaped objects, so version bumps stay in sync without a hardcoded list.

Surfaced from #2500.

Release note

NONE

Summary by CodeRabbit

  • Tests

    • Added an end-to-end test that pre-pulls platform container images before the installation workflow to improve install reliability.
  • Chores

    • Added an automated pre-pull utility that reads image lists, deduplicates and sorts them, deploys a short-lived pre-puller across nodes, waits for completion, then cleans up.

@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 improves the reliability of end-to-end installations by addressing image-pull stagger across nodes. By ensuring that all required images are cached on every node before the main workload deployment, it prevents race conditions and timeouts in distributed systems that are sensitive to replica startup synchronization.

Highlights

  • Automated Image Pre-pulling: Introduced a new DaemonSet-based mechanism to pre-pull container images across all cluster nodes before Helm installations occur.
  • Dynamic Image Discovery: Implemented a dynamic discovery process using yq to extract image references directly from rendered Helm charts, ensuring the pre-pull list stays synchronized with version updates.
  • Improved Reliability: Mitigates startup failures in timing-sensitive workloads like kube-ovn and LINSTOR by ensuring all replicas start with cached images, preventing raft connection timeouts.

🧠 New Feature in Public Preview: You can now enable Memory 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 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 counter productive. 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. ↩

@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review size/L This PR changes 100-499 lines, ignoring generated files labels May 2, 2026
@coderabbitai

coderabbitai Bot commented May 2, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds an e2e pre-pull step: a new Bats test renders kubeovn and linstor charts, extracts container and initContainer image refs, and pipes them to a new Bash script that deploys a temporary DaemonSet to pre-pull those images on all cluster nodes before installing Cozystack.

Changes

Image Pre-caching for E2E Tests

Layer / File(s) Summary
Test Integration
hack/e2e-install-cozystack.bats
New Bats test Pre-pull platform images renders packages/system/kubeovn and packages/system/linstor via helm template, collects container and initContainer .image values with yq, and pipes the list to the pre-pull script before Install Cozystack.
Image Parsing & Validation
hack/e2e-prepull-images.sh (lines 1–30)
Script reads image refs from stdin, ignores blank lines and # comments, deduplicates and sorts, and exits early if no images are provided.
DaemonSet Construction
hack/e2e-prepull-images.sh (lines 31–49)
Builds YAML fragment with one sleep infinity container per image (pull-<n>), imagePullPolicy: IfNotPresent, small resource requests.
Deployment & Readiness
hack/e2e-prepull-images.sh (lines 50–85)
Applies an inline DaemonSet in kube-system (app: e2e-image-prepuller) with hostNetwork: true and automountServiceAccountToken: false, waits up to 10 minutes for rollout, prints completion, and deletes the DaemonSet on exit (cleanup trap).

Sequence Diagram

sequenceDiagram
    participant Bats as Bats Test
    participant Helm as Helm/YQ
    participant Script as Pre-pull Script
    participant K8s as Kubernetes API
    participant Node as Cluster Node

    Bats->>Helm: Render charts (`kubeovn`, `linstor`)
    Helm-->>Bats: YAML with containers & initContainers
    Bats->>Helm: Extract `.image` fields (yq)
    Helm-->>Bats: Deduplicated image list
    Bats->>Script: Pipe image list to stdin
    Script->>Script: Parse, dedupe, build DaemonSet manifest
    Script->>K8s: kubectl apply DaemonSet
    K8s->>Node: Schedule Pod per node (sleep containers)
    Node->>Node: Pull images (IfNotPresent)
    K8s-->>Script: DaemonSet ready (rollout)
    Script->>K8s: Delete DaemonSet (cleanup)
    Script-->>Bats: Pre-pull complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through charts both big and small,
Sniffed image names and gathered them all,
A sleepy DaemonSet hopped on each node,
Pulling caches down on every road,
Now Cozystack springs—no pause, no stall! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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.
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.
Title check ✅ Passed The PR title accurately reflects the main change: adding a pre-pull step for platform images before Cozystack installation. It is specific, concise, and directly related to the changeset's core purpose.

✏️ 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 daniil/split-prepull

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 area/testing Issues or PRs related to testing (e2e, bats, unit tests) label May 2, 2026

@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 introduces an image pre-pulling mechanism for E2E tests to prevent timeouts in peer-sensitive workloads like OVN and LINSTOR. It adds a new test case to extract images from Helm charts and a bash script that deploys a temporary DaemonSet to cache these images across all nodes. Feedback includes improving the script's robustness by using a shell trap for cleanup, refining the yq extraction logic, and ensuring the commit scope adheres to the repository's style guide.

# status blocks until all pods are Ready (= all containers running = all
# images cached on every node), then we delete the DaemonSet.

set -euo pipefail

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

To ensure the e2e-image-prepuller DaemonSet is always cleaned up, even if the script fails or times out during the rollout status check, it's recommended to use a trap on EXIT.

Suggested change
set -euo pipefail
set -euo pipefail
cleanup() {
kubectl delete daemonset e2e-image-prepuller -n kube-system --ignore-not-found
}
trap cleanup EXIT

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — confirmed. Under set -euo pipefail, a kubectl rollout status timeout aborted the script before line 80 and leaked the e2e-image-prepuller DaemonSet until the next CI run overwrote it. Moved the delete into a cleanup() registered with trap … EXIT so it fires on every exit path (success, rollout timeout, signal). Fixed in 9df5950.

fi
}

@test "Pre-pull platform images" {

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

The PR title uses the scope e2e, which is not listed as a valid scope in the repository style guide. Please consider using tests or hack instead to adhere to the Conventional Commits requirements.

References
  1. Valid scopes for commits are defined in the repository style guide (lines 40-45). e2e is not among them. (link)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right — e2e is neither a Conventional Commits type nor one of the scopes listed in .gemini/styleguide.md (Meta scopes are api, hack, tests, ci, docs). Renamed the commit subject and the PR title to test(tests): pre-pull timing-sensitive platform images before install, which matches the repo's convention for tests-only changes. Fixed in 9df5950 (force-push).

Comment thread hack/e2e-install-cozystack.bats Outdated
Comment on lines +24 to +27
} | yq -N '
(..|select(has("containers"))|.containers[]|.image),
(..|select(has("initContainers"))|.initContainers[]|.image)
' | hack/e2e-prepull-images.sh

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

The yq query can be simplified and made more robust by using optional chaining and filtering out null values. This ensures that if any container or initContainer is missing an image field, it won't cause issues in the downstream script.

  } | yq -N '.. | (.containers, .initContainers) | .[]? | .image | select(.)' | hack/e2e-prepull-images.sh

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The longer form is intentional: each branch of the union (containers, initContainers) lines up with the comment block above it that explains "walk only PodSpec-shaped objects, take container images." The proposed .[]? does add safety against a missing image: field, but helm template of these charts always emits a fully-populated image: per container, so there's nothing to guard against today. Keeping the current shape for readability.


set -euo pipefail

mapfile -t images < <(grep -Ev '^[[:space:]]*(#|$)' | sort -u)

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

Adding --- to the grep filter ensures that any YAML document separators produced by yq (which can happen with multi-document input) are ignored.

Suggested change
mapfile -t images < <(grep -Ev '^[[:space:]]*(#|$)' | sort -u)
mapfile -t images < <(grep -Ev '^[[:space:]]*(#|$|---)' | sort -u)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yq -N (--no-doc) is upstream of this grep and explicitly suppresses YAML document separators by design — yq cannot emit --- with that flag set. Adding --- to the filter would be dead code, and worse, would silently mask a real upstream bug if -N ever got dropped. Prefer to keep the script erroring loudly on unexpected input.

Comment thread hack/e2e-prepull-images.sh Outdated
echo "Waiting for e2e-image-prepuller DaemonSet to become ready..."
kubectl rollout status daemonset/e2e-image-prepuller -n kube-system --timeout=10m

kubectl delete daemonset e2e-image-prepuller -n kube-system --ignore-not-found

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

With the implementation of a trap for cleanup, this explicit delete call is redundant and can be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed — the delete now runs from the EXIT trap installed at the top of the script (see the related thread on line 18). Fixed in 9df5950.

@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: 1

🧹 Nitpick comments (1)
hack/e2e-prepull-images.sh (1)

77-80: ⚡ Quick win

Orphaned DaemonSet left in kube-system on rollout timeout.

Because set -euo pipefail is active, a kubectl rollout status timeout causes the script to abort at line 78 and never reach the kubectl delete on line 80. The e2e-image-prepuller DaemonSet (and its sleeping pods) stays running until the next CI run overwrites it via kubectl apply.

Add a cleanup trap immediately after the apply so it fires on every exit path:

🛡️ Proposed fix
 kubectl apply -f - <<EOF
 ...
 EOF
+
+trap 'kubectl delete daemonset e2e-image-prepuller -n kube-system --ignore-not-found' EXIT

 echo "Waiting for e2e-image-prepuller DaemonSet to become ready..."
 kubectl rollout status daemonset/e2e-image-prepuller -n kube-system --timeout=10m

-kubectl delete daemonset e2e-image-prepuller -n kube-system --ignore-not-found
 echo "Image pre-pull complete."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/e2e-prepull-images.sh` around lines 77 - 80, After applying the
e2e-image-prepuller DaemonSet, add a cleanup function and install a trap so the
DaemonSet is deleted on every exit path (including rollout timeout); implement a
small function (e.g., cleanup_prepuller) that runs kubectl delete daemonset
e2e-image-prepuller -n kube-system --ignore-not-found and register it with trap
'cleanup_prepuller' EXIT (and optionally INT TERM) immediately after the kubectl
apply, so the existing kubectl rollout status timeout cannot leave orphaned
pods.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@hack/e2e-install-cozystack.bats`:
- Around line 21-27: The pipeline silently swallows failures from the two helm
template commands; to fix it, capture each helm template output into variables
(e.g., render_kubeovn and render_linstor) by assigning their stdout to vars
before piping, so Bash's set -e will fail the test if a helm template fails,
then feed those variables into yq and finally into hack/e2e-prepull-images.sh;
update the block that runs "helm template packages/system/kubeovn" and "helm
template packages/system/linstor" to perform assignments and only proceed to the
yq | hack/e2e-prepull-images.sh pipeline when both assignments succeed.

---

Nitpick comments:
In `@hack/e2e-prepull-images.sh`:
- Around line 77-80: After applying the e2e-image-prepuller DaemonSet, add a
cleanup function and install a trap so the DaemonSet is deleted on every exit
path (including rollout timeout); implement a small function (e.g.,
cleanup_prepuller) that runs kubectl delete daemonset e2e-image-prepuller -n
kube-system --ignore-not-found and register it with trap 'cleanup_prepuller'
EXIT (and optionally INT TERM) immediately after the kubectl apply, so the
existing kubectl rollout status timeout cannot leave orphaned pods.
🪄 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: 6e53384a-2faa-4dda-83c4-3996c8038901

📥 Commits

Reviewing files that changed from the base of the PR and between 5786afe and 0d53f13.

📒 Files selected for processing (2)
  • hack/e2e-install-cozystack.bats
  • hack/e2e-prepull-images.sh

Comment thread hack/e2e-install-cozystack.bats Outdated
@github-actions github-actions Bot removed the size:L label May 4, 2026
Some workloads (kube-ovn raft, LINSTOR controller / piraeus-server)
fail when replicas start at materially different times due to
image-pull stagger across nodes. Add a DaemonSet-based pre-pull step
that runs before `helm install`, ensuring every node has the images
cached so replicas start within milliseconds of each other.

The script accepts image refs on stdin and creates one container per
image so pulls run in parallel — total time = max(any single image)
rather than sum. The bats test sources image refs directly from the
rendered platform charts via `yq`, walking only PodSpec-shaped
objects, so version bumps stay in sync without a hardcoded list.

Signed-off-by: Myasnikov Daniil <[email protected]>
Assisted-By: Claude <[email protected]>
@myasnikovdaniil
myasnikovdaniil force-pushed the daniil/split-prepull branch from 0d53f13 to 9df5950 Compare May 4, 2026 07:12
@dosubot dosubot Bot added size:L and removed size/L This PR changes 100-499 lines, ignoring generated files labels May 4, 2026
@myasnikovdaniil myasnikovdaniil changed the title e2e: pre-pull timing-sensitive platform images before install test(tests): pre-pull timing-sensitive platform images before install May 4, 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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@hack/e2e-prepull-images.sh`:
- Around line 37-43: The DaemonSet currently injects command:
["sleep","infinity"] into each container (see the containers+="... name:
pull-${i} ... command: [\"sleep\",\"infinity\"] ..." block), which makes
readiness depend on the image providing sleep; remove the command and related
readiness dependence and instead detect success by verifying each pulled
container has an imageID (status.containerStatuses[].imageID). Update both
places where the containers block is constructed (the pull-${i} block and the
similar block later) to omit the command and imagePullPolicy logic as needed,
and change the rollout/wait logic to poll kubectl -n kube-system get pods -l
app=e2e-image-prepuller -o json and confirm every pod's
.status.containerStatuses[].imageID is non-empty (or use the jq snippet from the
comment inverted to assert no missing imageIDs) before considering the DaemonSet
ready.
🪄 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: 2df58e3e-f7fa-4127-b8b8-8e20fae3042e

📥 Commits

Reviewing files that changed from the base of the PR and between 0d53f13 and 9df5950.

📒 Files selected for processing (2)
  • hack/e2e-install-cozystack.bats
  • hack/e2e-prepull-images.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • hack/e2e-install-cozystack.bats

Comment thread hack/e2e-prepull-images.sh

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.

LGTM. All bot findings folded into 9df5950:

  • Cleanup moved to a trap … EXIT so the e2e-image-prepuller DaemonSet is removed on every exit path (success, rollout timeout, signal) instead of only the success path under set -euo pipefail.
  • PR title renamed from e2e scope to test(tests): per .gemini/styleguide.md.
  • The helm template … | yq … | prepull pipeline now captures each render into a variable first (kubeovn_manifests=$(helm template …)) so set -e fires on a render failure — bats deliberately doesn't enable pipefail, so the previous brace-group form would have masked a helm template error behind yq's exit code and silently skipped pre-pull while the test passed.
  • sleep infinity tradeoff is reasonable for the current image set (all three are Debian/Ubuntu-based — linstor-csi on debian:trixie-slim, piraeus-server on debian:bookworm, kubeovn on Ubuntu — all ship sleep from coreutils). If a distroless image ever joins the list, kubectl rollout status times out loudly rather than silently passing — acceptable safety net.
  • --- filter in the grep dedup step would be dead code given upstream yq -N (--no-doc) and would mask the bug if -N ever got dropped — keeping current shape is correct.

Substance:

  • The peer-sensitive workload diagnosis matches the OVN raft and LINSTOR controller behavior — replicas that lose their startup window because of pull stagger consistently hit raft/connection-retry exhaustion. This class of flake also shows up in cnpg-webhook bootstrap and similar peer-sensitive paths. The DaemonSet approach (one container per image so pulls go in parallel — total time = max image, not sum) is the right shape and avoids deps like kube-fledged.
  • hostNetwork: true is correct: pre-pull runs before kube-ovn installs, so a normal pod stays ContainerCreating with NetworkPluginNotReady and never reaches image-pull. Mirroring the cozystack-operator's pattern.
  • tolerations: [operator: Exists] reaches every node including control-plane and nodes still coming up — required because pre-pull happens during cluster bootstrap when nodes still carry not-ready taints.
  • Image discovery via helm template … | yq '.. | select(has("containers")) | .containers[].image' keeps version bumps automatically synced — no hardcoded list to drift against make update.
  • automountServiceAccountToken: false on the DaemonSet's pod spec — good hygiene for a pod that just sleeps.

Two non-blocking observations, neither requiring a change:

  • The script's cpu: 1m / memory: 1Mi requests with no limits are fine for a sleeping process. Worth a one-line comment in the YAML so a future maintainer doesn't pattern-match this to "unset limits = bug" and add limits that don't help.
  • kubectl rollout status daemonset/… --timeout=10m is generous; in practice the timeout is bounded by the slowest registry. If oracle-runner GHCR pulls regress further, a --timeout=15m bump might be needed before this test starts flaking on its own. Leave alone for now.

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label May 4, 2026
@myasnikovdaniil
myasnikovdaniil merged commit 36dbd6b into main May 4, 2026
15 checks passed
@myasnikovdaniil
myasnikovdaniil deleted the daniil/split-prepull branch May 4, 2026 13:52
@github-actions github-actions Bot added the size/L This PR changes 100-499 lines, ignoring generated files label May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/testing Issues or PRs related to testing (e2e, bats, unit tests) area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review lgtm This PR has been approved by a maintainer size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants