Skip to content

Quickstart

Everything below acts on one file. Read it first — building something you have not seen is how charly stops making sense three commands in.

This is tutorial-shell — a real box in the repository, and a check bed re-proves it on every acceptance run. It composes three layers on the bare Fedora base: a tool (ripgrep), a service (sshd), and the supervisord init.

Four things are going on, and they are the whole model:

  • candy: is the only entity keyword. This node carries base:, which makes it a box — a buildable container image. A node without base: would be a layer instead.
  • base: fedora points at another box defined next door, not an external registry image. A base can be either.
  • the candy: list composes three layers: a tool (ripgrep), a service (sshd), and the supervisord init. Each installs one concern; composing them is what turns the bare Fedora base into a working shell.
  • plan: is the acceptance spec, and it is mandatory. Note what it checks: not that rg and sshd are present — each candy’s own plan already proves that, and those plans run against this same image — but that composing the service candy next to the init candy made sshd a supervisord program. A check belongs on the behaviour’s provider; it belongs on the composing box only when the claim is about the composition itself.

New to the vocabulary? The words defines candy, box and candybox in five minutes.

Terminal window
charly --repo opencharly/distro-fedora box validate # the schema gate — silence is the pass
charly --repo opencharly/distro-fedora box build tutorial-shell

box build resolves the candy graph, generates a multi-stage Containerfile, and builds the image. The acceptance plan of every composed candy is baked into the result as an OCI label, so the image can be tested later without this repository.

The --repo opencharly/distro-fedora says which project to read, fetched and cached for you — so this works with nothing but the charly binary installed, no clone required. If you do have the repository checked out, -C box/fedora points at the same project on disk.

Terminal window
charly --repo opencharly/distro-fedora shell tutorial-shell

You are now inside the candybox — the running, isolated form of that box. There is no command filter: dnf works, rg works, anything you install works. It runs rootless at uid 1000 with no --privileged. Why that is the safe arrangement →

Terminal window
charly --repo opencharly/distro-fedora check box tutorial-shell # run the baked plan against the image
24 steps: 19 passed, 0 failed, 5 skipped

The five skips are three different things, and the distinction is worth reading:

Skipped Why
sshd-service-running, sshd-port-reachable, and supervisord’s control-socket check context: [runtime] — a service cannot be running inside an image
a run: step that grants the uid-1000 account passwordless sudo it mutates, and check box is verify-only
an agent-check: step it is graded by an agent, and no grader is bound here

Only the first group needs a deployment. That is what the bed provides:

Terminal window
charly --repo opencharly/distro-fedora check run check-tutorial-shell
[image-build] PASS after 36s
[check-image] PASS after 17s
[deploy-add] PASS after 16s
[start] PASS after 9s
[check-live] PASS after 16s
...
[update] PASS after 63s
[check-live-rebuild] PASS after 16s
[cleanup] PASS after 6s
PASS (steps=13)

Eight of the thirteen steps are shown; ... marks the elided ones (config, bring-up-members, feature-run, feature-run-rebuild, cleanup-members).

One command: build, deploy, bring to steady state, run the plan, then destroy and rebuild from scratch and run it again, then tear everything down. That second check-live-rebuild is the interesting one — it is what separates “it worked once” from “it reproduces”.

Rebuilding from scratch on every run is affordable only because the deploy is declared disposable: true — charly may destroy it without asking, so proving reproducibility costs one flag rather than a separate environment.

The same candies are not tied to containers. A local: deploy applies them to a machine — and because that means real packages and real units on whatever it targets, this repository demonstrates it against a disposable VM guest rather than a workstation:

# charly.yml — a local: deploy nested INSIDE a disposable VM guest, so the
# "machine" it changes is the guest and never yours
check-group:
vm:
from: eval-vm
disposable: true
lifecycle: dev
check-group-member:
local:
from: check-group-app

Tree position is what makes it safe: because check-group-member is a member of the vm: node (check-group) node, the local: deploy lands inside the guest rather than on the host. It carries no host: field at all — the venue comes from its parent. check-group-app is itself a local: template composing one candy that drops a marker file, and the bed asserts that marker exists in the guest.

Point a local: deploy at your own machine only when you actually mean to change your own machine.

Swap the substrate and the same list installs into a VM guest over SSH, generates Kubernetes manifests, or installs apps onto a phone. No second vocabulary.

One recipe, many molds →