Back to feed

sledtools/pika branch #82

pika-cloud-incus-host-tools

Tighten pikaci Incus CLI test

Target branch: master

Merge Commit: 816f7c638dd3be06e7209a509b05c2d10660df60

branch: merged tutorial: ready ci: success
Open CI Details

Continuous Integration

CI: success

Compact status on the review page, with full logs on the CI page.

Open CI Details

Latest run #107 success

7 passed

head 955328043dc7f017f5bb6f2a6d555e0f3a209244 · queued 2026-03-26 00:47:38 · 7 lane(s)

queued 5s · ran 1m 58s

check-notifications · success check-agent-contracts · success check-pikachat · success check-pikachat-typescript · success check-apple-host-sanity · success check-pikachat-openclaw-e2e · success check-fixture · success

Summary

This branch centralizes Incus device and resource configuration logic that was previously duplicated across pika-server and pikaci into shared helper functions in pika-cloud. Three new public functions — incus_runtime_config, incus_disk_device_config, and incus_mount_device_config — along with well-named constants for Incus config keys, are introduced in the pika-cloud crate. A RuntimeSpec::for_incus constructor and a with_bootstrap builder method eliminate boilerplate struct construction at every call site. The pikaci executor and pika-server agent API are then refactored to consume these shared helpers, removing local duplicates like incus_cpu_limit/incus_memory_limit and inline device map assembly. The pikaci CLI test is tightened to assert that the positional 'disk' argument appears but the key=value 'type=disk' does not, validating the new argument-building contract.

Tutorial Steps

Define Incus configuration key constants

Intent: Replace scattered string literals for Incus device and resource keys with named constants, making it impossible for different call sites to drift out of sync.

Affected files: crates/pika-cloud/src/incus.rs

Evidence
@@ -11,6 +11,14 @@
+pub const INCUS_LIMITS_CPU_KEY: &str = "limits.cpu";
+pub const INCUS_LIMITS_MEMORY_KEY: &str = "limits.memory";
+pub const INCUS_DISK_DEVICE_TYPE: &str = "disk";
+pub const INCUS_DEVICE_TYPE_KEY: &str = "type";
+pub const INCUS_DEVICE_SOURCE_KEY: &str = "source";
+pub const INCUS_DEVICE_PATH_KEY: &str = "path";
+pub const INCUS_DEVICE_READONLY_KEY: &str = "readonly";
+pub const INCUS_DEVICE_IO_BUS_KEY: &str = "io.bus";

Eight new pub const strings are added to crates/pika-cloud/src/incus.rs immediately after the existing INCUS_READ_ONLY_DISK_IO_BUS constant. These cover:

  • Resource limitslimits.cpu and limits.memory
  • Disk device fieldstype, source, path, readonly, and io.bus
  • Disk device type value"disk"

By centralizing these strings, any future Incus schema change only needs to be updated in one place.

Add shared helper functions for Incus config maps

Intent: Extract the duplicated logic for building Incus resource-limit config maps and disk-device property maps into three reusable functions that return BTreeMap<String, String>.

Affected files: crates/pika-cloud/src/incus.rs, crates/pika-cloud/src/lib.rs

Evidence
@@ -42,6 +50,52 @@
+pub fn incus_runtime_config(plan: &IncusRuntimePlan) -> BTreeMap<String, String> {
+    let mut config = BTreeMap::new();
+    if let Some(memory_mib) = plan.resources.memory_mib {
+        config.insert(
+            INCUS_LIMITS_MEMORY_KEY.to_string(),
+            format!("{memory_mib}MiB"),
+        );
+    }
+    if let Some(vcpu_count) = plan.resources.vcpu_count {
+        config.insert(INCUS_LIMITS_CPU_KEY.to_string(), vcpu_count.to_string());
+    }
+    config
+}
@@ -42,6 +50,52 @@
+pub fn incus_disk_device_config(
+    source: impl Into<String>,
+    guest_path: impl Into<String>,
+    read_only: bool,
+    io_bus: Option<&str>,
+) -> BTreeMap<String, String> {
@@ -42,6 +50,52 @@
+pub fn incus_mount_device_config(mount: &IncusMountPlan) -> BTreeMap<String, String> {
+    incus_disk_device_config(
+        mount.source.clone(),
+        mount.guest_path.clone(),
+        mount.read_only,
+        mount.io_bus.as_deref(),
+    )
+}
@@ -5,7 +5,10 @@
+pub use incus::{
+    INCUS_READ_ONLY_DISK_IO_BUS, IncusMountPlan, IncusRuntimePlan, incus_disk_device_config,
+    incus_mount_device_config, incus_runtime_config,
+};

Three functions are introduced:

  1. incus_runtime_config — takes an &IncusRuntimePlan and returns a map containing only the resource-limit keys that are Some (CPU count and/or memory MiB). This replaces the per-site if let Some(…) blocks.

  2. incus_disk_device_config — accepts source path, guest path, read-only flag, and an optional I/O bus string. It always inserts type=disk, source, and path; conditionally adds readonly=true and io.bus. The io_bus filter skips empty/whitespace-only values.

  3. incus_mount_device_config — thin convenience wrapper that unpacks an IncusMountPlan and delegates to incus_disk_device_config.

All three are re-exported from pika_cloud::lib.rs so downstream crates can import them directly.

Add RuntimeSpec::for_incus constructor and with_bootstrap builder

Intent: Eliminate repetitive struct-literal construction of RuntimeSpec by providing a constructor that fills in shared defaults (provider, lifecycle_root, paths, policies, bootstrap, labels, metadata) and a chainable builder for bootstrap overrides.

Affected files: crates/pika-cloud/src/spec.rs

Evidence
@@ -133,6 +133,32 @@
+    pub fn for_incus(
+        identity: RuntimeIdentity,
+        incus: IncusRuntimeConfig,
+        resources: RuntimeResources,
+        mounts: Vec<RuntimeMount>,
+    ) -> Self {
@@ -133,6 +133,32 @@
+    pub fn with_bootstrap(mut self, bootstrap: RuntimeBootstrap) -> Self {
+        self.bootstrap = bootstrap;
+        self
+    }

RuntimeSpec::for_incus is a named constructor that accepts only the four fields that vary per call site — identity, Incus config, resources, and mounts — and fills everything else with well-defined defaults:

  • provider is always ProviderKind::Incus
  • lifecycle_root uses default_runtime_root()
  • paths, policies, and bootstrap use Default
  • labels and metadata start empty

with_bootstrap is a consuming builder method that lets callers override the bootstrap config inline:

RuntimeSpec::for_incus(identity, incus, resources, mounts)
    .with_bootstrap(RuntimeBootstrap { ... })

A new unit test for_incus_applies_shared_defaults verifies every defaulted field. The existing sample_runtime_spec() helper in the test module is rewritten to use the new constructor, serving as a living example.

Refactor pika-server agent API to use shared helpers

Intent: Remove inline device-map assembly and resource-limit insertion in the Incus managed runtime provider, replacing them with calls to incus_mount_device_config and incus_runtime_config.

Affected files: crates/pika-server/src/agent_api.rs

Evidence
@@ -1573,36 +1573,15 @@
+            let mut device = serde_json::Map::from_iter(
+                incus_mount_device_config(mount)
+                    .into_iter()
+                    .map(|(key, value)| (key, serde_json::Value::String(value))),
+            );
+            device.insert(
+                "pool".to_string(),
+                serde_json::Value::String(self.resolved.storage_pool.clone()),
+            );
@@ -1628,17 +1607,8 @@
+        for (key, value) in incus_runtime_config(&runtime_plan) {
+            instance_config.insert(key, serde_json::Value::String(value));
+        }
@@ -1824,36 +1794,29 @@
+        RuntimeSpec::for_incus(
+            RuntimeIdentity {
@@ -36,12 +36,12 @@
+    incus_mount_device_config, incus_runtime_config, AgentProvisionRequest, AgentStartupPhase,

In IncusManagedRuntimeProvider:

Mount device assembly — The 30-line inline serde_json::Map construction with manual type, source, path, readonly, and io.bus insertions is replaced by a two-step pattern:

  1. Call incus_mount_device_config(mount) to get the canonical BTreeMap<String, String>.
  2. Convert entries to serde_json::Value::String and insert the server-specific pool key afterward.

Resource limits — The two if let Some(…) blocks for limits.memory and limits.cpu are replaced by iterating over incus_runtime_config(&runtime_plan).

Runtime plan construction — The verbose struct literal in build_runtime_plan is replaced with RuntimeSpec::for_incus(…), dropping six lines of default-value boilerplate.

Several imports (ProviderKind, RuntimeBootstrap, RuntimePaths, RuntimePolicies) are removed from the import block since they are no longer referenced directly.

Refactor pikaci executor to use shared helpers and restructure CLI argument building

Intent: Replace local incus_cpu_limit/incus_memory_limit functions and inline device argument assembly in pikaci with the shared helpers, and change build_remote_incus_device_add_args to accept a properties map instead of positional parameters.

Affected files: crates/pikaci/src/executor.rs, crates/pikaci/src/executor/incus.rs

Evidence
@@ -113,14 +107,6 @@
-fn incus_cpu_limit(plan: &IncusRuntimePlan) -> String {
-    plan.resources.vcpu_count.unwrap_or(2).to_string()
-}
-
-fn incus_memory_limit(plan: &IncusRuntimePlan) -> String {
-    format!("{}MiB", plan.resources.memory_mib.unwrap_or(4096))
-}
@@ -291,6 +277,7 @@
+    let runtime_config = incus_runtime_config(&runtime_plan);
@@ -302,26 +289,30 @@
+    let mut init_args = vec![
+        "init".to_string(),
+        ...
+    ];
+    for (key, value) in runtime_config {
+        init_args.push("--config".to_string());
+        init_args.push(format!("{key}={value}"));
+    }
@@ -367,12 +358,13 @@
 pub(super) fn build_remote_incus_device_add_args(
     remote: &RemoteIncusContext,
     device_name: &str,
-    source: &Path,
-    guest_path: &str,
-    readonly: bool,
-    io_bus: &str,
+    properties: &std::collections::BTreeMap<String, String>,
 ) -> Vec<String> {
@@ -740,14 +742,13 @@
+    let properties = incus_disk_device_config(
+        realized_source.display().to_string(),
+        guest_path.to_string(),
+        readonly,
+        Some(io_bus),
+    );
+    let args = build_remote_incus_device_add_args(remote, device_name, &properties);

Several significant changes happen in the pikaci executor:

Deleted local helpersincus_cpu_limit and incus_memory_limit (which hard-coded fallback defaults of 2 CPUs / 4096 MiB) are removed. The shared incus_runtime_config now handles this, and it only emits keys that are Some, avoiding silent default injection.

incus init argument building — Previously the init command hard-coded two --config limits.cpu=… / --config limits.memory=… arguments. Now the function calls incus_runtime_config(&runtime_plan) upfront and iterates over the resulting map to push --config {key}={value} pairs dynamically. This means if neither resource limit is set, no --config flags are emitted at all.

build_remote_incus_device_add_args signature change — The function's five positional parameters (source, guest_path, readonly, io_bus) are replaced with a single &BTreeMap<String, String> properties map. Inside, the function:

  1. Extracts the type key from properties (defaulting to "disk") and places it as the positional device-type argument.
  2. Iterates remaining properties as key=value pairs.
  3. Appends shift=false at the end.

This is important because Incus CLI expects the device type as a positional argument (incus config device add <instance> <name> disk …) rather than as a type=disk key=value pair.

build_remote_incus_runtime_plan — Uses RuntimeSpec::for_incus(…).with_bootstrap(…) instead of a full struct literal. The unused job parameter is prefixed with _.

add_remote_incus_disk_device — Now calls incus_disk_device_config to build the property map, then passes it to the restructured build_remote_incus_device_add_args.

The test-only build_device_add_args wrapper is similarly updated to construct properties via incus_disk_device_config before delegating.

Tighten pikaci Incus CLI test assertions

Intent: Verify the new argument-building contract: 'disk' must appear as a positional argument, and 'type=disk' must NOT appear as a key=value property.

Affected files: crates/pikaci/src/executor.rs

Evidence
@@ -2683,6 +2683,7 @@
+        assert!(args.contains(&"disk".to_string()));
@@ -2691,6 +2692,7 @@
+        assert!(!args.contains(&"type=disk".to_string()));

Two new assertions are added to the existing device-add test in crates/pikaci/src/executor.rs:

  1. assert!(args.contains(&"disk".to_string())) — Confirms that the device type appears as a bare positional argument, which is what the Incus CLI expects.

  2. assert!(!args.contains(&"type=disk".to_string())) — Confirms that type=disk does not appear as a key=value property, since the build_remote_incus_device_add_args function deliberately extracts the type from the properties map and emits it positionally.

These two assertions together encode the Incus CLI contract: the device type is a positional argument, not a configuration property. This is the core behavioral guarantee that the branch title ("Tighten pikaci Incus CLI test") refers to.

Diff