Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Day-2 ops: status, logs, k get/apply/exec

This is the chapter to read after the cluster is up and BNK is deployed and you’re now living with the result. It opens with roksbnkctl status — the workspace-level read of what’s deployed — then covers the per-resource verbs: read pod state, tail logs, apply a manifest, port-forward to a service, exec into a pod. Sprint 2 internalises all the per-resource verbs into native Go via client-go so you no longer need kubectl on PATH for the everyday workflow.

The full design rationale lives in PRD 02. This chapter is the user-facing surface — the canonical “what’s the kubectl-equivalent in roksbnkctl?” reference.

Why internalise

Three reasons, in order of weight:

  1. Single binary. roksbnkctl is meant to be the one thing you install. After Sprint 2, the only required external prerequisite for the happy path is terraform. Everything else — kubectl, oc, iperf3, dig — is either built-in or an optional escape hatch.
  2. No version skew. The vendored client-go matches the kube API the bundled HCL targets. You can’t accidentally use kubectl 1.20 against a 1.28 cluster and have its print column heuristics go sideways.
  3. First-class output formats. cli-runtime gives byte-identical -o yaml/-o json/-o jsonpath output to kubectl. The validator agent’s golden-file tests in internal/k8s/golden_test.go assert this for representative resources.

roksbnkctl status

The one-shot read of a workspace’s posture. Always best-effort — every section reports its own missing pieces so a partial state still produces useful output rather than a hard error.

$ roksbnkctl status
Workspace:        canada-roks
Region:           ca-tor
Resource group:   default
Cluster:          canada-roks  (attach existing)
TF source:        jgruberf5/ibmcloud_terraform_bigip_next_for_kubernetes_2_3@v1.3.0
Cluster phase:    deployed (last apply 2026-05-13 14:08:33 MST)
BNK trial:        deployed (last apply 2026-05-13 14:15:01 MST)
Kubeconfig:       /home/you/.kube/config
Cluster:          2/2 nodes ready

The header rows (workspace, region, resource group, cluster identity, TF source pin, kubeconfig path, cluster reachability) are the same across every workspace shape. The per-phase deployment lines below them are shape-dependent: roksbnkctl status reads each phase’s terraform.tfstate mtime independently and emits one line per phase. The mapping from workspace shape to per-phase output:

The two Cluster: lines are by design: the first (in the header block) reports cluster identity — which cluster you’re targeting and whether the workspace creates it or attaches to an existing one. The second (the trailer line) reports cluster reachability — node count and ready count from a live API call. The label is reused because both pieces of information are about “the cluster”; the column to the right disambiguates.

TF source: reflects the workspace’s tf_source.type: github renders as <Repo>@<Ref> (the canonical happy-path shape since Sprint 5 — e.g., jgruberf5/ibmcloud_terraform_bigip_next_for_kubernetes_2_3@v1.3.0); local renders as local:<Path>; embedded or unset renders as (unset). The samples below use the github shape since it’s what most readers will see.

ShapeEmpty — fresh workspace, neither phase deployed

$ roksbnkctl status
Workspace:        dev
Region:           us-south
Resource group:   default
Cluster:          (unset)  (attach existing)
TF source:        jgruberf5/ibmcloud_terraform_bigip_next_for_kubernetes_2_3@v1.3.0
Cluster phase:    not deployed
BNK trial:        not deployed
Kubeconfig:       (none — run `roksbnkctl kubeconfig --download`)

Both phases report not deployed — the state directories either don’t exist or hold zero-resource state files. Running roksbnkctl cluster up (or roksbnkctl up for the monolithic path) advances the workspace to ShapeClusterOnly.

ShapeClusterOnly — cluster phase deployed, no BNK trial yet

$ roksbnkctl status
Workspace:        canada-roks
Region:           ca-tor
Resource group:   default
Cluster:          canada-roks  (attach existing)
TF source:        jgruberf5/ibmcloud_terraform_bigip_next_for_kubernetes_2_3@v1.3.0
Cluster phase:    deployed (last apply 2026-05-13 14:08:33 MST)
BNK trial:        not deployed
Kubeconfig:       /home/you/.kube/config
Cluster:          2/2 nodes ready

The Cluster phase line reads the mtime of <state-cluster-dir>/terraform.tfstate; the BNK trial line reads <state-dir>/terraform.tfstate and falls back to not deployed when the trial state is empty or missing. Running roksbnkctl bnk up advances the workspace to ShapeSplit.

ShapeSplit — both phases deployed (the v1.1+ steady state)

$ roksbnkctl status
Workspace:        canada-roks
Region:           ca-tor
Resource group:   default
Cluster:          canada-roks  (attach existing)
TF source:        jgruberf5/ibmcloud_terraform_bigip_next_for_kubernetes_2_3@v1.3.0
Cluster phase:    deployed (last apply 2026-05-13 14:08:33 MST)
BNK trial:        deployed (last apply 2026-05-13 14:15:01 MST)
Kubeconfig:       /home/you/.kube/config
Cluster:          2/2 nodes ready

Each phase has its own mtime; the timestamps move independently. Re-running roksbnkctl bnk down then roksbnkctl bnk up updates the BNK trial line without touching the Cluster phase line — useful for confirming which phase you most recently exercised.

ShapeLegacySingle — v1.0.x workspace, cluster + trial in one tfstate

$ roksbnkctl status
Workspace:        legacy-canada
Region:           ca-tor
Resource group:   default
Cluster:          canada-roks  (attach existing)
TF source:        (unset)
Shape:            legacy single-state (cluster + trial in one tfstate)
Last apply:       2026-05-13 14:15:01 MST  (4h22m18s ago)
Kubeconfig:       /home/you/.kube/config
Cluster:          2/2 nodes ready

Script-compat note. ShapeLegacySingle preserves the v1.0.x Last apply: line verbatim. Scripts that parsed roksbnkctl status output for the Last apply line on a legacy workspace continue to work unchanged. New script targets should switch to the per-phase Cluster phase: / BNK trial: lines (or to roksbnkctl cluster show + bnk show for a structured read); the per-phase lines are emitted for ShapeEmpty, ShapeClusterOnly, and ShapeSplit, not for ShapeLegacySingle. The Shape: line is a one-line callout so you don’t have to grep Chapter 8 to figure out which shape you’re on.

The shape detection logic lives in internal/config/tfstate.go::DetectShape; the per-phase emission in runStatus is in internal/cli/inspect.go. See PRD 06 §“status command integration” for the design rationale.

The k command tree

All internalised verbs live under roksbnkctl k:

roksbnkctl k get          # fetch resources
roksbnkctl k describe     # human-readable detail
roksbnkctl k apply        # server-side apply from file/dir
roksbnkctl k delete       # delete with cascade options
roksbnkctl k logs         # pod or component logs
roksbnkctl k exec         # exec into a pod (SPDY)
roksbnkctl k port-forward # forward local ports to a pod (SPDY)

Two of those have top-level shortcuts for muscle-memory convenience — the verbs you’d type a hundred times a day:

roksbnkctl get  ↔  roksbnkctl k get
roksbnkctl logs ↔  roksbnkctl k logs

apply, exec, delete, describe, and port-forward only work under the k prefix.

Two verbs are deliberately not aliased to avoid shadowing existing top-level commands:

  • roksbnkctl apply is the existing top-level lifecycle verb that runs terraform apply against the workspace (Sprint 0/1 surface). Adding a second apply would shadow it and break roksbnkctl up / roksbnkctl apply muscle memory. Use roksbnkctl k apply -f ... explicitly for the Kubernetes-side server-side apply.
  • roksbnkctl exec runs a command on the host with the workspace’s env loaded (Sprint 1’s host-exec verb — see Chapter 16, specifically the “Working examples” section). roksbnkctl k exec runs in a pod. The split keeps both meanings unambiguous without surprising name-collision behaviour.

kubectl/oc passthroughs stay as escape hatches

The existing roksbnkctl kubectl <args...> and roksbnkctl oc <args...> passthroughs are preserved post-Sprint 2. They still shell out to the host binary (with the workspace’s KUBECONFIG and credentials loaded) for anything outside the internalised subset.

When to reach for the passthrough:

Use caseWhy passthrough
kubectl rollout (status/history/undo/restart)Out of scope for v1.0; PRD 02 explicitly defers
kubectl scale / kubectl autoscaleOut of scope; passthrough is fine
kubectl edit / kubectl patchLow frequency for BNK ops; out of scope for v1.0
kubectl auth can-i / RBAC introspectionOut of scope; passthrough is fine
kubectl drain / cordon / taintCluster admin operations; not roksbnkctl’s role
kubectl run / kubectl createImperative resource creation; use k apply -f instead
oc adm / oc image / OpenShift admin verbsNiche enough to defer; passthrough
Niche flag combosAnything not in the internalised verb’s flag set

If kubectl is missing from PATH, the passthrough errors with:

Error: kubectl not on PATH; use `roksbnkctl k get/apply/...` for the in-process path,
       or install kubectl

Same for oc. The doctor check (post-Sprint 2) treats both as informational rather than warnings — see Chapter 5 — Doctor.

Worked examples

The verbs in everyday order. Every example below assumes roksbnkctl k and accepts the top-level alias where one exists.

roksbnkctl k get

The most-used verb. Resource type + optional name + optional flags:

# All pods in the default namespace
roksbnkctl k get pods

# Pods in a specific namespace
roksbnkctl k get pods -n f5-bnk

# Pods across all namespaces
roksbnkctl k get pods -A

# A specific pod by name
roksbnkctl k get pod flo-controller-abc123 -n f5-bnk

# Label selector
roksbnkctl k get pods -A -l app.kubernetes.io/name=f5-lifecycle-operator

# Cluster-scoped resources (no namespace)
roksbnkctl k get nodes
roksbnkctl k get storageclasses

Output formats — these match kubectl byte-for-byte:

roksbnkctl k get pods -n f5-bnk -o yaml
roksbnkctl k get pods -n f5-bnk -o json
roksbnkctl k get pods -n f5-bnk -o wide
roksbnkctl k get pods -n f5-bnk -o name
roksbnkctl k get pods -n f5-bnk -o jsonpath='{.items[*].metadata.name}'
roksbnkctl k get pods -n f5-bnk -o go-template='{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}'

Plural / singular / shortname handling comes from the cluster’s RESTMapper via the discovery client, so pod, pods, po all work and pick up CRDs without a hardcoded list. roksbnkctl k get cneinstances (a BNK CRD) works as soon as the CRD is registered with the API server — no rebuild required.

Using the top-level alias:

roksbnkctl get pods -A

roksbnkctl k describe

Delegates to k8s.io/kubectl/pkg/describe — the same library kubectl uses internally. Output is identical to kubectl describe:

roksbnkctl k describe pod flo-controller-abc123 -n f5-bnk
roksbnkctl k describe node 10.243.0.4
roksbnkctl k describe service flo-webhook -n f5-bnk
roksbnkctl k describe cneinstance my-instance -n f5-bnk

The describe output’s “Events” section is especially useful for debugging stuck resources — pod scheduling failures, image pull errors, finaliser hangs all surface here.

roksbnkctl k apply

Server-side apply (SSA) with field-manager roksbnkctl. Inputs:

# Single file
roksbnkctl k apply -f pod.yaml

# Directory of YAMLs (recurses *.yaml)
roksbnkctl k apply -f manifests/

# Kustomize base (auto-detected if kustomization.yaml is present)
roksbnkctl k apply -f my-kustomize-base/

# stdin
cat pod.yaml | roksbnkctl k apply -f -

# Apply into a specific namespace (overrides metadata.namespace if absent)
roksbnkctl k apply -f manifests/ -n f5-bnk

# Force conflicts (SSA force-conflicts=true)
roksbnkctl k apply -f manifests/ --force

There is no top-level roksbnkctl apply alias for this verb — roksbnkctl apply is the lifecycle command that runs terraform apply. Always use roksbnkctl k apply for the Kubernetes-side apply.

Differences from kubectl apply:

  • Always SSA. Field manager is roksbnkctl. Client-side apply is not supported.
  • Kustomize auto-detect. A directory containing kustomization.yaml is built via sigs.k8s.io/kustomize/api before applying — no -k flag needed.
  • --force maps to SSA’s force-conflicts=true. Without it, conflicts with another field manager produce a clean error rather than silently winning.

For a vanilla kubectl apply -f workflow, the behaviour is functionally identical. For workflows that depend on client-side three-way merge or specific --server-side flag combinations, fall back to the passthrough.

roksbnkctl k delete

Cascade-aware deletion via the dynamic client:

# Delete by name
roksbnkctl k delete pod flo-controller-abc123 -n f5-bnk

# Cascade: orphan, background (default), foreground
roksbnkctl k delete deployment flo -n f5-bnk --cascade=foreground

# Force (bypass graceful deletion; immediate)
roksbnkctl k delete pod stuck-pod -n f5-bnk --force

# Custom grace period (seconds)
roksbnkctl k delete pod my-pod -n f5-bnk --grace-period=5

Use --cascade=foreground when you want to wait for owned resources (Pods owned by a Deployment, etc.) to be deleted before the parent disappears — useful for tearing down BNK trial CRs cleanly so finalisers run in order.

roksbnkctl k logs and roksbnkctl logs

Two paths, one verb. The component-aware path was introduced in Sprint 1 for BNK-specific workflows; the raw pod-name path is new in Sprint 2.

Component-aware (existing — by label selector):

roksbnkctl logs flo                # F5 Lifecycle Operator (label selector under the hood)
roksbnkctl logs cis                # F5 BNK CIS controller
roksbnkctl logs cert-manager       # cert-manager
roksbnkctl logs cneinstance        # BIG-IP TMM data plane pods

Raw pod-name (new in Sprint 2):

roksbnkctl k logs flo-controller-abc123 -n f5-bnk

Common flags (both paths):

-f, --follow              # stream live (kubectl logs -f)
-c, --container <name>    # specific container in a multi-container pod
--previous                # logs from the previous instance (after a crash)
--since=10m               # only logs in the last 10 minutes
--tail=100                # last N lines only

Top-level alias:

roksbnkctl logs flo -f --since=5m

If the named first arg matches one of the well-known BNK components (flo, cis, cert-manager, cneinstance), the component-aware path is used; otherwise it’s treated as a pod name. The component map lives in internal/cli/inspect.go and is keyed off the upstream chart’s default labels.

roksbnkctl k exec

SPDY exec into a pod. Same semantics as kubectl exec:

# One-shot command
roksbnkctl k exec flo-controller-abc123 -n f5-bnk -- ls -la /

# stdin attached
roksbnkctl k exec flo-controller-abc123 -n f5-bnk -i -- cat /etc/hostname

# Interactive PTY (the bash-style use)
roksbnkctl k exec flo-controller-abc123 -n f5-bnk -i -t -- bash

# Specific container in a multi-container pod
roksbnkctl k exec flo-controller-abc123 -n f5-bnk -c sidecar -- env

The -i and -t flags map directly to kubectl exec’s -i (stdin) and -t (PTY). For top / bash / interactive Python sessions, pass both.

There is no roksbnkctl exec (top-level) alias — roksbnkctl exec runs on the host. See “Disambiguating roksbnkctl exec” in PRD 02.

roksbnkctl k port-forward

SPDY port-forward to a pod:

# Forward localhost:8080 → pod's :80
roksbnkctl k port-forward flo-controller-abc123 -n f5-bnk 8080:80

# Multiple ports
roksbnkctl k port-forward flo-controller-abc123 -n f5-bnk 8080:80 8443:443

# Random local port (let the kernel pick)
roksbnkctl k port-forward flo-controller-abc123 -n f5-bnk :80

Ctrl+C closes the tunnel cleanly — no orphaned local listeners. The forward survives idle (reads/writes are bidirectional); it’s torn down only on signal or pod restart.

For a Service rather than a Pod, port-forward via the Service’s underlying pod or use kubectl port-forward svc/<name> through the passthrough — Service-targeted port-forwarding is currently passthrough-only.

Output format compatibility

The biggest user-visible promise: -o yaml / -o json / -o wide / -o jsonpath produce the same bytes as kubectl, modulo a small set of timestamp-and-resourceVersion fields that change between calls.

Concretely, the validator agent’s golden-file tests at internal/k8s/golden_test.go capture kubectl get <resource> -o yaml and roksbnkctl k get <resource> -o yaml against a live ROKS cluster and diff them, ignoring:

  • metadata.managedFields (ordering varies between callers; not user-visible)
  • metadata.resourceVersion (monotonic counter; changes on every read)
  • metadata.creationTimestamp (set server-side; not under our control)

Anything else differing is a test failure. The covered resources at v1.0 are Node, Pod, Service, ConfigMap — representative both of cluster-scoped (Node) and namespace-scoped (Pod, Service, ConfigMap), and of the typed-client (Node, Pod, Service) and dynamic-client (anything via cli-runtime’s resource.Builder) paths.

Run them locally with:

make test-live

…against a KUBECONFIG that points at a real ROKS cluster. They’re not part of the unit-test CI run because they need a live cluster; the integrator runs them before tagging a release. Documented in CONTRIBUTING.md.

OpenShift extensions

Beyond the core kubectl-equivalent verbs, ROKS clusters surface OpenShift-specific resource types — Project, Route, ImageStream, BuildConfig. roksbnkctl k get discovers these natively today via the dynamic client + RESTMapper path (the cluster advertises them through the API discovery doc; the deferred-discovery mapper picks them up):

roksbnkctl k get projects                    # OpenShift projects (vs Kubernetes namespaces)
roksbnkctl k get routes -n f5-bnk            # OpenShift Routes (vs Ingress)
roksbnkctl k get imagestreams -n f5-bnk      # OpenShift ImageStreams
roksbnkctl k get buildconfigs                # BuildConfigs (mostly empty in BNK trials)

Same verb shape (get / describe / delete); the dynamic-client + RESTMapper combination handles type discovery without needing a per-type Go-side scheme registration.

Phase 2.1 of PRD 02 adds typed clients via github.com/openshift/client-go for nicer printing and describe integration of these resources. This is on the v1.x roadmap (see docs/PLAN.md §“What’s deliberately deferred to post-v1.0”). Until typed clients land, roksbnkctl k get/describe still works against OpenShift CRDs — just with the generic unstructured printer. If you want richer per-type output today, fall back to the oc passthrough:

roksbnkctl oc get projects                   # typed-client output today
roksbnkctl oc describe route f5-bnk-svc      # typed Route fields

Doctor change recap

A reminder of what changed in Sprint 2’s doctor (covered in Chapter 5):

  • kubectl — was “needed (warning when missing)”; now informational (no warning when missing).
  • oc — same downgrade.

A fresh dev box without kubectl / oc installed should run roksbnkctl doctor and see green-or-informational across the board for the everyday workflow. The host-binary requirement is gone; the binaries are nice-to-have for the passthroughs.

kubectl muscle-memory cheat sheet

A reader migrating from kubectl should be able to use this section as a Rosetta Stone:

kubectl ...roksbnkctl ...
kubectl get podsroksbnkctl get pods (or roksbnkctl k get pods)
kubectl get pods -Aroksbnkctl get pods -A
kubectl get pods -o yamlroksbnkctl get pods -o yaml
kubectl describe pod <name>roksbnkctl k describe pod <name>
kubectl apply -f manifests/roksbnkctl k apply -f manifests/
kubectl apply -k overlay/roksbnkctl k apply -f overlay/ (auto-detects kustomize)
kubectl delete pod <name>roksbnkctl k delete pod <name>
kubectl logs <pod> -froksbnkctl logs <pod> -f (or roksbnkctl k logs <pod> -f)
kubectl exec -it <pod> -- bashroksbnkctl k exec <pod> -i -t -- bash
kubectl port-forward <pod> 8080:80roksbnkctl k port-forward <pod> 8080:80
kubectl rollout status deploy/fooroksbnkctl kubectl rollout status deploy/foo (passthrough)
kubectl edit deployment fooroksbnkctl kubectl edit deployment foo (passthrough)
kubectl scale deployment foo --replicas=3roksbnkctl kubectl scale deployment foo --replicas=3 (passthrough)
oc projectsroksbnkctl k get projects (works today via dynamic-client) or roksbnkctl oc projects for typed-client output

The general pattern: if it’s get / describe / apply / delete / logs / exec / port-forward against a typed or unstructured Kubernetes resource, the internalised verb is the right answer. Anything else, fall back to the passthrough.

Cross-references