kubectl block
The kubectl: block provides a structured alternative to commands: for the most common assertion patterns. Each subcommand maps directly to the kubectl command people already know — kubectl.get, kubectl.logs, kubectl.describe, kubectl.exec, kubectl.port-forward.
Raw commands: stays for anything that doesn’t fit a subcommand — though kubectl.apply and kubectl.patch now cover the most common mutation patterns.
Structure
kubectl: sits alongside resources: and commands: in each expect: entry:
expect:
- name: Deployment has correct resource profile
after: cr-applied
timeout: 60s
resources:
- kind: Deployment
name: my-service
namespace: default
kubectl:
get:
- kind: Deployment
name: my-service
field: .spec.template.spec.containers[0].resources.requests.cpu
equals: 200m
All subcommands in a kubectl: block are checked in the same polling loop as resources: and commands:. All must pass for the checkpoint to pass.
Assertion fields
Every subcommand supports the same assertion fields:
| Field | Description |
|---|---|
equals | Output (trimmed) must exactly match this string |
notEquals | Output must not exactly match this string |
outputContains | Output must contain this substring |
outputNotContains | Output must not contain this substring |
greaterThan | Output (trimmed, parsed as a number) must be greater than this value |
lessThan | Output (trimmed, parsed as a number) must be less than this value |
Multiple assertions on the same entry all apply. Empty fields are ignored. greaterThan and lessThan parse the output as float64 — the check fails if the output is not numeric when either is set.
kubectl.get
Generates: kubectl get <kind> <name> -n <namespace> -o jsonpath='{<field>}'
kubectl:
get:
# jsonpath field extraction
- kind: Deployment
name: my-service
namespace: default
field: .spec.template.spec.containers[0].resources.requests.cpu
equals: 200m
# full JSON output with jq extraction
- kind: ResourceQuota
name: my-service-quota
namespace: default
format: json
jq: .status.hard.pods
equals: "10"
# full YAML output with yq extraction
- kind: ConfigMap
name: my-config
namespace: default
format: yaml
yq: .data.maxConnections
outputContains: "100"
| Field | Required | Description |
|---|---|---|
kind | yes | Kubernetes resource kind |
name | yes | Resource name |
namespace | no | Namespace. Default: default |
field | no | jsonpath expression to extract. e.g. .spec.replicas |
format | no | json or yaml — returns the full resource. Ignored when field is set |
jq | no | jq expression applied to output before asserting. Requires format: json |
yq | no | yq expression applied to output before asserting. Requires format: yaml |
kubectl.logs
Generates: kubectl logs -n <ns> [-l <selector> | <name>] [-c <container>] [--since=<since>]
kubectl:
logs:
# assert a log line exists
- labelSelector: app=my-service
namespace: default
since: 30s
outputContains: "server started on port 8080"
# assert no error logs (JSON structured logging)
- labelSelector: app=my-service
namespace: default
jq: .level
outputNotContains: error
# assert exact log message in a named pod
- name: my-service-abc123
container: sidecar
outputContains: "config reloaded"
| Field | Required | Description |
|---|---|---|
name | no | Pod name. Use labelSelector to match by label instead |
labelSelector | no | Label selector (e.g. app=my-service). One of name or labelSelector required |
namespace | no | Namespace. Default: default |
container | no | Container name. Defaults to the first container |
since | no | Limit output to logs from the last duration (e.g. 30s, 2m) |
jq | no | jq expression applied to each log line. Useful for JSON-structured logs |
kubectl.describe
Generates: kubectl describe <kind> [-n <ns>] [<name> | -l <selector>]
Useful for asserting Kubernetes events, conditions, and resource details that don’t appear in structured fields.
kubectl:
describe:
# assert image was pulled successfully
- kind: Pod
labelSelector: app=my-service
namespace: default
outputContains: "Successfully pulled image"
# assert no crash events
- kind: Pod
labelSelector: app=my-service
namespace: default
outputNotContains: "Back-off restarting failed container"
| Field | Required | Description |
|---|---|---|
kind | yes | Kubernetes resource kind |
name | no | Resource name. Use labelSelector to match by label instead |
labelSelector | no | Label selector |
namespace | no | Namespace. Default: default |
kubectl.exec
Generates: kubectl exec -n <ns> <pod> [-c <container>] -- <command>
kubectl:
exec:
# verify a config file was mounted correctly
- labelSelector: app=my-service
namespace: default
command: [cat, /etc/config/app.conf]
outputContains: "maxConnections=100"
# verify a secret is accessible inside the container
- labelSelector: app=my-service
namespace: default
container: app
command: [sh, -c, "echo $DB_PASSWORD"]
outputNotContains: ""
| Field | Required | Description |
|---|---|---|
name | no | Pod name. Use labelSelector to match by label instead |
labelSelector | no | Label selector. One of name or labelSelector required |
namespace | no | Namespace. Default: default |
container | no | Container name. Defaults to the first container |
command | yes | Command to run as a list (no shell interpolation) |
jq | no | jq expression applied to the output before asserting |
yq | no | yq expression applied to the output before asserting |
kubectl.port-forward
Opens a port-forward to a service or pod, makes an HTTP request via curl, and asserts the response. The runner manages the port-forward lifecycle — background start, port-open polling, curl, cleanup. No shell scripting required.
curl, jq, and yq are installed automatically if not present when detected in the spec.
kubectl:
port-forward:
# assert Orkestra introspection API response
- service: orkestra-runtime
namespace: orkestra-system
port: 8080
path: /katalog/service
jq: .workers
equals: "1"
# assert a YAML API endpoint
- service: my-api
namespace: default
port: 9090
path: /config
method: GET
yq: .maxConnections
outputContains: "100"
# just assert the HTTP endpoint responds
- service: my-api
namespace: default
port: 9090
path: /healthz
outputContains: "ok"
| Field | Required | Description |
|---|---|---|
service | no | Service name to port-forward to. One of service or pod required |
pod | no | Pod name to port-forward to |
namespace | no | Namespace. Default: default |
port | yes | Port to forward (used as both local and remote) |
path | no | HTTP path to request via curl after port-forward is ready |
method | no | HTTP method. Default: GET |
jq | no | jq expression applied to the response before asserting |
yq | no | yq expression applied to the response before asserting |
kubectl.apply
Applies manifests during an expect checkpoint. Use file to reference a path on disk or inline to embed the manifest directly. kubectl apply is idempotent so re-running inside the poll loop is safe.
Generates: kubectl apply -f <file> or echo '<inline>' | kubectl apply -f -
kubectl:
apply:
# apply a file relative to the e2e.yaml directory
- file: ./fixtures/v2-cr.yaml
# apply an inline manifest
- inline: |
apiVersion: v1
kind: ConfigMap
metadata:
name: feature-flags
namespace: default
data:
v2: enabled
# apply with a namespace override
- file: ./fixtures/tenant-quota.yaml
namespace: team-alpha
| Field | Required | Description |
|---|---|---|
file | no | Path to a manifest file. Relative paths resolve from the e2e.yaml directory. Mutually exclusive with inline |
inline | no | Raw YAML or JSON manifest applied via stdin. Mutually exclusive with file |
namespace | no | Namespace override for resources that don’t declare one |
kubectl.patch
Patches a Kubernetes resource in-place. Useful for triggering state transitions — driving a state machine forward, updating a field to test a reconciler’s reaction, etc.
Generates: kubectl patch <kind> <name> -n <namespace> --type=<type> -p '<patch>'
kubectl:
patch:
# merge patch (default) — scale up replicas
- kind: Deployment
name: my-service
namespace: default
patch: '{"spec":{"replicas":3}}'
# strategic merge patch — update a container image
- kind: Deployment
name: my-service
namespace: default
type: strategic
patch: |
spec:
template:
spec:
containers:
- name: app
image: my-service:v2
# json patch — set a specific field by path
- kind: MyResource
name: my-resource
namespace: default
type: json
patch: '[{"op":"replace","path":"/spec/phase","value":"active"}]'
| Field | Required | Description |
|---|---|---|
kind | yes | Kubernetes resource kind |
name | yes | Resource name |
namespace | no | Namespace. Default: default |
type | no | Patch strategy: merge (default), strategic, or json |
patch | yes | Patch content as a YAML or JSON string |
kubectl.events
Lists Kubernetes events for a specific resource and asserts the output. Useful for verifying that the operator emitted expected events or that no error events occurred.
Generates: kubectl events --for=<kind>/<name> -n <namespace>
kubectl:
events:
# assert the operator emitted a Reconciled event
- kind: Deployment
name: my-service
namespace: default
outputContains: Reconciled
# assert no BackOff events occurred
- kind: Pod
name: my-service-abc123
namespace: default
outputNotContains: BackOff
| Field | Required | Description |
|---|---|---|
kind | yes | Kubernetes resource kind |
name | yes | Resource name |
namespace | no | Namespace. Default: default |
kubectl.auth
Checks permissions via kubectl auth can-i and asserts the result (yes or no). Useful for verifying that the operator created the correct RBAC resources — ServiceAccounts, ClusterRoles, ClusterRoleBindings.
Generates: kubectl auth can-i <verb> <resource> [-n <namespace>] [--as <as>]
kubectl:
auth:
# assert the operator's service account can list pods
- verb: list
resource: pods
namespace: default
as: system:serviceaccount:default:my-operator
equals: "yes"
# assert it cannot delete secrets (principle of least privilege)
- verb: delete
resource: secrets
namespace: default
as: system:serviceaccount:default:my-operator
equals: "no"
| Field | Required | Description |
|---|---|---|
verb | yes | Action to check: get, list, create, delete, patch, etc. |
resource | yes | Kubernetes resource type: pods, deployments, secrets, etc. |
namespace | no | Namespace scope. Omit for cluster-scoped checks |
as | no | User or service account to impersonate. Use system:serviceaccount:<ns>:<name> form |
kubectl.cp
Copies a file out of a running container and asserts its content. Resolves the pod by name or label selector, copies to a temporary path, applies assertions, and cleans up. Supports jq and yq extraction for structured file content.
Generates: kubectl cp <ns>/<pod>:<src> <tempfile>
kubectl:
cp:
# assert a generated config file contains the expected value
- labelSelector: app=my-service
namespace: default
src: /etc/config/app.conf
outputContains: "maxConnections=100"
# assert a JSON file field via jq
- labelSelector: app=my-service
namespace: default
src: /etc/config/settings.json
jq: .database.host
equals: "postgres.default.svc"
# assert from a named pod with a specific container
- name: my-service-abc123
container: app
namespace: default
src: /tmp/generated-cert.pem
outputContains: "BEGIN CERTIFICATE"
| Field | Required | Description |
|---|---|---|
name | no | Pod name. Use labelSelector to match by label instead |
labelSelector | no | Label selector. One of name or labelSelector required |
namespace | no | Namespace. Default: default |
container | no | Container name. Defaults to the first container |
src | yes | Path inside the container to copy from |
jq | no | jq expression applied to the file content before asserting |
yq | no | yq expression applied to the file content before asserting |
kubectl.top
Queries live CPU and memory usage via kubectl top and asserts the output. Requires metrics-server; the runner installs it automatically via Helm when any top entry is present. On kind clusters, --kubelet-insecure-tls is set automatically.
Generates: kubectl top <kind> [-n <namespace>] [<name> | -l <selector>] [--containers]
kubectl:
top:
# assert both probe pods appear in metrics output
- kind: pod
namespace: default
labelSelector: app=my-service
outputContains: my-service
# assert a specific pod's metrics row is present
- kind: pod
name: my-service-abc123
namespace: default
outputContains: my-service-abc123
# per-container breakdown
- kind: pod
namespace: default
labelSelector: app=my-service
containers: true
outputContains: app
# assert node metrics are available
- kind: node
outputContains: cpu
| Field | Required | Description |
|---|---|---|
kind | yes | Resource type: pod (or pods) or node (or nodes) |
name | no | Pod or node name. Omit to list all |
labelSelector | no | Filter pods by label. Applies to pods only |
namespace | no | Namespace. Applies to pods only. Default: default |
containers | no | Show per-container metrics (--containers). Pods only |
Tool pre-flight
When ork e2e loads the spec, it scans for tool requirements and installs missing ones before assertions run:
| Tool | Required when | Installed via |
|---|---|---|
curl | Any port-forward entry has a path | apt-get / apk / brew |
jq | Any entry has a jq: field | apt-get / apk / brew |
yq | Any entry has a yq: field | apt-get / apk / brew |
metrics-server | Any top entry is present | Helm (../metrics-server/metrics-server) |
Installation is automatic. A spinner shows progress. On kind clusters, metrics-server is installed with --kubelet-insecure-tls automatically.
Combining with resources: and commands:
All three blocks work together in the same checkpoint:
expect:
- name: Service is healthy and correctly configured
after: cr-applied
timeout: 90s
resources:
- kind: Deployment
name: my-service
namespace: default
ready: true
kubectl:
get:
- kind: Deployment
name: my-service
field: .spec.template.spec.containers[0].resources.requests.cpu
equals: 200m
logs:
- labelSelector: app=my-service
outputContains: "ready to serve"
outputNotContains: FATAL
commands:
- run: "curl -sf http://my-service:8080/healthz"
outputContains: ok
→ Back: 06-discovery | Schema index