Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions api/v1alpha1/coderprovisioner_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ const (

// CoderProvisionerConditionControlPlaneReady indicates whether the referenced control plane is reachable.
CoderProvisionerConditionControlPlaneReady = "ControlPlaneReady"
// CoderProvisionerConditionBootstrapSecretReady indicates whether the bootstrap credentials secret is available.
CoderProvisionerConditionBootstrapSecretReady = "BootstrapSecretReady"
// CoderProvisionerConditionOperatorAccessReady indicates whether operator-managed
// access from the referenced control plane is ready for provisioner key management.
CoderProvisionerConditionOperatorAccessReady = "OperatorAccessReady"
// CoderProvisionerConditionProvisionerKeyReady indicates whether the provisioner key exists in coderd.
CoderProvisionerConditionProvisionerKeyReady = "ProvisionerKeyReady"
// CoderProvisionerConditionProvisionerKeySecretReady indicates whether the provisioner key secret is populated.
Expand All @@ -32,13 +33,6 @@ const (
ProvisionerKeyCleanupFinalizer = "coder.com/provisioner-key-cleanup"
)

// CoderProvisionerBootstrapSpec configures credentials for provisioner key management.
type CoderProvisionerBootstrapSpec struct {
// CredentialsSecretRef points to a Secret containing a Coder session token
// with permission to manage provisioner keys.
CredentialsSecretRef SecretKeySelector `json:"credentialsSecretRef"`
}

// CoderProvisionerKeySpec configures provisioner key naming and storage.
type CoderProvisionerKeySpec struct {
// Name is the provisioner key name in coderd. Defaults to the CR name.
Expand All @@ -55,12 +49,12 @@ type CoderProvisionerKeySpec struct {
// CoderProvisionerSpec defines the desired state of a CoderProvisioner.
type CoderProvisionerSpec struct {
// ControlPlaneRef identifies which CoderControlPlane instance to join.
// Provisioner key management uses operator-managed access from this
// control plane's status.operatorTokenSecretRef.
ControlPlaneRef corev1.LocalObjectReference `json:"controlPlaneRef"`
// OrganizationName is the Coder organization. Defaults to "default".
// +kubebuilder:validation:MaxLength=128
OrganizationName string `json:"organizationName,omitempty"`
// Bootstrap configures credentials for provisioner key management.
Bootstrap CoderProvisionerBootstrapSpec `json:"bootstrap"`
// Key configures provisioner key naming and secret storage.
Key CoderProvisionerKeySpec `json:"key,omitempty"`
// Replicas is the desired number of provisioner pods.
Expand Down
18 changes: 0 additions & 18 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 4 additions & 24 deletions config/crd/bases/coder.com_coderprovisioners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,30 +50,11 @@ spec:
spec:
description: CoderProvisionerSpec defines the desired state of a CoderProvisioner.
properties:
bootstrap:
description: Bootstrap configures credentials for provisioner key
management.
properties:
credentialsSecretRef:
description: |-
CredentialsSecretRef points to a Secret containing a Coder session token
with permission to manage provisioner keys.
properties:
key:
description: Key is the key inside the Secret data map.
type: string
name:
description: Name is the Kubernetes Secret name.
type: string
required:
- name
type: object
required:
- credentialsSecretRef
type: object
controlPlaneRef:
description: ControlPlaneRef identifies which CoderControlPlane instance
to join.
description: |-
ControlPlaneRef identifies which CoderControlPlane instance to join.
Provisioner key management uses operator-managed access from this
control plane's status.operatorTokenSecretRef.
properties:
name:
default: ""
Expand Down Expand Up @@ -369,7 +350,6 @@ spec:
format: int64
type: integer
required:
- bootstrap
- controlPlaneRef
type: object
status:
Expand Down
2 changes: 1 addition & 1 deletion config/samples/coder_v1alpha1_codercontrolplane.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: coder.com/v1alpha1
kind: CoderControlPlane
metadata:
name: codercontrolplane-sample
namespace: default
namespace: coder
Comment thread
ThomasK33 marked this conversation as resolved.
spec:
image: "ghcr.io/coder/coder:latest"
# Optional Enterprise license upload:
Expand Down
15 changes: 3 additions & 12 deletions config/samples/coder_v1alpha1_coderprovisioner.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,16 @@
#
# Prerequisites:
# 1. A CoderControlPlane resource must exist in the same namespace.
# 2. Create the bootstrap credentials Secret with a valid Coder session token:
#
# kubectl create secret generic coder-bootstrap-token \
# --namespace=default \
# --from-literal=token=<YOUR_SESSION_TOKEN>
#
# IMPORTANT: Never commit real tokens to source control.
# 2. The referenced CoderControlPlane status must report operator access ready:
# status.operatorAccessReady=true and status.operatorTokenSecretRef set.
apiVersion: coder.com/v1alpha1
kind: CoderProvisioner
metadata:
name: coderprovisioner-sample
namespace: default
namespace: coder
Comment thread
ThomasK33 marked this conversation as resolved.
spec:
controlPlaneRef:
name: codercontrolplane-sample
bootstrap:
credentialsSecretRef:
name: coder-bootstrap-token
key: token
# key:
# name: my-provisioner-key # Provisioner key name in coderd (defaults to CR name)
# secretName: my-key-secret # K8s Secret to store the key (defaults to "{name}-provisioner-key")
Expand Down
11 changes: 1 addition & 10 deletions docs/reference/api/coderprovisioner.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@

| Field | Type | Description |
| --- | --- | --- |
| `controlPlaneRef` | [LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core) | ControlPlaneRef identifies which CoderControlPlane instance to join. |
| `controlPlaneRef` | [LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core) | ControlPlaneRef identifies which CoderControlPlane instance to join. Provisioner key management uses operator-managed access from this control plane's status.operatorTokenSecretRef. |
| `organizationName` | string | OrganizationName is the Coder organization. Defaults to "default". |
| `bootstrap` | [CoderProvisionerBootstrapSpec](#coderprovisionerbootstrapspec) | Bootstrap configures credentials for provisioner key management. |
| `key` | [CoderProvisionerKeySpec](#coderprovisionerkeyspec) | Key configures provisioner key naming and secret storage. |
| `replicas` | integer | Replicas is the desired number of provisioner pods. |
| `tags` | object (keys:string, values:string) | Tags are attached to the provisioner key for job routing. |
Expand Down Expand Up @@ -45,14 +44,6 @@

## Referenced types

### CoderProvisionerBootstrapSpec

CoderProvisionerBootstrapSpec configures credentials for provisioner key management.

| Field | Type | Description |
| --- | --- | --- |
| `credentialsSecretRef` | [SecretKeySelector](#secretkeyselector) | CredentialsSecretRef points to a Secret containing a Coder session token with permission to manage provisioner keys. |

### CoderProvisionerKeySpec

CoderProvisionerKeySpec configures provisioner key naming and storage.
Expand Down
12 changes: 9 additions & 3 deletions docs/tutorials/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ Install CRDs into your cluster:
kubectl apply -f config/crd/bases/
```

Create the sample namespace used by shipped manifests:

```bash
kubectl create namespace coder
```

## 2) Run the controller locally

Start controller mode (terminal A):
Expand All @@ -58,19 +64,19 @@ Check resource status:

```bash
kubectl get codercontrolplanes -A
kubectl describe codercontrolplane codercontrolplane-sample -n default
kubectl describe codercontrolplane codercontrolplane-sample -n coder
```

The controller creates a Deployment + Service named after the control plane (`codercontrolplane-sample`) in the same namespace.

```bash
kubectl get deploy,svc -n default
kubectl get deploy,svc -n coder
```

## 5) Clean up (optional)

```bash
kubectl delete codercontrolplane codercontrolplane-sample -n default
kubectl delete codercontrolplane codercontrolplane-sample -n coder
```

If you used `kind-dev-up`, you can remove the cluster with:
Expand Down
117 changes: 82 additions & 35 deletions internal/controller/coderprovisioner_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const (
provisionerRateLimitJitterRatio = 0.2

externalProvisionerEntitlementRetryInterval = 2 * time.Minute
provisionerOperatorAccessRetryInterval = 15 * time.Second

// #nosec G101 -- this is a field index key, not a credential.
provisionerControlPlaneRefNameFieldIndex = ".spec.controlPlaneRef.name"
Expand Down Expand Up @@ -171,24 +172,27 @@ func (r *CoderProvisionerReconciler) Reconcile(ctx context.Context, req ctrl.Req
organizationName := provisionerOrganizationName(provisioner.Spec.OrganizationName)
keyName, keySecretName, keySecretKey := provisionerKeyConfig(provisioner)

sessionToken, err := r.readBootstrapSessionToken(ctx, provisioner)
sessionToken, operatorAccessResult, operatorAccessReason, operatorAccessMessage, err := r.readOperatorSessionToken(ctx, provisioner, controlPlane)
if err != nil {
return ctrl.Result{}, err
}
if operatorAccessResult.RequeueAfter > 0 {
setCondition(
provisioner,
coderv1alpha1.CoderProvisionerConditionBootstrapSecretReady,
coderv1alpha1.CoderProvisionerConditionOperatorAccessReady,
metav1.ConditionFalse,
"BootstrapSecretUnavailable",
fmt.Sprintf("Failed to read bootstrap credentials: %v", err),
operatorAccessReason,
operatorAccessMessage,
)
_ = r.Status().Update(ctx, provisioner)
return ctrl.Result{}, err
return operatorAccessResult, nil
}
setCondition(
provisioner,
coderv1alpha1.CoderProvisionerConditionBootstrapSecretReady,
coderv1alpha1.CoderProvisionerConditionOperatorAccessReady,
metav1.ConditionTrue,
"BootstrapSecretAvailable",
"Bootstrap credentials secret is available",
operatorAccessReason,
operatorAccessMessage,
)

entitlementResult, entitlementErr := r.reconcileExternalProvisionerEntitlement(ctx, provisioner, controlPlane, sessionToken)
Expand Down Expand Up @@ -643,28 +647,40 @@ func (r *CoderProvisionerReconciler) reconcileDeletion(ctx context.Context, prov
}

// Best-effort remote key cleanup: if the referenced control plane,
// its URL, bootstrap credentials, or any other prerequisite is
// its URL, operator access token, or any other prerequisite is
// unavailable, log a warning and proceed to finalizer removal so the
// CR does not get stuck in Terminating. This is common during
// namespace teardown, when the control plane was never ready, or
// when credentials were misconfigured.
// when operator access bootstrap has not completed.
controlPlaneURL := provisioner.Status.ControlPlaneURL
if controlPlaneURL == "" {
controlPlane, err := r.fetchControlPlane(ctx, provisioner)
if err != nil {
log.Info("unable to reach referenced CoderControlPlane during deletion, skipping remote key cleanup",
"controlPlaneRef", provisioner.Spec.ControlPlaneRef.Name, "error", err)
} else {
controlPlaneURL = controlPlane.Status.URL
controlPlaneName := strings.TrimSpace(provisioner.Spec.ControlPlaneRef.Name)
var controlPlane *coderv1alpha1.CoderControlPlane
if controlPlaneName != "" {
controlPlane = &coderv1alpha1.CoderControlPlane{}
namespacedName := types.NamespacedName{Name: controlPlaneName, Namespace: provisioner.Namespace}
if err := r.Get(ctx, namespacedName, controlPlane); err != nil {
log.Info("unable to read referenced CoderControlPlane during deletion, skipping remote key cleanup",
"controlPlaneRef", controlPlaneName, "error", err)
controlPlane = nil
} else if controlPlane.Name != controlPlaneName || controlPlane.Namespace != provisioner.Namespace {
return ctrl.Result{}, fmt.Errorf("assertion failed: fetched control plane %s/%s does not match expected %s/%s",
controlPlane.Namespace, controlPlane.Name, provisioner.Namespace, controlPlaneName)
}
}
if controlPlaneURL == "" && controlPlane != nil {
controlPlaneURL = strings.TrimSpace(controlPlane.Status.URL)
}

if controlPlaneURL != "" {
sessionToken, tokenErr := r.readBootstrapSessionToken(ctx, provisioner)
if tokenErr != nil {
log.Info("unable to read bootstrap credentials during deletion, skipping remote key cleanup",
"credentialsSecretRef", provisioner.Spec.Bootstrap.CredentialsSecretRef.Name, "error", tokenErr)
} else {
if controlPlaneURL != "" && controlPlane != nil {
sessionToken, tokenResult, tokenReason, tokenMessage, tokenErr := r.readOperatorSessionToken(ctx, provisioner, controlPlane)
switch {
case tokenErr != nil:
log.Info("unable to resolve operator access token during deletion, skipping remote key cleanup",
"error", tokenErr)
case tokenResult.RequeueAfter > 0:
log.Info("operator access is not ready during deletion, skipping remote key cleanup",
"reason", tokenReason, "message", tokenMessage)
default:
if deleteErr := r.BootstrapClient.DeleteProvisionerKey(
ctx,
controlPlaneURL,
Expand Down Expand Up @@ -726,23 +742,54 @@ func (r *CoderProvisionerReconciler) fetchControlPlane(ctx context.Context, prov
return controlPlane, nil
}

func (r *CoderProvisionerReconciler) readBootstrapSessionToken(ctx context.Context, provisioner *coderv1alpha1.CoderProvisioner) (string, error) {
credentialsRef := provisioner.Spec.Bootstrap.CredentialsSecretRef
if credentialsRef.Name == "" {
return "", fmt.Errorf("coderprovisioner %s/%s spec.bootstrap.credentialsSecretRef.name is required", provisioner.Namespace, provisioner.Name)
func (r *CoderProvisionerReconciler) readOperatorSessionToken(
ctx context.Context,
provisioner *coderv1alpha1.CoderProvisioner,
controlPlane *coderv1alpha1.CoderControlPlane,
) (string, ctrl.Result, string, string, error) {
if provisioner == nil {
return "", ctrl.Result{}, "", "", fmt.Errorf("assertion failed: coder provisioner must not be nil")
}
if controlPlane == nil {
return "", ctrl.Result{}, "", "", fmt.Errorf("assertion failed: coder control plane must not be nil")
}

credentialsKey := credentialsRef.Key
if credentialsKey == "" {
credentialsKey = coderv1alpha1.DefaultTokenSecretKey
if !controlPlane.Status.OperatorAccessReady {
return "", ctrl.Result{RequeueAfter: provisionerOperatorAccessRetryInterval},
"OperatorAccessNotReady",
fmt.Sprintf("Waiting for CoderControlPlane %s/%s status.operatorAccessReady=true before reconciling provisioner keys.", controlPlane.Namespace, controlPlane.Name),
nil
}

operatorTokenRef := controlPlane.Status.OperatorTokenSecretRef
if operatorTokenRef == nil {
return "", ctrl.Result{RequeueAfter: provisionerOperatorAccessRetryInterval},
"OperatorTokenSecretRefMissing",
fmt.Sprintf("Waiting for CoderControlPlane %s/%s status.operatorTokenSecretRef to be set by operator access bootstrap.", controlPlane.Namespace, controlPlane.Name),
nil
}

operatorTokenSecretName := strings.TrimSpace(operatorTokenRef.Name)
if operatorTokenSecretName == "" {
return "", ctrl.Result{RequeueAfter: provisionerOperatorAccessRetryInterval},
"OperatorTokenSecretRefInvalid",
fmt.Sprintf("Waiting for CoderControlPlane %s/%s status.operatorTokenSecretRef.name to be non-empty.", controlPlane.Namespace, controlPlane.Name),
nil
}
operatorTokenSecretKey := strings.TrimSpace(operatorTokenRef.Key)
if operatorTokenSecretKey == "" {
operatorTokenSecretKey = coderv1alpha1.DefaultTokenSecretKey
}

token, err := r.readSecretValue(ctx, provisioner.Namespace, credentialsRef.Name, credentialsKey)
token, err := r.readSecretValue(ctx, provisioner.Namespace, operatorTokenSecretName, operatorTokenSecretKey)
if err != nil {
return "", fmt.Errorf("read bootstrap credentials secret %q/%q key %q: %w", provisioner.Namespace, credentialsRef.Name, credentialsKey, err)
return "", ctrl.Result{RequeueAfter: provisionerOperatorAccessRetryInterval},
"OperatorTokenSecretUnavailable",
fmt.Sprintf("Failed to read operator token Secret %q/%q key %q from referenced control plane status: %v", provisioner.Namespace, operatorTokenSecretName, operatorTokenSecretKey, err),
nil
}

return token, nil
return token, ctrl.Result{}, "OperatorAccessReady", "Operator-managed access token from referenced control plane is available", nil
}

func (r *CoderProvisionerReconciler) reconcileExternalProvisionerEntitlement(
Expand All @@ -761,7 +808,7 @@ func (r *CoderProvisionerReconciler) reconcileExternalProvisionerEntitlement(
return ctrl.Result{}, fmt.Errorf("assertion failed: coder control plane URL must not be empty")
}
if sessionToken == "" {
return ctrl.Result{}, fmt.Errorf("assertion failed: bootstrap session token must not be empty")
return ctrl.Result{}, fmt.Errorf("assertion failed: operator session token must not be empty")
}

if controlPlane.Status.EntitlementsLastChecked != nil {
Expand Down Expand Up @@ -806,7 +853,7 @@ func (r *CoderProvisionerReconciler) reconcileExternalProvisionerEntitlement(
message = "Coder deployment does not expose /api/v2/entitlements; cannot verify license."
case http.StatusUnauthorized, http.StatusForbidden:
reason = "Forbidden"
message = "Bootstrap token is not authorized to read entitlements; retrying."
message = "Operator access token is not authorized to read entitlements; retrying."
}
}

Expand Down
Loading
Loading