Skip to content

Latest commit

 

History

History
560 lines (446 loc) · 29.7 KB

File metadata and controls

560 lines (446 loc) · 29.7 KB

Command reference

docket has eight commands. Running docket with no subcommand prints the list:

Command What it does
docket init Scaffold a starter recipe.
docket validate Check a recipe offline, without contacting a server.
docket fmt Canonically format a recipe.
docket plan Preview what apply would change.
docket apply Run the recipe, making changes as needed.
docket export Read a live server and write a recipe describing it.
docket schema Print the machine-readable catalog of task types.
docket version Print the binary's version.

apply, plan, validate, and fmt all accept either YAML or JSON5 recipes. When --tasks is omitted they probe tasks.yml, then tasks.yaml, then tasks.json, and use the first that exists. A directory holding more than one of them is ambiguous, so the probe names the one it took in a warning on stderr - tasks.yml, tasks.json both exist; using tasks.yml (pass --tasks to choose) - leaving stdout to the command's own output.

apply, plan, and validate also accept the recipe as a single positional argument (for example docket validate staging/tasks.yml); passing both a positional path and --tasks, or more than one positional path, is an error. Naming the recipe either way silences the warning.

All four also read the recipe from stdin when the path is -, so a recipe generated by another tool can be piped straight in. On apply, plan, and validate that is spelled either --tasks - or as a bare positional -; fmt takes the positional form. Any inputs: the piped recipe declares still become real --<name> flags, and a - recipe takes precedence over a tasks.yml sitting in the working directory:

docket export --output - | docket apply -
docket init --output - | docket validate -
cat tasks.yml | docket plan --tasks - --app api
docket export --output - --format json5 | docket apply --tasks-format json5 -

The format normally comes from the file extension, and for stdin from the first non-whitespace byte ([, {, //, or /* means JSON5; anything else means YAML). --tasks-format yaml|json5 overrides both. Reach for it when the extension is absent or misleading (--tasks recipe.txt, or a URL whose path carries no extension), or when a YAML recipe written in flow style would be sniffed as JSON5 because it opens with [.

--tasks-format is the reading side. On the writing side, init and export take --format yaml|json5 to state the format of what they emit. Without it, stdout can only ever be YAML, since there is no extension to infer from.

Reading the recipe from stdin consumes it, so a dokku command that would otherwise have inherited the terminal's stdin sees end-of-file instead. No task depends on this - every task that streams data to dokku supplies it explicitly.

docket init

docket init writes a starter recipe from a built-in template. It is offline only: no server contact and no git subprocess. The default scaffold ships four tasks (dokku_app, dokku_config, dokku_domains, dokku_git_sync) in a single play with app and repo inputs, and round-trips cleanly through docket validate.

The output format follows --format when given, otherwise the --output extension: .json / .json5 writes a JSON5 scaffold with // ... comments, anything else writes YAML. Streaming to stdout (--output -) has no extension to read, so it writes YAML unless --format json5 says otherwise. Passing --format json5 without an --output writes ./tasks.json rather than a JSON5 document under a .yml name.

# Use the current directory name as the app and remote.origin.url as the repo.
docket init

# Same scaffold in JSON5, written to ./tasks.json.
docket init --format json5

# Stream the scaffold to stdout for piping.
docket init --output -

# Stream a JSON5 scaffold to stdout.
docket init --output - --format json5

init closes by printing the commands to run next. Those run without --tasks and so rely on the default probe, which only reaches the new scaffold when it was written to tasks.yml. Written anywhere else (tasks.json from --format json5, or an --output in another directory) the printed commands name the file, so they cannot end up describing a stale recipe sitting beside it:

$ docket init --format json5
==> Created tasks.json (4 tasks, 1 play)

Next steps:
  $ docket validate --tasks tasks.json          # offline check
  $ docket plan --tasks tasks.json              # preview against the server
  $ docket apply --tasks tasks.json             # apply
Flag Effect
(default) Write ./tasks.yml; refuse if it already exists.
--output <path> Write to a path; - writes to stdout. Format inferred from the extension unless --format says otherwise. A stream touches no file on disk, so combining - with --force is an error rather than a silently ignored flag.
--format <fmt> Write yaml or json5 regardless of the --output extension. Without an explicit --output, --format json5 writes ./tasks.json.
--force Overwrite an existing file. Not valid with --output -.
--name <name> Set the play and app input default (defaults to the directory name).
--repo <url> Set the repo input default (defaults to remote.origin.url in ./.git/config).
--minimal A one-task example with no inputs: block.

docket validate

docket validate performs offline schema and template checks on a recipe without contacting any Dokku server. It is built for CI lint jobs that should reject a broken recipe before it reaches a deploy.

The checks cover: the file parses; the recipe shape is a list of plays; each task entry has the right envelope keys plus exactly one task-type key; the task type is registered (with a "did you mean" for typos); required fields decode; a task's conditional/semantic input rules hold (for example a list that must be non-empty when state: present, or mutually-exclusive fields) - the same checks plan and apply run, surfaced offline as invalid_task_input; each input name is a valid {{ .name }} template variable (a hyphenated name is rejected as invalid_input_name); sigil templates render against the input defaults, and an input value that would break the scalar it is substituted into is reported as unsafe_input_value (naming the input) rather than a cryptic YAML error - see special characters in values; expr predicates parse; and .item / .index are only used inside a loop:.

docket validate --tasks path/to/tasks.yml

Exit code is 0 when no problems are found, 1 otherwise.

Flag Effect
--tasks <path> Use a specific recipe. Pass - to read it from stdin.
- Read the recipe from stdin, as a positional argument.
--tasks-format <fmt> Parse the recipe as yaml or json5 instead of detecting it.
--json Emit one JSON-lines problem per line with a stable schema, for a CI annotator.
--strict Also flag any required: true input with no default and no supplied value, and verify that --play / --start-at-task references resolve to real names.
--vars-file <path> Load input values from a YAML or JSON file (repeatable). Values here count as overrides for --strict. See inputs.
--play <name> (strict only) Verify the named play exists.
--start-at-task <name> (strict only) Verify a task with this name exists; narrowed by --play.

docket fmt

docket fmt is a canonical formatter for recipes, in the spirit of gofmt. It reorders task and play keys into a stable order, normalizes indentation to a 2-space step, and inserts blank lines between top-level plays and task entries. It works on both YAML and JSON5, detected per file from the extension, and both formats share the same canonical key order so a YAML recipe and its JSON5 twin lay out identically. Comments are preserved in both formats.

# Rewrite ./tasks.yml in place (probes tasks.yml -> tasks.yaml -> tasks.json with no argument).
docket fmt

# Format a JSON5 recipe in place.
docket fmt tasks.json

# CI gate: print the diff and exit 1 if anything is not canonical.
docket fmt --check --diff

# Read from stdin, write canonical to stdout (format sniffed from the first byte).
cat tasks.yml | docket fmt -

# Same, but keep a flow-style YAML recipe as YAML instead of sniffing it as JSON5.
cat tasks.yml | docket fmt --tasks-format yaml -
Flag Effect
(default) Format the file in place; a no-op when it is already canonical.
--check Exit 1 if any file is not canonical; no writes. Composes with --diff.
--diff Print a unified diff against canonical; no writes. Composes with --check.
--color <when> Colorize the diff: auto (default), always, or never.
--tasks-format <fmt> Format the recipe as yaml or json5 instead of detecting it.
- Read from stdin, write canonical to stdout. Cannot be combined with other paths.
<path...> Format the named files; each argument is expanded as a glob.

The diff is a standard unified diff (--- / +++ / @@ headers) and applies with git apply or patch -p0 once colors are stripped. Before writing, fmt re-parses its own output and aborts if the result does not match the input, so a formatting bug can never corrupt a recipe.

fmt operates on single-document recipes. An empty or comment-only file is left untouched, and a YAML file containing more than one document (separated by ---) is rejected rather than having its trailing documents silently dropped.

docket plan

docket plan reads each task's current state from the live server and reports what apply would change, without running any mutating command. The output uses the same play header and column layout as apply, with a marker set focused on drift:

Marker Meaning
[ok] In sync; apply would change nothing.
[+] apply would create new state.
[~] apply would modify existing state.
[-] apply would remove existing state.
[!] The read-state probe itself errored, so drift is unknown.

A task may also be preceded by an informational [deprecated] or [warning] line (a task-type deprecation notice, or a property probe diagnostic such as an unknown report key). These do not count toward the summary or the exit code.

Tasks that perform several operations itemize them under the task line:

==> Play: tasks
[~]       configure  (2 key(s) to set)
          - set KEY_ONE (new)
          - set KEY_TWO (was set)

Plan: 1 task(s); 1 would change, 0 in sync, 0 error(s).

The same probe drives apply: each task reads the server once, and apply reuses that read to decide whether to mutate. How much of its own state a task can read is a per-task property: each task's reference page carries a Probe support section stating whether it is supported, partial (some fields have no read command), or not supported. A task that is not supported reports drift on every run - it can never converge, so a recipe containing one never exits 0 under --detailed-exitcode, no matter how many times you apply it. The tasks index marks those with (never converges), and --list-tasks marks them per recipe (see Inspecting and resuming). Gate a deploy on the rest by moving those tasks behind a tag and planning with --skip-tags.

When the probe command cannot run at all - for example the local dokku CLI is not installed, or the configured SSH host is unreachable - the task renders [!] and plan exits 1, rather than optimistically predicting [+] create for state it never actually read.

Flag Effect
--tasks <path> Use a specific recipe. Accepts a local path, an http(s):// URL (fetched over HTTP), or - for stdin.
- Read the recipe from stdin, as a positional argument.
--tasks-format <fmt> Parse the recipe as yaml or json5 instead of detecting it.
--json Emit JSON-lines events instead of the human formatter. See JSON output.
--detailed-exitcode Exit 0 for no drift, 2 for drift, 1 on error. Errors win over drift. Mirrors terraform plan -detailed-exitcode.
--vars-file <path> Load input values from a file (repeatable). See inputs.
--play <name> Plan only the named play. Composes with --tags.
--tags <list> Plan only tasks whose tags intersect the list. See task envelope.
--skip-tags <list> Skip tasks whose tags intersect the list. See task envelope.
--list-tasks Print the resolved plan and exit without contacting the server. See inspecting and resuming.
--host <user@host:port> Plan against a remote server over SSH. Overrides DOKKU_HOST. See remote execution.
--sudo Wrap the remote dokku call in sudo -n. See remote execution.
--accept-new-host-keys Trust an unknown SSH host key on first connect. See remote execution.
# CI gate: fail the job if any task would change the server.
docket plan --detailed-exitcode || exit $?

docket apply

docket apply runs every task in the recipe, mutating the live server as needed. Each task line gets a status marker:

Marker Meaning
[ok] Ran, no change.
[changed] Ran, changed state.
[skipped] Filtered out by when: or --start-at-task.
[error] Errored.

As in plan, a task may be preceded by an informational [deprecated] or [warning] line that does not count toward the summary or the exit code.

A play header precedes the task lines, and a summary closes the run:

==> Play: tasks
[changed] dokku apps:create api
[ok]      dokku config:set api KEY=value

Summary: 2 tasks · 1 changed · 1 ok · 0 skipped · 0 errors  (took 0.8s)

On error, the message prints on a !-prefixed line and the run aborts with exit 1 (unless --fail-fast is off and only the play aborts). The summary still prints with partial counts.

By default apply exits 0 whether or not anything changed, because "the server now matches the recipe" is the same outcome either way. Pass --detailed-exitcode when the caller needs to know: 0 means nothing changed, 2 means at least one task changed, and 1 still means an error. Errors win over changes, matching plan. An error swallowed by ignore_errors is not an error for this purpose. --list-tasks returns before any task runs, so it never exits 2; it exits 1 when the recipe cannot be loaded or a when: fails to evaluate, and 0 otherwise.

Flag Effect
--tasks <path> Use a specific recipe. Accepts a local path, an http(s):// URL (fetched over HTTP), or - for stdin.
- Read the recipe from stdin, as a positional argument.
--tasks-format <fmt> Parse the recipe as yaml or json5 instead of detecting it.
--verbose After each task, echo every resolved Dokku command it ran, one per line. Masked against sensitive values. Ignored with --json (which already includes commands).
--json Emit JSON-lines events instead of the human formatter. See JSON output.
--detailed-exitcode Exit 0 when nothing changed, 2 when something did, 1 on error. Errors win over changes.
--vars-file <path> Load input values from a file (repeatable). See inputs.
--play <name> Run only the named play. Composes with --tags.
--tags <list> Run only tasks whose tags intersect the list. See task envelope.
--skip-tags <list> Skip tasks whose tags intersect the list. See task envelope.
--fail-fast Abort the whole run on the first error. Without it, an error aborts only the current play.
--list-tasks Print the resolved plan and exit without running. See inspecting and resuming.
--start-at-task <name> Skip every task before the named one, then run from there. See inspecting and resuming.
--host <user@host:port> Apply against a remote server over SSH. Overrides DOKKU_HOST. See remote execution.
--sudo Wrap the remote dokku call in sudo -n. See remote execution.
--accept-new-host-keys Trust an unknown SSH host key on first connect. See remote execution.

A multi-command task renders one continuation line per invocation under --verbose:

[changed] add buildpacks
          → dokku --quiet buildpacks:add app https://github.com/heroku/heroku-buildpack-nodejs.git
          → dokku --quiet buildpacks:add app https://github.com/heroku/heroku-buildpack-nginx.git

Color output respects NO_COLOR: set NO_COLOR=1 to disable ANSI escapes. Output is also plain automatically when piped to a non-TTY.

Inspecting and resuming

Two flags help when a recipe grows long. --list-tasks previews the resolved plan without running, and --start-at-task resumes a partially-applied recipe from a specific task. Both work on apply; --list-tasks also works on plan.

--list-tasks walks the resolved plan - after --play / --tags filtering, after loop: expansion, after when: evaluation against inputs - and prints one line per task:

$ docket apply --list-tasks
==> Play: api
[0] dokku apps:create api  [tags=core]
[1] dokku git:sync api  [tags=deploy]
[2] dokku config:set api  [tags=core,deploy]
[3] dokku ports:add api  [tags=deploy]

Those are the name: values from the recipe. A task with no name: is listed by its resource address instead:

$ docket apply --list-tasks
==> Play: api
[0] dokku_app[app=api]
[1] dokku_config[app=api]
[2] dokku_docker_options[app=api,phase=deploy,option=--memory=512m]

A when: that is false against the inputs renders as [skipped]. A when: whose truth value depends on a value only a run can supply renders as [unknown] rather than guessing: one that references .registered.<name> cannot be decided without running earlier tasks, and a rescue: child that branches on failed_task cannot be decided without a block child having failed.

Everything else is evaluated, and a when: that fails to evaluate renders as [when?] and exits 1. Once the undecidable references are set aside, a predicate that errors here errors the same way at run time - failed_task outside a rescue: child is nil during a real run too, so dereferencing it there is a broken predicate, not an artifact of listing offline.

$ docket apply --list-tasks
==> Play: api
[when?]   gated
[1] dokku apps:create api
$ echo $?
1

The walk is never short-circuited - every remaining task and play still lists, and the failure is carried by the exit code alone. A play-level when: that fails to evaluate is the same, except that the play's header carries the error and its tasks are left unlisted:

$ docket plan --list-tasks
==> Play: broken  (when error: cannot fetch tag from <nil> (1:9)
 | release.tag == "v1"
 | ........^)
==> Play: api
[0] dokku apps:create api
$ echo $?
1

The one exception is reachability. A run never evaluates the children of a group whose own when: rendered [skipped], [unknown], or [when?], so a broken predicate underneath one still prints its [when?] marker but leaves the exit code alone - the same reason a skipped play's tasks are not listed at all.

A task whose type is deprecated is marked (deprecated). A task whose type cannot read its own state is marked (never converges), and one that can read only part of it is marked (partial probe) - both come from the same declaration the reference pages render as Probe support, so this is the way to find out which tasks in a recipe will report drift forever before running anything:

$ docket apply --list-tasks
==> Play: api
[0] dokku apps:create api  [tags=core]
[1] dokku git:auth github.com  (never converges)
[2] dokku git:from-image api  (partial probe)

With --json, the same information is a probe field (unsupported or partial) plus a probe_caveat naming what cannot be read; a task that probes everything it manages carries neither.

--start-at-task <name> takes an exact task name:. Earlier tasks render as [skipped] with a (before --start-at-task) reason and do not run; the matched task and everything after it run:

$ docket apply --start-at-task "dokku config:set api"
==> Play: api
[skipped] dokku apps:create api    (before --start-at-task)
[skipped] dokku git:sync api       (before --start-at-task)
[ok]      dokku config:set api
[changed] dokku ports:add api

Summary: 4 tasks · 1 changed · 1 ok · 2 skipped · 0 errors  (took 1.1s)

A task with no name: is targeted by its resource address. Quote it - the brackets are shell glob characters:

docket apply --start-at-task 'dokku_config[app=api]'

Filters apply in this order: --start-at-task selects first, then --tags / --skip-tags, then per-task when: at execution time. The name search walks every play in source order, narrowed by --play. An unmatched name exits 1 with the available names listed. A --start-at-task target that is itself excluded by --tags / --skip-tags still establishes the resume point but does not itself run - tasks after it that pass the tag filter do.

validate --strict --start-at-task checks the same names offline. It stands down for a play that contains a loop: entry or an input with no default, because neither can be named without the values a real run has; the check at apply time is the authoritative one.

docket export

docket export reads a live Dokku server and writes a recipe describing it - the inverse of apply. It enumerates the apps on the server and reconstructs each one's declarative state, so you can capture an existing server as a recipe instead of hand-writing one. This is the starting point for a migration.

Because a faithful recipe would otherwise embed secrets, export writes two files: the recipe, and a companion vars-file holding the sensitive values (every config value, plus any field a task marks sensitive). The recipe references those values through per-play inputs: and {{ .name }} templates, so the pair is applied together:

# Export the local server to tasks.yml + tasks.vars.yml.
docket export

# Export a remote server over SSH.
docket export --host deploy@dokku.example.com

# Apply the exported pair somewhere else.
docket apply --tasks tasks.yml --vars-file tasks.vars.yml

# Stream a JSON5 recipe to stdout and pipe it straight back in.
docket export --output - --format json5 | docket apply --tasks-format json5 -

# Read back a single resource instead of a whole app.
docket export --resource 'dokku_config[app=api]' --output -

The correctness contract is idempotency: applying an exported pair back to the same server reports no drift (plan shows every task [ok]).

Exporting one resource

--resource takes a resource address - the same string an unnamed task is named after - and exports only what it matches:

docket export --resource 'dokku_config[app=api]' --output -
docket export --resource 'dokku_apps_property[global=true,property=disable-autocreation]' --output -

Drop the brackets to take every resource of a type, across every app:

docket export --resource dokku_domains --output -

The flag is repeatable and cannot be combined with --app, since an address already names its app. Each address is checked against the registry before the server is read, so an unknown task type, a type no exporter reaches, or a key the task does not declare fails immediately. An address that matches nothing on the server is reported by name and exits non-zero, the same way a nonexistent --app is.

Flag Effect
--output <path> Where to write the recipe (default tasks.yml). Pass - to stream a single self-contained recipe (values inlined, no vars-file) to stdout for inspection. Because a stream has no vars-file and touches no file on disk, combining - with --vars-output or --overwrite is an error rather than a silently ignored flag.
--format <fmt> Write yaml or json5 regardless of the --output extension, for both the recipe and the vars-file. Without an explicit --output, --format json5 writes ./tasks.json and ./tasks.vars.json. Required to stream JSON5 with --output -, which has no extension to read.
--vars-output <path> Where to write the companion vars-file (default <output-base>.vars.<ext>, e.g. tasks.vars.yml). Not valid with --output -. When the server holds nothing sensitive there is no vars-file to write, and an explicit path is reported as unwritten rather than passed over.
--overwrite Overwrite existing output files without prompting. Without it, export prompts before replacing either file, and aborts writing nothing if declined (or if stdin is not interactive). Not valid with --output -.
--redact Write placeholder values into the vars-file instead of real secrets, producing a shareable recipe plus a fill-in-the-blanks vars template. The required inputs mean apply fails loudly until the vars-file is filled in.
--app <name> Restrict the export to the named app. Repeatable.
--resource <address> Restrict the export to a resource address, e.g. dokku_config[app=api]. A bare task type takes every resource of that type. Repeatable; not valid with --app. See exporting one resource.
--host <user@host:port> Read a remote server over SSH. Overrides DOKKU_HOST. See remote execution.
--sudo Wrap the remote dokku call in sudo -n.
--accept-new-host-keys Trust an unknown SSH host key on first connect.

The output format follows --format when given, otherwise the --output extension (.json / .json5 writes JSON5, anything else YAML); the vars-file follows the recipe, or its own --vars-output extension when --format is not given. Which task types export is a per-task property: each task's reference page carries an Export support section stating whether it is supported, partial (for example a value that is lifted into the vars-file), or not exportable (write-only credentials such as dokku_git_auth, or dokku_service_property, which no datastore plugin can read back). Resources that cannot be read back are reported as warnings and left out of the recipe.

docket schema

docket schema prints a machine-readable description of every task type docket registers - the recipe keys each one accepts, their types, defaults, choices and descriptions, which values are secrets, what resource the task addresses, its export and probe support, its documented examples, and for a property task the exact set of property names it accepts. It is the same data the task reference pages are rendered from, in a form something other than a reader can consume.

The output is a single pretty-printed JSON document on stdout, described by schemas/task-catalog-v1.schema.json. See task catalog for the key-by-key contract.

# Print the catalog.
docket schema

# List every task type.
docket schema | jq -r '.tasks[].type'

# Only the task types you name.
docket schema --task dokku_config --task dokku_domains | jq -r '.tasks[].type'

# What fields does dokku_config take?
docket schema | jq '.tasks[] | select(.type=="dokku_config") | .fields'

# Which property names does dokku_nginx_property accept?
docket schema | jq -r '.tasks[] | select(.type=="dokku_nginx_property") | .property_schema.properties[].name'

Like init and validate, schema is offline: it opens no subprocess and contacts no server. It also reads no recipe, so it takes no --tasks and no positional argument - the --task below names a task type, not a recipe file. Two runs of the same binary emit byte-identical output, which is what makes diffing catalogs across docket versions useful.

Flag Effect
(default) Write the whole catalog to stdout.
--output <path> Write to a path instead; - writes to stdout. An existing file is overwritten, since the catalog is wholly derived and holds nothing of yours.
--task <type> Restrict the catalog to the named task type, such as dokku_config. Repeatable. The document keeps its shape, a version and a tasks array, so anything that reads the whole catalog reads a narrowed one unchanged. Tasks stay sorted by type whatever order the flags came in, and naming one twice emits it once. An unknown type is an error naming the closest match, not an empty array.

docket version

docket version prints the binary's version and exits.

docket version

See also