Skip to content

Release avocadoctl 0.6.0#11

Merged
mobileoverlord merged 27 commits intomainfrom
rel/0.6.0
Mar 9, 2026
Merged

Release avocadoctl 0.6.0#11
mobileoverlord merged 27 commits intomainfrom
rel/0.6.0

Conversation

@mobileoverlord
Copy link
Contributor

Summary

  • Varlink daemon architecture: socket-activated daemon for extension management, streaming log messages via varlink more/continues
  • TUF update support: download extension images from artifacts URL, report root.json version in device shadow
  • Runtime list improvements: active runtime shown first, then sorted by newest build date
  • Extension status fix: varlink status endpoint now sorts extensions by merge priority (overlay order) instead of alphabetically
  • Systemd hardening: fix ordering cycles, remove restrictive sandboxing that broke mount namespaces
  • Misc: stderr logging for varlink errors, unified runtime version display, clippy/fmt cleanup

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test — 176 tests passing
  • Manual validation on qemux86-64 QEMU device

Walk the delegation chain from targets.json when a delegations block is
present: fetch each delegation file from metadata/delegations/<name>.json,
verify its hash/length against snapshot.json, verify its signature against
the content key declared in the delegation keys map, then merge the
delegated targets into the download set.

Old flat-targets repos (no delegations block) continue to work unchanged.
Passing true to cargo_build_tosource ensures generated .rs files are
formatted consistently, preventing them from appearing as modified after
every cargo build.
Yocto builds do not have rustfmt available as a build-time tool, causing
build-script-build to fail. cargo fmt --check runs before cargo build in
CI so the committed formatted files are checked correctly regardless.
- perform_update gains auth_token: Option<&str>; all HTTP fetches
  (metadata, delegations, targets) use a make_request helper that
  injects Authorization: Bearer when a token is present
- varlink AddFromUrl(url, authToken: ?string) — existing callers
  omitting authToken get None → plain HTTP unchanged
- CLI runtime add reads AVOCADO_TUF_AUTH_TOKEN env var (keeps JWT
  out of process listings)
- Sideload path (avocado deploy, no env var) is unaffected
…lvable generic

RequestBuilder<B> requires an explicit type parameter in ureq 3.x.
Inline the auth header match directly in fetch_url and fetch_url_bytes
to let the compiler infer the body type from context.
cargo build regenerates these from the .varlink schema files (rustfmt=false).
All avocadoctl commands (ext, hitl, runtime, root-authority, and top-level
aliases) now connect to the running varlink daemon socket and forward requests
as RPC calls, rather than calling service functions directly.  This ensures
concurrent CLI invocations are queued and serialised by the daemon, preventing
race conditions when multiple processes attempt to modify system state (merge
extensions, activate runtimes, etc.) simultaneously.

Changes:
- src/config.rs: add optional `avocado.socket` config key; add
  Config::socket_address() helper (default: unix:/run/avocado/avocadoctl.sock)
- src/varlink_client.rs (new): connect_or_exit(), exit_with_rpc_error(), and
  output-formatting helpers for all four varlink interfaces
- src/main.rs: add --socket global flag; dispatch all commands through the
  generated VarlinkClient types; keep `serve` as a direct path (it starts the
  daemon); add AVOCADO_TEST_MODE bypass (handle_direct) so existing integration
  tests with mock executables continue to work without a live daemon
- tests/ext_integration_tests.rs: add AVOCADO_TEST_MODE=1 to three tests that
  were invoking the CLI without it
- tests/varlink_interface_tests.rs (new): 8 integration tests that start a real
  daemon on a temp socket and exercise the full varlink client→daemon→service
  path, including a concurrent-requests test and a no-daemon error-message test
avocado-extension.service and avocado-extension-initrd.service call
avocadoctl refresh/unmerge, which now route through the varlink daemon
socket.  Add Before= ordering to avocadoctl.socket and avocadoctl.service
so systemd guarantees the socket is listening before either extension
service starts.
…rd ordering cycle

The socket unit's implicit After=sysinit.target (from DefaultDependencies=yes)
created an ordering cycle with avocado-extension-initrd.service, which must
run Before=sysinit.target but Requires=avocadoctl.socket. Replace with an
explicit After=local-fs.target anchor so both units can start in early boot.
avocadoctl.service declared Before=avocado-extension*.service, pulling it
into the early boot graph. But PrivateTmp=yes (and DefaultDependencies=yes)
create an implicit After=systemd-tmpfiles-setup dependency, which comes
after the extension services — forming a cycle.

The socket unit already declares Before=avocado-extension*.service, which is
sufficient. The daemon is socket-activated on demand: the extension service
connects to the ready socket, systemd queues the daemon startup, and the
client waits until the daemon answers. No explicit ordering on the service
unit needed.
…st use varlink path

Three bugs fixed together:

1. All varlink client calls in main.rs used .recv() which requires .send() to have
   been called first. Use .call() (= send + recv) instead. This caused IteratorOldReply
   on every RPC — the varlink routing introduced in the previous commit never worked.

2. Tests in varlink_interface_tests.rs set AVOCADO_TEST_MODE=1 on the client process,
   causing it to bypass varlink entirely via handle_direct(). Remove it from client
   invocations so tests actually exercise the daemon → varlink → service path.

3. list_extensions() returned ConfigurationError when the extensions directory did not
   exist. Treat NotFound as an empty list — consistent with how the direct path behaves
   and what ext_integration_tests expects.

Also improve exit_with_rpc_error to show Debug (full chain) output when --verbose.
- Inline format string literals (print_literal, uninlined_format_args)
- Use .cloned() instead of .map(|s| s.clone())
…activation

With DefaultDependencies=yes, avocadoctl.service implicitly gets
After=sysinit.target. When socket-activated by avocado-extension-initrd.service
(which runs Before=sysinit.target), systemd cannot start the daemon until
sysinit completes — a deadlock that causes the extension service to time out.

Removing DefaultDependencies avoids the implicit ordering constraint, allowing
socket activation to start the daemon immediately in early boot. The Before=
declarations are intentionally absent (removed in a prior commit) to prevent
the separate ordering cycle that was present previously.
These sandboxing directives create a private mount namespace for the
daemon. Loop mounts for extensions created inside the daemon are
invisible to the host, causing systemd-sysext merge to see empty
directories and fail silently.
ProtectHome=yes makes /home, /root, /run/user inaccessible by creating
a mount namespace as a side effect. Loop mounts created by the daemon
for extensions remain trapped in this namespace and are invisible to the
host, so systemd-sysext still sees empty directories.
… daemon stdout

status_extensions() was calling show_enhanced_status() which printed
directly to stdout (captured by systemd journal) and returned an empty
vector. The CLI client received no data and displayed "No extensions
currently merged." regardless of actual state.

Add collect_extension_status() that gathers the same data but returns
structured ExtensionStatus values through the varlink reply.
The service layer functions (add_from_url, add_from_manifest,
activate_runtime) did not call refresh_extensions after activating a
runtime. The CLI direct path did this in commands/runtime.rs, but the
varlink daemon path skipped it, leaving old extensions merged after
a deploy.
The service layer unmounted NFS shares while extensions were still
merged as sysext/confext overlays, leaving broken overlay state. Match
the CLI path ordering: unmerge → cleanup dropins → reload → unmount →
merge remaining.
…caller

Thread OutputManager through the post-merge and pre-unmerge command
chains so that log messages are captured in daemon mode and returned
via varlink replies instead of being printed to the daemon's journal.
Updated varlink interfaces to return log vectors from merge, unmerge,
refresh, and runtime add/activate operations.
…nues

Replace batch log buffering with channel-based streaming so that
lifecycle events (on_merge, on_unmerge, depmod, etc.) are sent to
the CLI caller as they happen rather than all at once after completion.

- Add OutputManager::new_streaming(SyncSender) mode that sends each
  log message through an mpsc channel immediately
- Add _streaming() service functions that spawn work on a thread and
  return (Receiver, JoinHandle) for the caller to drain
- Use varlink's native more/continues protocol: server calls
  set_continues(true) and sends per-message replies, client iterates
  with .more()
- Re-colorize [INFO]/[SUCCESS] prefixes on the client side via
  print_single_log()
- Backward-compatible: wants_more() check falls back to batch mode
  for non-streaming clients
- Remove unused capture mode (RefCell buffer) from OutputManager
Use consistent `{name} {version} ({8-char-id})` format everywhere:
runtime list, inspect, activate, remove, add, status, and ambiguous
match messages. Removes the `v` prefix and matches the web UI's
display_version format.
Always log key TUF metadata info (target counts, delegation status,
manifest extensions). Warn when delegation has no targets or when
extension images are missing after staging. Verbose flag still
controls per-item detail logging.
Route .raw TUF targets to the artifacts CDN URL when available,
falling back to the TUF repo targets/ path for local deploy.
Always send auth token for artifact downloads. Fail activation
when extension images are missing instead of warning.
Download failures and other runtime errors were silently returned via
varlink without logging, making them invisible in avocadoctl's journal.
Now drain_stream() and map_rt_error!() print errors to stderr before
sending the varlink error reply.
…rge priority

- Runtime list now shows active runtime first, then remaining by build date (newest first)
- Extension status via varlink now sorts by merge_index (priority order) instead of alphabetically
@mobileoverlord mobileoverlord merged commit 4127caf into main Mar 9, 2026
2 checks passed
@mobileoverlord mobileoverlord deleted the rel/0.6.0 branch March 9, 2026 01:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant