Skip to main content

zlayer_builder/backend/buildah_sidecar/
mod.rs

1//! gRPC-client backend that talks to a `zlayer-buildd` sidecar.
2//!
3//! Spawns or connects to a `zlayer-buildd` process (Go binary wrapping
4//! `imagebuildah.BuildDockerfiles`) and exchanges build requests / streamed
5//! events over TCP+mTLS.
6//!
7//! Lifecycle, mTLS setup, and request translation are implemented across
8//! tasks 3.1–3.5. This module currently contains only the skeleton + the
9//! `BuildBackend` impl that returns `NotSupported` so Stage 1 compiles
10//! cleanly.
11
12use std::path::Path;
13use std::sync::Arc;
14
15use async_trait::async_trait;
16
17use crate::backend::BuildBackend;
18use crate::builder::{BuildOptions, BuiltImage, RegistryAuth};
19use crate::dockerfile::Dockerfile;
20use crate::error::Result;
21use crate::tui::BuildEvent;
22
23/// Generated tonic + prost bindings for `proto/buildah_sidecar.proto`.
24pub mod proto {
25    #![allow(clippy::all, missing_docs, clippy::pedantic, clippy::nursery)]
26    tonic::include_proto!("zlayer.buildah_sidecar.v1");
27}
28
29pub mod build;
30pub mod discover;
31pub mod lifecycle;
32pub mod ops;
33pub mod tls;
34
35pub use lifecycle::{LiveSidecar, SidecarLifecycle};
36pub use tls::{ensure_tls_material, TlsMaterial};
37
38/// gRPC-client backend for the `zlayer-buildd` sidecar.
39///
40/// Construction does not connect or spawn — that happens lazily on the
41/// first call to [`BuildahSidecarBackend::lifecycle`] (or via
42/// `is_available`, which probes spawn-and-handshake). The backend:
43///
44/// - holds the resolved `SidecarConfig` (transport address, TLS dir,
45///   idle timeout),
46/// - owns the [`SidecarLifecycle`] manager that spawns / dials the
47///   sidecar and caches the gRPC channel,
48/// - returns `BuildError::NotSupported` from every trait method until
49///   tasks 3.3 / 3.4 wire the build flow through the gRPC channel.
50#[derive(Debug, Clone)]
51pub struct BuildahSidecarBackend {
52    config: Arc<zlayer_types::builder::SidecarConfig>,
53    lifecycle: Arc<SidecarLifecycle>,
54}
55
56impl BuildahSidecarBackend {
57    /// Stable trait-level name. Mirrors `BuildBackend::name`.
58    pub const NAME: &'static str = "buildah-sidecar";
59
60    /// Create a new sidecar backend with the supplied configuration.
61    #[must_use]
62    pub fn new(config: zlayer_types::builder::SidecarConfig) -> Self {
63        let config = Arc::new(config);
64        let lifecycle = Arc::new(SidecarLifecycle::new(Arc::clone(&config)));
65        Self { config, lifecycle }
66    }
67
68    /// Borrow the underlying configuration.
69    #[must_use]
70    pub fn config(&self) -> &zlayer_types::builder::SidecarConfig {
71        &self.config
72    }
73
74    /// Access the lifecycle manager. Used by the RPC wiring landing in
75    /// tasks 3.3 / 3.4 to acquire a live `BuildServiceClient`.
76    #[must_use]
77    pub fn lifecycle(&self) -> &Arc<SidecarLifecycle> {
78        &self.lifecycle
79    }
80}
81
82impl Default for BuildahSidecarBackend {
83    fn default() -> Self {
84        Self::new(zlayer_types::builder::SidecarConfig::default())
85    }
86}
87
88#[async_trait]
89impl BuildBackend for BuildahSidecarBackend {
90    async fn build_image(
91        &self,
92        context: &Path,
93        dockerfile: &Dockerfile,
94        options: &BuildOptions,
95        event_tx: Option<std::sync::mpsc::Sender<BuildEvent>>,
96    ) -> Result<BuiltImage> {
97        self.build_image_impl(context, dockerfile, options, event_tx)
98            .await
99    }
100
101    async fn push_image(&self, tag: &str, auth: Option<&RegistryAuth>) -> Result<()> {
102        self.push_image_impl(tag, auth).await
103    }
104
105    async fn tag_image(&self, image: &str, new_tag: &str) -> Result<()> {
106        self.tag_image_impl(image, new_tag).await
107    }
108
109    async fn manifest_create(&self, name: &str) -> Result<()> {
110        self.manifest_create_impl(name).await
111    }
112
113    async fn manifest_add(&self, manifest: &str, image: &str) -> Result<()> {
114        self.manifest_add_impl(manifest, image).await
115    }
116
117    async fn manifest_push(
118        &self,
119        name: &str,
120        destination: &str,
121        auth: Option<&RegistryAuth>,
122    ) -> Result<()> {
123        self.manifest_push_impl(name, destination, auth).await
124    }
125
126    async fn is_available(&self) -> bool {
127        // Real probe: try to spawn (or dial-remote) the sidecar and
128        // complete the mTLS handshake. Any failure (missing binary,
129        // bad TLS material, handshake timeout, dial error) flips us
130        // unavailable so `detect_backend` falls back to the CLI path.
131        self.lifecycle.ensure().await.is_ok()
132    }
133
134    fn name(&self) -> &'static str {
135        Self::NAME
136    }
137}
138
139#[cfg(test)]
140#[allow(unsafe_code)]
141mod tests {
142    use super::*;
143    use crate::TEST_ENV_LOCK;
144
145    #[test]
146    fn new_holds_config() {
147        let cfg = zlayer_types::builder::SidecarConfig {
148            addr: Some("127.0.0.1:1234".into()),
149            tls_dir: None,
150            idle_secs: 99,
151            ..Default::default()
152        };
153        let backend = BuildahSidecarBackend::new(cfg.clone());
154        assert_eq!(backend.config(), &cfg);
155        assert_eq!(backend.name(), "buildah-sidecar");
156    }
157
158    // The env-lock guard must be held across `.await` so no other
159    // test races us on env mutation; see lifecycle.rs for the
160    // matching justification.
161    #[tokio::test]
162    #[allow(clippy::await_holding_lock)]
163    async fn default_is_unavailable_when_binary_missing() {
164        let _g = TEST_ENV_LOCK
165            .lock()
166            .unwrap_or_else(std::sync::PoisonError::into_inner);
167
168        // Snapshot env, then strip anything that would let the
169        // discovery path succeed.
170        let prev_path = std::env::var_os("PATH");
171        let prev_buildd_bin = std::env::var_os("ZLAYER_BUILDD_BIN");
172        let prev_data_dir = std::env::var_os("ZLAYER_DATA_DIR");
173
174        let tmp = tempfile::tempdir().unwrap();
175        // SAFETY: env mutation serialized by `TEST_ENV_LOCK`.
176        unsafe {
177            std::env::remove_var("ZLAYER_BUILDD_BIN");
178            std::env::set_var("PATH", "/nonexistent-zlayer-test-dir");
179            // Point ZLAYER_DATA_DIR at a fresh tempdir so the
180            // `${data}/bin/zlayer-buildd` candidate is also missing.
181            std::env::set_var("ZLAYER_DATA_DIR", tmp.path());
182        }
183
184        let cfg = zlayer_types::builder::SidecarConfig {
185            addr: None,
186            // Keep TLS material out of the real ${data}/buildd by
187            // pointing at the same tempdir.
188            tls_dir: Some(tmp.path().to_path_buf()),
189            idle_secs: 30,
190            ..Default::default()
191        };
192        let backend = BuildahSidecarBackend::new(cfg);
193        let available = backend.is_available().await;
194
195        // SAFETY: env mutation serialized by `TEST_ENV_LOCK`.
196        unsafe {
197            match prev_path {
198                Some(v) => std::env::set_var("PATH", v),
199                None => std::env::remove_var("PATH"),
200            }
201            match prev_buildd_bin {
202                Some(v) => std::env::set_var("ZLAYER_BUILDD_BIN", v),
203                None => std::env::remove_var("ZLAYER_BUILDD_BIN"),
204            }
205            match prev_data_dir {
206                Some(v) => std::env::set_var("ZLAYER_DATA_DIR", v),
207                None => std::env::remove_var("ZLAYER_DATA_DIR"),
208            }
209        }
210
211        assert!(
212            !available,
213            "is_available should be false when zlayer-buildd cannot be discovered"
214        );
215    }
216
217    #[test]
218    fn proto_types_compile() {
219        // Smoke: the generated module is wired and visible.
220        let req = proto::BuildRequest::default();
221        assert!(req.context_dir.is_empty());
222        let _client_module_exists: Option<
223            proto::build_service_client::BuildServiceClient<tonic::transport::Channel>,
224        > = None;
225    }
226}