Skip to content

hosting-image-mirror: copy the pinned platform images from ACR into the fleet registry when it lacks them (Plugins#1722) - #4110

Merged
rbuergi merged 4 commits into
mainfrom
hosting/1722-image-mirror
Sep 13, 2026
Merged

rbuergi merged 4 commits into
mainfrom
hosting/1722-image-mirror

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Operator half of MeshWeaver.Plugins#1722 (the plan half — Mirror platform images as the first Provision/Roll step — is the paired Plugins PR; Pairs-with: none — this PR removes no public surface).

The hand step it replaces

Memex docs/new-deployment.md step 4:

crane copy meshweaver.azurecr.io/memex-portal-ai:<tag> cr.meshweaver.cloud/memex-portal-ai:<tag> · crane copy meshweaver.azurecr.io/memex-migration:<tag> cr.meshweaver.cloud/memex-migration:<tag>

What changes

New command hosting-image-mirror --registry <fleet host> --repository <path> --tag <tag> [--also <path>]… --vault … --publisher-secret … [--source <acr host>] [--publisher <user>]:

  • present in the fleet registry → KEPT, never re-pushed; its digest is compared with the source's and a difference is the fact image_drift (a re-tagged upstream build is the operator's call);
  • absent → the source is asked (absent there too → refusal naming the sealed set to pin), crane copy runs, and both digests are read back and must match before it reports;
  • portal AND migration ride the same tag (Memex#141) via --also;
  • the publisher password (vault) and the ACR token (az acr login --expose-token) reach crane on stdin (--password-stdin) into a per-process DOCKER_CONFIG — never argv, never printed, never a fact;
  • a non-ACR --source is refused (the fleet's upstream is ACR and the token comes from az); --registry equal to --source is refused.

Dockerfile: adds crane (go-containerregistry v0.20.3, selected per TARGETARCH) — nothing else in the image can do a manifest+blob copy, and the image's own tool check now asserts it.

Rights the operator identity still lacks

AcrPull on meshweaver (plus Reader on the registry resource so az acr login --name meshweaver --expose-token resolves it) — hosting-operator does not hold it today. Until it is granted the step is RED naming the grant and the exact crane copy fallback; nothing is copied and nothing else is touched. Key Vault get on memexcloud-Registry-PublisherPassword is under the existing Key Vault Secrets Officer grant.

Gates run

$ bash deploy/aks/operator/test/run-tests.sh
326 passed, 0 failed          (291 on main; +35 new hosting-image-mirror cases)
$ shellcheck -S warning deploy/aks/operator/bin/hosting-* deploy/aks/operator/test/stubs/image-mirror/*
(clean)

The crane/az stubs record logins by host and user only — never a credential — and the suite asserts no sentinel appears in any output or argv.

hosting-operator.yml: plan-command list, tracked-file floor 23 → 24, stub list, and crane added to the in-image tool assertion. (The floor line conflicts textually with the sibling PRs #4100/#4103/#4105/#4109, which bump the same number; whichever lands last merges main and re-counts.)

Ships how

Hop (C): hosting-operator:<sha> on merge to main; reaches a Provision when operator.image on Deployments/memex moves to it. Merging is not shipping.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 12, 2026 14:59
…he fleet registry when it lacks them (MeshWeaver.Plugins#1722)

cr.meshweaver.cloud serves only what was pushed to it; CD publishes to ACR. A record
pinning a tag the fleet registry lacked sent a human to `crane copy` as the
registry's publisher (runbook step 4) — or, before #1546, into ImagePullBackOff at
step 11. This command is that copy, for the portal image and (--also) the migration
image, which must ride the same tag (Memex#141):

  present → KEPT, never re-pushed; digest compared with ACR's, a difference is the
            fact image_drift (a re-tagged upstream build is the operator's call).
  absent  → the source is asked (absent there too → refused naming the sealed set
            to pin), crane copy runs, BOTH digests are read back and must match.

Credentials: the publisher password from the vault and the ACR token from
`az acr login --expose-token` reach crane on stdin (--password-stdin) into a
per-process DOCKER_CONFIG — never argv, never printed, never a fact. The ACR short
name derives from the source host; a non-ACR source is refused (the fleet's upstream
is ACR). Rights: AcrPull on the source registry for the operator identity, not
granted today — without it the step is RED naming the grant and the crane commands.

Dockerfile: crane (go-containerregistry v0.20.3, per TARGETARCH) — nothing else in
the image can do a manifest+blob copy. Tests: 35 cases against crane/az stubs that
record logins by host+user only. Workflow: plan list, floor 24, stubs, tool check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rbuergi

rbuergi commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Corrected the tracked-file floor in hosting-operator.yml: main already tracks 24 files under deploy/aks/operator/bin/ (21 commands + run.sh + _common.sh + _audit.jq) while the guard declared 23 and its comment said "20 commands" — stale, so one lost file would have passed. This PR adds one command, so the floor is now 25 with the comment re-counted. 🚨 The four sibling PRs (#4103, #4105, #4109, #4110) each do the same on the same line: whichever lands after the first must merge main and set the floor to the real count at that point (26, 27, 28), not keep 25.

🤖 Generated with Claude Code

@rbuergi

rbuergi commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Why crane rather than the mesh's own registry package, recorded because a reviewer will ask: the in-mesh Container Registry package (the /v2 surface a portal served from blob storage) was retired on 2026-09-08 — no image was ever published into it and portal hosts now answer 404 there (MeshWeaver.Plugins/WhatsNew/2026-09-08-…, MeshWeaver#3353/#3697). cr.meshweaver.cloud is a separate deployed service (distribution + docker_auth), so there is no in-mesh API to push through, and the operator image carried no registry client at all (az, kubectl, helm, pg_dump, jq).

The push credential is deliberately the registry's publisher account, not an instance key: an mwi_ key is what the auth service grants pull for and cannot push. That is the same credential the hand step in MeshWeaver.Plugins#1722 used.

🤖 Generated with Claude Code

Copilot AI 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.

🔵 Needs a closer look

Critical and moderate findings remain in argument handling, registry error classification, drift detection, validation, and workflow completeness.

Pull request overview

Adds hosting-image-mirror to copy pinned platform images from ACR into the fleet registry with credential isolation, digest verification, tests, and deployment integration.

Changes:

  • Adds multi-image mirroring, validation, drift detection, and secure authentication.
  • Adds Crane tooling, Azure/Crane stubs, and behavior tests.
  • Updates lifecycle documentation and workflow image checks.
File summaries
File Review summary
deploy/aks/operator/test/stubs/image-mirror/crane Adds Crane test stub; no final comments.
deploy/aks/operator/test/stubs/image-mirror/az Adds Azure CLI test stub; no final comments.
deploy/aks/operator/test/run-tests.sh Nit (2 votes): read-back digest mismatch is not tested. Nit (1 vote): copy-failure coverage does not exercise the mismatch guard; add successful-copy/wrong-digest coverage.
deploy/aks/operator/Dockerfile Adds architecture-specific Crane tooling; no final comments.
deploy/aks/operator/bin/hosting-image-mirror Critical (1 vote): missing argument handling can loop indefinitely. Critical (1 vote): registry errors are treated as absent images. Moderate (3 votes): all-present images skip source drift comparison. Moderate (2 votes): case-sensitive self-mirror validation can be bypassed. Moderate (1 vote): source validation permits multi-label ACR hosts. Nit (2 votes): read-back digest mismatch lacks test coverage.
deploy/aks/INSTANCE-LIFECYCLE.md Nit (1 vote): documents AcrPull but omits the required ACR Reader grant.
.github/workflows/hosting-operator.yml Moderate (1 vote): tracked-file floor is too low for the 22-command plan list and supporting files.
Review details

Suppressed comments (3)

.github/workflows/hosting-operator.yml:69

  • The plan list below contains 22 hosting-* commands, not 21; with run.sh, _common.sh, and _audit.jq, the tracked-file floor should be 25. At 24, losing one of the 25 files still passes this guard, so a clean checkout can ship an incomplete operator image without CI noticing.
          if [ "$tracked" -lt 25 ]; then
            echo "::error::only ${tracked} file(s) tracked under deploy/aks/operator/bin/ — expected 25 (22 commands + run.sh + _common.sh + _audit.jq). The floor is the REAL count — raise it with every command added, or a lost file passes."

deploy/aks/INSTANCE-LIFECYCLE.md:127

  • This lifecycle summary says the operator only needs AcrPull, but the command's own refusal and the provisioning requirements also require Reader on the ACR resource so az acr login --name ... --expose-token can resolve the registry. Omitting that grant sends operators to provision an incomplete permission set and leaves the step red.
  argv, never printed. Needs `AcrPull` on the source registry for the operator identity.

deploy/aks/operator/test/run-tests.sh:403

  • HOSTING_IM_COPY_FAIL=1 makes crane copy exit before a destination is written, so this only covers the command-failure branch. It never exercises the read-back mismatch guard at lines 141–143; a regression that accepts a different destination digest would still pass. Add a stub mode that writes a different destination digest and assert that the command refuses without reporting it as mirrored.
# A copy that does not land as the same digest is a failure, not a pass.
im "$IM_SRC" HOSTING_IM_COPY_FAIL=1 -- "${IM_ARGS[@]}"
[ "$_im_rc" -ne 0 ] && ok "a failed copy fails the step" || bad "copy fail" "exited 0"
rm -rf "$_im_state"
  • Files reviewed: 6/7 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

390 tests  ±0   390 ✅ ±0   46s ⏱️ -7s
  2 suites ±0     0 💤 ±0 
  2 files   ±0     0 ❌ ±0 

Results for commit 6accccb. ± Comparison against base commit 930e6d4.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

508 tests  ±0   508 ✅ ±0   43s ⏱️ -1s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 6accccb. ± Comparison against base commit 930e6d4.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

261 tests  ±0   261 ✅ ±0   2m 14s ⏱️ +29s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 6accccb. ± Comparison against base commit 930e6d4.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 331 tests  ±0   1 329 ✅ ±0   4m 0s ⏱️ +5s
    3 suites ±0       2 💤 ±0 
    3 files   ±0       0 ❌ ±0 

Results for commit 6accccb. ± Comparison against base commit 930e6d4.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 031 tests  ±0   2 031 ✅ ±0   5m 8s ⏱️ -1s
    4 suites ±0       0 💤 ±0 
    4 files   ±0       0 ❌ ±0 

Results for commit 6accccb. ± Comparison against base commit 930e6d4.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    6 files  ±0      6 suites  ±0   9m 0s ⏱️ -2s
2 891 tests ±0  2 699 ✅ ±0  192 💤 ±0  0 ❌ ±0 
2 895 runs  ±0  2 703 ✅ ±0  192 💤 ±0  0 ❌ ±0 

Results for commit 6accccb. ± Comparison against base commit 930e6d4.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   17 files  ±0     17 suites  ±0   21m 55s ⏱️ +26s
7 412 tests ±0  7 218 ✅ ±0  194 💤 ±0  0 ❌ ±0 
7 416 runs  ±0  7 222 ✅ ±0  194 💤 ±0  0 ❌ ±0 

Results for commit 6accccb. ± Comparison against base commit 930e6d4.

♻️ This comment has been updated with latest results.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 12, 2026
#4100 (hosting-kv-ensure --db-connection) landed first. Every conflict was "both
sides added": the INSTANCE-LIFECYCLE guarantee list keeps main's extended kv-ensure
bullet AND the image-mirror one, the behaviour suite keeps both blocks (353 passed,
0 failed = 326 + main's 27), and the workflow's tracked-stub list is the union
(image-mirror's crane and az stubs included).

The tracked-file floor is CORRECTED rather than carried: main declared 23 while
already tracking 24 (its comment said "20 commands" — stale, so one lost script would
have passed). With hosting-image-mirror the real count is 25, and the floor and its
comment now say 25.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi removed this pull request from the merge queue due to a manual request Sep 12, 2026
@rbuergi

rbuergi commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Merge resolved by content; floor settled at 25 — measured, not assumed.

origin/main merged in (no rebase), one push. All three conflicts were both sides added, and both sides are kept:

File Resolution
deploy/aks/INSTANCE-LIFECYCLE.md main's extended hosting-kv-ensure bullet (#4100) and this PR's guarantee bullet
deploy/aks/operator/test/run-tests.sh main's hosting-kv-ensure block and this PR's block
.github/workflows/hosting-operator.yml tracked-stub list = the union of both lists

The floor, and how it was counted. git ls-tree -r --name-only origin/main deploy/aks/operator/bin/ | wc -l = 24 on main (tip 69e587c5c): 21 hosting-* commands + run.sh + _common.sh + _audit.jq. main declares 23 with a comment saying "20 commands" — stale, so one lost script would have passed. This PR adds exactly one command, so the true count for this tree is 25 (22 commands + 3), and the guard and its comment now say 25. Verified on the pushed head: tracked=25, commands=22, floor=25.

Guards run on the pushed head: deploy/aks/operator/test/run-tests.sh green (both test blocks coexist), shellcheck -S warning clean over _common.sh, run.sh and every hosting-*. No run was re-run wholesale.

The follow-up is still needed and is now precisely scoped. Each of #4103/#4105/#4109/#4110 sets the floor true for its own merge, so as the siblings land the real count rises 25 → 26 → 27 → 28 while the floor stays 25 — up to 3 files of slack. After all four have merged, a one-line PR sets -lt 28 and expected 28 (25 commands + run.sh + _common.sh + _audit.jq). It cannot be pre-set here: 28 would fail the guard for whichever lands first.

🤖 Generated with Claude Code

rbuergi and others added 2 commits September 12, 2026 22:34
…(workflow floor 27 = 24 commands, the stub list, run-tests.sh sections, INSTANCE-LIFECYCLE bullets); operator suite 415/415 on the merged tree

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqTDZe4Skkn81GuGkwMrXS
…t union; the bin floor is the COUNT of the merged tree: 28 (25 commands + 3); operator suite 453/453

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqTDZe4Skkn81GuGkwMrXS
@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 13, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 13, 2026
@meshweaver-cloud

Copy link
Copy Markdown
Contributor

⛔ Merge-queue steward: left dequeued — the group build failed on an assertion the flake catalogue does not know.
Group build: https://github.com/Systemorph/MeshWeaver/actions/runs/34744471576

  • MeshWeaver.Graph.Test.DanglingNodeTypeUpdateTest.Upsert_WithTheNamedImportEscapeHatch_StillLands — MeshWeaver.Reactive.Assertions.AssertionException : Expected value to be true because AllowUnresolvableNodeType is the import ordering escape hatch — refusing it turns every re-import of an already-present cycle member into a baseline-holdi

Not a catalogued flake. Fix the failure, or — with run URLs, an issue and an assertion-message pattern — add it to .github/known-flakes.json (see Doc/Architecture/MergeQueue). Re-queue with gh pr merge <n> --auto once the head is green; label queue-rejected marks this PR as needing a person.

@meshweaver-cloud meshweaver-cloud Bot added the queue-rejected The merge queue rejected this PR on an uncatalogued failure; a person owns it now label Sep 13, 2026
@rbuergi
rbuergi added this pull request to the merge queue Sep 13, 2026
@rbuergi
rbuergi removed this pull request from the merge queue due to a manual request Sep 13, 2026
@meshweaver-cloud

Copy link
Copy Markdown
Contributor

ℹ️ Merge-queue steward: no action — removed from the queue with reason MANUAL — the steward takes no action for this reason.

@rbuergi

rbuergi commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

The merge-queue red on this PR's group build (34744471576, shard 4, DanglingNodeTypeUpdateTest.Upsert_WithTheNamedImportEscapeHatch_StillLands, OwnerUnreachable … no verdict within 31s) is unrelated to this PR: the diff here is deploy/aks/operator/* + hosting-operator.yml, zero src/, and main is green on the same base (cdb2878bb, run 34720891908). The failure is a race in the hub reply path — the owner posted its PatchDataResponse at +2 ms and the requester never received it — filed as #4159 with the trail and the instrument that will name the losing hub next time (#4154). Re-queueing this PR is safe with respect to that failure.

@rbuergi
rbuergi added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit dc07ddb Sep 13, 2026
57 of 59 checks passed
@rbuergi
rbuergi deleted the hosting/1722-image-mirror branch October 10, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

queue-rejected The merge queue rejected this PR on an uncatalogued failure; a person owns it now

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants