Skip to content

Commit 57aee35

Browse files
authored
🤖 ci: replace e2e smoke test with CNPG + Templates merge-queue E2E (#76)
## Summary Replace the existing lightweight Kind smoke test (`e2e-kind`) with a full end-to-end test that provisions CloudNativePG, a real `CoderControlPlane`, the aggregated API server, and validates `CoderTemplate` creation via `kubectl`. ## Background The previous `e2e-kind` job only verified that a `CoderControlPlane` CR could be created (no real Postgres, no Coder deployment, no aggregated API). This left significant integration gaps uncovered by CI. This PR wires up the full stack in the merge queue (`merge_group` event) while keeping the job as a fast no-op on PRs so the required check stays green without wasting resources. ## Implementation ### `config/e2e/deployment.yaml` - Changed `--app=controller` → `--app=all` to run both controller and aggregated API server in the same pod. - Added HTTPS port 6443 (used by the `APIService` to reach the aggregated API server). ### `config/e2e/codertemplate.yaml` (new) - Minimal `CoderTemplate` manifest for the E2E test to apply via `kubectl`. - Uses the `default.<name>` naming convention with a trivial Terraform `null_resource`. ### `.github/workflows/ci.yaml` (`e2e-kind` job) - Job name updated to `E2E (Kind + CNPG + Templates)` (id `e2e-kind` unchanged for branch protection). - PR runs: instant skip with `::notice::` message. - Merge-queue runs: full E2E flow: 1. Kind cluster + build + load image 2. Apply CRDs, RBAC, APIService, and deployment 3. Wait for operator + APIService availability 4. Install CloudNativePG 1.25.0, provision PostgreSQL 5. Apply CloudNativePG example manifests (CoderControlPlane + Postgres) 6. Wait for Coder readiness + operator access bootstrap 7. Apply `CoderTemplate` and verify it exists via `kubectl` - Failure diagnostics step dumps pods, logs, and resource state on failure. ## Validation - YAML validity: all 3 modified/new files parse cleanly - `actionlint`: no errors in CI workflow - Spot-checked: job id, PR skip gate, merge_group gates, failure diagnostics, publish-main dependency all correct ## Risks - **CloudNativePG download**: fetches CNPG manifest from GitHub raw at a pinned URL; could fail if GitHub is down or the URL changes. Mitigation: pinned to `release-1.25/releases/cnpg-1.25.0.yaml`. - **Timeout tuning**: merge-queue E2E has generous timeouts (10min for Postgres/Coder). May need adjustment based on runner performance. --- <details> <summary>📋 Implementation Plan</summary> # Plan: Merge-queue E2E (Kind + CloudNativePG + CoderTemplates) ## Context / Why We want an end-to-end test that can run in GitHub’s merge queue (`merge_group`) to ensure: 1. `coder-k8s` (controller) can reconcile a **real** `CoderControlPlane` backed by PostgreSQL. 2. The in-cluster **aggregated API server** is available. 3. We can use **`kubectl`** to create and retrieve **`CoderTemplate`** resources (served by the aggregated API server and backed by the Coder API). This replaces the current Kind “smoke” test that only verifies a `CoderControlPlane` CR can be created. ## Evidence (repo facts) - Existing CI + current Kind smoke job: `.github/workflows/ci.yaml` (job `e2e-kind`). - Current in-cluster e2e Deployment runs controller-only: `config/e2e/deployment.yaml`. - Aggregated API server can be enabled in Kind using `APIService.insecureSkipTLSVerify: true`: - `deploy/apiserver-service.yaml` - `deploy/apiserver-apiservice.yaml` - CloudNativePG example manifests already exist and are suitable to reuse: - `examples/cloudnativepg/00-namespace.yaml` - `examples/cloudnativepg/cnpg-cluster.yaml` - `examples/cloudnativepg/codercontrolplane.yaml` - Aggregated API server selects a `CoderControlPlane` in the request namespace and reads operator URL/token from status+Secret (Explore report: “CoderTemplate Aggregated API Usage Guide”). - `CoderTemplate` create path can upload `spec.files` as a zip, create a template version, then create the template (no synchronous wait in codersdk): `internal/aggregated/storage/template.go`. ## Implementation details ### 1) Enable aggregated API server in the e2e Deployment **File:** `config/e2e/deployment.yaml` Change e2e deployment from controller-only to `--app=all` and expose the HTTPS port used by the APIService. Proposed diff shape: ```yaml containers: - name: manager image: ghcr.io/coder/coder-k8s:e2e args: ["--app=all"] # was --app=controller ports: - containerPort: 8081 name: health - containerPort: 6443 name: https ``` Rationale: the `deploy/apiserver-service.yaml` targets port **6443** on pods labeled `app: coder-k8s`. --- ### 2) Add an E2E `CoderTemplate` manifest **New file:** `config/e2e/codertemplate.yaml` Create a minimal template in the same namespace as the E2E `CoderControlPlane` (the existing CNPG example uses `namespace: coder`). ```yaml apiVersion: aggregation.coder.com/v1alpha1 kind: CoderTemplate metadata: name: default.e2e-template namespace: coder spec: organization: default displayName: "E2E Template" description: "Created by coder-k8s merge-queue e2e" files: main.tf: | terraform { required_version = ">= 1.0" } resource "null_resource" "example" {} ``` Notes: - `metadata.name` **must** be `<org>.<template-name>` and `spec.organization` must match the `<org>` prefix (enforced server-side). - Keeping the template minimal avoids turning this into a workspace/provisioner E2E. --- ### 3) Replace the CI Kind smoke job with full CNPG + templates E2E **File:** `.github/workflows/ci.yaml` #### 3.1 Keep the job identity stable Keep the job id `e2e-kind` (and ideally keep the `name:` similar) so that existing branch protection / required checks don’t unexpectedly change. #### 3.2 Runtime control: run *full* E2E in merge queue only Full CNPG + Coder bring-up will be materially slower than the current smoke test. Recommended approach: - Keep the `e2e-kind` job **running** on PRs (so the check exists), but make it a fast no-op. - Run the full E2E only when `github.event_name == 'merge_group'`. This satisfies “only run it in the merge queue” while still allowing the check to be configured as **required**. Implementation pattern: ```yaml e2e-kind: name: E2E (Kind + CNPG + Templates) ... steps: - name: Skip full E2E outside merge queue if: github.event_name != 'merge_group' run: | echo "Skipping full E2E on ${GITHUB_EVENT_NAME}; it runs in merge queue (merge_group)." - name: Checkout if: github.event_name == 'merge_group' uses: actions/checkout@... # all heavy steps also gated on merge_group ``` If you *do* want some signal on PRs, a compromise is to keep the old smoke steps under `pull_request` and the full E2E under `merge_group`. #### 3.3 Full merge-queue E2E steps (merge_group only) Replace the existing “apply sample CR and verify it exists” with: 1. Create Kind cluster (existing) 2. Build + load `ghcr.io/coder/coder-k8s:e2e` image (existing) 3. Apply base manifests (existing): - `kubectl apply -f config/e2e/namespace.yaml` - `kubectl apply -f config/crd/bases/` - `kubectl apply -f config/rbac/` 4. Enable aggregated API (new): - `kubectl apply -f deploy/apiserver-service.yaml` - `kubectl apply -f deploy/apiserver-apiservice.yaml` 5. Deploy `coder-k8s` (updated `config/e2e/deployment.yaml`) 6. Wait for operator: - `kubectl wait --for=condition=Available deploy/coder-k8s -n coder-system --timeout=120s` - `kubectl wait --for=condition=Available apiservice/v1alpha1.aggregation.coder.com --timeout=180s` 7. Install CloudNativePG operator (pinned version) + wait: - `kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.25/releases/cnpg-1.25.0.yaml` - `kubectl wait --for=condition=Available deploy/cnpg-controller-manager -n cnpg-system --timeout=180s` 8. Provision Postgres + CoderControlPlane using existing repo examples: - `kubectl apply -f examples/cloudnativepg/` - `kubectl -n coder wait --for=condition=Ready cluster/coder-db --timeout=10m` 9. Wait for Coder to come up and for operator access bootstrap: - `kubectl -n coder rollout status deployment/coder --timeout=10m` - `kubectl -n coder wait --for=jsonpath='{.status.phase}'=Ready codercontrolplane/coder --timeout=10m` - `kubectl -n coder wait --for=jsonpath='{.status.operatorAccessReady}'=true codercontrolplane/coder --timeout=10m` - Verify the token Secret exists: ```bash TOKEN_SECRET=$(kubectl -n coder get codercontrolplane coder -o jsonpath='{.status.operatorTokenSecretRef.name}') test -n "$TOKEN_SECRET" kubectl -n coder get secret "$TOKEN_SECRET" ``` 10. Create + retrieve a CoderTemplate via kubectl (new): - `kubectl apply -f config/e2e/codertemplate.yaml` - `kubectl -n coder get codertemplates` - `kubectl -n coder get codertemplate default.e2e-template -o yaml` #### 3.4 Add failure diagnostics (high value for flake triage) Add `if: failure()` steps to dump state: - `kubectl get pods -A` - `kubectl describe apiservice v1alpha1.aggregation.coder.com` - `kubectl -n coder-system logs deploy/coder-k8s --tail=200` - `kubectl -n coder describe codercontrolplane coder` - `kubectl -n coder logs deploy/coder --tail=200` (best-effort) - `kubectl -n cnpg-system logs deploy/cnpg-controller-manager --tail=200` --- ### 4) Make it required for merge queue completion This is a **repo settings** change (not a code change): mark the check run name (e.g., `E2E (Kind + CNPG + Templates)`) as a **required status check** on the protected branch that uses merge queue. Keep the job id/name stable to avoid having to reconfigure required checks. --- ## Validation plan (when implementing) - Manually run the new job steps locally once (optional) using `hack/kind-dev.sh` + `kubectl apply` commands. - In CI, confirm on a test PR: - PR event: `e2e-kind` completes quickly (skipped heavy steps) and is green. - Merge queue (`merge_group`): full E2E runs, creates `CoderControlPlane` + `CoderTemplate`, and succeeds. </details> --- _Generated with `mux` • Model: `anthropic:claude-opus-4-6` • Thinking: `xhigh` • Cost: `$1.34`_ <!-- mux-attribution: model=anthropic:claude-opus-4-6 thinking=xhigh costs=1.34 -->
1 parent 687e6bd commit 57aee35

3 files changed

Lines changed: 107 additions & 16 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -155,29 +155,40 @@ jobs:
155155
run: go build ./...
156156

157157
e2e-kind:
158-
name: E2E Smoke (Kind)
158+
name: E2E (Kind + CNPG + Templates)
159159
needs: [changes, test]
160160
if: needs.changes.outputs.go == 'true' && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]')
161161
runs-on: depot-ubuntu-24.04-8
162162
timeout-minutes: 45
163163
steps:
164+
# ---- PR fast-path: skip heavy E2E on pull requests ----
165+
- name: Skip full E2E on pull requests
166+
if: github.event_name == 'pull_request'
167+
run: |
168+
echo "::notice::Skipping full E2E on ${GITHUB_EVENT_NAME}; runs in merge queue and on push to main."
169+
170+
# ---- Full CNPG + Templates E2E (merge queue + push) ----
164171
- name: Checkout
172+
if: github.event_name != 'pull_request'
165173
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
166174
with:
167175
persist-credentials: false
168176

169177
- name: Setup Go
178+
if: github.event_name != 'pull_request'
170179
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
171180
with:
172181
go-version-file: go.mod
173182
cache: true
174183

175184
- name: Create Kind cluster
185+
if: github.event_name != 'pull_request'
176186
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
177187
with:
178188
cluster_name: e2e
179189

180190
- name: Build binary
191+
if: github.event_name != 'pull_request'
181192
env:
182193
GOFLAGS: -mod=vendor
183194
CGO_ENABLED: "0"
@@ -186,33 +197,96 @@ jobs:
186197
run: go build -o coder-k8s ./
187198

188199
- name: Build and load image
200+
if: github.event_name != 'pull_request'
189201
run: |
190202
docker build -f Dockerfile.goreleaser -t ghcr.io/coder/coder-k8s:e2e .
191203
kind load docker-image ghcr.io/coder/coder-k8s:e2e --name e2e
192204
193205
- name: Apply namespace, CRDs, and RBAC
206+
if: github.event_name != 'pull_request'
194207
run: |
195208
kubectl apply -f config/e2e/namespace.yaml
196209
kubectl apply -f config/crd/bases/
197210
kubectl apply -f config/rbac/
198211
199-
- name: Deploy controller
212+
- name: Enable aggregated API server
213+
if: github.event_name != 'pull_request'
214+
run: |
215+
kubectl apply -f deploy/apiserver-service.yaml
216+
kubectl apply -f deploy/apiserver-apiservice.yaml
217+
218+
- name: Deploy coder-k8s (controller + aggregated API server)
219+
if: github.event_name != 'pull_request'
200220
run: kubectl apply -f config/e2e/deployment.yaml
201221

202-
- name: Wait for controller
203-
run: kubectl wait --for=condition=Available deploy/coder-k8s -n coder-system --timeout=120s
222+
- name: Wait for operator and APIService
223+
if: github.event_name != 'pull_request'
224+
run: |
225+
kubectl wait --for=condition=Available deploy/coder-k8s -n coder-system --timeout=120s
226+
kubectl wait --for=condition=Available apiservice/v1alpha1.aggregation.coder.com --timeout=180s
227+
228+
- name: Install CloudNativePG operator
229+
if: github.event_name != 'pull_request'
230+
run: |
231+
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/v1.25.0/releases/cnpg-1.25.0.yaml
232+
kubectl wait --for=condition=Available deploy/cnpg-controller-manager -n cnpg-system --timeout=180s
233+
234+
- name: Provision PostgreSQL and CoderControlPlane
235+
if: github.event_name != 'pull_request'
236+
run: kubectl apply -f examples/cloudnativepg/
204237

205-
- name: Apply sample CR
206-
run: kubectl apply -f config/samples/coder_v1alpha1_codercontrolplane.yaml
238+
- name: Wait for PostgreSQL
239+
if: github.event_name != 'pull_request'
240+
run: kubectl -n coder wait --for=condition=Ready cluster/coder-db --timeout=600s
241+
242+
- name: Wait for Coder deployment
243+
if: github.event_name != 'pull_request'
244+
run: kubectl -n coder rollout status deployment/coder --timeout=600s
245+
246+
- name: Wait for CoderControlPlane readiness
247+
if: github.event_name != 'pull_request'
248+
run: |
249+
kubectl -n coder wait --for=jsonpath='{.status.phase}'=Ready codercontrolplane/coder --timeout=600s
250+
kubectl -n coder wait --for=jsonpath='{.status.operatorAccessReady}'=true codercontrolplane/coder --timeout=600s
251+
252+
- name: Verify operator token secret
253+
if: github.event_name != 'pull_request'
254+
run: |
255+
TOKEN_SECRET=$(kubectl -n coder get codercontrolplane coder -o jsonpath='{.status.operatorTokenSecretRef.name}')
256+
test -n "$TOKEN_SECRET"
257+
kubectl -n coder get secret "$TOKEN_SECRET"
258+
259+
- name: Create CoderTemplate via kubectl
260+
if: github.event_name != 'pull_request'
261+
run: kubectl apply -f config/e2e/codertemplate.yaml
262+
263+
- name: Verify CoderTemplate exists
264+
if: github.event_name != 'pull_request'
265+
run: |
266+
kubectl -n coder get codertemplates
267+
kubectl -n coder get codertemplate default.e2e-template -o yaml
207268
208-
- name: Verify CR exists
269+
# ---- Failure diagnostics ----
270+
- name: Dump cluster state on failure
271+
if: failure() && github.event_name != 'pull_request'
209272
run: |
210-
kubectl get codercontrolplanes -A
211-
COUNT=$(kubectl get codercontrolplanes -A -o json | jq '.items | length')
212-
if [ "$COUNT" -lt 1 ]; then
213-
echo "assertion failed: expected at least 1 CoderControlPlane resource" >&2
214-
exit 1
215-
fi
273+
echo "=== Pods (all namespaces) ==="
274+
kubectl get pods -A || true
275+
echo ""
276+
echo "=== APIService ==="
277+
kubectl describe apiservice v1alpha1.aggregation.coder.com || true
278+
echo ""
279+
echo "=== coder-k8s logs ==="
280+
kubectl -n coder-system logs deploy/coder-k8s --tail=200 || true
281+
echo ""
282+
echo "=== CoderControlPlane ==="
283+
kubectl -n coder describe codercontrolplane coder || true
284+
echo ""
285+
echo "=== Coder deployment logs ==="
286+
kubectl -n coder logs deploy/coder --tail=200 || true
287+
echo ""
288+
echo "=== CNPG controller logs ==="
289+
kubectl -n cnpg-system logs deploy/cnpg-controller-manager --tail=200 || true
216290
217291
terraform:
218292
name: Terraform (fmt/validate/tflint/trivy)

config/e2e/codertemplate.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: aggregation.coder.com/v1alpha1
2+
kind: CoderTemplate
3+
metadata:
4+
name: default.e2e-template
5+
namespace: coder
6+
spec:
7+
organization: default
8+
displayName: "E2E Template"
9+
description: "Created by coder-k8s merge-queue e2e"
10+
files:
11+
main.tf: |
12+
terraform {
13+
required_version = ">= 1.0"
14+
}
15+
resource "null_resource" "example" {}

config/e2e/deployment.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ spec:
1919
containers:
2020
- name: manager
2121
image: ghcr.io/coder/coder-k8s:e2e
22-
# E2E tests validate controller mode only; all mode requires
23-
# additional infrastructure (APIService, TLS) not provisioned here.
24-
args: ["--app=controller"]
22+
# E2E tests validate both controller and aggregated API server
23+
# behavior in one pod for APIService-backed integration coverage.
24+
args: ["--app=all"]
2525
imagePullPolicy: Never
2626
ports:
2727
- containerPort: 8081
2828
name: health
29+
- containerPort: 6443
30+
name: https
2931
livenessProbe:
3032
httpGet:
3133
path: /healthz

0 commit comments

Comments
 (0)