fix(rpm): keep importing after an archive containerd cannot read (MK8S-418) - #27
ezekiel-alexrod wants to merge 3 commits into
Conversation
The loop ran under `set -e` with nothing catching a failed import, so the first archive `ctr` refused ended the run. Every archive the glob listed after it stayed out of containerd, and the log named only that first one. It hurts most on a node with no registry to fall back on. One torn archive keeps the static pod images out of the content store, and the ten minute timer replays the same failure without ever getting further. Every archive is tried now. The run still exits non-zero when at least one of them failed, and it names all of them, so the unit reports the failure without hiding the images it did import. The summary says what became of every archive the cache held, since one torn file out of forty reads nothing like a containerd that is down. Trying everything means the loop outlives the glob that listed it, and the agent collects a resource by taking its directory away. An archive that is already gone when its turn comes is therefore skipped rather than reported: without that, a garbage collection mid-run would have the unit name every archive under the directory the agent dropped. One that `ctr` refuses stays a reported failure even if it is gone afterwards, which is also what happens to one removed between the check and `ctr` opening it. A stat taken after the fact proves the file is absent now, not that absence is why the import failed, and swallowing a containerd outage that happened to coincide with a cleanup would be worse than reporting a rare race. A run that ends up importing nothing says so on stderr and still exits zero: a whole cache being collected at once is legitimate, and this script is not the one to decide otherwise. The tests that came with this needed a stubbed `ctr` that can refuse a named archive and delete one mid-run, stdout and stderr kept apart, and assertions that hold on bats 1.5.0, which is what Rocky 8 ships. Refs: MK8S-418
The suite ran ShellCheck over sources/*.sh and build.sh and left its own test files out, which is where the trickier shell now lives: a stubbed `ctr` written through a heredoc, positional slicing, a grep whose pattern comes from a path. ShellCheck has a bats mode and it pays for itself immediately. It reports SC2314 on `! grep -q ...` used as anything but the last command of a test, because bash exempts a `!`-prefixed command from errexit and the assertion then passes whatever grep finds. One such assertion was already on main, and writing the tests for the import fix would have added another. It only runs on one leg. ShellCheck learned to parse bats in 0.9.0, Rocky 8 ships 0.6.0, and there the file stops parsing at the first @test, so the case skips on EL8 and gates on EL9. The version probe fails the test rather than skipping when ShellCheck is missing altogether, which is the failure mode that would quietly delete the gate. All three linter gates now go through one assertion that echoes the captured output before checking the status. `run` was swallowing the only thing a lint failure has to say, so CI named the test and nothing else: no rule, no line, no message. Refs: MK8S-418
| echo "Importing ${tar}" | ||
| ctr -n k8s.io images import --platform "${IMAGE_PLATFORM}" "${tar}" | ||
| if ! ctr -n k8s.io images import --platform "${IMAGE_PLATFORM}" "${tar}"; then | ||
| echo "Failed to import ${tar}" >&2 |
There was a problem hiding this comment.
It would be interesting to add timestamp
There was a problem hiding this comment.
The journal already timestamps each line, so I'd rather not add one in the message: it would show twice in journalctl. But you're right about the level, stderr was landing at info like stdout. Failures are err now, so journalctl -u containerd-image-preload -p err shows only them.
| echo "Failed to import ${failures} of the ${scanned} cached archives" \ | ||
| "(${imported} imported, ${skipped} no longer there)" >&2 |
There was a problem hiding this comment.
same comment about timestamp
There was a problem hiding this comment.
Ditto, this one is err now.
| # Nothing failed, yet nothing made it in either. A whole cache going away | ||
| # under the run is legitimate (every resource collected at once) and is not | ||
| # this script's call to make, but it must not pass for a quiet success. | ||
| echo "None of the ${scanned} cached archives were still there to import" >&2 |
There was a problem hiding this comment.
same comment about timestamp
There was a problem hiding this comment.
Ditto, warning there.
There was a problem hiding this comment.
Maybe add introduction and conclusion messages "images import is starting/finished", with timestamp and INFO level on stdout
There was a problem hiding this comment.
systemd already logs when the unit starts and ends (Starting ..., then Finished ... or Failed with result 'exit-code'). What was missing is what the run did, so it now ends with Imported N of the M cached archives at info. Is it ok for you?
Under systemd, stdout and stderr both land in the journal at info, so a failed import did not show with `journalctl -p err`. Lines are now prefixed with their syslog priority, which journald reads and strips. The prefix is only written when the stream really is the journal. JOURNAL_STREAM is inherited by shells started from a logged session, so the descriptor is compared to it, and a run by hand stays plain. A run now also ends with a line saying what it imported. systemd already logs when the unit starts and ends, and the journal adds the timestamps. Refs: MK8S-418
Component
rpm
Problem
containerd-image-preload.shruns underset -euo pipefailand calledctr images importbare inside its loop. The first archivectrrefused ended the run, so every archive the glob listed after it stayed out of containerd and the log only named that first one. Which images survived depended on file names.That's worst on a node with no registry to fall back on: one torn archive keeps the rest of the static pod images out, and the ten minute timer replays the same failure without ever getting further.
Fix
ctrrefuses is named on stderr as it happens, and the run exits 1 at the end with a summary:Failed to import N of the M cached archives (X imported, Y no longer there). The unit still fails, but the node gets every image the cache could still give it.ctropening it. A stat taken afterctrrefused it says the file is absent now, not that absence is why the import failed, and I'd rather report a rare race than hide a containerd outage that happened to line up with a cleanup.errfor failures,warningfor an empty run,infootherwise), sojournalctl -p errfinds the failed imports. Without the prefix, stderr lands atinfolike stdout. It's only written when the stream really is the journal:JOURNAL_STREAMis inherited by shells started from a logged session, so the descriptor is compared to it. A successful run ends withImported N of the M cached archives.Two changes go beyond the ticket:
package.batsnow runs ShellCheck over the bats files themselves. In bats,! cmdthat isn't the last line of a test asserts nothing (SC2314): a test with! grep -q notes.txton a log that containsnotes.txt, followed bytrue, passes.script.bats:71onmainwas one of those, and the new tests would have added more. ShellCheck parses bats from 0.9.0 only and Rocky 8 ships 0.6.0, so that case skips on EL8 and gates on EL9. It fails, not skips, when ShellCheck is missing.runwas swallowing it, so a lint failure in CI showed the test name and nothing else..claude/REVIEW.mdsaid "partial cache means skip, not fail" for the import script, which no longer matches. The row now describes the skip, the failure, and what keeps an unfinished agent directory out of the import (the glob).Test
make -C rpm test EL=8andEL=9: 33/33 on both, with the bats ShellCheck case skipped on EL8 as expected.script.batscases run against the script frommain: 8 fail (the unreadable archive, every failure named, the summaries, the skip), the 7 existing ones pass.systemd-run --user:journalctl -o jsonshows priority 3 on the failures and 6 on the rest. The same run from a terminal that inheritedJOURNAL_STREAMprints no prefix.ctrrefuses archives by path and can delete a directory on its first call, which is how the tests cover a failure in the flat layout and in a subdirectory, and an agent collecting a resource mid-run.Out of scope
rpm/README.mdowns it.agent/DESIGN.md. Not applicable: the layout doesn't change.Relates-to: MK8S-418