mj_controller/controller/worker_binary/
binary_select.rs1use super::*;
2use mj_core::hex::lower_hex;
3
4pub(super) fn display_path(path: &Path) -> String {
7 let text = path.to_string_lossy();
8 text.strip_suffix(" (deleted)").unwrap_or(&text).to_owned()
9}
10
11pub(super) fn template_architecture(
18 template: &mj_core::config::TargetTemplate,
19) -> Option<&'static str> {
20 use mj_core::config::TargetTemplate as Template;
21 let platform = match template {
22 Template::LocalPodman { container }
23 | Template::LocalDocker { container }
24 | Template::AppleContainer { container }
25 | Template::SshPodman { container, .. }
26 | Template::SshDocker { container, .. } => container.platform.as_deref()?,
27 Template::LocalBare | Template::SshBare { .. } | Template::AwsEc2 { .. } => return None,
28 };
29 platform
31 .split('/')
32 .find_map(|part| targets::normalize_architecture(part.trim()).ok())
33}
34
35pub(super) fn preflight_architectures(
42 template: &mj_core::config::TargetTemplate,
43) -> Vec<&'static str> {
44 use mj_core::config::TargetTemplate as Template;
45 if let Some(arch) = template_architecture(template) {
46 return vec![arch];
47 }
48 match template {
49 Template::LocalBare
50 | Template::LocalPodman { .. }
51 | Template::LocalDocker { .. }
52 | Template::AppleContainer { .. } => vec![std::env::consts::ARCH],
53 Template::SshBare { .. }
54 | Template::SshPodman { .. }
55 | Template::SshDocker { .. }
56 | Template::AwsEc2 { .. } => {
57 vec!["x86_64", "aarch64"]
58 }
59 }
60}
61
62pub(in crate::controller) fn preflight_worker_binary(
71 template: &mj_core::config::TargetTemplate,
72 executor: &impl CommandExecutor,
73) -> Result<()> {
74 if let mj_core::config::TargetTemplate::SshBare { ssh, .. } = template {
75 let command = targets::ssh_command(&SshTarget::from(ssh), ["uname", "-sm"])
76 .purpose("detect target platform");
77 let platform = probe_platform(executor, command)?;
78 return materialize_worker_source(worker_binary_for_arch(
79 platform.architecture,
80 WorkerBinaryRequirement::for_os(platform.os),
81 )?)
82 .map(|_| ());
83 }
84 let requirement = if matches!(template, mj_core::config::TargetTemplate::LocalBare) {
87 WorkerBinaryRequirement::LocalHost
88 } else {
89 WorkerBinaryRequirement::PortableLinux
90 };
91 let mut failure = None;
92 for arch in preflight_architectures(template) {
93 match worker_binary_for_arch(arch, requirement).and_then(materialize_worker_source) {
94 Ok(_) => return Ok(()),
95 Err(error) => failure = Some(error),
96 }
97 }
98 match failure {
99 Some(error) => Err(error).context("preflight the worker binary before provisioning"),
102 None => Ok(()),
103 }
104}
105
106pub(crate) fn worker_source_problem(
116 template: &mj_core::config::TargetTemplate,
117 executor: &impl CommandExecutor,
118) -> Option<String> {
119 if let mj_core::config::TargetTemplate::SshBare { ssh, .. } = template {
120 let command = targets::ssh_command(&SshTarget::from(ssh), ["uname", "-sm"])
121 .purpose("detect target platform");
122 let platform = probe_platform(executor, command).ok()?;
123 return worker_binary_for_arch(
124 platform.architecture,
125 WorkerBinaryRequirement::for_os(platform.os),
126 )
127 .err()
128 .map(|error| format!("{error:#}"));
129 }
130 let requirement = if matches!(template, mj_core::config::TargetTemplate::LocalBare) {
131 WorkerBinaryRequirement::LocalHost
132 } else {
133 WorkerBinaryRequirement::PortableLinux
134 };
135 let mut failure = None;
136 for arch in preflight_architectures(template) {
137 match worker_binary_for_arch(arch, requirement) {
138 Ok(_) => return None,
139 Err(error) => failure = Some(format!("{error:#}")),
140 }
141 }
142 failure
143}
144
145pub fn ssh_worker_binary_prerequisite(
150 template: &mj_core::config::TargetTemplate,
151 executor: &impl CommandExecutor,
152) -> Option<(String, Result<WorkerBinaryAvailability>)> {
153 let mj_core::config::TargetTemplate::SshBare { ssh, .. } = template else {
154 return None;
155 };
156 let command = targets::ssh_command(&SshTarget::from(ssh), ["uname", "-sm"])
157 .purpose("detect target platform");
158 let platform = probe_platform(executor, command).ok()?;
159 let requirement = WorkerBinaryRequirement::for_os(platform.os);
160 Some((
161 requirement.triple(platform.architecture),
162 worker_binary_for_arch(platform.architecture, requirement),
163 ))
164}
165
166pub(in crate::controller) fn worker_binary_for(
167 locator: &targets::TargetLocator,
168 executor: &impl CommandExecutor,
169) -> Result<PathBuf> {
170 let platform = probe_platform(executor, targets::platform_probe(locator))?;
171 let requirement = if matches!(locator, targets::TargetLocator::LocalBare { .. }) {
172 WorkerBinaryRequirement::LocalHost
173 } else {
174 WorkerBinaryRequirement::for_os(platform.os)
175 };
176 materialize_worker_source(worker_binary_for_arch(platform.architecture, requirement)?)
177}
178
179fn materialize_worker_source(source: WorkerBinaryAvailability) -> Result<PathBuf> {
180 let path = match source {
181 WorkerBinaryAvailability::Local { path, .. } => Ok(path),
182 WorkerBinaryAvailability::Remote {
183 url,
184 sha256,
185 triple,
186 } => download_worker(&url, &sha256, &triple),
187 }?;
188 verify_worker_build(&path)?;
189 Ok(path)
190}
191
192pub(in crate::controller) fn target_architecture(
193 locator: &targets::TargetLocator,
194 executor: &impl CommandExecutor,
195) -> Result<&'static str> {
196 let command = targets::locator_command(locator, vec!["uname".into(), "-m".into()])
197 .purpose("detect target architecture");
198 let output = execute_checked(executor, command)?;
199 targets::normalize_architecture(String::from_utf8(output.stdout)?.trim())
200}
201
202fn probe_platform(
203 executor: &impl CommandExecutor,
204 command: CommandSpec,
205) -> Result<targets::TargetPlatform> {
206 let output = execute_checked(executor, command)?;
207 targets::TargetPlatform::parse(std::str::from_utf8(&output.stdout)?)
208}
209
210pub(super) fn download_worker(url: &str, expected_sha256: &str, triple: &str) -> Result<PathBuf> {
211 validate_worker_sha256(expected_sha256)?;
212 let digest = expected_sha256.to_ascii_lowercase();
213 let directory = data_dir().join("workers").join("pinned");
214 let destination = directory.join(&digest).join("hel");
215 std::fs::create_dir_all(destination.parent().unwrap_or(&directory))?;
216 if destination.is_file() {
217 let bytes = std::fs::read(&destination).with_context(|| {
218 format!(
219 "read cached worker for {triple} from {}",
220 destination.display()
221 )
222 })?;
223 if lower_hex(Sha256::digest(&bytes)).eq_ignore_ascii_case(expected_sha256) {
224 verify_worker_build(&destination)?;
225 return Ok(destination);
226 }
227 bail!(
228 "content-addressed worker cache {} does not match {} checksum",
229 destination.display(),
230 expected_sha256
231 );
232 }
233 let bytes = on_dedicated_thread(|| {
234 Ok(reqwest::blocking::Client::builder()
235 .timeout(std::time::Duration::from_secs(120))
236 .build()?
237 .get(url)
238 .send()?
239 .error_for_status()?
240 .bytes()?)
241 })?;
242 let actual = lower_hex(Sha256::digest(&bytes));
243 if !actual.eq_ignore_ascii_case(expected_sha256) {
244 bail!("downloaded worker checksum mismatch: expected {expected_sha256}, got {actual}");
245 }
246 std::fs::create_dir_all(&directory)?;
247 let mut temporary = tempfile::NamedTempFile::new_in(&directory)?;
248 std::io::Write::write_all(&mut temporary, &bytes)?;
249 temporary.as_file_mut().sync_all()?;
250 #[cfg(unix)]
251 {
252 use std::os::unix::fs::PermissionsExt;
253 std::fs::set_permissions(temporary.path(), std::fs::Permissions::from_mode(0o700))?;
254 }
255 publish_cached_worker(temporary, &directory, &digest)
256}
257
258pub(super) fn validate_worker_sha256(expected_sha256: &str) -> Result<()> {
259 if expected_sha256.len() != 64 || !expected_sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
260 {
261 bail!("MJ_WORKER_SHA256 must be a 64-character hexadecimal digest");
262 }
263 Ok(())
264}