Skip to main content

opal/executor/
orbstack.rs

1use super::core::ExecutorCore;
2use crate::engine::EngineCommandContext;
3use crate::{EngineKind, ExecutorConfig};
4use anyhow::Result;
5use std::process::{Command, Stdio};
6
7#[derive(Debug, Clone)]
8pub struct OrbstackExecutor {
9    core: ExecutorCore,
10}
11
12impl OrbstackExecutor {
13    pub fn new(mut config: ExecutorConfig) -> Result<Self> {
14        config.engine = EngineKind::Orbstack;
15        let core = ExecutorCore::new(config)?;
16        Ok(Self { core })
17    }
18
19    pub async fn run(&self) -> Result<()> {
20        self.core.run().await
21    }
22
23    pub fn build_command(ctx: &EngineCommandContext<'_>) -> Command {
24        OrbstackCommandBuilder::new(ctx)
25            .with_workspace_volume()
26            .with_volumes()
27            .with_network()
28            .with_platform()
29            .with_image_options()
30            .with_privileges()
31            .with_env()
32            .build()
33    }
34}
35
36struct OrbstackCommandBuilder<'a> {
37    ctx: &'a EngineCommandContext<'a>,
38    command: Command,
39    workspace_mount: String,
40}
41
42impl<'a> OrbstackCommandBuilder<'a> {
43    fn new(ctx: &'a EngineCommandContext<'a>) -> Self {
44        let workspace_mount = format!("{}:{}", ctx.workdir.display(), ctx.container_root.display());
45        let mut command = Command::new("docker");
46        command
47            .stdout(Stdio::piped())
48            .stderr(Stdio::piped())
49            .arg("run")
50            .arg("--name")
51            .arg(ctx.container_name)
52            .arg("--workdir")
53            .arg(ctx.container_root);
54        Self {
55            ctx,
56            command,
57            workspace_mount,
58        }
59    }
60
61    fn with_volumes(mut self) -> Self {
62        for mount in self.ctx.mounts {
63            self.command.arg("--volume").arg(mount.to_arg());
64        }
65        self
66    }
67
68    fn with_workspace_volume(mut self) -> Self {
69        self.command.arg("--volume").arg(&self.workspace_mount);
70        self
71    }
72
73    fn with_network(mut self) -> Self {
74        if let Some(network) = self.ctx.network {
75            self.command.arg("--network").arg(network);
76        }
77        self
78    }
79
80    fn with_platform(mut self) -> Self {
81        if let Some(platform) = self.ctx.image_platform {
82            self.command.arg("--platform").arg(platform);
83        }
84        self
85    }
86
87    fn with_image_options(mut self) -> Self {
88        if let Some(user) = self.ctx.image_user {
89            self.command.arg("--user").arg(user);
90        }
91        if !self.ctx.image_entrypoint.is_empty() {
92            self.command
93                .arg("--entrypoint")
94                .arg(self.ctx.image_entrypoint.join(" "));
95        }
96        self
97    }
98
99    fn with_privileges(mut self) -> Self {
100        if self.ctx.privileged {
101            self.command.arg("--privileged");
102        }
103        for capability in self.ctx.cap_add {
104            self.command.arg("--cap-add").arg(capability);
105        }
106        for capability in self.ctx.cap_drop {
107            self.command.arg("--cap-drop").arg(capability);
108        }
109        self
110    }
111
112    fn with_env(mut self) -> Self {
113        for (key, value) in self.ctx.env_vars {
114            self.command.arg("--env").arg(format!("{key}={value}"));
115        }
116        self
117    }
118
119    fn build(mut self) -> Command {
120        self.command
121            .arg(self.ctx.image)
122            .arg("sh")
123            .arg(self.ctx.container_script);
124        self.command
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::OrbstackExecutor;
131    use crate::engine::EngineCommandContext;
132    use std::path::Path;
133
134    #[test]
135    fn build_command_includes_platform_when_requested() {
136        let ctx = EngineCommandContext {
137            workdir: Path::new("/workspace"),
138            container_root: Path::new("/builds/workspace"),
139            container_script: Path::new("/opal/script.sh"),
140            container_name: "opal-job",
141            image: "alpine:3.19",
142            image_platform: Some("linux/arm64/v8"),
143            image_user: None,
144            image_entrypoint: &[],
145            mounts: &[],
146            env_vars: &[],
147            network: None,
148            preserve_runtime_objects: false,
149            arch: None,
150            privileged: false,
151            cap_add: &[],
152            cap_drop: &[],
153            cpus: None,
154            memory: None,
155            dns: None,
156        };
157
158        let args: Vec<String> = OrbstackExecutor::build_command(&ctx)
159            .get_args()
160            .map(|arg| arg.to_string_lossy().to_string())
161            .collect();
162
163        assert!(
164            args.windows(2)
165                .any(|pair| pair == ["--platform", "linux/arm64/v8"])
166        );
167    }
168}