opal/executor/
orbstack.rs1use 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}