kubectl block

9 min read

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:

FieldDescription
equalsOutput (trimmed) must exactly match this string
notEqualsOutput must not exactly match this string
outputContainsOutput must contain this substring
outputNotContainsOutput must not contain this substring
greaterThanOutput (trimmed, parsed as a number) must be greater than this value
lessThanOutput (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"
FieldRequiredDescription
kindyesKubernetes resource kind
nameyesResource name
namespacenoNamespace. Default: default
fieldnojsonpath expression to extract. e.g. .spec.replicas
formatnojson or yaml — returns the full resource. Ignored when field is set
jqnojq expression applied to output before asserting. Requires format: json
yqnoyq 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"
FieldRequiredDescription
namenoPod name. Use labelSelector to match by label instead
labelSelectornoLabel selector (e.g. app=my-service). One of name or labelSelector required
namespacenoNamespace. Default: default
containernoContainer name. Defaults to the first container
sincenoLimit output to logs from the last duration (e.g. 30s, 2m)
jqnojq 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"
FieldRequiredDescription
kindyesKubernetes resource kind
namenoResource name. Use labelSelector to match by label instead
labelSelectornoLabel selector
namespacenoNamespace. 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: ""
FieldRequiredDescription
namenoPod name. Use labelSelector to match by label instead
labelSelectornoLabel selector. One of name or labelSelector required
namespacenoNamespace. Default: default
containernoContainer name. Defaults to the first container
commandyesCommand to run as a list (no shell interpolation)
jqnojq expression applied to the output before asserting
yqnoyq 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"
FieldRequiredDescription
servicenoService name to port-forward to. One of service or pod required
podnoPod name to port-forward to
namespacenoNamespace. Default: default
portyesPort to forward (used as both local and remote)
pathnoHTTP path to request via curl after port-forward is ready
methodnoHTTP method. Default: GET
jqnojq expression applied to the response before asserting
yqnoyq 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
FieldRequiredDescription
filenoPath to a manifest file. Relative paths resolve from the e2e.yaml directory. Mutually exclusive with inline
inlinenoRaw YAML or JSON manifest applied via stdin. Mutually exclusive with file
namespacenoNamespace 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"}]'
FieldRequiredDescription
kindyesKubernetes resource kind
nameyesResource name
namespacenoNamespace. Default: default
typenoPatch strategy: merge (default), strategic, or json
patchyesPatch 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
FieldRequiredDescription
kindyesKubernetes resource kind
nameyesResource name
namespacenoNamespace. 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"
FieldRequiredDescription
verbyesAction to check: get, list, create, delete, patch, etc.
resourceyesKubernetes resource type: pods, deployments, secrets, etc.
namespacenoNamespace scope. Omit for cluster-scoped checks
asnoUser 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"
FieldRequiredDescription
namenoPod name. Use labelSelector to match by label instead
labelSelectornoLabel selector. One of name or labelSelector required
namespacenoNamespace. Default: default
containernoContainer name. Defaults to the first container
srcyesPath inside the container to copy from
jqnojq expression applied to the file content before asserting
yqnoyq 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
FieldRequiredDescription
kindyesResource type: pod (or pods) or node (or nodes)
namenoPod or node name. Omit to list all
labelSelectornoFilter pods by label. Applies to pods only
namespacenoNamespace. Applies to pods only. Default: default
containersnoShow 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:

ToolRequired whenInstalled via
curlAny port-forward entry has a pathapt-get / apk / brew
jqAny entry has a jq: fieldapt-get / apk / brew
yqAny entry has a yq: fieldapt-get / apk / brew
metrics-serverAny top entry is presentHelm (../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