Conversation
The Containerfile's Nomad assertion only has teeth when the copied client came from the feature build. On the arm64 path compose passes a released client whose Nomad cannot move with PMM_FB_TAG, so the assertion re-confirms a pairing nobody repinned and a repin that left NOMAD_VERSION stale still builds. Add NOMAD_VERSION_FB_TAG, naming the feature build NOMAD_VERSION was read from, and a second RUN that refuses a build pinning any other tag, but only when a client image was supplied, so the amd64 path keeps the existing assertion as its sole authority, byte for byte. That closes the local window; it cannot tell a witness bumped honestly from one bumped to silence the error, so a pull_request workflow on pmm reads the real client's tools/nomad on a native amd64 runner and holds NOMAD_VERSION to it. Both callers read the pins through one checker, which also makes the pmm-server/PMM_FB_TAG agreement enforced rather than documented.
There was a problem hiding this comment.
🟡 Changes recommended
The guard tests are not automated on pmm pull requests, and malformed build-argument mappings are not normalized.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds layered safeguards binding NOMAD_VERSION to its PMM feature-build artifact.
Changes:
- Adds build-time witness validation and CI artifact verification.
- Introduces pin parsing and regression tests.
- Updates operator documentation and bootstrap messaging.
File summaries
| File | Description |
|---|---|
.github/workflows/pmm-fb-nomad-pin.yaml |
Verifies Nomad against the feature-build client. |
scripts/check_pmm_fb_pins.py |
Validates committed compose pins. |
sidecar/pmm-fb/Containerfile.mysql |
Adds the witness guard. |
sidecar/pmm-fb/compose.yaml |
Declares the witness pin. |
sidecar/pmm-fb/bootstrap.sh |
Clarifies arm64 behavior. |
sidecar/pmm-fb/README.md |
Documents layered verification. |
sidecar/pmm-fb/mysql-target.md |
Updates repinning guidance. |
tests/sidecar/test_pmm_fb_nomad_pin.py |
Tests guard and checker behavior. |
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 10
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Follow-up from this review's own pass, deliberately not taken in this PR.
Left here because moving them splits a module that currently reads as one mechanism, and the workflow's path filter and pytest invocation would both have to name two paths. That trade is worth deciding on its own rather than inside a change about the Nomad pin. |
|
Follow-up from this review's own pass: a sibling of the empty-pin refusal added in this PR, deliberately not changed.
Narrow — a blank |
Summary
sidecar/pmm-fb/Containerfile.mysqlasserts that the PMM Client copied intosep-mysqlships the Nomad versioncompose.yamlnames. Whether that assertionmeans anything depends on which client was copied, and
PMM_CLIENT_IMAGEdecides:
PMM_FB_TAG, so theassertion binds
NOMAD_VERSIONto the artifact a repin moves.bootstrap.shselects the releasedpercona/pmm-client:3.9.1. That client's Nomad cannot move withPMM_FB_TAG,so the assertion compares 2.0.5 against 2.0.5 and a repin that left
NOMAD_VERSIONstale still builds. The client/server mismatch then shipsundetected: registration succeeds and only
raw_execplacement misbehaves.Two layers close that, keyed on
PMM_CLIENT_IMAGErather than on architecture —the blind spot is "the copied client is not the feature-build client", which
today coincides with arm64 but is not the same property.
NOMAD_VERSION_FB_TAGand a secondRUN(Containerfile.mysql:53-64). Thenew build arg records which feature build
NOMAD_VERSIONwas read from, and theguard refuses a build that supplies a client image while pinning a different
PMM_FB_TAG. It abstains whenPMM_CLIENT_IMAGEis empty, so lines 49-51 staythe sole authority on the amd64 path and are unchanged byte for byte. The
existing
NOMAD_VERSION=""opt-out still bypasses both checks. The threeARGre-declarations at
:59-61are load-bearing:PMM_FB_TAGandPMM_CLIENT_IMAGEare declared before the firstFROM, which Docker scopes toFROMlines only, so omitting a re-declaration expands them to empty and theguard would pass every build while looking installed.
.github/workflows/pmm-fb-nomad-pin.yaml. The guard can distinguish awitness left behind from one restated, but not an honestly restated witness from
one bumped to silence the error — and bumping it is the minimum edit that
silences the message. A
pull_requestjob onpmmpulls the feature-buildclient on a native amd64 runner and holds its
tools/nomad versiontoNOMAD_VERSION. Nothing runs on the arm64 build host, which is what keeps thatpath free of the amd64 pull it exists to avoid. The same job runs
tests/sidecar/test_pmm_fb_nomad_pin.py, so the guard's truth table and theARGre-declarations are gated rather than merely covered locally; the modulereaches
app/__init__.pyandtests/conftest.pyand nothing else, so pytest,PyYAML and cryptography are its whole requirement.
The one case that may skip is narrow, deliberately: a pin the PR inherited
whose feature build has since been collected. The job resolves the base
revision's
PMM_FB_TAGand compares it to the one under test, so a pin this PRintroduces that cannot be pulled — a typo, an unpublished build — fails red.
That case is not otherwise catchable by the agreement check, which proves the
three slots match each other and is satisfied by setting all three to the same
nonexistent value. Rate limiting, an auth change and registry outages fail red
too. A green check that verified nothing would be the same defect this ticket is
about, one layer up. The path filter covers
scripts/check_pmm_fb_pins.py, thetest module and the workflow file as well as
sidecar/pmm-fb/**, so a PRediting only the reader the gate depends on cannot merge unrun.
scripts/check_pmm_fb_pins.pyis the single reader both the new test and thejob use, so the two cannot drift. Its default mode also makes the
pmm-serverimage /PMM_FB_TAGagreement enforced — that pairing wasdocumented as "a repin edits both" and nothing checked it. Each slot is held to
the variable it is keyed by, not merely to carrying some
${NAME:-default}:a slot quietly rewired to another variable agrees on committed defaults while an
exported
PMM_FB_TAGmoves only its siblings, which is the same mismatchreached without changing a literal. A slot is also required to resolve to
something: three empty defaults agree with each other, which would have been the
same vacuous comparison one layer up, and a
build.argsnode carrying nomapping at all is refused by name rather than raising out of the reader. The
same check runs in pre-commit, keyed on the compose file and the reader, so a
local repin is answered before the push.
tests/sidecar/test_pmm_fb_nomad_pin.pycovers the guard's truth table byextracting the shipped
RUNbody and running it undersh, asserts staticallythat the final stage declares every argument either guard reads (
NOMAD_VERSIONincluded — it is read by both guards before either can fail, so moving its
declaration up would retire both at once), holds the job's image reference and
binary path to the ones the Containerfile names, and checks the pin reader
against the committed file and against tampered copies. Every value it uses is
read from the artifact rather than restated, so a repin cannot leave it passing
against an old tag.
Docs updated to separate the three checks rather than describe them as one:
README.md,mysql-target.md(a repin now moves three values, not two), andtwo
bootstrap.shcomments that described the vacuous comparison as if it werea real check. Both docs state the skip condition as the job enforces it — both
pin values inherited unchanged, and that build since collected — rather than as
"collected tags are waved through", which a repin would have read as covering
itself.
The job this PR adds has been observed running on it
It declares no
needs:, so the label gate does not hold it back, and it ran onthis PR's own head rather than only being read as YAML. It took the verifying
branch, not the warn-and-skip one:
So the gate pulled the feature-build client on a native amd64 runner and held
the committed
NOMAD_VERSIONto its realtools/nomad— which is the half ofthis change that verifies rather than attests. The pytest step reported
29 passedin the same run. Re-check both after any further push, since a laterhead has not been observed.
Known limitations
feature build, not that anyone re-read the client's Nomad. Only the CI job
establishes the value itself.
perconalab, the job warns and skips rather than failing: this harness pinsthrowaway artifacts, so failing red there would block every later PR touching
these paths behind an unrelated repin. That is the one unverified path, it is
reported as a warning naming the tag, and it cannot cover a pin the PR itself
introduced.
ci.ymlis scoped tomain, wheresidecar/pmm-fb/does not exist, so theproject's pytest tier still never fires for the PRs these tests protect. The
job added here runs the module directly instead, which covers it for this base
branch but leaves the tests outside the suite every other test in the repo
belongs to.
RUNsits beside the existing assertion, so a tag-only repin onthe arm64 path now invalidates the cached layers below it, including the
test_dbarchive fetch. Keeping the two guards adjacent was deliberate — theyare one mechanism and read as a pair — and the cost is one archive re-fetch per
repin, not a rebuild of the package layers, which stay above the guard.
Unrelated local failure
tests/sidecar/test_encryption_key.py::test_a_peer_holding_the_state_lock_defers_then_refusestimes out locally. It exercises
sidecar/encryption_key.pyandapp/core/encryption.py; this diff touches neither. The failure is a lock-waittimeout, not an assertion. The only workflow running on this base branch is the
one added here, which is scoped to the pin and does not exercise that module, so
there is no CI result to corroborate against.
Tested
PMM_FB_TAG(in
compose.yamlor the environment) without touchingNOMAD_VERSION_FB_TAG, and rebuildsep-mysqlforcing a rebuild of theimage: the build is refused and the message names both tags and points at
the amd64 remedy.
sep-mysqlbuilds to completionand the node registers with
pmm-serveras before.PMM_FB_TAGandleave
NOMAD_VERSIONstale: the original assertion still fails the build,with its original message.
NOMAD_VERSIONset to the empty string on either engine, bothbuild-time checks are skipped, as before this change.
PMM FB Nomad pinjob on this PR is green, and its log shows thefeature-build client's Nomad version matching the committed
NOMAD_VERSION.Checklist
make makemigrations) — N/A, no models changed