Test a template version with CoderTemplateTest¶
This guide shows how to check that a Coder template version can start a workspace whose agents become ready. A CoderTemplateTest creates a throwaway workspace from one template version, waits until every top-level agent is connected and ready, records Succeeded or Failed, and deletes the workspace. For every field, see the CoderTemplateTest reference.
You need:
- a
CoderControlPlanewhosestatus.operatorAccessReadyistrue(see Deploy the controller), - a Coder user that owns the test workspaces (the tester), described in step 1.
1. Create the tester user¶
Every start build hands the owner's token to Terraform, so the controller refuses an owner whose token is worth more than a plain workspace user's. The tester must:
- have status
active. Coder creates users through its API asdormantand refuses a dormant owner's agents, so activate the tester first, - be a service account or a password user (login type
passwordornone), never a person's OIDC or GitHub login, - have no site role other than
member, - have only the
organization-memberororganization-workspace-accessrole in each of its organizations, and no organization may grant other roles by default.
Point the control plane at the tester's user ID:
apiVersion: coder.com/v1alpha1
kind: CoderControlPlane
metadata:
name: coder
namespace: coder
spec:
templateTests:
ownerUserID: 6f1c... # the tester's Coder user ID
The operator user (the control plane's operator token) sends every Coder request. It needs the site owner role: only a site owner can read every workspace, so only its "not found" answer proves that no workspace exists.
2. Run a test¶
apiVersion: coder.com/v1alpha1
kind: CoderTemplateTest
metadata:
generateName: docker-
namespace: coder
spec:
controlPlaneRef:
name: coder
template: default.docker # <organization>.<template>
version:
active: true # or name: v2, or id: <uuid>
timeoutSeconds: 900 # the default
ttlSecondsAfterFinished: 3600 # optional
Create it with kubectl create -f, then watch it:
kubectl -n coder get codertemplatetests -w
The spec is immutable. To test again, create a new object.
3. Grant access¶
The operator's own RBAC comes with the install. People who run tests need create, get, list, watch, and delete on codertemplatetests.coder.com in their namespace. They never need access to Coder or to the operator token.
4. Read the result¶
status.phase is Pending until the controller may have sent a create request, then Running, and finally Succeeded or Failed. status.reason and status.message say what the controller waits for or why the test failed. status.templateVersionName shows which version ran.
Waiting reasons¶
While the test waits, the condition Reconciling is True with the same reason. Every wait ends at spec.timeoutSeconds: the test then fails as DeadlineExceeded, and the message names the last wait.
| Reason | Meaning | What to do |
|---|---|---|
Initializing |
The controller started the test. | Nothing. |
ControlPlaneNotReady |
The CoderControlPlane does not exist, is being deleted, or has no usable status.url. |
Check spec.controlPlaneRef and the control plane status. |
OperatorAccessNotReady |
status.operatorAccessReady of the control plane is not true, or its operator token Secret is missing or has no token. |
See Deploy the controller, and check the Secret in status.operatorTokenSecretRef of the control plane. |
OwnerNotConfigured |
The control plane has no spec.templateTests.ownerUserID. |
Set it, as in step 1. |
OwnerNotEligible |
The tester does not exist, is not active, has a person's login type, has a role that is not allowed, or is not a member of the template's organization. |
Fix the tester as described in step 1. The message names the problem. |
TemplateNotFound |
The organization or the template in spec.template does not exist. |
Check spec.template (<organization>.<template>). |
TemplateVersionNotFound |
The version in spec.version does not exist. |
Check spec.version, or push the version. |
TemplateVersionImporting |
The version is still importing. | Wait. |
CreatingWorkspace, CreateRetrying |
The controller sends the create request, or sends it again after a request that had no effect. | Wait. |
ConfirmingCreate |
The answer to the create request was lost, or does not prove what Coder did, for example a proxy's 408, 499 or 5xx. The controller reads Coder until the workspace appears. | Wait. |
WaitingForBuild, WaitingForAgents |
The start build runs, or an agent is not yet connected and ready. |
Wait. The message names the agent. |
AgentsReady, DeletingWorkspace |
Every top-level agent was ready. The controller does not check devcontainer sub-agents. The controller deletes the workspace. | Wait. |
CoderUnavailable |
A Coder request failed. After HTTP 429, the controller retries with backoff. | Check Coder and its logs. |
CoderAnswerMismatch |
Coder answered about another object than the one asked for. The controller logs it and retries. | Check proxies between the controller and Coder. |
Failure reasons¶
A failed test has Ready=False and Stalled=True with the same reason. It does not run again: create a new test.
| Reason | Meaning | What to do |
|---|---|---|
DeadlineExceeded |
The test did not finish within spec.timeoutSeconds. |
Fix the last wait in the message, or raise spec.timeoutSeconds. |
TemplateDeprecated |
The template is deprecated and accepts no new workspaces. | Test another template. |
TemplateVersionArchived, TemplateVersionImportFailed |
The version is archived, or its import did not succeed. | Push a working version. |
TemplateVersionMismatch |
The version in spec.version.id belongs to another template. |
Fix spec.version or spec.template. |
CreateRejected |
Coder rejected the create request. The message holds Coder's answer. No workspace exists. | Fix the cause, for example spec.parameters, and create a new test. |
CreateOutcomeUnknown |
No workspace from this test appeared after an uncertain create request. | Check Coder, then create a new test. |
WorkspaceNameConflict |
A workspace with the test's name exists, and this test did not create it, or nothing proves that it did. | Read the WorkspaceDeleted condition. NotCreated means that this test never created a workspace: the test releases its finalizer by itself, and the workspace belongs to someone else, so leave it alone. For OwnershipUnknown, see the escape hatch below. |
BuildFailed, BuildCanceled |
The start build failed, or someone else canceled it. | Read the build logs in Coder. |
NoAgents |
The workspace has no top-level agents, so nothing proves that it works. Devcontainer sub-agents do not count. | Add a top-level agent to the template. |
AgentConnectionTimeout, AgentStartError, AgentStartTimeout, AgentStopped |
An agent did not connect in time, its startup script failed or timed out, or it stopped. | Read the agent logs in Coder. The message names the agent. |
WorkspaceChangedExternally, WorkspaceDeletedExternally |
Someone else started a build of the workspace, or deleted it. | Leave test workspaces alone, and create a new test. |
DeleteBuildFailed |
Every top-level agent was ready, but the delete build failed. The controller retries the delete. | Read the delete build logs in Coder. |
ControlPlaneGone |
The CoderControlPlane was deleted while the workspace could exist. |
Look for the workspace in Coder and delete it there. |
WorkspaceDeleted reasons¶
The condition WorkspaceDeleted tracks the workspace after the test ends or while the test is being deleted. The controller keeps its finalizer until the condition is True, or the reason is Retained or ControlPlaneGone.
| Status | Reasons | Meaning |
|---|---|---|
True |
Deleted, DeletedExternally, NotCreated |
No workspace of this test exists. ttlSecondsAfterFinished applies. |
False |
CleanupPending, Deleting, DeleteRetrying |
The controller deletes the workspace. |
False |
ControlPlaneUnavailable, CoderAnswerMismatch |
Coder is not usable, or answered about another object. The controller retries. |
False |
Retained |
retain is set and allowed, so the workspace stays in Coder. |
Unknown |
CreateOutcomeUnknown |
The controller reads Coder to learn whether a workspace exists. |
Unknown |
OwnershipUnknown, ControlPlaneGone |
The workspace can exist, and the controller cannot delete it. See the escape hatch below. |
Limit the cost¶
Each test is a real workspace: it uses compute and provisioner time until the controller deletes it. To cap the number of tests in a namespace, use a ResourceQuota. The quota counts finished tests too, until they expire or someone deletes them:
apiVersion: v1
kind: ResourceQuota
metadata:
name: template-tests
namespace: coder
spec:
hard:
count/codertemplatetests.coder.com: "10"
Run nightly checks¶
A CronJob can test the active version every night. It needs only create on codertemplatetests. ttlSecondsAfterFinished removes old results:
apiVersion: v1
kind: ServiceAccount
metadata:
name: nightly-template-test
namespace: coder
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: nightly-template-test
namespace: coder
rules:
- apiGroups: ["coder.com"]
resources: ["codertemplatetests"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: nightly-template-test
namespace: coder
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: nightly-template-test
subjects:
- kind: ServiceAccount
name: nightly-template-test
namespace: coder
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-template-test
namespace: coder
spec:
schedule: "0 3 * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
backoffLimit: 0 # a retry after a lost create response makes a second test
template:
spec:
serviceAccountName: nightly-template-test
restartPolicy: Never
containers:
- name: create-test
image: <an image with sh and kubectl>
command:
- sh
- -ec
- |
kubectl create -f - <<'TEST'
apiVersion: coder.com/v1alpha1
kind: CoderTemplateTest
metadata:
generateName: docker-nightly-
namespace: coder
spec:
controlPlaneRef:
name: coder
template: default.docker
version:
active: true
ttlSecondsAfterFinished: 604800
TEST
To find failed tests, run kubectl -n coder get codertemplatetests, or alert on the condition Stalled=True. To gate promotions instead, see Gate template promotion with GitOps.
Cleanup and the escape hatch¶
The controller keeps a finalizer on each test until it has deleted the workspace or proved that none exists. These cases need an admin:
- A test stuck as
ControlPlaneUnavailableorCoderUnavailable. Coder is unreachable, so the finalizer stays and the controller retries every 60 s. To release the test, remove the finalizer by hand, or useretainas described below. Either way, check Coder for a workspace namedstatus.workspaceNameand delete it there. - A test with
WorkspaceDeleted=Unknownand reasonOwnershipUnknown. A workspace with the test's name exists, but nothing proves that the test created it, so the controller never touches it. Only removing the finalizer orretainreleases the test.
The annotation coder.com/deletion-policy: retain releases a finished or deleted test without deleting its workspace. Anyone who can create a test can set this annotation, so it works only when the control plane opts in with spec.templateTests.allowRetain: true. Opting in gives that power to everyone who can create tests against the control plane: they can leave workspaces, and the tester's session keys, in Coder. Without the opt-in, the controller ignores retain, says so in the WorkspaceDeleted message, and deletes the workspace. If you switch a retained final test back to coder.com/deletion-policy: delete, the controller adds the finalizer again and deletes the workspace.
Known limits¶
- TTL.
ttlSecondsAfterFinishedapplies only whenWorkspaceDeleted=True(reasonDeleted,DeletedExternally, orNotCreated). A test withRetainedorControlPlaneGonenever expires, because its workspace can still exist. Delete such tests by hand after you check Coder. - Tester API keys. In Coder v2.37.2, each start build creates a session token for the workspace owner, and the stop or delete build removes it. A test that deletes its workspace leaves no key behind: the Kind E2E counted 0 tester keys before and after three passing tests. A retained workspace keeps its owner's key until the workspace is stopped or deleted.
- More than 100 builds. After an uncertain create request, the controller reads the newest 100 builds of the workspace to prove that the test created it. A workspace with more builds stays unproven: the test fails as
WorkspaceNameConflict, keeps its finalizer, and never touches the workspace. - TLS (#198). With TLS on the control plane,
CoderTemplateTestcalls Coder at the control plane's internalhttp://service URL. Its operator token therefore crosses the cluster network as plain HTTP, without encryption. Separately, theCoderProvisionercontroller and the aggregated API server use thehttps://service URL, whose certificate often does not cover the service name, so their Coder calls can fail TLS verification. This is tracked in #198 and not verified in a live cluster.