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