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
49 changes: 31 additions & 18 deletions docs/how-to/deploy-controller.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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

Expand All @@ -39,17 +39,20 @@ 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
```

## 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
Expand All @@ -69,42 +72,52 @@ 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 |
|---|---|---|
| `True` | `Resolved` | The Secret key contains a `postgres://` or `postgresql://` URL. |
| `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.
20 changes: 10 additions & 10 deletions docs/how-to/mcp-server.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -43,15 +43,15 @@ 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

```bash
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 <token>` 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 <token>` 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

Expand Down
20 changes: 10 additions & 10 deletions docs/tutorials/deploy-coderprovisioner.md
Original file line number Diff line number Diff line change
@@ -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 \
Expand All @@ -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

Expand All @@ -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 |
| --- | --- |
Expand All @@ -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).
43 changes: 22 additions & 21 deletions docs/tutorials/deploy-with-argocd.md
Original file line number Diff line number Diff line change
@@ -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).

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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)

Expand All @@ -89,7 +90,7 @@ kubectl get codertemplates.aggregation.coder.com -A
kubectl get coderworkspaces.aggregation.coder.com -A
```

`starter-scratch` appears as `<organization>.starter-scratch`. Inspect it:
In Kubernetes, the template `starter-scratch` has the name `<organization>.starter-scratch`. To see all its fields:

```bash
TEMPLATE=$(kubectl get codertemplates.aggregation.coder.com -A \
Expand All @@ -101,15 +102,15 @@ 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

```bash
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.
Loading
Loading