feat(imagehub): reuse the create-time robot secret instead of refreshing it (AIP-2424) - #89
Draft
pranludi wants to merge 2 commits into
Conversation
…r updating it ImageRegistryRobotReconciler.reconcileInternal fell through from the "robot already exists" branch into the creation branch. After successfully updating an existing robot's permissions, it immediately tried to create another robot with the same username. Harbor rejects the duplicate, so the reconcile threw, requeued, and only settled on the next pass (where the permissions now matched and the early return kicked in). Net effect: every permission change on an existing robot — i.e. every edit to a project's spec.binding.imageHubs — logged a spurious failure and burned a requeue cycle. It self-healed, which is why it went unnoticed. A robot is one-per-username, so an update completes the reconcile. Return right after it instead of falling through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing it
AIP-2424
aipub-backend now propagates Harbor's plaintext robot secret and robot id
all the way to the REST create response (aipub-backend c456b007). Consume
it so first-time robot creation stops making a second Harbor round-trip
just to obtain a password it was already handed.
Previously createImageRegistryRobot was void: the secret Harbor returns at
creation was discarded, and ImageRegistrySecretReconciler had to call
PUT /imageregistryrobots/{id}/refreshsecret afterwards to get a usable
password for the .dockerconfigjson Secret. Two Harbor round-trips for one
robot, purely because nothing carried the first one's secret forward.
The robot is created by ImageRegistryRobotReconciler but the K8s Secret is
written by ImageRegistrySecretReconciler, so there is no call path between
them to hand the secret over. ImageRegistryRobotSecretStore bridges the
gap: the create path deposits the secret keyed by robot id, and
AipubDockerConfigJsonResolver.getPassword takes it from there, falling back
to refreshsecret when it comes up empty.
This is purely an optimization — every path stays correct with an empty
store. A restart between create and Secret reconciliation just means the
old two-round-trip behavior for that project.
The store deliberately does not hold secrets for long:
- take() consumes the entry; a create-time secret is used at most once.
- entries expire after 5 minutes. An out-of-band refreshsecret (web UI,
direct API) silently invalidates what we hold, and writing a stale
password into the Secret is exactly the failure mode fe84b5b fixed.
Note AipubConfiguration builds ImageRegistryRobotServiceImpl twice, once
per consumer, so the store must be a shared singleton bean — putting it
inside the service would split it across two instances and the handoff
would never connect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Consumes the create-time robot secret that aipub-backend now propagates, so first-time robot creation drops from two Harbor round-trips to one. This is the cross-repo follow-up called out in #88.
Paired PR: ten1010-io/aipub-backend#438 (AIP-2424) — makes
POST /api/v1alpha1/imageregistryrobotsreturn{"robotId":"42","secret":"..."}instead of an empty body.Previously
ImageRegistryRobotService.createImageRegistryRobotwasvoid: the plaintext secret Harbor returns at creation was thrown away, soImageRegistrySecretReconcilerhad to callPUT /imageregistryrobots/{id}/refreshsecretafterwards just to obtain a password for the.dockerconfigjsonSecret.Design: why a store and not a direct write
The obvious approach — have the create path seed the K8s Secret itself — doesn't work. The robot is created by
ImageRegistryRobotReconciler(cluster-scoped, keyed by project) but the Secret is written byImageRegistrySecretReconciler(namespaced), which owns that object's name, type, labels and owner references. There is no call path between them, and making both write the same object would mean two writers.So
ImageRegistryRobotSecretStorebridges them: the create path deposits the secret keyed by robot id, andAipubDockerConfigJsonResolver.getPasswordtakes it from there, falling back torefreshsecreton a miss.This is purely an optimization — every path stays correct with an empty store. A restart between robot creation and Secret reconciliation just means the old two-round-trip behavior for that project.
The store deliberately doesn't hold secrets for long:
take()consumes the entry — a create-time secret is used at most once.refreshsecret(web UI, direct API) silently invalidates what we hold, and writing a stale password into the Secret is exactly the failure mode fix(imagehub): stop rotating image registry robot secrets on every reconcile #88 fixed.Keyed by robot id rather than username on purpose: if a robot is deleted and recreated the id changes, so a stale entry can never be served for a different robot.
AipubConfigurationbuildsImageRegistryRobotServiceImpltwice, once per consumer, so the store has to be its own singleton bean. Putting it inside the service would split it across two instances and the handoff would silently never connect.Contract verification
Checked against aipub-backend#438 rather than assumed:
robotId(create) andid(list) both come from Harbor'sRobot.getId()— same value, so the store key matches what the resolver looks up.refreshsecret.Also included: unrelated pre-existing bug (3f29487)
ImageRegistryRobotReconciler.reconcileInternalfell through from the "robot already exists" branch into the creation branch. After successfully updating an existing robot's permissions it immediately tried to create another robot with the same username; Harbor rejects the duplicate, so the reconcile threw, requeued, and only settled on the next pass.Net effect: every edit to a project's
spec.binding.imageHubson an already-connected project logged a spurious failure and burned a requeue cycle. It self-healed, which is why it went unnoticed. Kept as its own commit so it can be reviewed and reverted independently.Test plan
./gradlew buildpasses; full suite 97 tests, 0 failuresImageRegistryRobotSecretStoreTest(consume-once, TTL expiry, id keying, purge-on-put) andAipubDockerConfigJsonResolverTest— the key assertion being thatrefreshSecretis never called when the store has the secret, and that a miss still falls back to itrefreshsecretcall, and thatdocker loginworksrefreshsecretRollout order
Merge/deploy aipub-backend#438 first. Shipping this beforehand is not harmful — the gateway would still return an empty body,
Optional.empty()results, and everything falls back to the currentrefreshsecretpath — but the optimization simply won't engage.Opened as a draft because it stacks on #88 (also draft) and can't merge until both that and aipub-backend#438 land. The code itself is review-ready.
🤖 Generated with Claude Code