diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 00000000..6bf7f04f --- /dev/null +++ b/.cspell.json @@ -0,0 +1,37 @@ +{ + "version": "0.2", + "language": "en", + "words": [ + "Diátaxis", + "GOFLAGS", + "apiregistration", + "apiserverapp", + "apiserver", + "apiservice", + "clusterrole", + "clusterrolebinding", + "coder-k8s", + "codercontrolplane", + "codercontrolplanes", + "codertemplate", + "codertemplates", + "coderworkspace", + "coderworkspaces", + "controllerapp", + "devshell", + "gofumpt", + "javascripts", + "kubeconfig", + "kubebuilder", + "metav", + "mkdocs", + "pymdownx", + "superfences" + ], + "ignorePaths": [ + ".git/**", + "node_modules/**", + "site/**", + "vendor/**" + ] +} diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 00000000..d6ed18c5 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,137 @@ +name: Docs + +on: + pull_request: + paths: + - docs/** + - mkdocs.yml + - .cspell.json + - .markdownlint-cli2.yaml + - .github/workflows/docs.yaml + push: + branches: [main] + paths: + - docs/** + - mkdocs.yml + - .cspell.json + - .markdownlint-cli2.yaml + - .github/workflows/docs.yaml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: github-pages + cancel-in-progress: false + +jobs: + docs-quality: + runs-on: depot-ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + + - name: Install docs lint tools + run: npm install --global cspell@8.19.4 markdownlint-cli2@0.18.1 + + - name: Lint Markdown + run: markdownlint-cli2 "docs/**/*.md" + + - name: Spell-check docs + run: cspell --no-progress --config .cspell.json "docs/**/*.md" "mkdocs.yml" + + - name: Check links (including external) + uses: lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2 # v2.6.1 + with: + fail: true + args: >- + --verbose + --no-progress + --accept 200,429 + --max-retries 2 + --retry-wait-time 2 + --exclude '^https://github.com/coder/coder-k8s$' + docs/*.md docs/*/*.md docs/*/*/*.md + mkdocs.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build: + if: github.event_name == 'pull_request' + needs: docs-quality + runs-on: depot-ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + cache: pip + cache-dependency-path: docs/requirements.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r docs/requirements.txt + + - name: Build docs (strict) + run: mkdocs build --strict + + deploy: + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + needs: docs-quality + runs-on: depot-ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - name: Set up Pages + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + cache: pip + cache-dependency-path: docs/requirements.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r docs/requirements.txt + + - name: Build docs (strict) + run: mkdocs build --strict + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + with: + path: site + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/.gitignore b/.gitignore index 99110e54..08ca617b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ # Temporary external clones/forks /tmpfork/ + +# MkDocs +/site/ +/.cache/ diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 00000000..183f636b --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,3 @@ +config: + default: true + MD013: false diff --git a/AGENTS.md b/AGENTS.md index d1b956a3..31f3226f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,6 +66,8 @@ Run from repository root. - **Vendor consistency:** `make verify-vendor` - **Manifest generation:** `make manifests` (or `bash ./hack/update-manifests.sh`) - **Code generation:** `make codegen` (or `bash ./hack/update-codegen.sh`) +- **Docs (serve):** `make docs-serve` +- **Docs (strict build):** `make docs-check` - **Clean:** `go clean -cache -testcache && rm -f ./coder-k8s && rm -rf ./dist` - **Shell scripts:** `find . -type f -name '*.sh' -not -path './vendor/*'` @@ -89,6 +91,9 @@ Run from repository root. - **Do** keep controller, aggregated API server, and storage changes paired with focused tests (`main_test.go`, `internal/controller/*_test.go`, and package tests under `internal/app/`/`internal/aggregated/`). **Don’t** add behavior without coverage for critical assumptions. +- **Do** update the docs in `docs/` when you change user-facing behavior (APIs, flags, manifests, deployment). + **Don’t** let docs drift from the implementation. + ## Anti-patterns - Unpinned GitHub Action versions in workflow files (CI uses SHA-pinned actions). @@ -111,6 +116,8 @@ Run from repository root. 4. Run `make lint` (or explain why it was skipped). 5. If API types changed, run `make codegen` and `make manifests`, then include generated updates. 6. If `.github/workflows/*` changed, run `go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.10`. +7. If your change affects user-facing behavior (APIs, flags, manifests, deployment), update the documentation in `docs/` and run `make docs-check`. + ### Commit messages - Match repository history style: short imperative summary, optionally prefixed by type (e.g., `chore: ...`). diff --git a/Makefile b/Makefile index 11d57e33..0c6047d5 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ MODULE_FILES := go.mod $(wildcard go.sum) ENVTEST_K8S_VERSION ?= 1.35.x ENVTEST_ASSETS_DIR := $(shell pwd)/bin/envtest -.PHONY: vendor test test-integration setup-envtest build lint vuln verify-vendor codegen manifests +.PHONY: vendor test test-integration setup-envtest build lint vuln verify-vendor codegen manifests docs-serve docs-build docs-check $(VENDOR_STAMP): $(MODULE_FILES) go mod tidy @@ -47,3 +47,16 @@ manifests: $(VENDOR_STAMP) codegen: $(VENDOR_STAMP) bash ./hack/update-codegen.sh + + +docs-serve: + @command -v mkdocs >/dev/null || (echo "mkdocs not found; use nix develop" && exit 1) + mkdocs serve + +docs-build: + @command -v mkdocs >/dev/null || (echo "mkdocs not found; use nix develop" && exit 1) + mkdocs build + +docs-check: + @command -v mkdocs >/dev/null || (echo "mkdocs not found; use nix develop" && exit 1) + mkdocs build --strict diff --git a/README.md b/README.md index d748cc75..55ed6595 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,10 @@ ## Project description -`coder-k8s` is a Go-based Kubernetes operator for managing `CoderControlPlane` resources (`coder.com/v1alpha1`). It is built with `sigs.k8s.io/controller-runtime`. +`coder-k8s` is a Go-based Kubernetes control-plane project with two app modes: + +- A `controller-runtime` operator for managing `CoderControlPlane` resources (`coder.com/v1alpha1`). +- An aggregated API server for `CoderWorkspace` and `CoderTemplate` resources (`aggregation.coder.com/v1alpha1`). ## Prerequisites @@ -20,7 +23,7 @@ make manifests kubectl apply -f config/crd/bases/ # Run the controller locally (uses your kubeconfig context) -GOFLAGS=-mod=vendor go run . +GOFLAGS=-mod=vendor go run . --app=controller # In another terminal: apply the sample CR kubectl apply -f config/samples/coder_v1alpha1_codercontrolplane.yaml @@ -40,6 +43,8 @@ kubectl get codercontrolplanes -A | `make verify-vendor` | Verify vendor consistency | | `make lint` | Run linter (requires `golangci-lint`) | | `make vuln` | Run vulnerability check (requires `govulncheck`) | +| `make docs-serve` | Serve the documentation site locally (requires `mkdocs`) | +| `make docs-check` | Build docs in strict mode (CI-equivalent) | ## Testing strategy diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md new file mode 100644 index 00000000..a8f4cf00 --- /dev/null +++ b/docs/explanation/architecture.md @@ -0,0 +1,38 @@ +# Architecture + +`coder-k8s` builds a single binary (`coder-k8s`) that can run in one of two modes: + +- `--app=controller` +- `--app=aggregated-apiserver` + +The dispatch logic lives in `app_dispatch.go`. The `--app` flag is required, and the code intentionally fails fast with an `assertion failed:` error when it is missing or invalid. + +## Controller mode + +In controller mode, the binary runs a `controller-runtime` manager and registers the `CoderControlPlane` API types: + +- API group: `coder.com/v1alpha1` +- Kind: `CoderControlPlane` + +Key code paths: + +- `internal/app/controllerapp/` — scheme construction and manager startup +- `internal/controller/` — reconciliation logic (`CoderControlPlaneReconciler`) + +## Aggregated API server mode + +In aggregated API server mode, the binary starts an aggregated API server that installs storage for: + +- API group: `aggregation.coder.com/v1alpha1` +- Resources: `coderworkspaces`, `codertemplates` + +Key code paths: + +- `internal/app/apiserverapp/` — API server bootstrap and API group installation +- `internal/aggregated/storage/` — storage implementations (currently hardcoded in-memory objects) + +## Manifests and generated assets + +- `config/` — generated CRDs and RBAC (via `make manifests`) +- `deploy/` — example deployment manifests for controller and aggregated API server +- `vendor/` — vendored dependencies (required by the repo workflow) diff --git a/docs/how-to/deploy-aggregated-apiserver.md b/docs/how-to/deploy-aggregated-apiserver.md new file mode 100644 index 00000000..6c28d82f --- /dev/null +++ b/docs/how-to/deploy-aggregated-apiserver.md @@ -0,0 +1,61 @@ +# Deploy the aggregated API server (in-cluster) + +This guide shows how to deploy the `coder-k8s` **aggregated API server** and register it with the Kubernetes API aggregation layer. + +The aggregated API server serves: + +- API group: `aggregation.coder.com` +- Version: `v1alpha1` +- Resources: `coderworkspaces`, `codertemplates` + +## 1. Create the namespace + +```bash +kubectl create namespace coder-system +``` + +## 2. Apply RBAC + +The RBAC manifest includes service accounts for both the controller and the aggregated API server. + +```bash +kubectl apply -f deploy/rbac.yaml +``` + +## 3. Deploy the service and deployment + +```bash +kubectl apply -f deploy/apiserver-service.yaml +kubectl apply -f deploy/apiserver-deployment.yaml +``` + +## 4. Register the APIService + +```bash +kubectl apply -f deploy/apiserver-apiservice.yaml +``` + +## 5. Verify + +Wait for the deployment: + +```bash +kubectl rollout status deployment/coder-k8s-apiserver -n coder-system +``` + +Check the APIService: + +```bash +kubectl get apiservice v1alpha1.aggregation.coder.com +``` + +List resources served by the aggregated API server: + +```bash +kubectl get coderworkspaces.aggregation.coder.com -A +kubectl get codertemplates.aggregation.coder.com -A +``` + +## TLS note + +`deploy/apiserver-apiservice.yaml` currently sets `insecureSkipTLSVerify: true`, which is convenient for development but not appropriate for production. diff --git a/docs/how-to/deploy-controller.md b/docs/how-to/deploy-controller.md new file mode 100644 index 00000000..bf2ba44d --- /dev/null +++ b/docs/how-to/deploy-controller.md @@ -0,0 +1,42 @@ +# Deploy the controller (in-cluster) + +This guide shows how to deploy the `coder-k8s` **controller** to a Kubernetes cluster using the manifests in `deploy/`. + +## 1. Create the namespace + +The deployment manifests expect a `coder-system` namespace: + +```bash +kubectl create namespace coder-system +``` + +## 2. Install the CRDs + +Install the `CoderControlPlane` CRD: + +```bash +kubectl apply -f config/crd/bases/ +``` + +## 3. Apply RBAC + +```bash +kubectl apply -f deploy/rbac.yaml +``` + +## 4. Deploy the controller + +```bash +kubectl apply -f deploy/controller-deployment.yaml +``` + +## 5. Verify + +```bash +kubectl rollout status deployment/coder-k8s-controller -n coder-system +kubectl get pods -n coder-system +``` + +## Customizing the image + +By default, `deploy/controller-deployment.yaml` uses `ghcr.io/coder/coder-k8s:latest`. For a different image tag, edit the deployment manifest before applying it. diff --git a/docs/how-to/troubleshooting.md b/docs/how-to/troubleshooting.md new file mode 100644 index 00000000..3bdd7e67 --- /dev/null +++ b/docs/how-to/troubleshooting.md @@ -0,0 +1,61 @@ +# Troubleshooting + +## The binary exits immediately with "--app flag is required" + +`coder-k8s` requires an explicit application mode. + +For the controller: + +```bash +GOFLAGS=-mod=vendor go run . --app=controller +``` + +For the aggregated API server: + +```bash +GOFLAGS=-mod=vendor go run . --app=aggregated-apiserver +``` + +## "no matches for kind" when applying a `CoderControlPlane` + +This usually means the CRD isn't installed in the cluster. + +```bash +make manifests +kubectl apply -f config/crd/bases/ +``` + +## The controller is running, but reconciliation doesn't happen + +- Check controller logs: + + ```bash + kubectl logs -n coder-system deploy/coder-k8s-controller + ``` + +- Confirm RBAC is applied: + + ```bash + kubectl get clusterrole coder-k8s-controller + kubectl get clusterrolebinding coder-k8s-controller + ``` + +## Aggregated APIService shows `False` / `Unavailable` + +- Ensure the deployment and service exist: + + ```bash + kubectl get deploy,svc -n coder-system | grep coder-k8s-apiserver + ``` + +- Inspect APIService status: + + ```bash + kubectl describe apiservice v1alpha1.aggregation.coder.com + ``` + +- Check the aggregated API server logs: + + ```bash + kubectl logs -n coder-system deploy/coder-k8s-apiserver + ``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..8bb72940 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,16 @@ +# coder-k8s documentation + +`coder-k8s` is a Go-based Kubernetes control-plane project with two app modes: + +- **Controller**: a `controller-runtime` operator for `CoderControlPlane` (`coder.com/v1alpha1`). +- **Aggregated API server**: an aggregated API server for `CoderWorkspace` and `CoderTemplate` + (`aggregation.coder.com/v1alpha1`). + +This site follows the **Diátaxis** documentation framework: + +- **Tutorials**: learning-oriented, step-by-step guides. +- **How-to guides**: task-focused recipes. +- **Reference**: factual documentation (APIs, flags, manifests). +- **Explanation**: concepts and architecture. + +If you're new to the project, start with **Tutorials → Getting started**. diff --git a/docs/javascripts/mermaid.js b/docs/javascripts/mermaid.js new file mode 100644 index 00000000..c0545c0a --- /dev/null +++ b/docs/javascripts/mermaid.js @@ -0,0 +1,11 @@ +// Initialize Mermaid diagrams. +// +// Keep this tiny: we don't enable MkDocs Material instant navigation by default, +// so DOMContentLoaded is sufficient for now. +window.addEventListener("DOMContentLoaded", () => { + if (typeof mermaid === "undefined") { + return + } + + mermaid.initialize({ startOnLoad: true }) +}) diff --git a/docs/reference/api/codercontrolplane.md b/docs/reference/api/codercontrolplane.md new file mode 100644 index 00000000..d2e746a2 --- /dev/null +++ b/docs/reference/api/codercontrolplane.md @@ -0,0 +1,25 @@ +# `CoderControlPlane` + +## API identity + +- Group/version: `coder.com/v1alpha1` +- Kind: `CoderControlPlane` +- Resource: `codercontrolplanes` +- Scope: namespaced + +## Spec + +| Field | Type | Description | +| --- | --- | --- | +| `spec.image` | `string` | Placeholder container image for the control plane deployment. | + +## Status + +| Field | Type | Description | +| --- | --- | --- | +| `status.phase` | `string` | Placeholder status field for future reconciliation stages. | + +## Source + +- Go type: `api/v1alpha1/codercontrolplane_types.go` +- Generated CRD: `config/crd/bases/coder.com_codercontrolplanes.yaml` diff --git a/docs/reference/api/codertemplate.md b/docs/reference/api/codertemplate.md new file mode 100644 index 00000000..cbb68fa0 --- /dev/null +++ b/docs/reference/api/codertemplate.md @@ -0,0 +1,26 @@ +# `CoderTemplate` + +## API identity + +- Group/version: `aggregation.coder.com/v1alpha1` +- Kind: `CoderTemplate` +- Resource: `codertemplates` +- Scope: namespaced + +## Spec + +| Field | Type | Description | +| --- | --- | --- | +| `spec.running` | `bool` | Indicates whether the template should be marked as running. | + +## Status + +| Field | Type | Description | +| --- | --- | --- | +| `status.autoShutdown` | `metav1.Time` | Next planned shutdown time for workspaces created by this template. | + +## Source + +- Go type: `api/aggregation/v1alpha1/types.go` +- Storage implementation: `internal/aggregated/storage/template.go` +- APIService registration manifest: `deploy/apiserver-apiservice.yaml` diff --git a/docs/reference/api/coderworkspace.md b/docs/reference/api/coderworkspace.md new file mode 100644 index 00000000..1f41aa5e --- /dev/null +++ b/docs/reference/api/coderworkspace.md @@ -0,0 +1,26 @@ +# `CoderWorkspace` + +## API identity + +- Group/version: `aggregation.coder.com/v1alpha1` +- Kind: `CoderWorkspace` +- Resource: `coderworkspaces` +- Scope: namespaced + +## Spec + +| Field | Type | Description | +| --- | --- | --- | +| `spec.running` | `bool` | Indicates whether the workspace should be running. | + +## Status + +| Field | Type | Description | +| --- | --- | --- | +| `status.autoShutdown` | `metav1.Time` | Next planned shutdown time for the workspace. | + +## Source + +- Go type: `api/aggregation/v1alpha1/types.go` +- Storage implementation: `internal/aggregated/storage/workspace.go` +- APIService registration manifest: `deploy/apiserver-apiservice.yaml` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..61e03c76 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +mkdocs-material +pymdown-extensions diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md new file mode 100644 index 00000000..1190d389 --- /dev/null +++ b/docs/tutorials/getting-started.md @@ -0,0 +1,56 @@ +# Getting started (local development) + +This tutorial walks through running the `coder-k8s` **controller** locally against a Kubernetes cluster. + +## Prerequisites + +- Go 1.25+ (`go.mod` currently declares Go 1.25.7) +- A Kubernetes cluster (OrbStack is recommended for local development; any cluster works) +- `kubectl` configured to point at your cluster context + +If you use the Nix devshell, run: + +```bash +nix develop +``` + +## 1. Generate and install CRDs + +Generate the CRD and RBAC manifests: + +```bash +make manifests +``` + +Install the CRDs into your cluster: + +```bash +kubectl apply -f config/crd/bases/ +``` + +## 2. Run the controller locally + +Run the controller in **controller** mode (uses your kubeconfig context): + +```bash +GOFLAGS=-mod=vendor go run . --app=controller +``` + +## 3. Create a sample `CoderControlPlane` + +In another terminal: + +```bash +kubectl apply -f config/samples/coder_v1alpha1_codercontrolplane.yaml +``` + +## 4. Verify + +```bash +kubectl get codercontrolplanes -A +``` + +## Next steps + +- Learn how to deploy the controller in-cluster: **How-to guides → Deploy controller**. +- Learn how the project is structured: **Explanation → Architecture**. diff --git a/flake.nix b/flake.nix index d9367c6e..253a11a4 100644 --- a/flake.nix +++ b/flake.nix @@ -16,6 +16,11 @@ devShells = forAllSystems (system: let pkgs = import nixpkgs { inherit system; }; + docsPython = pkgs.python3.withPackages (ps: [ + ps.mkdocs + ps."mkdocs-material" + ps."pymdown-extensions" + ]); in { default = pkgs.mkShell { packages = with pkgs; [ @@ -27,6 +32,8 @@ zizmor golangci-lint govulncheck + + docsPython ]; }; } diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..e7043e2a --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,48 @@ +site_name: coder-k8s +repo_url: https://github.com/coder/coder-k8s +edit_uri: edit/main/docs/ + +theme: + name: material + features: + - navigation.tabs + - navigation.sections + - content.action.edit + - search.suggest + - search.highlight + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + +plugins: + - search + +extra_javascript: + - https://unpkg.com/mermaid@11/dist/mermaid.min.js + - javascripts/mermaid.js + +nav: + - Home: index.md + - Tutorials: + - Getting started: tutorials/getting-started.md + - How-to guides: + - Deploy controller: how-to/deploy-controller.md + - Deploy aggregated API server: how-to/deploy-aggregated-apiserver.md + - Troubleshooting: how-to/troubleshooting.md + - Reference: + - API: + - CoderControlPlane: reference/api/codercontrolplane.md + - CoderWorkspace: reference/api/coderworkspace.md + - CoderTemplate: reference/api/codertemplate.md + - Explanation: + - Architecture: explanation/architecture.md