🤖 feat: reconcile CoderControlPlane licenses from Secret#66
Conversation
|
@codex review Please review this change set for |
dc3ca49 to
ca6a2a8
Compare
|
@codex review Rebased on latest |
Add CoderControlPlane license Secret support and operator-managed license upload reconciliation with readiness/operator-access preconditions, status tracking (`licenseLastApplied`, `licenseLastAppliedHash`), idempotency hashing, Secret rotation handling, and license status conditions. Also wire a codersdk-backed uploader, add referenced Secret field indexing and watches, update controller app wiring, add focused controller tests, and regenerate CRD/deepcopy/docs/sample artifacts. Includes docs reference regeneration to align with CI output formatting. --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$1.21`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=1.21 -->
ca6a2a8 to
6f550f4
Compare
|
@codex review Applied CI alignment updates and force-pushed latest commit history. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6f550f4acd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Retry CoderControlPlane status updates with `retry.RetryOnConflict` and a fresh read of the latest object before update. This ensures transient resourceVersion conflicts do not drop status updates after a successful license upload, which could otherwise trigger duplicate non-idempotent license posts. --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$1.21`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=1.21 -->
|
@codex review Addressed the status-update conflict concern by adding RetryOnConflict around |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b5ba757697
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Address Codex review findings by: - verifying coderd still reports at least one license before idempotent hash-based skip, so the controller re-uploads when backend license state is reset while Secret content is unchanged, - reading through `APIReader` during `RetryOnConflict` status updates so conflict retries are based on uncached latest resource versions. Also adds coverage for re-upload behavior when backend licenses are absent. --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$1.21`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=1.21 -->
|
@codex review Addressed both open findings:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7fa58d9935
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
When retrying CoderControlPlane status updates, skip writes if the latest resource generation no longer matches `nextStatus.ObservedGeneration`. This prevents older reconciles from overwriting fresher status computed by newer reconciles while still retaining conflict retry behavior. --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$1.21`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=1.21 -->
|
@codex review Added a stale-generation guard in status conflict retries to avoid older |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9184ed38af
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Use the reconcile-start status snapshot as a delta base and only apply fields that this reconcile actually changed when retrying status writes on conflicts. This prevents same-generation stale reconciles from clearing fresher license status updates written by newer reconciles. --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$1.21`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=1.21 -->
|
@codex review Updated status conflict retries to apply only reconcile-intended status deltas |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b09da13fce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Treat duplicate-license upload responses as converged success and persist `licenseLastApplied*`/Applied condition, so rollback rotations (A→B→A) do not loop on non-idempotent backend errors. Also adds a regression test for rollback duplicate-upload convergence. --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$1.21`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=1.21 -->
|
@codex review Handled rollback duplicate uploads (A→B→A) as converged success by detecting |
|
Codex Review: Didn't find any major issues. 👍 ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Add automatic Coder Enterprise license management to
CoderControlPlane.Background
Previously, the operator had no API surface for license configuration and no reconcile logic to upload licenses after control plane bootstrap.
This change allows operators to point at a Secret and have the controller apply licenses once the control plane and operator access are ready, including rotation behavior.
Implementation
spec.licenseSecretReftoCoderControlPlane.licenseLastAppliedandlicenseLastAppliedHash.DefaultLicenseSecretKey = "license".LicenseAppliedcondition type and condition updates in reconcile paths.LicenseUploader,NotSupported) and auth/error handling.licenseSecretRefSecrets.Validation
make verify-vendormake testmake buildmake lintmake codegenmake manifestsmake docs-referenceRisks
CoderControlPlane.📋 Implementation Plan
Plan:
CoderControlPlanelicense Secret reference + automatic license applicationContext / Why
We want the
coder-k8soperator to manage Coder Enterprise licensing automatically:spec.licenseSecretReftocoder.com/v1alpha1.CoderControlPlaneto reference a Secret key containing a Coder license JWT.LicenseAppliedidea with an optional timestamp:status.licenseLastApplied.This aligns with existing patterns in the repo (SecretKeySelector usage, operator-managed API token creation, reconcile-with-requeue behavior), and avoids requiring users to manually run
coder licenses addafter every deployment/rotation.Evidence (what we verified)
CoderControlPlanetype today has no license field; it already usesSecretKeySelectorin status for the operator token secret:api/v1alpha1/codercontrolplane_types.goapi/v1alpha1/types_shared.godeployment.Status.ReadyReplicas > 0, stored instatus.phase(Pending→Ready). The controller also computes an in-cluster URL:internal/controller/codercontrolplane_controller.go(desiredStatussetsURL = http://<svc>.<ns>.svc.cluster.local:<port>)readSecretValue()helper with strong assertions.github.com/coder/coder/v2/codersdkviainternal/coderbootstrap.POST /api/v2/licenseswith JSON body{ "license": "<jwt>" }Coder-Session-Token: <token>(Bearer token also supported)201 Createdlicenses.jwt, and the server returns500on duplicate insert./licensesroutes are not registered →404 Not Found.Design decisions
API surface
Add:
spec.licenseSecretRef(optional) — reference to Secret name + key.status.licenseLastApplied(optionalmetav1.Time) — when the operator last successfully uploaded the currently-observed license.Additionally (needed for correctness / idempotency):
status.licenseLastAppliedHash(optional string) — SHA-256 hex of the trimmed license JWT that was last successfully applied.Rationale: Coder rejects duplicate uploads (500). Without persisting a stable identity (hash), the controller can’t safely be re-entrant and would spam POSTs on every reconcile.
Preconditions for applying a license
Only attempt license upload when:
spec.licenseSecretRef != nil.status.phase == Ready(deployment has ≥1 ready replica).status.operatorAccessReady == trueANDstatus.operatorTokenSecretRef != nil.If operator access is disabled or not yet ready, we should not attempt license application (no credentials).
Rotation semantics
status.licenseLastAppliedHash.status.licenseLastApplied = nowand updatestatus.licenseLastAppliedHash.Watching the referenced Secret
The controller currently only watches Secrets it owns (
Owns(&corev1.Secret{})). User-provided license Secrets are not owned, so we must add an explicit watch for Secrets referenced byspec.licenseSecretRef.Use a field index + watch mapping so we only enqueue
CoderControlPlanereconciles for Secrets actually referenced by control planes.Enterprise-only behavior
If the license API returns 404:
NotSupported.Status Conditions (recommended)
CoderControlPlaneStatusalready hasconditions []metav1.Conditionbut it’s not currently populated. Introduce a single condition type for license:type: LicenseAppliedstatus: True|False|UnknownApplied,Pending,SecretMissing,Forbidden,NotSupported,ErrorKeep messages stable to avoid noisy status updates.
Alternatives considered (kept short)
GET /licensesresponse: adds JWT parsing/validation complexity; storing a SHA-256 hash is simpler and avoids relying on claim structure.Implementation details (concrete edits)
1) API / CRD changes
Files:
api/v1alpha1/codercontrolplane_types.goapi/v1alpha1/types_shared.goapi/v1alpha1/zz_generated.deepcopy.goconfig/crd/bases/coder.com_codercontrolplanes.yamldocs/reference/api/codercontrolplane.mdconfig/samples/coder_v1alpha1_codercontrolplane.yamla) Add spec field
b) Add status fields
c) Add a default key constant
In
api/v1alpha1/types_shared.go(or a new shared constants file):Controller will treat empty
licenseSecretRef.keyasDefaultLicenseSecretKey.d) Regenerate generated artifacts
make codegenmake manifestsmake docs-reference2) Controller changes
File:
internal/controller/codercontrolplane_controller.goa) Reconciler fields (for testability)
Add an interface that can be faked in tests:
Add to reconciler:
Wire it in
internal/app/controllerapp/controllerapp.go:b) Reconcile flow changes
After
reconcileOperatorAccessand beforereconcileStatus, callreconcileLicense:Where
mergeResultschooses a non-zero requeue request deterministically (e.g., prefer the shorterRequeueAfterif both set).c) Implement
reconcileLicenseShape:
Logic:
cp.Spec.LicenseSecretRef == nil: clear/leave license condition as Unknown; return.nextStatus.Phase != Ready: set condition False (Pending); return.!nextStatus.OperatorAccessReady || nextStatus.OperatorTokenSecretRef == nil: set condition False (Pending); return.readSecretValue(namespace, nextStatus.OperatorTokenSecretRef.Name, key).readSecretValue(namespace, cp.Spec.LicenseSecretRef.Name, resolvedKey).licenseJWT = strings.TrimSpace(licenseJWT); error if empty.hash := sha256hex(licenseJWT).hash == nextStatus.LicenseLastAppliedHashandnextStatus.LicenseLastApplied != nil: consider applied; set condition True; return.err := r.LicenseUploader.AddLicense(ctx, nextStatus.URL, token, licenseJWT)NotSupported; return without aggressive requeue.Forbidden; requeue afteroperatorAccessRetryInterval.Error; requeue afteroperatorAccessRetryInterval.now := metav1.Now(); nextStatus.LicenseLastApplied = &nownextStatus.LicenseLastAppliedHash = hashAppliedd) Implement SDK uploader
Use the same HTTP client setup pattern as
internal/coderbootstrap/SDKClient(dedicated transport clone, timeout).Pseudo-shape:
e) Watch referenced license Secrets
In
SetupWithManager:.spec.licenseSecretRef.namecorev1.Secretevents that maps toCoderControlPlanerequests via that index.Sketch:
3) Tests
File:
internal/controller/codercontrolplane_controller_test.goAdd a
fakeLicenseUploadersimilar tofakeOperatorAccessProvisioner.Test cases (table-driven preferred):
LicenseSecretRef=nilresults in no uploader calls.deployment.Status.ReadyReplicas=0, uploader not called.ExtraEnvcontainingCODER_PG_CONNECTION_URLvaluelicenseand some valuestatus.licenseLastApplied != nilandstatus.licenseLastAppliedHash != ""*codersdk.Errorwith status 404; controller sets condition reasonNotSupportedand does not tight-loop.4) Documentation + samples
config/samples/coder_v1alpha1_codercontrolplane.yamlto include an example:make docs-referencesodocs/reference/api/codercontrolplane.mdincludes the new fields.Validation / completion checklist
Run (in this repo root):
make codegenmake manifestsmake docs-referencemake testmake buildmake lintDefinition of done:
Generated with
mux• Model:openai:gpt-5.3-codex• Thinking:xhigh• Cost:$1.21