diff --git a/docs/how-to/deploy-controller.md b/docs/how-to/deploy-controller.md index 429580edf..1dd2ca449 100644 --- a/docs/how-to/deploy-controller.md +++ b/docs/how-to/deploy-controller.md @@ -1,8 +1,8 @@ # Deploy the controller -Run `coder-k8s` as an operator only (`--app=controller`). It reconciles `CoderControlPlane`, `CoderProvisioner`, and `CoderWorkspaceProxy`. +Run `coder-k8s` as an operator only (`--app=controller`). The operator reconciles `CoderControlPlane`, `CoderProvisioner`, and `CoderWorkspaceProxy` resources. -Commands run from a clone of this repository. +Run the commands from a clone of this repository. ## 1. Install CRDs and RBAC @@ -13,7 +13,7 @@ kubectl apply -f config/crd/bases/ -f config/rbac/ ## 2. Deploy in controller-only mode -`deploy/deployment.yaml` defaults to `--app=all`. Switch it to the controller: +The default of `deploy/deployment.yaml` is `--app=all`. Change it to the controller: ```bash kubectl apply -f deploy/deployment.yaml @@ -22,7 +22,7 @@ kubectl -n coder-system patch deployment/coder-k8s --type=json \ ``` !!! tip "Pin the image" - The manifest uses `ghcr.io/coder/coder-k8s:latest`. Edit the tag before applying to pin a version. + The manifest uses `ghcr.io/coder/coder-k8s:latest`. To pin a version, change the tag before you apply the manifest. ## 3. Verify @@ -39,9 +39,9 @@ kubectl apply -f config/samples/coder_v1alpha1_codercontrolplane.yaml kubectl get codercontrolplanes -A ``` -## Want everything instead? +## Run all components instead -Skip the `kubectl patch` step to keep `--app=all` (operator plus aggregated API server), and register the aggregated API: +To keep `--app=all` (the operator and the aggregated API server), do not do the `kubectl patch` step. Then register the aggregated API: ```bash kubectl apply -f deploy/apiserver-service.yaml -f deploy/apiserver-apiservice.yaml @@ -49,7 +49,10 @@ kubectl apply -f deploy/apiserver-service.yaml -f deploy/apiserver-apiservice.ya ## Connect an external PostgreSQL database -Coder needs a PostgreSQL connection URL. Store it in a Secret in the same namespace as the `CoderControlPlane`, then reference the Secret key in `spec.database.connectionSecretRef`: +Coder requires a PostgreSQL connection URL. + +1. Put the URL in a Secret in the same namespace as the `CoderControlPlane`. +2. Set `spec.database.connectionSecretRef` to the name and key of that Secret: ```yaml apiVersion: coder.com/v1alpha1 @@ -69,13 +72,17 @@ The value must be a `postgres://` or `postgresql://` URL. The controller then do 1. It sets `CODER_PG_CONNECTION_URL` in the Coder container with `valueFrom.secretKeyRef`. The Deployment holds a reference to the Secret, not a copy of the URL. 2. It reads the URL from the Secret to create the `coder-k8s-operator` user and API token (operator access bootstrap). -Both `name` and `key` are required. Do not also set `CODER_PG_CONNECTION_URL` in `spec.extraEnv`: the API server rejects that combination. The controller does not check `spec.envFrom` for this variable, so do not provide it there either. +Rules: + +- You must set both `name` and `key`. +- Do not also set `CODER_PG_CONNECTION_URL` in `spec.extraEnv`. The API server rejects that combination. +- Do not set this variable in `spec.envFrom`. The controller does not examine `spec.envFrom` for this variable. -Without `spec.database`, the controller keeps the earlier behavior: set `CODER_PG_CONNECTION_URL` in `spec.extraEnv`, as a literal value or with `valueFrom.secretKeyRef`. +If you do not set `spec.database`, the controller keeps the earlier behavior. You set `CODER_PG_CONNECTION_URL` in `spec.extraEnv`, as a literal value or with `valueFrom.secretKeyRef`. ### Check the `DatabaseSecretResolved` condition -While `spec.database` is set, the controller reports the `DatabaseSecretResolved` condition. It re-checks the condition when the Secret is created, updated, or deleted. When you remove `spec.database`, the controller removes the condition. +While `spec.database` has a value, the controller reports the `DatabaseSecretResolved` condition. The controller does a new check of the condition when the Secret is created, updated, or deleted. When you remove `spec.database`, the controller removes the condition. | Status | Reason | Meaning | |---|---|---| @@ -83,28 +90,34 @@ While `spec.database` is set, the controller reports the `DatabaseSecretResolved | `False` | `SecretNotFound` | The Secret does not exist in the namespace. | | `False` | `KeyNotFound` | The Secret exists but does not contain the key. | | `False` | `EmptyValue` | The value is empty or contains only whitespace. | -| `False` | `InvalidURL` | The value does not parse as a `postgres://` or `postgresql://` URL. The scheme must be exactly lowercase: for example, `Postgres://` is rejected. | -| `False` | `ConflictingConfiguration` | `spec.extraEnv` also sets `CODER_PG_CONNECTION_URL`. The controller leaves the Deployment unchanged. | +| `False` | `InvalidURL` | The value is not a valid `postgres://` or `postgresql://` URL. The scheme must be lowercase. For example, the controller rejects `Postgres://`. | +| `False` | `ConflictingConfiguration` | `spec.extraEnv` also sets `CODER_PG_CONNECTION_URL`. The controller does not change the Deployment. | -`Resolved` does not mean that the database is healthy or reachable. The controller does not connect to PostgreSQL to set this condition. Condition messages name the Secret and key, but never contain the URL or credentials. +`Resolved` does not mean that the database is healthy or that the network can reach it. The controller does not connect to PostgreSQL to set this condition. The condition messages contain the names of the Secret and the key, but never the URL or credentials. ```bash kubectl -n coder get codercontrolplane coder \ -o jsonpath='{range .status.conditions[?(@.type=="DatabaseSecretResolved")]}{.status} {.reason}: {.message}{"\n"}{end}' ``` -You can create the `CoderControlPlane` before its Secret. Until the Secret exists, the condition reason is `SecretNotFound`, the Coder pod cannot start, and the operator access bootstrap waits. After you create the Secret, the controller reconciles again and Kubernetes starts the pod. +You can create the `CoderControlPlane` before its Secret. Until the Secret exists: + +- The condition reason is `SecretNotFound`. +- The Coder pod cannot start. +- The operator access bootstrap waits. + +After you create the Secret, the controller reconciles again and Kubernetes starts the pod. ### Rotate database credentials -Kubernetes does not update environment variables in running pods when a Secret changes. After you change the connection URL in the Secret (for example, a new password): +When a Secret changes, Kubernetes does not update the environment variables in running pods. After you change the connection URL in the Secret (for example, to set a new password), do these steps: -1. Confirm that the condition reason is `Resolved`. -2. Restart the Coder Deployment so new pods read the new value: +1. Make sure that the condition reason is `Resolved`. +2. Restart the Coder Deployment, so that the new pods read the new value: ```bash kubectl -n coder rollout restart deployment/coder kubectl -n coder rollout status deployment/coder ``` -The controller does not restart pods automatically. The operator access bootstrap reads the Secret again on each reconcile, so it uses the new URL without a restart. +The controller does not restart pods automatically. The operator access bootstrap reads the Secret again on each reconcile. Thus it uses the new URL without a restart. diff --git a/docs/how-to/mcp-server.md b/docs/how-to/mcp-server.md index e9819e8ad..34a02095d 100644 --- a/docs/how-to/mcp-server.md +++ b/docs/how-to/mcp-server.md @@ -1,19 +1,19 @@ # Run the MCP server -The MCP server gives MCP clients (such as AI agents) tools to inspect and operate `coder-k8s` resources over HTTP. +The MCP (Model Context Protocol) server gives tools to MCP clients, for example AI agents. The clients use these tools over HTTP to inspect and operate `coder-k8s` resources. -It is off by default. `--app=all` does not include it. It runs only with `--app=mcp-http`, and only with a bearer token file. +The MCP server is off by default, and `--app=all` does not include it. It runs only with `--app=mcp-http`, and only with a bearer token file. -!!! danger "What the token grants" - The MCP server calls every tool with its own Kubernetes ServiceAccount's permissions (in the default install, the operator's). The token is a shared administrative credential for those tool powers. It does not identify callers and there is no per-caller RBAC: anyone who holds the token can use every tool. +!!! danger "What the token gives" + The MCP server does all tool calls with the permissions of its own Kubernetes ServiceAccount. In the default install, this is the ServiceAccount of the operator. The token is a shared administrative credential for all the tools. It does not identify callers, and there is no RBAC for each caller. Anyone who has the token can use all the tools. -The server listens on `127.0.0.1:8090` inside the Pod only. To reach it, a client needs the token **and** a way into the Pod's loopback interface, such as `kubectl port-forward` (which Kubernetes authorizes with `pods/portforward` on that Pod). Other containers in the same Pod can reach it too. Do not add a proxy that exposes it on the Pod network. +The server listens on `127.0.0.1:8090` in the Pod only. To connect to it, a client needs the token and a path to the loopback interface of the Pod. An example of such a path is `kubectl port-forward`, which Kubernetes authorizes with `pods/portforward` on that Pod. Other containers in the same Pod can also connect to it. Do not add a proxy that makes it available on the Pod network. ## 1. Create the token -The token file must hold one token of at least 32 bytes without whitespace (a trailing newline is ignored). +The token file must contain one token of 32 bytes or more, without whitespace. The server ignores a trailing newline. -The server reads the token file once, at startup. To rotate the token, update the Secret, then restart the pod (for example, `kubectl -n coder-system rollout restart deployment/coder-k8s`). Until the restart, the old token keeps working. +The server reads the token file one time, at startup. To rotate the token, update the Secret. Then restart the pod, for example with `kubectl -n coder-system rollout restart deployment/coder-k8s`. Until the restart, the old token continues to work. ```bash openssl rand -hex 32 > mcp-token @@ -23,7 +23,7 @@ kubectl -n coder-system create secret generic coder-k8s-mcp-token --from-file=to ## 2. Run the MCP server next to the operator -From a clone of this repository, deploy the operator, then append an `mcp` container that runs `--app=mcp-http`. The JSON patch keeps the operator as the first container, so other patches that address `containers/0` still target it: +Run these commands from a clone of this repository. Deploy the operator. Then add an `mcp` container that runs `--app=mcp-http`. The JSON patch adds the new container at the end, so the operator stays the first container. Thus other patches that use `containers/0` still change the operator container: ```bash kubectl apply -f config/rbac/ -f deploy/deployment.yaml @@ -43,7 +43,7 @@ kubectl -n coder-system patch deployment coder-k8s --type=json -p '[ Use the same image as the operator container. -The `mcp` container exits at startup if `--mcp-token-file` is missing, unreadable, empty, or too short. The operator container is not affected. +If `--mcp-token-file` is missing, cannot be read, is empty, is too short, or contains whitespace, the `mcp` container stops at startup. This does not affect the operator container. ## 3. Connect @@ -51,7 +51,7 @@ The `mcp` container exits at startup if `--mcp-token-file` is missing, unreadabl kubectl -n coder-system port-forward deploy/coder-k8s 8090:8090 ``` -Point your MCP client at `http://127.0.0.1:8090/mcp` and send `Authorization: Bearer ` on every request. Requests without the correct token get `401 Unauthorized`, including requests that carry an existing `Mcp-Session-Id`. +Set the URL of your MCP client to `http://127.0.0.1:8090/mcp`. Send `Authorization: Bearer ` with each request. A request without the correct token gets `401 Unauthorized`. This is also true for a request that has an existing `Mcp-Session-Id`. ## 4. Check health diff --git a/docs/tutorials/deploy-coderprovisioner.md b/docs/tutorials/deploy-coderprovisioner.md index f1143a625..9252eea14 100644 --- a/docs/tutorials/deploy-coderprovisioner.md +++ b/docs/tutorials/deploy-coderprovisioner.md @@ -1,14 +1,14 @@ # Deploy an External Provisioner -Add a `CoderProvisioner` (an external provisioner daemon) to an existing control plane. +In this tutorial, you add a `CoderProvisioner` to a control plane that already exists. A `CoderProvisioner` runs external provisioner daemons for Coder. -**Time:** 5 minutes. +Time: 5 minutes. ## Prerequisites -Finish [Deploy a Coder Control Plane](getting-started.md). You need `codercontrolplane-sample` in namespace `coder`, `Ready`, with operator access ready (the default). +Complete [Deploy a Coder Control Plane](getting-started.md) first. The control plane `codercontrolplane-sample` in the namespace `coder` must be `Ready`, and its operator access must be ready (the default). -Check: +Make sure that the phase is `Ready` and that `operatorAccessReady` is `true`: ```bash kubectl get codercontrolplane codercontrolplane-sample -n coder \ @@ -24,15 +24,15 @@ true ## 1. Check the license entitlement -External provisioners need the matching Coder license entitlement: +External provisioners require the applicable Coder license entitlement. Show its value: ```bash kubectl get codercontrolplane codercontrolplane-sample -n coder \ -o jsonpath='{.status.externalProvisionerDaemonsEntitlement}{"\n"}' ``` -- `entitled` or `grace_period`: continue. -- `not_entitled`: update the control plane's license first. +- If the value is `entitled` or `grace_period`, continue to step 2. +- If the value is `not_entitled`, update the license of the control plane first. ## 2. Deploy the provisioner @@ -47,9 +47,9 @@ kubectl get coderprovisioner coderprovisioner-sample -n coder \ -o jsonpath='{.status.phase}{"\n"}{range .status.conditions[*]}{.type}={.status} {.reason}{"\n"}{end}' ``` -Expected: phase `Ready` and `DeploymentReady=True`. +Expected result: the phase is `Ready`, and the conditions include `DeploymentReady=True`. -The operator creates these resources in `coder`: +The operator creates these resources in the `coder` namespace: | Kind | Name | | --- | --- | @@ -71,4 +71,4 @@ kubectl get secret coderprovisioner-sample-provisioner-key -n coder kubectl delete coderprovisioner coderprovisioner-sample -n coder ``` -To remove everything, follow [the control plane cleanup](getting-started.md#5-clean-up-optional). +To remove all the other resources, do the steps in [the control plane cleanup](getting-started.md#5-clean-up-optional). diff --git a/docs/tutorials/deploy-with-argocd.md b/docs/tutorials/deploy-with-argocd.md index 42d1d3832..215aab873 100644 --- a/docs/tutorials/deploy-with-argocd.md +++ b/docs/tutorials/deploy-with-argocd.md @@ -1,19 +1,19 @@ # Deploy with Argo CD -Deploy `coder-k8s` and a CloudNativePG-backed Coder instance from one Argo CD `ApplicationSet`. +In this tutorial, you use one Argo CD `ApplicationSet` to deploy `coder-k8s` and a Coder instance that uses a CloudNativePG database. -**Time:** 20–30 minutes. +Time: 20–30 minutes. -**You end up with:** +When you complete the tutorial, you have: -- The `coder-k8s` operator and aggregated API (`aggregation.coder.com/v1alpha1`). -- The CloudNativePG operator and a PostgreSQL cluster named `coder-db`. -- A `CoderControlPlane` named `coder`, served by `svc/coder`. +- The `coder-k8s` operator and the aggregated API (`aggregation.coder.com/v1alpha1`). +- The CloudNativePG operator and a PostgreSQL cluster with the name `coder-db`. +- A `CoderControlPlane` with the name `coder`, served by `svc/coder`. ## Prerequisites -- A Kubernetes cluster with `kubectl` v1.26+ access. -- Argo CD, including the ApplicationSet controller, running in namespace `argocd`. +- A Kubernetes cluster, and access to it with `kubectl` v1.26 or later. +- Argo CD, including the ApplicationSet controller, that runs in the namespace `argocd`. - `jq` and `curl`. - The `coder` CLI (only for the optional steps). @@ -23,7 +23,7 @@ Deploy `coder-k8s` and a CloudNativePG-backed Coder instance from one Argo CD `A kubectl -n argocd get deploy,pods ``` -`argocd-application-controller`, `argocd-applicationset-controller`, `argocd-repo-server`, and `argocd-server` should be running. +Make sure that `argocd-application-controller`, `argocd-applicationset-controller`, `argocd-repo-server`, and `argocd-server` are running. ## 2. Apply the ApplicationSet @@ -49,7 +49,7 @@ kubectl get apiservice v1alpha1.aggregation.coder.com \ -o jsonpath='{range .status.conditions[*]}{.type}={.status} reason={.reason} message={.message}{"\n"}{end}' ``` -The APIService should report `Available=True`. +Make sure that the APIService shows `Available=True`. ## 5. Check the database and Coder @@ -62,13 +62,14 @@ kubectl -n coder get codercontrolplane coder -o yaml ## 6. Create the first admin user -Port-forward Coder in a separate terminal and keep it running: +1. In a different terminal, start a port-forward to Coder. Keep it running: -```bash -kubectl -n coder port-forward svc/coder 3000:80 -``` + ```bash + kubectl -n coder port-forward svc/coder 3000:80 + ``` -Open `http://127.0.0.1:3000/setup`, create the admin user, and confirm the templates page loads. +2. Open `http://127.0.0.1:3000/setup` and create the admin user. +3. Make sure that the templates page opens. ## 7. Push a template (optional) @@ -89,7 +90,7 @@ kubectl get codertemplates.aggregation.coder.com -A kubectl get coderworkspaces.aggregation.coder.com -A ``` -`starter-scratch` appears as `.starter-scratch`. Inspect it: +In Kubernetes, the template `starter-scratch` has the name `.starter-scratch`. To see all its fields: ```bash TEMPLATE=$(kubectl get codertemplates.aggregation.coder.com -A \ @@ -101,10 +102,10 @@ kubectl -n coder get codertemplates.aggregation.coder.com "$TEMPLATE" -o yaml | Symptom | Fix | | --- | --- | -| Sync error `metadata.annotations: Too long` on CNPG CRDs | Make sure `ServerSideApply=true` is in `spec.template.spec.syncPolicy.syncOptions` before applying the `ApplicationSet`. | -| CloudNativePG pod crashes with `no matches for kind "Pooler"` | The CRD apply failed. Fix the sync options and re-sync. | -| APIService `v1alpha1.aggregation.coder.com` is not Available | Check the `coder-k8s` logs and that Service `coder-k8s-apiserver` exists in `coder-system`. | -| Coder Deployment does not roll out | Check that `coder-db` is Ready and Secret `coder-db-app` exists. | +| The sync fails with `metadata.annotations: Too long` on the CloudNativePG CRDs | Make sure that `ServerSideApply=true` is in `spec.template.spec.syncPolicy.syncOptions` before you apply the `ApplicationSet`. | +| The CloudNativePG pod crashes with `no matches for kind "Pooler"` | The CRDs were not applied. Correct the sync options and sync again. | +| The APIService `v1alpha1.aggregation.coder.com` is not Available | Examine the `coder-k8s` logs. Make sure that the Service `coder-k8s-apiserver` exists in `coder-system`. | +| The Coder Deployment does not roll out | Make sure that `coder-db` is Ready and that the Secret `coder-db-app` exists. | ## Clean up @@ -112,4 +113,4 @@ kubectl -n coder get codertemplates.aggregation.coder.com "$TEMPLATE" -o yaml kubectl -n argocd delete applicationset coder-k8s-stack ``` -The generated `Application` has the resources finalizer, so its managed resources are deleted too. +The generated `Application` has the resources finalizer. Thus Argo CD also deletes the resources that the `Application` manages. diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md index 4f9749d64..36494be72 100644 --- a/docs/tutorials/getting-started.md +++ b/docs/tutorials/getting-started.md @@ -1,23 +1,23 @@ # Deploy a Coder Control Plane -Install the `coder-k8s` operator, then create one Coder instance from a `CoderControlPlane` resource. +In this tutorial, you install the `coder-k8s` operator. Then you create one Coder instance from a `CoderControlPlane` resource. -**Time:** 10–15 minutes. +Time: 10–15 minutes. ## Prerequisites -- A Kubernetes cluster and `kubectl` pointed at it. -- Permission to create what `dist/install.yaml` contains: a Namespace, CustomResourceDefinitions, a ServiceAccount, a ClusterRole, a ClusterRoleBinding, and a Deployment. Step 1 also creates the `coder` namespace. +- A Kubernetes cluster, and `kubectl` configured to use it. +- Permission to create the objects in `dist/install.yaml`: a Namespace, CustomResourceDefinitions, a ServiceAccount, a ClusterRole, a ClusterRoleBinding, and a Deployment. Step 1 also creates the `coder` namespace. ## 1. Install the operator -Set the source once. For reproducible installs, use a release tag that contains `dist/install.yaml` instead of `main`. +Set the source one time. For an install that you can repeat with the same result, use a release tag that contains `dist/install.yaml`, not `main`. ```bash BASE="https://raw.githubusercontent.com/coder/coder-k8s/main" ``` -Apply the install bundle, then create the namespace for the control plane: +Apply the install bundle. Then create the namespace for the control plane: ```bash kubectl apply -f "$BASE/dist/install.yaml" @@ -26,7 +26,11 @@ kubectl rollout status deployment/coder-k8s -n coder-system kubectl create namespace coder ``` -`dist/install.yaml` installs the operator in controller mode: the `coder-system` namespace, the `coder.com` CRDs, RBAC, and the operator Deployment. It does not deploy Coder; step 2 does that. It also does not include the aggregated API server; to add it, see [Deploy the aggregated API server](../how-to/deploy-aggregated-apiserver.md). The bundle runs the `ghcr.io/coder/coder-k8s:latest` image, so pin the image too if you need a fixed operator version. +`dist/install.yaml` installs the operator in controller mode. It contains the `coder-system` namespace, the `coder.com` CRDs, the RBAC, and the operator Deployment. + +- The bundle does not deploy Coder. Step 2 does that. +- The bundle does not include the aggregated API server. To add it, see [Deploy the aggregated API server](../how-to/deploy-aggregated-apiserver.md). +- The bundle uses the `ghcr.io/coder/coder-k8s:latest` image. If you need a fixed operator version, pin the image too. ## 2. Create a control plane @@ -43,11 +47,11 @@ kubectl rollout status deployment/codercontrolplane-sample -n coder kubectl get deployment,service codercontrolplane-sample -n coder ``` -You should see: +Make sure that: - `status.phase` is `Ready`. -- `status.url` is set, for example `http://codercontrolplane-sample.coder.svc.cluster.local:80`. -- A Deployment and Service named `codercontrolplane-sample` exist in `coder`. +- `status.url` has a value, for example `http://codercontrolplane-sample.coder.svc.cluster.local:80`. +- A Deployment and a Service with the name `codercontrolplane-sample` exist in the `coder` namespace. ## 4. Open Coder (optional) @@ -55,11 +59,14 @@ You should see: kubectl port-forward svc/codercontrolplane-sample -n coder 3000:80 ``` -Then browse to `http://127.0.0.1:3000`. +Then open `http://127.0.0.1:3000` in a browser. ## 5. Clean up (optional) -Delete the control plane first, while the operator is still running, so it can clean up and remove its finalizer. Then remove the bundle. The block sets `BASE` again in case you are in a new shell: +Delete the control plane first, while the operator still runs. The operator then cleans up and removes its finalizer. After that, remove the bundle. The commands set `BASE` again, because you can be in a new shell. + +!!! warning + Delete all other `CoderControlPlane`, `CoderProvisioner`, and `CoderWorkspaceProxy` resources before you delete the bundle. When you delete the bundle, you also delete the `coder.com` CRDs, and Kubernetes then removes all remaining resources of these kinds in the cluster. When you delete the `coder` namespace, you also delete all other objects in it, for example Secrets and PersistentVolumeClaims. ```bash BASE="https://raw.githubusercontent.com/coder/coder-k8s/main" @@ -70,8 +77,6 @@ kubectl delete -f "$BASE/dist/install.yaml" --ignore-not-found kubectl delete namespace coder --ignore-not-found ``` -Deleting the bundle also deletes the `coder.com` CRDs, which removes every remaining `CoderControlPlane`, `CoderProvisioner`, and `CoderWorkspaceProxy` in the cluster; delete those first. Deleting the `coder` namespace removes everything else in it, such as Secrets and PersistentVolumeClaims. - ## Next steps - [Deploy an External Provisioner](deploy-coderprovisioner.md)