diff --git a/api/v1alpha1/coderprovisioner_types.go b/api/v1alpha1/coderprovisioner_types.go index f80593f4..6dfd13ba 100644 --- a/api/v1alpha1/coderprovisioner_types.go +++ b/api/v1alpha1/coderprovisioner_types.go @@ -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. @@ -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. @@ -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. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 51d201c3..8e91114a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -181,23 +181,6 @@ func (in *CoderProvisioner) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CoderProvisionerBootstrapSpec) DeepCopyInto(out *CoderProvisionerBootstrapSpec) { - *out = *in - out.CredentialsSecretRef = in.CredentialsSecretRef - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoderProvisionerBootstrapSpec. -func (in *CoderProvisionerBootstrapSpec) DeepCopy() *CoderProvisionerBootstrapSpec { - if in == nil { - return nil - } - out := new(CoderProvisionerBootstrapSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CoderProvisionerKeySpec) DeepCopyInto(out *CoderProvisionerKeySpec) { *out = *in @@ -251,7 +234,6 @@ func (in *CoderProvisionerList) DeepCopyObject() runtime.Object { func (in *CoderProvisionerSpec) DeepCopyInto(out *CoderProvisionerSpec) { *out = *in out.ControlPlaneRef = in.ControlPlaneRef - out.Bootstrap = in.Bootstrap out.Key = in.Key if in.Replicas != nil { in, out := &in.Replicas, &out.Replicas diff --git a/config/crd/bases/coder.com_coderprovisioners.yaml b/config/crd/bases/coder.com_coderprovisioners.yaml index f8f62166..e5e78be9 100644 --- a/config/crd/bases/coder.com_coderprovisioners.yaml +++ b/config/crd/bases/coder.com_coderprovisioners.yaml @@ -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: "" @@ -369,7 +350,6 @@ spec: format: int64 type: integer required: - - bootstrap - controlPlaneRef type: object status: diff --git a/config/samples/coder_v1alpha1_codercontrolplane.yaml b/config/samples/coder_v1alpha1_codercontrolplane.yaml index db70cd61..d3ff0e46 100644 --- a/config/samples/coder_v1alpha1_codercontrolplane.yaml +++ b/config/samples/coder_v1alpha1_codercontrolplane.yaml @@ -2,7 +2,7 @@ apiVersion: coder.com/v1alpha1 kind: CoderControlPlane metadata: name: codercontrolplane-sample - namespace: default + namespace: coder spec: image: "ghcr.io/coder/coder:latest" # Optional Enterprise license upload: diff --git a/config/samples/coder_v1alpha1_coderprovisioner.yaml b/config/samples/coder_v1alpha1_coderprovisioner.yaml index fddfbfc3..019d5a2c 100644 --- a/config/samples/coder_v1alpha1_coderprovisioner.yaml +++ b/config/samples/coder_v1alpha1_coderprovisioner.yaml @@ -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= -# -# 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 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") diff --git a/docs/reference/api/coderprovisioner.md b/docs/reference/api/coderprovisioner.md index dfd81167..438762b4 100644 --- a/docs/reference/api/coderprovisioner.md +++ b/docs/reference/api/coderprovisioner.md @@ -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. | @@ -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. diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md index 02e556bd..63a432dc 100644 --- a/docs/tutorials/getting-started.md +++ b/docs/tutorials/getting-started.md @@ -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): @@ -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: diff --git a/internal/controller/coderprovisioner_controller.go b/internal/controller/coderprovisioner_controller.go index 45f2062d..0a41dab9 100644 --- a/internal/controller/coderprovisioner_controller.go +++ b/internal/controller/coderprovisioner_controller.go @@ -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" @@ -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) @@ -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, @@ -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( @@ -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 { @@ -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." } } diff --git a/internal/controller/coderprovisioner_controller_test.go b/internal/controller/coderprovisioner_controller_test.go index e5f0f886..b14f563e 100644 --- a/internal/controller/coderprovisioner_controller_test.go +++ b/internal/controller/coderprovisioner_controller_test.go @@ -39,7 +39,8 @@ func createTestNamespace(ctx context.Context, t *testing.T, prefix string) strin return namespaceName } -// createTestControlPlane creates a test CoderControlPlane and optionally sets status.url. +// createTestControlPlane creates a test CoderControlPlane. When url is non-empty, +// it also provisions an operator token Secret and marks operator access ready. func createTestControlPlane(ctx context.Context, t *testing.T, namespace, name, url string) *coderv1alpha1.CoderControlPlane { t.Helper() @@ -51,7 +52,25 @@ func createTestControlPlane(ctx context.Context, t *testing.T, namespace, name, } require.NoError(t, k8sClient.Create(ctx, controlPlane)) if url != "" { + operatorSecretName := fmt.Sprintf("%s-operator-token", name) + operatorTokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: operatorSecretName, Namespace: namespace}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + coderv1alpha1.DefaultTokenSecretKey: []byte("operator-session-token"), + }, + } + require.NoError(t, k8sClient.Create(ctx, operatorTokenSecret)) + t.Cleanup(func() { + _ = k8sClient.Delete(context.Background(), operatorTokenSecret) + }) + controlPlane.Status.URL = url + controlPlane.Status.OperatorAccessReady = true + controlPlane.Status.OperatorTokenSecretRef = &coderv1alpha1.SecretKeySelector{ + Name: operatorSecretName, + Key: coderv1alpha1.DefaultTokenSecretKey, + } require.NoError(t, k8sClient.Status().Update(ctx, controlPlane)) } t.Cleanup(func() { @@ -61,28 +80,6 @@ func createTestControlPlane(ctx context.Context, t *testing.T, namespace, name, return controlPlane } -// createBootstrapSecret creates the bootstrap credentials secret used by provisioner reconciliation. -func createBootstrapSecret(ctx context.Context, t *testing.T, namespace, name, key, value string) *corev1.Secret { - t.Helper() - - if key == "" { - key = coderv1alpha1.DefaultTokenSecretKey - } - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - key: []byte(value), - }, - } - require.NoError(t, k8sClient.Create(ctx, secret)) - t.Cleanup(func() { - _ = k8sClient.Delete(context.Background(), secret) - }) - - return secret -} - func expectedProvisionerResourceName(name string) string { const prefix = "provisioner-" candidate := prefix + name @@ -183,7 +180,6 @@ func createTestProvisioner( namespace string, name string, controlPlaneName string, - bootstrapSecretName string, ) *coderv1alpha1.CoderProvisioner { t.Helper() @@ -191,12 +187,6 @@ func createTestProvisioner( ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlaneName}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{ - Name: bootstrapSecretName, - Key: coderv1alpha1.DefaultTokenSecretKey, - }, - }, }, } require.NoError(t, k8sClient.Create(ctx, provisioner)) @@ -217,11 +207,10 @@ func TestCoderProvisionerReconciler_EntitlementFastPathNotEntitled(t *testing.T) controlPlane.Status.EntitlementsLastChecked = &now controlPlane.Status.ExternalProvisionerDaemonsEntitlement = string(codersdk.EntitlementNotEntitled) require.NoError(t, k8sClient.Status().Update(ctx, controlPlane)) - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-ent-fast", coderv1alpha1.DefaultTokenSecretKey, "session-token") bootstrapClient := &fakeBootstrapClient{} reconciler := &controller.CoderProvisionerReconciler{Client: k8sClient, Scheme: scheme, BootstrapClient: bootstrapClient} - provisioner := createTestProvisioner(ctx, t, namespace, "provisioner-ent-fast", controlPlane.Name, bootstrapSecret.Name) + provisioner := createTestProvisioner(ctx, t, namespace, "provisioner-ent-fast", controlPlane.Name) namespacedName := types.NamespacedName{Name: provisioner.Name, Namespace: provisioner.Namespace} result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: namespacedName}) @@ -252,7 +241,6 @@ func TestCoderProvisionerReconciler_EntitlementFallbackNotEntitled(t *testing.T) ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-ent-fallback") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-ent-fallback", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-ent-fallback", coderv1alpha1.DefaultTokenSecretKey, "session-token") bootstrapClient := &fakeBootstrapClient{ entitlementsResponse: codersdk.Entitlements{ @@ -262,7 +250,7 @@ func TestCoderProvisionerReconciler_EntitlementFallbackNotEntitled(t *testing.T) }, } reconciler := &controller.CoderProvisionerReconciler{Client: k8sClient, Scheme: scheme, BootstrapClient: bootstrapClient} - provisioner := createTestProvisioner(ctx, t, namespace, "provisioner-ent-fallback", controlPlane.Name, bootstrapSecret.Name) + provisioner := createTestProvisioner(ctx, t, namespace, "provisioner-ent-fallback", controlPlane.Name) namespacedName := types.NamespacedName{Name: provisioner.Name, Namespace: provisioner.Namespace} result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: namespacedName}) @@ -292,7 +280,6 @@ func TestCoderProvisionerReconciler_EntitlementFastPathNotEntitledStaleRechecks( controlPlane.Status.EntitlementsLastChecked = &staleCheckedAt controlPlane.Status.ExternalProvisionerDaemonsEntitlement = string(codersdk.EntitlementNotEntitled) require.NoError(t, k8sClient.Status().Update(ctx, controlPlane)) - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-ent-stale", coderv1alpha1.DefaultTokenSecretKey, "session-token") bootstrapClient := &fakeBootstrapClient{ entitlementsResponse: codersdk.Entitlements{ @@ -308,7 +295,7 @@ func TestCoderProvisionerReconciler_EntitlementFastPathNotEntitledStaleRechecks( }}, } reconciler := &controller.CoderProvisionerReconciler{Client: k8sClient, Scheme: scheme, BootstrapClient: bootstrapClient} - provisioner := createTestProvisioner(ctx, t, namespace, "provisioner-ent-stale", controlPlane.Name, bootstrapSecret.Name) + provisioner := createTestProvisioner(ctx, t, namespace, "provisioner-ent-stale", controlPlane.Name) namespacedName := types.NamespacedName{Name: provisioner.Name, Namespace: provisioner.Namespace} result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: namespacedName}) @@ -338,7 +325,6 @@ func TestCoderProvisionerReconciler_EntitlementFastPathEntitledStaleRechecks(t * controlPlane.Status.EntitlementsLastChecked = &staleCheckedAt controlPlane.Status.ExternalProvisionerDaemonsEntitlement = string(codersdk.EntitlementEntitled) require.NoError(t, k8sClient.Status().Update(ctx, controlPlane)) - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-entitled-stale", coderv1alpha1.DefaultTokenSecretKey, "session-token") bootstrapClient := &fakeBootstrapClient{ entitlementsResponse: codersdk.Entitlements{ @@ -348,7 +334,7 @@ func TestCoderProvisionerReconciler_EntitlementFastPathEntitledStaleRechecks(t * }, } reconciler := &controller.CoderProvisionerReconciler{Client: k8sClient, Scheme: scheme, BootstrapClient: bootstrapClient} - provisioner := createTestProvisioner(ctx, t, namespace, "provisioner-entitled-stale", controlPlane.Name, bootstrapSecret.Name) + provisioner := createTestProvisioner(ctx, t, namespace, "provisioner-entitled-stale", controlPlane.Name) namespacedName := types.NamespacedName{Name: provisioner.Name, Namespace: provisioner.Namespace} result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: namespacedName}) @@ -374,13 +360,12 @@ func TestCoderProvisionerReconciler_EntitlementFallbackForbidden(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-ent-forbidden") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-ent-forbidden", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-ent-forbidden", coderv1alpha1.DefaultTokenSecretKey, "session-token") bootstrapClient := &fakeBootstrapClient{ entitlementsErr: codersdk.NewTestError(http.StatusForbidden, http.MethodGet, "/api/v2/entitlements"), } reconciler := &controller.CoderProvisionerReconciler{Client: k8sClient, Scheme: scheme, BootstrapClient: bootstrapClient} - provisioner := createTestProvisioner(ctx, t, namespace, "provisioner-ent-forbidden", controlPlane.Name, bootstrapSecret.Name) + provisioner := createTestProvisioner(ctx, t, namespace, "provisioner-ent-forbidden", controlPlane.Name) namespacedName := types.NamespacedName{Name: provisioner.Name, Namespace: provisioner.Namespace} result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: namespacedName}) @@ -406,7 +391,6 @@ func TestCoderProvisionerReconciler_BasicCreate(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-basic") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-basic", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") organizationID := uuid.New() provisionerKeyID := uuid.New() @@ -434,12 +418,6 @@ func TestCoderProvisionerReconciler_BasicCreate(t *testing.T) { Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, OrganizationName: "acme", - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{ - Name: bootstrapSecret.Name, - Key: coderv1alpha1.DefaultTokenSecretKey, - }, - }, Key: coderv1alpha1.CoderProvisionerKeySpec{ Name: "provisioner-key-name", SecretName: "provisioner-basic-key", @@ -468,6 +446,8 @@ func TestCoderProvisionerReconciler_BasicCreate(t *testing.T) { require.Contains(t, reconciledProvisioner.Finalizers, coderv1alpha1.ProvisionerKeyCleanupFinalizer) require.Equal(t, 1, bootstrapClient.provisionerKeyCalls) + require.Len(t, bootstrapClient.provisionerKeyRequests, 1) + require.Equal(t, "operator-session-token", bootstrapClient.provisionerKeyRequests[0].SessionToken) require.Equal(t, 0, bootstrapClient.deleteKeyCalls) keySecret := &corev1.Secret{} @@ -559,7 +539,6 @@ func TestCoderProvisionerReconciler_ExistingSecret(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-existing") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-existing", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisionerName := "provisioner-existing" secretName := fmt.Sprintf("%s-provisioner-key", provisionerName) @@ -579,10 +558,7 @@ func TestCoderProvisionerReconciler_ExistingSecret(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: provisionerName, Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, - Image: "provisioner-image:test", + Image: "provisioner-image:test", }, } require.NoError(t, k8sClient.Create(ctx, provisioner)) @@ -653,16 +629,12 @@ func TestCoderProvisionerReconciler_Deletion(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-delete") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-delete", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisioner := &coderv1alpha1.CoderProvisioner{ ObjectMeta: metav1.ObjectMeta{Name: "provisioner-delete", Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, - Image: "provisioner-image:test", + Image: "provisioner-image:test", Key: coderv1alpha1.CoderProvisionerKeySpec{ Name: "cleanup-key", }, @@ -713,16 +685,12 @@ func TestCoderProvisionerReconciler_DeletionControlPlaneGone(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-delete-cpgone") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-cpgone", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisioner := &coderv1alpha1.CoderProvisioner{ ObjectMeta: metav1.ObjectMeta{Name: "provisioner-cpgone", Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, - Image: "provisioner-image:test", + Image: "provisioner-image:test", }, } require.NoError(t, k8sClient.Create(ctx, provisioner)) @@ -747,9 +715,9 @@ func TestCoderProvisionerReconciler_DeletionControlPlaneGone(t *testing.T) { reconcileProvisioner(ctx, t, reconciler, namespacedName) - // DeleteProvisionerKey should still be called once using the persisted - // status.ControlPlaneURL even when the control plane object is already gone. - require.Equal(t, 1, bootstrapClient.deleteKeyCalls) + // DeleteProvisionerKey cannot run once operator token source is gone with the + // deleted control plane, but finalizer cleanup must still complete. + require.Equal(t, 0, bootstrapClient.deleteKeyCalls) // The finalizer should still be removed. require.Eventually(t, func() bool { @@ -834,15 +802,11 @@ func TestCoderProvisionerReconciler_ControlPlaneNotReady(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-cpnotready") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-notready", "") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisioner := &coderv1alpha1.CoderProvisioner{ ObjectMeta: metav1.ObjectMeta{Name: "provisioner-notready", Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, }, } require.NoError(t, k8sClient.Create(ctx, provisioner)) @@ -868,15 +832,11 @@ func TestCoderProvisionerReconciler_RotationOnSecretLoss(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-rotation") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-rotation", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisioner := &coderv1alpha1.CoderProvisioner{ ObjectMeta: metav1.ObjectMeta{Name: "provisioner-rotation", Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, Key: coderv1alpha1.CoderProvisionerKeySpec{ Name: "rotation-key", SecretName: "provisioner-rotation-key", @@ -919,17 +879,13 @@ func TestCoderProvisionerReconciler_TagsDrift(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-tags-drift") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-tags", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisioner := &coderv1alpha1.CoderProvisioner{ ObjectMeta: metav1.ObjectMeta{Name: "provisioner-tags", Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, - Key: coderv1alpha1.CoderProvisionerKeySpec{Name: "tags-drift-key"}, - Tags: map[string]string{"region": "us-east"}, + Key: coderv1alpha1.CoderProvisionerKeySpec{Name: "tags-drift-key"}, + Tags: map[string]string{"region": "us-east"}, }, } require.NoError(t, k8sClient.Create(ctx, provisioner)) @@ -982,15 +938,11 @@ func TestCoderProvisionerReconciler_KeyNameDrift(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-key-drift") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-key", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisioner := &coderv1alpha1.CoderProvisioner{ ObjectMeta: metav1.ObjectMeta{Name: "provisioner-key-drift", Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, Key: coderv1alpha1.CoderProvisionerKeySpec{ Name: "key-v1", SecretName: "provisioner-key-drift-secret", @@ -1045,15 +997,11 @@ func TestCoderProvisionerReconciler_ReadyPhaseAndConditions(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-ready") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-ready", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisioner := &coderv1alpha1.CoderProvisioner{ ObjectMeta: metav1.ObjectMeta{Name: "provisioner-ready", Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, }, } require.NoError(t, k8sClient.Create(ctx, provisioner)) @@ -1090,7 +1038,7 @@ func TestCoderProvisionerReconciler_ReadyPhaseAndConditions(t *testing.T) { require.Equal(t, "MinimumReplicasReady", deploymentReadyCondition.Reason) requireCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionControlPlaneReady, metav1.ConditionTrue) - requireCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionBootstrapSecretReady, metav1.ConditionTrue) + requireCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionOperatorAccessReady, metav1.ConditionTrue) requireCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionProvisionerKeyReady, metav1.ConditionTrue) requireCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionProvisionerKeySecretReady, metav1.ConditionTrue) } @@ -1102,15 +1050,11 @@ func TestCoderProvisionerReconciler_ConditionsOnFailure(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-cond-cp") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-cond-cp", "") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisioner := &coderv1alpha1.CoderProvisioner{ ObjectMeta: metav1.ObjectMeta{Name: "provisioner-cond-cp", Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, }, } require.NoError(t, k8sClient.Create(ctx, provisioner)) @@ -1134,18 +1078,18 @@ func TestCoderProvisionerReconciler_ConditionsOnFailure(t *testing.T) { require.Equal(t, "ControlPlaneUnavailable", controlPlaneCondition.Reason) }) - t.Run("bootstrap secret unavailable", func(t *testing.T) { + t.Run("operator access not ready", func(t *testing.T) { ctx := context.Background() - namespace := createTestNamespace(ctx, t, "coderprov-cond-bootstrap") - controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-cond-bootstrap", "https://coder.example.com") + namespace := createTestNamespace(ctx, t, "coderprov-cond-operator-not-ready") + controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-cond-operator-not-ready", "https://coder.example.com") + controlPlane.Status.OperatorAccessReady = false + controlPlane.Status.OperatorTokenSecretRef = nil + require.NoError(t, k8sClient.Status().Update(ctx, controlPlane)) provisioner := &coderv1alpha1.CoderProvisioner{ - ObjectMeta: metav1.ObjectMeta{Name: "provisioner-cond-bootstrap", Namespace: namespace}, + ObjectMeta: metav1.ObjectMeta{Name: "provisioner-cond-operator-not-ready", Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: "missing-bootstrap-secret", Key: coderv1alpha1.DefaultTokenSecretKey}, - }, }, } require.NoError(t, k8sClient.Create(ctx, provisioner)) @@ -1159,35 +1103,148 @@ func TestCoderProvisionerReconciler_ConditionsOnFailure(t *testing.T) { reconcileProvisioner(ctx, t, reconciler, request) result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: request}) - require.ErrorContains(t, err, "read bootstrap credentials secret") - require.Equal(t, ctrl.Result{}, result) + require.NoError(t, err) + require.Greater(t, result.RequeueAfter, time.Duration(0)) require.Equal(t, 0, bootstrapClient.provisionerKeyCalls) reconciled := &coderv1alpha1.CoderProvisioner{} require.NoError(t, k8sClient.Get(ctx, request, reconciled)) requireCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionControlPlaneReady, metav1.ConditionTrue) - requireCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionBootstrapSecretReady, metav1.ConditionFalse) - bootstrapCondition := findCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionBootstrapSecretReady) - require.Equal(t, "BootstrapSecretUnavailable", bootstrapCondition.Reason) + requireCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionOperatorAccessReady, metav1.ConditionFalse) + operatorAccessCondition := findCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionOperatorAccessReady) + require.Equal(t, "OperatorAccessNotReady", operatorAccessCondition.Reason) + require.Contains(t, operatorAccessCondition.Message, "status.operatorAccessReady=true") + }) + + t.Run("operator token secret ref missing", func(t *testing.T) { + ctx := context.Background() + namespace := createTestNamespace(ctx, t, "coderprov-cond-operator-token-missing") + controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-cond-operator-token-missing", "https://coder.example.com") + controlPlane.Status.OperatorAccessReady = true + controlPlane.Status.OperatorTokenSecretRef = nil + require.NoError(t, k8sClient.Status().Update(ctx, controlPlane)) + + provisioner := &coderv1alpha1.CoderProvisioner{ + ObjectMeta: metav1.ObjectMeta{Name: "provisioner-cond-operator-token-missing", Namespace: namespace}, + Spec: coderv1alpha1.CoderProvisionerSpec{ + ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, + }, + } + require.NoError(t, k8sClient.Create(ctx, provisioner)) + t.Cleanup(func() { + _ = k8sClient.Delete(context.Background(), provisioner) + }) + + bootstrapClient := &fakeBootstrapClient{} + reconciler := &controller.CoderProvisionerReconciler{Client: k8sClient, Scheme: scheme, BootstrapClient: bootstrapClient} + request := types.NamespacedName{Name: provisioner.Name, Namespace: provisioner.Namespace} + + reconcileProvisioner(ctx, t, reconciler, request) + result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: request}) + require.NoError(t, err) + require.Greater(t, result.RequeueAfter, time.Duration(0)) + require.Equal(t, 0, bootstrapClient.provisionerKeyCalls) + + reconciled := &coderv1alpha1.CoderProvisioner{} + require.NoError(t, k8sClient.Get(ctx, request, reconciled)) + requireCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionOperatorAccessReady, metav1.ConditionFalse) + operatorAccessCondition := findCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionOperatorAccessReady) + require.Equal(t, "OperatorTokenSecretRefMissing", operatorAccessCondition.Reason) + }) + + t.Run("operator token secret ref invalid", func(t *testing.T) { + ctx := context.Background() + namespace := createTestNamespace(ctx, t, "coderprov-cond-operator-token-invalid") + controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-cond-operator-token-invalid", "https://coder.example.com") + controlPlane.Status.OperatorAccessReady = true + controlPlane.Status.OperatorTokenSecretRef = &coderv1alpha1.SecretKeySelector{ + Name: "", + Key: coderv1alpha1.DefaultTokenSecretKey, + } + require.NoError(t, k8sClient.Status().Update(ctx, controlPlane)) + + provisioner := &coderv1alpha1.CoderProvisioner{ + ObjectMeta: metav1.ObjectMeta{Name: "provisioner-cond-operator-token-invalid", Namespace: namespace}, + Spec: coderv1alpha1.CoderProvisionerSpec{ + ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, + }, + } + require.NoError(t, k8sClient.Create(ctx, provisioner)) + t.Cleanup(func() { + _ = k8sClient.Delete(context.Background(), provisioner) + }) + + bootstrapClient := &fakeBootstrapClient{} + reconciler := &controller.CoderProvisionerReconciler{Client: k8sClient, Scheme: scheme, BootstrapClient: bootstrapClient} + request := types.NamespacedName{Name: provisioner.Name, Namespace: provisioner.Namespace} + + reconcileProvisioner(ctx, t, reconciler, request) + result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: request}) + require.NoError(t, err) + require.Greater(t, result.RequeueAfter, time.Duration(0)) + require.Equal(t, 0, bootstrapClient.provisionerKeyCalls) + + reconciled := &coderv1alpha1.CoderProvisioner{} + require.NoError(t, k8sClient.Get(ctx, request, reconciled)) + requireCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionOperatorAccessReady, metav1.ConditionFalse) + operatorAccessCondition := findCondition(t, reconciled.Status.Conditions, coderv1alpha1.CoderProvisionerConditionOperatorAccessReady) + require.Equal(t, "OperatorTokenSecretRefInvalid", operatorAccessCondition.Reason) }) } +func TestCoderProvisionerReconciler_OperatorTokenSecretRefEmptyKeyDefaults(t *testing.T) { + t.Parallel() + + ctx := context.Background() + namespace := createTestNamespace(ctx, t, "coderprov-operator-token-default-key") + controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-operator-token-default-key", "https://coder.example.com") + require.NotNil(t, controlPlane.Status.OperatorTokenSecretRef) + controlPlane.Status.OperatorTokenSecretRef = &coderv1alpha1.SecretKeySelector{ + Name: controlPlane.Status.OperatorTokenSecretRef.Name, + Key: "", + } + require.NoError(t, k8sClient.Status().Update(ctx, controlPlane)) + + provisioner := &coderv1alpha1.CoderProvisioner{ + ObjectMeta: metav1.ObjectMeta{Name: "provisioner-operator-token-default-key", Namespace: namespace}, + Spec: coderv1alpha1.CoderProvisionerSpec{ + ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, + }, + } + require.NoError(t, k8sClient.Create(ctx, provisioner)) + t.Cleanup(func() { + _ = k8sClient.Delete(context.Background(), provisioner) + }) + + bootstrapClient := &fakeBootstrapClient{ + provisionerKeyResponses: []coderbootstrap.EnsureProvisionerKeyResponse{{ + KeyName: provisioner.Name, + Key: "provisioner-key-material", + }}, + } + reconciler := &controller.CoderProvisionerReconciler{Client: k8sClient, Scheme: scheme, BootstrapClient: bootstrapClient} + request := types.NamespacedName{Name: provisioner.Name, Namespace: provisioner.Namespace} + + reconcileProvisioner(ctx, t, reconciler, request) + reconcileProvisioner(ctx, t, reconciler, request) + + require.Equal(t, 1, bootstrapClient.provisionerKeyCalls) + require.Len(t, bootstrapClient.provisionerKeyRequests, 1) + require.Equal(t, "operator-session-token", bootstrapClient.provisionerKeyRequests[0].SessionToken) +} + func TestCoderProvisionerReconciler_RateLimitBackoff(t *testing.T) { t.Parallel() ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-rate-limit") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-rate-limit", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisioner := &coderv1alpha1.CoderProvisioner{ ObjectMeta: metav1.ObjectMeta{Name: "provisioner-rate-limit", Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, - Key: coderv1alpha1.CoderProvisionerKeySpec{Name: "rate-limit-key"}, + Key: coderv1alpha1.CoderProvisionerKeySpec{Name: "rate-limit-key"}, }, } require.NoError(t, k8sClient.Create(ctx, provisioner)) @@ -1234,7 +1291,6 @@ func TestCoderProvisionerReconciler_LongNameTruncation(t *testing.T) { ctx := context.Background() namespace := createTestNamespace(ctx, t, "coderprov-longname") controlPlane := createTestControlPlane(ctx, t, namespace, "controlplane-longname", "https://coder.example.com") - bootstrapSecret := createBootstrapSecret(ctx, t, namespace, "bootstrap-creds", coderv1alpha1.DefaultTokenSecretKey, "session-token") provisionerName := strings.Repeat("a", 180) deploymentCandidateName := fmt.Sprintf("provisioner-%s", provisionerName) @@ -1250,9 +1306,6 @@ func TestCoderProvisionerReconciler_LongNameTruncation(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: provisionerName, Namespace: namespace}, Spec: coderv1alpha1.CoderProvisionerSpec{ ControlPlaneRef: corev1.LocalObjectReference{Name: controlPlane.Name}, - Bootstrap: coderv1alpha1.CoderProvisionerBootstrapSpec{ - CredentialsSecretRef: coderv1alpha1.SecretKeySelector{Name: bootstrapSecret.Name, Key: coderv1alpha1.DefaultTokenSecretKey}, - }, }, } require.NoError(t, k8sClient.Create(ctx, provisioner))