How borescope works
borescope is three thin, independently-testable layers. Understanding the seams helps you read its behaviour when something goes wrong.
Three layers
borescope is built as three layers, each with a narrow job and a clean seam to the next:
- Transport: talk to a Pebble.
- Discovery: find the right Pebble.
- Shell: give you a prompt over it.
A session flows top-down: discovery turns your unit reference into a target, the transport opens a connection to that target's Pebble, and the shell runs a REPL whose commands call the transport.
Transport
The transport is the only code that touches Pebble. Everything above it
talks to a narrow Transport interface, a structural subset of
ops.pebble.Client (services, plan, changes, checks, notices, and the files
and exec APIs). There are two backends, both satisfying that interface:
CliTransport(the v1 default) drives the workload'spebblebinary through the charm container overjuju ssh, using shimmer, a drop-inops.pebble.Clientimplemented over the Pebble CLI.SocketTransportuses the realops.pebble.ClientHTTP API directly, when the Pebble socket is reachable (inside a charm, or a local Pebble).
Because the shell only ever sees the Transport interface, the backend choice
is invisible to it, and a future reimplementation of just this layer (in Go,
say) would sit entirely behind the same seam. How it reaches
Pebble covers the backends in detail.
Discovery
Discovery turns a unit reference (myapp/0), plus optional --container and
--model, into a fully-resolved target describing exactly which workload
container's Pebble to talk to. It:
- parses and validates the unit reference;
- reads
juju statusto confirm the model is Kubernetes and the unit exists; - reads the charm's
metadata.yamlfrom the charm container to learn the workload container names; - sanity-checks that the chosen container's Pebble answers and is new enough.
Crucially, everything here uses only your Juju model access (juju status
and juju ssh to read metadata). Never kubectl, never cluster-admin. borescope
inherits Juju's authority for free: if you can reach the unit with Juju,
discovery succeeds; if you can't, it fails with the same boundary Juju would
enforce.
When borescope runs inside a charm container (--here) or against an explicit
socket (--socket), discovery short-circuits: there's no unit to resolve, just
a socket to point at.
Shell
The shell is a small REPL: a line parser, a current-directory and environment
context, path-aware Tab-completion, per-unit history, and a registry of
commands. Commands are auto-discovered, each subclasses a common Command
base, declaring its name, summary, and usage, so adding one is a small,
self-contained change with no registration boilerplate.
The command set splits into three groups: shell built-ins (cd, pwd, …),
file commands implemented over the files API (ls, cat, grep, …), and
Pebble-native commands (services, logs, plan, …). The
exec command is the escape hatch for
everything else. See the command reference.
Why the separation
The layering isn't ceremony. It earns its keep:
- Testability. Discovery's argv construction and status parsing are tested with mocks, needing no live Juju or cluster. The shell is tested against a fake transport. Each layer is exercised in isolation.
- Authority containment. Only discovery and the CLI transport invoke
juju; only the transport speaks Pebble. The blast radius of each concern is small and obvious. - A swappable backend. Keeping the
Transportinterface narrow means the primary backend can change, or be rewritten in another language, without the shell noticing.