Skip to content

Commit ca6a2a8

Browse files
committed
πŸ€– feat: apply CoderControlPlane licenses from Secret
Add licenseSecretRef support on CoderControlPlane and reconcile Enterprise license uploads after control-plane/operator readiness. Track successful applies with licenseLastApplied and licenseLastAppliedHash to avoid duplicate uploads, and handle Secret rotation. Also adds Secret field indexing/watch wiring for referenced license Secrets, pluggable SDK-backed license uploader, generated CRD/docs updates, sample manifest updates, and controller tests for pending/apply/idempotency/rotation and NotSupported behavior. --- _Generated with `mux` β€’ Model: `openai:gpt-5.3-codex` β€’ Thinking: `xhigh` β€’ Cost: `.21`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=1.21 -->
1 parent a84bece commit ca6a2a8

9 files changed

Lines changed: 929 additions & 45 deletions

File tree

β€Žapi/v1alpha1/codercontrolplane_types.goβ€Ž

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const (
1010
CoderControlPlanePhasePending = "Pending"
1111
// CoderControlPlanePhaseReady indicates at least one control plane pod is ready.
1212
CoderControlPlanePhaseReady = "Ready"
13+
// CoderControlPlaneConditionLicenseApplied indicates whether the operator uploaded the configured license.
14+
CoderControlPlaneConditionLicenseApplied = "LicenseApplied"
1315
)
1416

1517
// CoderControlPlaneSpec defines the desired state of a CoderControlPlane.
@@ -32,6 +34,11 @@ type CoderControlPlaneSpec struct {
3234
// OperatorAccess configures bootstrap API access to the coderd instance.
3335
// +kubebuilder:default={}
3436
OperatorAccess OperatorAccessSpec `json:"operatorAccess,omitempty"`
37+
// LicenseSecretRef references a Secret key containing a Coder Enterprise
38+
// license JWT. When set, the controller uploads the license after the
39+
// control plane is ready and re-uploads when the Secret value changes.
40+
// +optional
41+
LicenseSecretRef *SecretKeySelector `json:"licenseSecretRef,omitempty"`
3542
}
3643

3744
// OperatorAccessSpec configures the controller-managed coderd operator user.
@@ -56,6 +63,14 @@ type CoderControlPlaneStatus struct {
5663
OperatorTokenSecretRef *SecretKeySelector `json:"operatorTokenSecretRef,omitempty"`
5764
// OperatorAccessReady reports whether operator API access bootstrap succeeded.
5865
OperatorAccessReady bool `json:"operatorAccessReady,omitempty"`
66+
// LicenseLastApplied is the timestamp of the most recent successful
67+
// operator-managed license upload.
68+
// +optional
69+
LicenseLastApplied *metav1.Time `json:"licenseLastApplied,omitempty"`
70+
// LicenseLastAppliedHash is the SHA-256 hex hash of the trimmed license JWT
71+
// that LicenseLastApplied refers to.
72+
// +optional
73+
LicenseLastAppliedHash string `json:"licenseLastAppliedHash,omitempty"`
5974
// Phase is a high-level readiness indicator.
6075
Phase string `json:"phase,omitempty"`
6176
// Conditions are Kubernetes-standard conditions for this resource.

β€Žapi/v1alpha1/types_shared.goβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import corev1 "k8s.io/api/core/v1"
55
const (
66
// DefaultTokenSecretKey is the default key used for proxy session tokens.
77
DefaultTokenSecretKey = "token"
8+
// DefaultLicenseSecretKey is the default key used for Coder license JWTs.
9+
DefaultLicenseSecretKey = "license"
810
)
911

1012
// ServiceSpec defines the Service configuration reconciled by the operator.

β€Žapi/v1alpha1/zz_generated.deepcopy.goβ€Ž

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žconfig/crd/bases/coder.com_codercontrolplanes.yamlβ€Ž

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,21 @@ spec:
226226
type: object
227227
x-kubernetes-map-type: atomic
228228
type: array
229+
licenseSecretRef:
230+
description: |-
231+
LicenseSecretRef references a Secret key containing a Coder Enterprise
232+
license JWT. When set, the controller uploads the license after the
233+
control plane is ready and re-uploads when the Secret value changes.
234+
properties:
235+
key:
236+
description: Key is the key inside the Secret data map.
237+
type: string
238+
name:
239+
description: Name is the Kubernetes Secret name.
240+
type: string
241+
required:
242+
- name
243+
type: object
229244
operatorAccess:
230245
default: {}
231246
description: OperatorAccess configures bootstrap API access to the
@@ -330,6 +345,17 @@ spec:
330345
- type
331346
type: object
332347
type: array
348+
licenseLastApplied:
349+
description: |-
350+
LicenseLastApplied is the timestamp of the most recent successful
351+
operator-managed license upload.
352+
format: date-time
353+
type: string
354+
licenseLastAppliedHash:
355+
description: |-
356+
LicenseLastAppliedHash is the SHA-256 hex hash of the trimmed license JWT
357+
that LicenseLastApplied refers to.
358+
type: string
333359
observedGeneration:
334360
description: ObservedGeneration tracks the spec generation this status
335361
reflects.

β€Žconfig/samples/coder_v1alpha1_codercontrolplane.yamlβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ metadata:
55
namespace: default
66
spec:
77
image: "ghcr.io/coder/coder-k8s:main"
8+
licenseSecretRef:
9+
name: coder-license
10+
key: license

β€Ždocs/reference/api/codercontrolplane.mdβ€Ž

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,55 +13,38 @@
1313

1414
| Field | Type | Description |
1515
| --- | --- | --- |
16-
| `image` | string | Image is the container image used for the Coder control plane pod. |
17-
| `replicas` | integer | Replicas is the desired number of control plane pods. |
18-
| `service` | [ServiceSpec](#servicespec) | Service controls the service created in front of the control plane. |
19-
| `extraArgs` | string array | ExtraArgs are appended to the default Coder server arguments. |
20-
| `extraEnv` | [EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#envvar-v1-core) array | ExtraEnv are injected into the Coder control plane container. |
21-
| `imagePullSecrets` | [LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core) array | ImagePullSecrets are used by the pod to pull private images. |
22-
| `operatorAccess` | [OperatorAccessSpec](#operatoraccessspec) | OperatorAccess configures bootstrap API access to the coderd instance. |
16+
| `image` | `string` | Image is the container image used for the Coder control plane pod. |
17+
| `replicas` | `int32` | Replicas is the desired number of control plane pods. |
18+
| `service` | `ServiceSpec` | Service controls the service created in front of the control plane. |
19+
| β”œβ”€ `type` | `corev1.ServiceType` | Type controls the Kubernetes service type. |
20+
| β”œβ”€ `port` | `int32` | Port controls the exposed service port. |
21+
| └─ `annotations` | `map[string]string` | Annotations are applied to the reconciled service object. |
22+
| `extraArgs` | `[]string` | ExtraArgs are appended to the default Coder server arguments. |
23+
| `extraEnv` | `[]corev1.EnvVar` | ExtraEnv are injected into the Coder control plane container. |
24+
| `imagePullSecrets` | `[]corev1.LocalObjectReference` | ImagePullSecrets are used by the pod to pull private images. |
25+
| └─ `name` | `string` | Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: [https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) |
26+
| `operatorAccess` | `OperatorAccessSpec` | OperatorAccess configures bootstrap API access to the coderd instance. |
27+
| β”œβ”€ `disabled` | `bool` | Disabled turns off creation and management of the `coder-k8s-operator` user and API token. |
28+
| └─ `generatedTokenSecretName` | `string` | GeneratedTokenSecretName stores the generated operator API token. |
29+
| `licenseSecretRef` | `SecretKeySelector` | LicenseSecretRef references a Secret key containing a Coder Enterprise license JWT. When set, the controller uploads the license after the control plane is ready and re-uploads when the Secret value changes. |
30+
| β”œβ”€ `name` | `string` | Name is the Kubernetes Secret name. |
31+
| └─ `key` | `string` | Key is the key inside the Secret data map. |
2332

2433
## Status
2534

2635
| Field | Type | Description |
2736
| --- | --- | --- |
28-
| `observedGeneration` | integer | ObservedGeneration tracks the spec generation this status reflects. |
29-
| `readyReplicas` | integer | ReadyReplicas is the number of ready pods observed in the deployment. |
30-
| `url` | string | URL is the in-cluster URL for the control plane service. |
31-
| `operatorTokenSecretRef` | [SecretKeySelector](#secretkeyselector) | OperatorTokenSecretRef points to the Secret key containing the `coder-k8s-operator` API token. |
32-
| `operatorAccessReady` | boolean | OperatorAccessReady reports whether operator API access bootstrap succeeded. |
33-
| `phase` | string | Phase is a high-level readiness indicator. |
34-
| `conditions` | [Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array | Conditions are Kubernetes-standard conditions for this resource. |
35-
36-
## Referenced types
37-
38-
### OperatorAccessSpec
39-
40-
OperatorAccessSpec configures the controller-managed coderd operator user.
41-
42-
| Field | Type | Description |
43-
| --- | --- | --- |
44-
| `disabled` | boolean | Disabled turns off creation and management of the `coder-k8s-operator` user and API token. |
45-
| `generatedTokenSecretName` | string | GeneratedTokenSecretName stores the generated operator API token. |
46-
47-
### SecretKeySelector
48-
49-
SecretKeySelector identifies a key in a Secret.
50-
51-
| Field | Type | Description |
52-
| --- | --- | --- |
53-
| `name` | string | Name is the Kubernetes Secret name. |
54-
| `key` | string | Key is the key inside the Secret data map. |
55-
56-
### ServiceSpec
57-
58-
ServiceSpec defines the Service configuration reconciled by the operator.
59-
60-
| Field | Type | Description |
61-
| --- | --- | --- |
62-
| `type` | [ServiceType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#servicetype-v1-core) | Type controls the Kubernetes service type. |
63-
| `port` | integer | Port controls the exposed service port. |
64-
| `annotations` | object (keys:string, values:string) | Annotations are applied to the reconciled service object. |
37+
| `observedGeneration` | `int64` | ObservedGeneration tracks the spec generation this status reflects. |
38+
| `readyReplicas` | `int32` | ReadyReplicas is the number of ready pods observed in the deployment. |
39+
| `url` | `string` | URL is the in-cluster URL for the control plane service. |
40+
| `operatorTokenSecretRef` | `SecretKeySelector` | OperatorTokenSecretRef points to the Secret key containing the `coder-k8s-operator` API token. |
41+
| β”œβ”€ `name` | `string` | Name is the Kubernetes Secret name. |
42+
| └─ `key` | `string` | Key is the key inside the Secret data map. |
43+
| `operatorAccessReady` | `bool` | OperatorAccessReady reports whether operator API access bootstrap succeeded. |
44+
| `licenseLastApplied` | `metav1.Time` | LicenseLastApplied is the timestamp of the most recent successful operator-managed license upload. |
45+
| `licenseLastAppliedHash` | `string` | LicenseLastAppliedHash is the SHA-256 hex hash of the trimmed license JWT that LicenseLastApplied refers to. |
46+
| `phase` | `string` | Phase is a high-level readiness indicator. |
47+
| `conditions` | `[]metav1.Condition` | Conditions are Kubernetes-standard conditions for this resource. |
6548

6649
## Source
6750

β€Žinternal/app/controllerapp/controllerapp.goβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ func SetupControllers(mgr manager.Manager) error {
9090
Client: client,
9191
Scheme: managerScheme,
9292
OperatorAccessProvisioner: coderbootstrap.NewPostgresOperatorAccessProvisioner(),
93+
LicenseUploader: controller.NewSDKLicenseUploader(),
9394
}
9495
if err := reconciler.SetupWithManager(mgr); err != nil {
9596
return fmt.Errorf("unable to create controller: %w", err)

0 commit comments

Comments
Β (0)