Skip to main content

zlayer_types/
builder.rs

1//! Shared types for build-backend dispatch and sidecar wire protocol.
2//!
3//! The `BuilderBackendKind` discriminator selects between the in-process buildah
4//! CLI shellout, the buildah-sidecar gRPC client, the macOS sandbox builder,
5//! and the Windows HCS builder. Other crates import this discriminator rather
6//! than redefining their own enum, per the workspace's types-first rule.
7
8use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Serialize};
12
13/// Selects which build backend handles a given build.
14///
15/// `BuildahCli` is the legacy in-process shellout to the `buildah` binary.
16/// `BuildahSidecar` talks to a Go sidecar (`zlayer-buildd`) over gRPC+mTLS.
17/// `Sandbox` is the macOS-native sandbox-exec builder.
18/// `Hcs` is the Windows-native HCS builder.
19/// `Wsl2Buildah` is the Windows→WSL2 buildah builder for Linux images.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case")]
22pub enum BuilderBackendKind {
23    BuildahCli,
24    BuildahSidecar,
25    Sandbox,
26    Hcs,
27    Wsl2Buildah,
28}
29
30impl BuilderBackendKind {
31    /// Stable string identifier used in CLI flags, env vars, and logs.
32    #[must_use]
33    pub const fn as_str(self) -> &'static str {
34        match self {
35            Self::BuildahCli => "buildah-cli",
36            Self::BuildahSidecar => "buildah-sidecar",
37            Self::Sandbox => "sandbox",
38            Self::Hcs => "hcs",
39            Self::Wsl2Buildah => "wsl2",
40        }
41    }
42}
43
44impl fmt::Display for BuilderBackendKind {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str(self.as_str())
47    }
48}
49
50impl FromStr for BuilderBackendKind {
51    type Err = String;
52
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        match s.trim().to_ascii_lowercase().as_str() {
55            "buildah-cli" | "buildah" | "cli" => Ok(Self::BuildahCli),
56            "buildah-sidecar" | "sidecar" | "buildd" => Ok(Self::BuildahSidecar),
57            "sandbox" | "macos-sandbox" => Ok(Self::Sandbox),
58            "hcs" | "windows-hcs" => Ok(Self::Hcs),
59            "wsl2" | "wsl2-buildah" | "wsl" => Ok(Self::Wsl2Buildah),
60            other => Err(format!(
61                "unknown builder backend kind: {other:?} (expected one of: \
62                 buildah-cli, buildah-sidecar, sandbox, hcs, wsl2)"
63            )),
64        }
65    }
66}
67
68/// Transport configuration for connecting to a `zlayer-buildd` sidecar.
69///
70/// Default operation: the daemon spawns its own local sidecar bound to
71/// `127.0.0.1:<auto-port>` with mTLS material under
72/// `${ZLAYER_DATA_DIR}/buildd/`. A remote builder (LAN build host) is selected
73/// by setting `addr` to a `host:port` reachable by the caller.
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct SidecarConfig {
76    /// `host:port` to dial. `None` means "spawn a local sidecar and use the
77    /// auto-allocated 127.0.0.1 port."
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub addr: Option<String>,
80
81    /// Directory containing the mTLS material (`ca.pem`, `cert.pem`, `key.pem`).
82    /// `None` means use the default under `${ZLAYER_DATA_DIR}/buildd/`.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub tls_dir: Option<std::path::PathBuf>,
85
86    /// Idle-shutdown timeout in seconds for an auto-spawned local sidecar.
87    /// Ignored when `addr` points at a remote sidecar (lifecycle is the
88    /// remote operator's responsibility).
89    #[serde(default = "SidecarConfig::default_idle_secs")]
90    pub idle_secs: u64,
91
92    /// Storage `GraphRoot` to pass to the sidecar. `None` → derive
93    /// `${ZLAYER_DATA_DIR}/buildd/storage/graph`.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub storage_graph_root: Option<std::path::PathBuf>,
96
97    /// Storage `RunRoot`. `None` → `${ZLAYER_DATA_DIR}/buildd/storage/run`.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub storage_run_root: Option<std::path::PathBuf>,
100
101    /// Storage driver name. `None` → `vfs` for rootless safety. Operators
102    /// running as root may switch to `overlay` for performance.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub storage_driver: Option<String>,
105
106    /// Build-context mount translation for a remote (cross-host) sidecar.
107    ///
108    /// When the sidecar runs in a different mount namespace than the client
109    /// (e.g. a `zlayer-buildd` inside a VZ-Linux container on a macOS host),
110    /// the build context must be shared in via a bind / virtiofs mount and
111    /// the `context_dir` sent on the wire must be the path *the sidecar*
112    /// sees, not the host path the client computed.
113    ///
114    /// `Some((host_prefix, guest_prefix))` rewrites any `context_dir` that
115    /// starts with `host_prefix` so the prefix becomes `guest_prefix`. The
116    /// dockerfile paths are translated the same way. `None` (the default,
117    /// same-host sidecar) passes paths through unchanged.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub context_mount: Option<(std::path::PathBuf, std::path::PathBuf)>,
120}
121
122impl SidecarConfig {
123    pub const DEFAULT_IDLE_SECS: u64 = 30;
124
125    #[must_use]
126    pub fn default_idle_secs() -> u64 {
127        Self::DEFAULT_IDLE_SECS
128    }
129}
130
131impl Default for SidecarConfig {
132    fn default() -> Self {
133        Self {
134            addr: None,
135            tls_dir: None,
136            idle_secs: Self::DEFAULT_IDLE_SECS,
137            storage_graph_root: None,
138            storage_run_root: None,
139            storage_driver: None,
140            context_mount: None,
141        }
142    }
143}
144
145/// Wire-shaped request for the sidecar's `Build` RPC.
146///
147/// This mirrors the proto schema 1:1 so the Rust client can deserialize
148/// without an intermediate translation type. Fields that are `Option` in
149/// the proto become `Option` here; repeated fields become `Vec`.
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151#[allow(clippy::struct_excessive_bools)]
152pub struct BuildSidecarRequest {
153    pub context_dir: String,
154    pub dockerfile_paths: Vec<String>,
155    pub tags: Vec<String>,
156    pub platforms: Vec<String>,
157    pub build_args: std::collections::BTreeMap<String, String>,
158    pub secrets: Vec<String>,
159    pub ssh: Vec<String>,
160    pub target_stage: Option<String>,
161    pub host_network: bool,
162    pub cache_from: Option<String>,
163    pub cache_to: Option<String>,
164    pub no_cache: bool,
165    pub squash: bool,
166    pub layers: bool,
167    pub format: Option<String>,
168    pub pull_policy: Option<String>,
169}
170
171/// Streamed event from the sidecar during a build.
172///
173/// Variants mirror the proto `BuildEvent.oneof event` arms and translate to
174/// the zlayer-builder `BuildEvent` enum at the client boundary.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176#[serde(tag = "type", rename_all = "snake_case")]
177pub enum BuildSidecarEvent {
178    StageStarted {
179        index: u32,
180        name: Option<String>,
181        base_image: String,
182    },
183    StageFinished {
184        index: u32,
185    },
186    InstructionStarted {
187        stage: u32,
188        index: u32,
189        instruction: String,
190    },
191    InstructionFinished {
192        stage: u32,
193        index: u32,
194        cached: bool,
195    },
196    Log {
197        line: String,
198        is_stderr: bool,
199    },
200    Warning {
201        message: String,
202    },
203    Finished {
204        image_id: String,
205        manifest_ref: Option<String>,
206    },
207    Error {
208        message: String,
209    },
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn backend_kind_round_trips_through_as_str() {
218        for kind in [
219            BuilderBackendKind::BuildahCli,
220            BuilderBackendKind::BuildahSidecar,
221            BuilderBackendKind::Sandbox,
222            BuilderBackendKind::Hcs,
223            BuilderBackendKind::Wsl2Buildah,
224        ] {
225            assert_eq!(BuilderBackendKind::from_str(kind.as_str()).unwrap(), kind);
226        }
227    }
228
229    #[test]
230    fn backend_kind_accepts_aliases() {
231        assert_eq!(
232            BuilderBackendKind::from_str("buildah").unwrap(),
233            BuilderBackendKind::BuildahCli
234        );
235        assert_eq!(
236            BuilderBackendKind::from_str("sidecar").unwrap(),
237            BuilderBackendKind::BuildahSidecar
238        );
239        assert_eq!(
240            BuilderBackendKind::from_str("BUILDAH-SIDECAR").unwrap(),
241            BuilderBackendKind::BuildahSidecar
242        );
243    }
244
245    #[test]
246    fn sidecar_config_idle_default() {
247        let cfg = SidecarConfig::default();
248        assert_eq!(cfg.idle_secs, 30);
249        assert!(cfg.addr.is_none());
250        assert!(cfg.tls_dir.is_none());
251        assert!(cfg.storage_graph_root.is_none());
252        assert!(cfg.storage_run_root.is_none());
253        assert!(cfg.storage_driver.is_none());
254    }
255}