Skip to content

feat(imagehub): reuse the create-time robot secret instead of refreshing it (AIP-2424) - #89

Draft
pranludi wants to merge 2 commits into
fix/image-registry-secret-unnecessary-rotationfrom
feature/AIP-2424-robot-secret-on-create
Draft

feat(imagehub): reuse the create-time robot secret instead of refreshing it (AIP-2424)#89
pranludi wants to merge 2 commits into
fix/image-registry-secret-unnecessary-rotationfrom
feature/AIP-2424-robot-secret-on-create

Conversation

@pranludi

Copy link
Copy Markdown
Contributor

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/imageregistryrobots return {"robotId":"42","secret":"..."} instead of an empty body.

Previously ImageRegistryRobotService.createImageRegistryRobot was void: the plaintext secret Harbor returns at creation was thrown away, so ImageRegistrySecretReconciler had to call PUT /imageregistryrobots/{id}/refreshsecret afterwards just to obtain a password for the .dockerconfigjson Secret.

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 by ImageRegistrySecretReconciler (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 ImageRegistryRobotSecretStore bridges them: the create path deposits the secret keyed by robot id, and AipubDockerConfigJsonResolver.getPassword takes it from there, falling back to refreshsecret on 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:

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.

⚠️ AipubConfiguration builds ImageRegistryRobotServiceImpl twice, 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) and id (list) both come from Harbor's Robot.getId() — same value, so the store key matches what the resolver looks up.
  • No Jackson naming-strategy override on the gateway, so the JSON is camelCase.
  • Both fields are nullable (backend skips creation when no permission is valid, or fails to parse Harbor's body) — handled, and an absent secret just falls back to refreshsecret.

Also included: unrelated pre-existing bug (3f29487)

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.

Net effect: every edit to a project's spec.binding.imageHubs on 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 build passes; full suite 97 tests, 0 failures
  • New unit tests (12): ImageRegistryRobotSecretStoreTest (consume-once, TTL expiry, id keying, purge-on-put) and AipubDockerConfigJsonResolverTest — the key assertion being that refreshSecret is never called when the store has the secret, and that a miss still falls back to it
  • Cluster verification — needs aipub-backend#438 deployed first. Connect an ImageHub to a fresh project, confirm the Secret is seeded from the create response with no refreshsecret call, and that docker login works
  • Verify the store-miss path on an existing project (restart project-controller mid-flow) still produces a working Secret via refreshsecret

Rollout 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 current refreshsecret path — 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

pranludi and others added 2 commits July 29, 2026 22:16
…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>
@pranludi pranludi added this to the v5.1.0 milestone Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant