Validate the Helm Charts
Check the Tektona charts against your cluster before you install them, then confirm that the release works.
Tektona installs on your own cluster from two Helm charts:
tektona-control-planeserves the API and the console, and it schedules and builds sandboxes.tektona-runnerruns the sandbox virtual machines and their gateways on the nodes that you mark as runners.
This page is a checklist. Each step fails on its own, before the step after it, so run them in order. The first five steps need no cluster changes.
The charts are an early draft
The value names can change between releases. Pin the chart to a release tag, and read the release notes before an upgrade.
Before you start
| You need | Detail |
|---|---|
| Helm 3.14 or later | helm version |
| Cluster access | kubectl auth can-i create deployment --namespace tektona-system |
| The chart source | Clone the repository at a release tag. The charts sit under deploy/charts. |
| A namespace | tektona-system in the examples below |
| Registry credentials | The images are private. You need a pull secret. |
| cert-manager | Needed for the gateway certificates. certManager.deploy installs it, in two passes. |
Decide four things first. Each one changes the values file:
| Decision | Options |
|---|---|
| PostgreSQL | Your own server, or a cluster that the chart installs. Required either way. |
| NATS | Your own server, or a server that the chart installs |
| Object storage | Your own S3 service, or RustFS that the chart installs |
| Sign-on | Federated through Zitadel, or direct for API access. See Configure Sign-On. |
| Developer platform | features.devPlatform adds the workflow surface. It needs its own database and a Hatchet engine. |
| Secrets | The tool that lands the credentials in the namespace |
The charts never create a secret. They only reference one. Any tool works that writes a normal Kubernetes secret, such as SOPS, Sealed Secrets, or the External Secrets Operator.
Fetch the chart dependencies
The charts install PostgreSQL, NATS, S3 and abuse detection only when you ask for them. Helm still needs the dependency archives before it renders anything.
Add the repositories first. A clean machine has none of them, and the build
stops with no repository definition for ...:
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo add nats https://nats-io.github.io/k8s/helm/charts/
helm repo add rustfs https://charts.rustfs.com
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm dependency build ./deploy/charts/tektona-control-plane
helm dependency build ./deploy/charts/tektona-runnerA chart pulled from the registry needs none of this. It carries its dependencies inside the package.
build installs the exact versions from the lock file of the chart. Use
helm dependency update only when you want newer versions.
Write the values files
Start from the smallest file that points at services which you already run.
# The registry holds no image tagged with the chart version, so a chart built
# from the repository needs the tag. A chart pulled from the registry does not.
global:
imageTag: "<commit sha>"
imagePullSecrets:
- name: "regcred"
domains:
api: "api.example.com"
app: "app.example.com"
sandboxIngress: "sandbox.example.com"
agentlinkTrustDomain: "agentlink.example.com"
# The API refuses to start without these two.
vnc: "vnc.example.com"
sshWebsocket: "ssh.example.com"
# The address that the CLI dials for direct SSH access.
sshHost: "ssh.example.com"
cluster:
# Must equal location in the runner file.
sandboxLocation: "eu"
database:
host: "postgres.example.com"
name: "platform_api"
existingSecret: "tektona-database"
messaging:
url: "nats://nats.example.com:4222"
# The chart always creates the JetStream streams. They are custom resources,
# and this installs the NATS resource controller that reconciles them.
controller:
deploy: true
nack:
jetstream:
nats:
# The controller needs the server address of its own.
url: "nats://nats.example.com:4222"
objectStorage:
endpoint: "https://s3.example.com"
bucket: "tektona-sandboxes"
existingSecret: "tektona-object-storage"
# See Configure Sign-On for a Microsoft Entra ID example.
auth:
issuer: "https://auth.example.com"
# Both URLs come from <issuer>/.well-known/openid-configuration.
jwksUrl: "https://auth.example.com/.well-known/jwks.json"
userinfoUrl: "https://auth.example.com/userinfo"
existingSecret: "tektona-oidc"
sessionSecret: "tektona-session"
credentials:
masterSecret: "tektona-credentials"
sandboxGateway:
certificate:
create: trueglobal:
imageTag: "<commit sha>"
imagePullSecrets:
- name: "regcred"
location: "eu"
sandboxRunner:
# kvm, or tcg for a node that cannot offer hardware virtualization.
# There is no default: see Runner Node Requirements.
acceleration: "kvm"
domains:
sandboxIngress: "sandbox.example.com"
agentlinkTrustDomain: "agentlink.example.com"
messaging:
url: "nats://nats.example.com:4222"
objectStorage:
endpoint: "https://s3.example.com"
bucket: "tektona-sandboxes"
existingSecret: "tektona-object-storage"
sandboxGateway:
hostKeySecret: "tektona-access-gateway-host-key"
egress:
certificate:
# The inspection sidecar of the egress gateway needs this authority. Set
# egress.injectCaSecret instead when you bring your own.
create: true
agentGateway:
certificate:
# No chart creates this certificate unless you ask for it.
create: truelocation in the runner file must equal cluster.sandboxLocation in the
control plane file. The API addresses runners by that token.
To install a backing service instead, set database.deploy, messaging.deploy
or objectStorage.deploy. Read the chart README first. The installed database
needs the CloudNativePG operator, installed separately, the JetStream streams
need the NATS resource controller, and the installed object storage needs a
second secret with its own server credentials.
No chart creates the S3 bucket. Create objectStorage.bucket yourself, before
the first sandbox.
The database connection takes a password, a client certificate, or a rotating token from a managed identity. The chart README covers all three.
Render the charts
helm template tektona-control-plane ./deploy/charts/tektona-control-plane \
--namespace tektona-system --values values-control-plane.yaml > /dev/null
helm template tektona-runner ./deploy/charts/tektona-runner \
--namespace tektona-system --values values-runner.yaml > /dev/nullA missing host name or a missing secret name fails here, and the message names the value:
Error: execution error at (tektona-control-plane/templates/platform-api-deployment.yaml:136:24): domains.vnc is requiredDo not gate on helm lint
helm lint reports a missing required value as [INFO] Missing required value
and still exits 0. A chart that cannot install passes it. Use helm template
for this check. Run helm lint --strict as well, for the checks it does make,
but never as the gate.
Render the manifests and read them
helm template tektona-control-plane ./deploy/charts/tektona-control-plane \
--namespace tektona-system \
--values values-control-plane.yaml > rendered-control-plane.yamlKeep the file. The two checks below read it, and it is also the document that your change process reviews.
First check the images. Every reference must resolve from your cluster. Mirror
them into your own registry when the nodes have no route to the public one, then
set global.imageRegistry.
grep -oE 'image: "[^"]+"' rendered-control-plane.yaml | sort -uThen check the secret references:
{ grep -A1 'secretKeyRef:' rendered-control-plane.yaml | grep -oE 'name: [A-Za-z0-9._-]+'
grep -oE 'secretName: [A-Za-z0-9._-]+' rendered-control-plane.yaml
} | awk '{print $2}' | sort -u > required-secrets.txtRepeat all three commands for the runner chart, into files of its own:
helm template tektona-runner ./deploy/charts/tektona-runner \
--namespace tektona-system \
--values values-runner.yaml > rendered-runner.yamlThe image command misses the images of the bundled dependencies, which are not
quoted in the output. Add grep -oE '^\s*image: [^"].*' to see those.
Confirm that the secrets exist
Every name in required-secrets.txt must exist in the namespace before the pods
start.
while read -r name; do
kubectl get secret "$name" --namespace tektona-system > /dev/null 2>&1 \
|| echo "missing: $name"
done < required-secrets.txtcert-manager writes some of these names during the install. Expect them to be missing now:
sandbox-gateway-tlsand the gateway authority secret beside it, whensandboxGateway.certificate.createis true. The runner list holds the samesandbox-gateway-tlsname, which the control plane release creates.egress-inject-ca, whenegress.certificate.createis true in the runner chart.hatchet-engine-tls, whenhatchet.certificate.createis true.
The control plane writes one more name itself, rather than cert-manager.
hatchet-client-config holds the token that the API uses to reach the build
engine. The bootstrap job mints it after the install.
Every other name is yours to create. Two are easy to miss, and no chart creates
either. agent-gateway-tls in the runner list carries the server certificate of
the agent gateway. hatchet-config carries the keys of the build engine, and
the control plane chart README has the command that makes it.
A pod that starts without its secret stays in CreateContainerConfigError. It
recovers on its own once the secret appears.
Dry run against the cluster
helm install tektona-control-plane ./deploy/charts/tektona-control-plane \
--namespace tektona-system --create-namespace \
--values values-control-plane.yaml \
--dry-run=serverA server side dry run checks the API versions of your cluster and the admission rules of your cluster. It catches a policy that rejects a privileged container, which the template render cannot see.
Custom resources need their definitions first
The dry run cannot validate a resource whose definition is not installed yet.
This affects the CloudNativePG Cluster, the JetStream Stream and KeyValue
objects, and the cert-manager Certificate objects. Install those operators
first, or repeat the dry run after the operators run.
Check the runner nodes
The runner pods only schedule on labelled nodes:
kubectl label node <node> tektona.ai/sandbox-runner=true
kubectl get nodes --selector tektona.ai/sandbox-runner=trueThen check the devices on each of those nodes:
lsmod | grep vhost_vsock
ls -l /dev/kvm /dev/net/tunvhost_vsockcarries the channel to the guest agent. A node without it fails every sandbox session./dev/net/tuncarries the sandbox network./dev/kvmgives hardware virtualization. The runner refuses to start without it whensandboxRunner.accelerationiskvm.
Runner Node Requirements covers the kernel modules, the settings and the speed each choice costs.
Install and watch the rollout
Install the control plane first. It issues the gateway certificate that the runner mounts.
helm install tektona-control-plane ./deploy/charts/tektona-control-plane \
--namespace tektona-system --create-namespace \
--values values-control-plane.yaml --wait --timeout 10m
helm install tektona-runner ./deploy/charts/tektona-runner \
--namespace tektona-system \
--values values-runner.yaml --wait --timeout 10m--wait holds the command until every pod is ready. A failure leaves the
release in place, so you can read the events:
kubectl get events --namespace tektona-system --sort-by .lastTimestamp | tail -20Confirm the release
Check the pods of both releases:
kubectl get pods --namespace tektona-system \
--selector app.kubernetes.io/instance=tektona-control-plane
kubectl get pods --namespace tektona-system \
--selector app.kubernetes.io/instance=tektona-runnerAsk the API whether its dependencies answer. /healthz returns 200 whatever
the state of the database, so use /readyz on the admin port:
kubectl port-forward --namespace tektona-system \
service/tektona-control-plane-platform-api 9090:9090 &
sleep 2
curl --silent --show-error --fail http://localhost:9090/readyz && echo " ready"
kill %1Every pod Ready is not enough. A control plane whose build engine refuses its worker token reports every pod Ready and serves an empty template catalog, so the first sandbox create has no template to name. Check the engine and the catalog before you go further:
kubectl port-forward --namespace tektona-system \
service/tektona-control-plane-platform-api 9090:9090 &
sleep 2
# 1 means a registered worker holds a session to the engine.
curl --silent http://localhost:9090/metrics \
| grep tektona_template_build_engine_connected
kill %1The gauge reads 1 within a minute of the pods settling. A 0 that does not move,
with invalid auth token in the API log, is the stale worker token in
When a check fails. Waiting longer never clears it.
Then the catalog, which the API reconciles once the engine answers:
tektona template lsIt lists tektona/ubuntu and tektona/desktop. An empty list after eleven
minutes means the reconcile gave up; restart platform-api rather than wait.
Then prove the whole path. Route your API host name and your console host name to the two services, sign in, and create a sandbox:
tektona login
tektona sandbox create tektona/desktop
tektona sandbox ls
tektona sandbox ssh <sandbox-id>A sandbox that reaches the running state proves the control plane, the messaging, the object storage and one runner node together. Delete it when you are done:
tektona sandbox rm <sandbox-id>When a check fails
| Symptom | Cause | Fix |
|---|---|---|
helm template fails and names a value | The values file misses a host name or a secret name | Set the value that the message names |
A pod stays in CreateContainerConfigError | A secret it reads as an environment variable does not exist | Create the secret. The pod recovers on its own |
A pod stays in ContainerCreating with a FailedMount event | A secret it mounts as a volume does not exist | Create the secret. Most of the runner secrets are volumes |
| The API pod is ready but every call fails | /healthz does not test the dependencies | Check /readyz on the admin port, then the database and NATS endpoints |
Every pod reports ImagePullBackOff | The registry needs credentials, or the tag does not exist | Set global.imagePullSecrets, and global.imageTag when you install from the repository |
tektona-runner-sandbox-runner stays in ContainerCreating | The node has no /dev/vhost-vsock | Load the vhost_vsock module on that node |
| A sandbox create call hangs | The JetStream streams do not exist | The NATS resource controller is not running. Set messaging.controller.deploy, or install it yourself |
The API pod restarts and logs open KV bucket and bucket not found | The JetStream key value buckets do not exist | The same. The chart creates both; the controller reconciles them |
| A sandbox create call fails on storage | The S3 bucket does not exist | Create the bucket that objectStorage.bucket names |
Every pod is Ready, the template list is empty, and the API logs invalid auth token | The Hatchet database was re-created and the worker token still names the old one | Delete the hatchet-client-config secret, helm upgrade, then restart platform-api |
| The runner DaemonSet has no pods | No node carries the runner label | Label the nodes that run sandboxes |
| A sandbox starts, but everything inside it is slow | The node runs tcg, or is itself a virtual machine | See Runner Node Requirements |
sandbox-runner exits with a KVM device error | acceleration is kvm and the node has no /dev/kvm | Fix the node, or set tcg and accept the loss |
| A preview URL does not answer | No route reaches the sandbox ingress domain | Point the wildcard domain at the access gateway |
Routes
Set ingress.kind to HTTPRoute or to Ingress and the control plane chart
creates the routes for the API and the console. The default is none.
HTTPRoute needs the Gateway API definitions on the cluster, and the chart
refuses to render without them. helm template reads no cluster, so add
--api-versions gateway.networking.k8s.io/v1 there. The sandbox preview domain
always needs a route of your own:
| Host | Service | Port |
|---|---|---|
domains.api | tektona-control-plane-platform-api | 8080 |
domains.app | tektona-control-plane-platform-frontend | 80 |
domains.sandboxIngress (wildcard) | tektona-runner-access-gateway | 8445 |