Skip to main content

xianyun_cluster/driver/
colima.rs

1//! Port of `internal/cluster/colima/driver.go`: a [`super::Driver`]
2//! implementation using Colima as the backend.
3//!
4//! This is the highest-churn driver file in the Go codebase (45 commits per
5//! VCS hotspot analysis), so its tests are ported with extra care for edge
6//! cases in `tests/colima_driver.rs`.
7//!
8//! **Executor injection.** Following the pattern established by
9//! `external::Driver` (see its module doc comment), this driver takes its
10//! `Arc<dyn CommandRunner + Send + Sync>` at construction rather than
11//! through an ambient context. [`Driver::new`] wires in the real
12//! `xianyun_executor::exec::Client` so existing callers (`driver::auto`)
13//! need no change; [`Driver::with_runner`] is the test-facing constructor
14//! that takes an explicit runner (typically
15//! `xianyun_testutil::MockCommandRunner`).
16
17use std::path::PathBuf;
18use std::sync::Arc;
19
20use super::{Driver as DriverTrait, DriverError, State, Status};
21use xianyun_core::config::types::{
22    ClusterConfig, ColimaConfig, K3sConfig, NetworkConfig, RegistryMirrorConfig,
23};
24use xianyun_executor::exec::{CommandRunner, Options};
25
26pub const DRIVER_NAME: &str = "colima";
27
28/// Port of colima's `ErrInvalidSizeFormat` et al.
29#[derive(Debug, thiserror::Error, PartialEq, Eq)]
30pub enum ColimaError {
31    #[error("invalid size format: {0}")]
32    InvalidSizeFormat(String),
33    #[error("invalid size value: {0}")]
34    InvalidSizeValue(String),
35    #[error("colima k3s version is not set: set cluster.colima.k3s_version in caproni.yaml")]
36    K3sVersionNotDefined,
37    #[error(
38        "upstream is not a bare registry host (path separators and \"..\" are not allowed): {0:?}"
39    )]
40    InvalidUpstream(String),
41    #[error("colima profile is running without Kubernetes enabled")]
42    KubernetesNotEnabled,
43    #[error("colima profile is running but k3s is not ready")]
44    K3sNotReady,
45    #[error("colima profile is running on a non-containerd runtime")]
46    RuntimeMismatch,
47    #[error("colima VM has no network IP")]
48    NoVmIp,
49    #[error("{0}")]
50    Other(String),
51}
52
53impl From<ColimaError> for DriverError {
54    fn from(err: ColimaError) -> Self {
55        DriverError::Other(err.to_string())
56    }
57}
58
59/// Port of `colima.Driver`.
60pub struct Driver {
61    pub cfg: ClusterConfig,
62    pub profile_name: String,
63    pub goos: String,
64    runner: Arc<dyn CommandRunner + Send + Sync>,
65}
66
67impl Driver {
68    /// Port of `colima.NewDriver`, wired to the real subprocess executor.
69    #[must_use]
70    pub fn new(cfg: ClusterConfig, profile_name: String) -> Self {
71        Self::with_runner(
72            cfg,
73            profile_name,
74            Arc::new(xianyun_executor::exec::Client::new()),
75        )
76    }
77
78    /// Test-facing constructor: takes the injected `CommandRunner`
79    /// explicitly. See the module doc comment.
80    #[must_use]
81    pub fn with_runner(
82        cfg: ClusterConfig,
83        profile_name: String,
84        runner: Arc<dyn CommandRunner + Send + Sync>,
85    ) -> Self {
86        Self {
87            cfg,
88            profile_name,
89            goos: std::env::consts::OS.to_string(),
90            runner,
91        }
92    }
93}
94
95impl DriverTrait for Driver {
96    fn name(&self) -> &str {
97        DRIVER_NAME
98    }
99
100    fn start(&self) -> Result<(), DriverError> {
101        let cfg = self.cfg.clone();
102
103        if !is_colima_available(self) {
104            return Err(DriverError::DriverNotFound("colima".to_string()));
105        }
106
107        let Some(colima_cfg) = cfg.colima.clone() else {
108            return Err(DriverError::MissingDriverConfig);
109        };
110
111        let memory_gib = parse_size_gib(&colima_cfg.memory)
112            .map_err(|e| DriverError::Other(format!("parsing memory: {e}")))?;
113
114        let already_running = check_running_cluster(self, memory_gib, &colima_cfg)?;
115
116        if already_running {
117            return reconcile_running_cluster(self, &cfg);
118        }
119
120        start_new_cluster(self, &cfg, memory_gib)
121    }
122
123    fn stop(&self) -> Result<(), DriverError> {
124        let status = status_impl(self)?;
125
126        if status.state == Some(State::Stopped) {
127            return Ok(());
128        }
129
130        tracing::info!(profile = %self.profile_name, "stopping colima cluster");
131
132        let args = vec![
133            "stop".to_string(),
134            profile_flag(),
135            self.profile_name.clone(),
136        ];
137
138        self.runner
139            .exec_streamed_command(
140                Some(xianyun_executor::exec::ExecStreamedOpts {
141                    env: colima_env(),
142                    ..Default::default()
143                }),
144                "colima",
145                &args,
146            )
147            .map_err(|e| DriverError::Other(format!("stopping colima: {e}")))
148    }
149
150    fn destroy(&self) -> Result<(), DriverError> {
151        if !is_colima_available(self) {
152            return Err(DriverError::DriverNotFound("colima".to_string()));
153        }
154
155        let status = status_impl(self)?;
156
157        if status.state == Some(State::Stopped) && !profile_exists(self) {
158            return Ok(());
159        }
160
161        tracing::info!(profile = %self.profile_name, "destroying colima cluster");
162
163        let args = vec![
164            "delete".to_string(),
165            profile_flag(),
166            self.profile_name.clone(),
167            "--data".to_string(),
168            "--force".to_string(),
169        ];
170
171        self.runner
172            .exec_streamed_command(
173                Some(xianyun_executor::exec::ExecStreamedOpts {
174                    env: colima_env(),
175                    ..Default::default()
176                }),
177                "colima",
178                &args,
179            )
180            .map_err(|e| DriverError::Other(format!("destroying colima: {e}")))
181    }
182
183    fn status(&self) -> Result<Status, DriverError> {
184        status_impl(self)
185    }
186
187    fn get_kubeconfig(&self) -> Result<String, DriverError> {
188        if !is_colima_available(self) {
189            return Err(DriverError::DriverNotFound("colima".to_string()));
190        }
191
192        let status = status_impl(self)?;
193
194        if status.state != Some(State::Ready) {
195            return Err(DriverError::ClusterNotRunning);
196        }
197
198        read_colima_stdout(
199            self,
200            &[
201                &profile_flag(),
202                &self.profile_name,
203                "ssh",
204                "cat",
205                "/etc/rancher/k3s/k3s.yaml",
206            ],
207        )
208        .map_err(|e| DriverError::Other(format!("reading kubeconfig: {e}")))
209    }
210}
211
212/// Port of `colima.profileFlag`.
213fn profile_flag() -> String {
214    "--profile".to_string()
215}
216
217fn start_new_cluster(
218    driver: &Driver,
219    cfg: &ClusterConfig,
220    memory_gib: f64,
221) -> Result<(), DriverError> {
222    let colima_cfg = cfg.colima.as_ref().expect("checked by caller");
223
224    let mut args = vec![
225        "start".to_string(),
226        profile_flag(),
227        driver.profile_name.clone(),
228    ];
229    args.extend(
230        build_start_args(colima_cfg, cfg.network.as_ref(), memory_gib, &driver.goos)
231            .map_err(DriverError::from)?,
232    );
233
234    ensure_colima_containerd_config(driver).map_err(|e| {
235        DriverError::Other(format!(
236            "configuring colima containerd registry config_path: {e}"
237        ))
238    })?;
239
240    tracing::info!(
241        profile = %driver.profile_name,
242        memory = format!("{memory_gib:.2}"),
243        cores = colima_cfg.cores,
244        "starting colima cluster"
245    );
246
247    let result = driver.runner.exec_streamed_command(
248        Some(xianyun_executor::exec::ExecStreamedOpts {
249            env: colima_env(),
250            ..Default::default()
251        }),
252        "colima",
253        &args,
254    );
255
256    if let Err(err) = result {
257        if driver.goos == "darwin"
258            && let Ok(status) = status_impl(driver)
259            && status.state == Some(State::Ready)
260            && status.ip_address.is_empty()
261        {
262            return Err(DriverError::Other(format!(
263                "{} (colima start: {err}): this may happen when Docker Desktop or Rancher Desktop is \
264                        running, or a stale `limactl` process from a previous run is still active. Run \
265                        `colima delete --profile {}` and try again",
266                ColimaError::NoVmIp,
267                driver.profile_name
268            )));
269        }
270
271        return Err(DriverError::Other(format!("starting colima: {err}")));
272    }
273
274    check_containerd_runtime(driver).map_err(DriverError::from)?;
275
276    if let Some(mirror) = &cfg.registry_mirror {
277        ensure_containerd_mirror(driver, mirror)
278            .map_err(|e| DriverError::Other(format!("ensuring containerd registry mirror: {e}")))?;
279    }
280
281    Ok(())
282}
283
284fn reconcile_running_cluster(driver: &Driver, cfg: &ClusterConfig) -> Result<(), DriverError> {
285    if let Some(mirror) = &cfg.registry_mirror {
286        if !registry_mirror_config_active(driver) {
287            tracing::warn!(
288                profile = %driver.profile_name,
289                "registry_mirror is configured but this colima VM has no containerd registry config_path, \
290                so the mirror is inactive; the VM was likely provisioned by an older caproni that predates \
291                this setting. Run `caproni destroy && caproni up` to re-provision the VM and activate the mirror."
292            );
293        }
294
295        ensure_containerd_mirror(driver, mirror)
296            .map_err(|e| DriverError::Other(format!("ensuring containerd registry mirror: {e}")))?;
297    }
298
299    Ok(())
300}
301
302fn check_running_cluster(
303    driver: &Driver,
304    memory_gib: f64,
305    colima_cfg: &ColimaConfig,
306) -> Result<bool, DriverError> {
307    let status = match status_impl(driver) {
308        Ok(status) => status,
309        Err(_) => return Ok(false),
310    };
311
312    if status.state == Some(State::Stopped) {
313        return Ok(false);
314    }
315
316    if status.kubernetes_version.is_empty() {
317        return Err(DriverError::Other(format!(
318            "{}: run `colima delete --profile {}` and try again",
319            ColimaError::KubernetesNotEnabled,
320            driver.profile_name
321        )));
322    }
323
324    check_k3s_ready(driver).map_err(DriverError::from)?;
325    check_containerd_runtime(driver).map_err(DriverError::from)?;
326
327    let (enough, running_memory, running_cores) =
328        enough_resources_provisioned(driver, memory_gib, colima_cfg.cores)
329            .map_err(|e| DriverError::Other(format!("checking running configuration: {e}")))?;
330
331    if enough {
332        return Ok(true);
333    }
334
335    Err(DriverError::Other(format!(
336        "cluster configuration mismatch: current config (memory={running_memory:.2}, cores={running_cores}) \
337        is less than requested config (memory={}, cores={})",
338        colima_cfg.memory, colima_cfg.cores
339    )))
340}
341
342fn status_impl(driver: &Driver) -> Result<Status, DriverError> {
343    if !is_colima_available(driver) {
344        return Err(DriverError::DriverNotFound("colima".to_string()));
345    }
346
347    let mut status = Status {
348        driver: DRIVER_NAME.to_string(),
349        state: Some(State::Stopped),
350        kube_context: format!("colima-{}", driver.profile_name),
351        ..Default::default()
352    };
353
354    let stdout = match read_colima_stdout(
355        driver,
356        &["list", &profile_flag(), &driver.profile_name, "--json"],
357    ) {
358        Ok(out) => out,
359        Err(_) => {
360            return Ok(status);
361        }
362    };
363
364    let colima_list: ColimaListJson = serde_json::from_str(&stdout)
365        .map_err(|e| DriverError::Other(format!("parsing colima list JSON: {e}")))?;
366
367    if colima_list.status == "Running" {
368        status.state = Some(State::Ready);
369    }
370
371    if colima_list.runtime.contains("k3s") {
372        status.kubernetes_version = "k3s".to_string();
373    }
374
375    status.ip_address = colima_list.ip_address;
376    status.host_ip = status.ip_address.clone();
377
378    Ok(status)
379}
380
381fn profile_exists(driver: &Driver) -> bool {
382    read_colima_stdout(
383        driver,
384        &["list", &profile_flag(), &driver.profile_name, "--json"],
385    )
386    .is_ok()
387}
388
389pub fn is_colima_available(driver: &Driver) -> bool {
390    driver.runner.look_path("colima").is_ok()
391}
392
393fn check_containerd_runtime(driver: &Driver) -> Result<(), ColimaError> {
394    let stdout = read_colima_stdout(
395        driver,
396        &["list", &profile_flag(), &driver.profile_name, "--json"],
397    )
398    .map_err(|e| ColimaError::Other(format!("reading colima profile list: {e}")))?;
399
400    let colima_list: ColimaListJson = serde_json::from_str(&stdout)
401        .map_err(|e| ColimaError::Other(format!("parsing colima list JSON: {e}")))?;
402
403    if !colima_list.runtime.contains("containerd") {
404        return Err(ColimaError::Other(format!(
405            "{} (got {:?}): run `caproni destroy && caproni up` to recreate the profile",
406            ColimaError::RuntimeMismatch,
407            colima_list.runtime
408        )));
409    }
410
411    Ok(())
412}
413
414fn enough_resources_provisioned(
415    driver: &Driver,
416    cfg_memory_gib: f64,
417    cfg_cores: i32,
418) -> Result<(bool, f64, i32), ColimaError> {
419    let stdout = read_colima_stdout(
420        driver,
421        &["list", &profile_flag(), &driver.profile_name, "--json"],
422    )
423    .map_err(|e| ColimaError::Other(format!("reading colima configuration: {e}")))?;
424
425    let colima_list: ColimaListJson = serde_json::from_str(&stdout)
426        .map_err(|e| ColimaError::Other(format!("parsing colima list JSON: {e}")))?;
427
428    let running_memory_gib = colima_list.memory as f64 / (1024.0 * 1024.0 * 1024.0);
429    let running_cores = colima_list.cpus;
430
431    let matches = running_memory_gib >= cfg_memory_gib && running_cores >= cfg_cores;
432
433    Ok((matches, running_memory_gib, running_cores))
434}
435
436/// Port of `colima.parseSizeGiB`.
437pub fn parse_size_gib(size: &str) -> Result<f64, ColimaError> {
438    let (number, unit) =
439        split_size(size).ok_or_else(|| ColimaError::InvalidSizeFormat(size.to_string()))?;
440
441    let value: f64 = number
442        .parse()
443        .map_err(|_| ColimaError::InvalidSizeValue(number.to_string()))?;
444
445    let gib = match unit {
446        "GiB" => value,
447        "GB" => value * (1_000_000_000.0 / (1024.0 * 1024.0 * 1024.0)),
448        "MiB" => value / 1024.0,
449        "MB" => value * (1_000_000.0 / (1024.0 * 1024.0 * 1024.0)),
450        _ => unreachable!("split_size only returns known units"),
451    };
452
453    Ok((gib * 100.0).round() / 100.0)
454}
455
456/// Splits a size string like "10GiB" into its numeric prefix and unit
457/// suffix, matching the Go regex `^(\d+(?:\.\d+)?)(GiB|GB|MiB|MB)$`.
458fn split_size(size: &str) -> Option<(&str, &str)> {
459    for unit in ["GiB", "GB", "MiB", "MB"] {
460        if let Some(number) = size.strip_suffix(unit)
461            && !number.is_empty()
462            && is_unsigned_decimal(number)
463        {
464            return Some((number, unit));
465        }
466    }
467    None
468}
469
470fn is_unsigned_decimal(s: &str) -> bool {
471    let mut seen_dot = false;
472    let mut seen_digit = false;
473
474    for c in s.chars() {
475        if c == '.' {
476            if seen_dot {
477                return false;
478            }
479            seen_dot = true;
480        } else if c.is_ascii_digit() {
481            seen_digit = true;
482        } else {
483            return false;
484        }
485    }
486
487    seen_digit
488}
489
490/// Port of `colima.parseDiskGiB`.
491pub fn parse_disk_gib(disk: &str) -> Result<i64, ColimaError> {
492    let gib = parse_size_gib(disk).map_err(|e| ColimaError::Other(format!("parsing disk: {e}")))?;
493    Ok(gib.round() as i64)
494}
495
496/// Port of `colima.colimaListJSON`, the JSON shape of `colima list --json`.
497#[derive(Debug, Clone, Default, serde::Deserialize)]
498#[serde(rename_all = "camelCase")]
499pub struct ColimaListJson {
500    #[serde(default)]
501    pub name: String,
502    #[serde(default)]
503    pub status: String,
504    #[serde(default)]
505    pub arch: String,
506    #[serde(default)]
507    pub cpus: i32,
508    #[serde(default)]
509    pub memory: i64,
510    #[serde(default)]
511    pub disk: i64,
512    #[serde(default)]
513    pub runtime: String,
514    #[serde(rename = "address", default)]
515    pub ip_address: String,
516}
517
518/// Port of `colima.colimaEnv`: sets HOME to the sudo user's home when
519/// running under sudo, so colima finds its profiles under sudo.
520pub fn colima_env() -> Option<Vec<String>> {
521    if std::env::var("SUDO_USER").unwrap_or_default().is_empty() {
522        return None;
523    }
524
525    let home = match colima_home_dir() {
526        Ok(home) => home,
527        Err(err) => {
528            tracing::warn!(error = %err, "failed to look up sudo user home directory, colima may not find cluster profile");
529            return None;
530        }
531    };
532
533    let mut env: Vec<String> = std::env::vars().map(|(k, v)| format!("{k}={v}")).collect();
534
535    for entry in &mut env {
536        if entry.starts_with("HOME=") {
537            *entry = format!("HOME={home}");
538            return Some(env);
539        }
540    }
541
542    env.push(format!("HOME={home}"));
543    Some(env)
544}
545
546/// Port of `colima.colimaHomeDir`.
547pub fn colima_home_dir() -> Result<String, ColimaError> {
548    let sudo_user = std::env::var("SUDO_USER").unwrap_or_default();
549
550    if !sudo_user.is_empty() {
551        return lookup_user_home_dir(&sudo_user).ok_or_else(|| {
552            ColimaError::Other("looking up sudo user home directory: user not found".to_string())
553        });
554    }
555
556    std::env::var("HOME")
557        .map_err(|_| ColimaError::Other("getting home directory: $HOME is not set".to_string()))
558}
559
560/// Minimal `/etc/passwd` lookup, standing in for `os/user.Lookup` (no such
561/// crate is a dependency of this crate; see `PORT_NOTES.md`).
562fn lookup_user_home_dir(username: &str) -> Option<String> {
563    let contents = std::fs::read_to_string("/etc/passwd").ok()?;
564
565    for line in contents.lines() {
566        let fields: Vec<&str> = line.split(':').collect();
567        if fields.len() >= 6 && fields[0] == username {
568            return Some(fields[5].to_string());
569        }
570    }
571
572    None
573}
574
575/// Port of `(*colima.Driver).readColimaStdout`.
576pub fn read_colima_stdout(driver: &Driver, args: &[&str]) -> Result<String, ColimaError> {
577    let args_owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
578
579    match colima_env() {
580        None => driver
581            .runner
582            .read_stdout("colima", &args_owned)
583            .map_err(|e| ColimaError::Other(format!("running colima: {e}"))),
584        Some(env) => {
585            let opts = Options {
586                env: Some(env),
587                disable_streaming: true,
588                log_level: Some(tracing::Level::DEBUG),
589                stderr_log_level: Some(tracing::Level::DEBUG),
590                ..Default::default()
591            };
592
593            let result = driver.runner.run("colima", &args_owned, Some(opts));
594
595            if let Some(err) = result.error {
596                return Err(ColimaError::Other(format!("running colima: {err}")));
597            }
598
599            Ok(result.stdout.join("\n"))
600        }
601    }
602}
603
604/// Port of `(*colima.Driver).buildStartArgs`.
605pub fn build_start_args(
606    colima_cfg: &ColimaConfig,
607    network: Option<&NetworkConfig>,
608    memory_gib: f64,
609    goos: &str,
610) -> Result<Vec<String>, ColimaError> {
611    if colima_cfg.k3s_version.is_empty() {
612        return Err(ColimaError::K3sVersionNotDefined);
613    }
614
615    // `start` and the `--profile <name>` pair are prepended by the caller
616    // (`start_new_cluster`), which knows the driver's profile name; this
617    // function is pure over `ColimaConfig` alone, matching the ported test
618    // table in `tests/colima_driver.rs` (none of which assert on `start`
619    // or the profile flag).
620    let mut args: Vec<String> = vec![
621        "--runtime".to_string(),
622        "containerd".to_string(),
623        "--activate=false".to_string(),
624        "--kubernetes".to_string(),
625        "--kubernetes-version".to_string(),
626        colima_cfg.k3s_version.clone(),
627        "--memory".to_string(),
628        format!("{memory_gib:.2}"),
629        "--cpu".to_string(),
630        colima_cfg.cores.to_string(),
631    ];
632
633    args.extend(k3s_server_args(colima_cfg.k3s.as_ref(), network));
634
635    if !colima_cfg.disk.is_empty() {
636        let disk_gib = parse_disk_gib(&colima_cfg.disk)?;
637        args.push("--disk".to_string());
638        args.push(disk_gib.to_string());
639    }
640
641    if goos == "darwin" {
642        args.push("--network-address".to_string());
643        args.push("--vm-type".to_string());
644        args.push("vz".to_string());
645    }
646
647    Ok(args)
648}
649
650/// Port of `colima.k3sServerArgs`.
651pub fn k3s_server_args(
652    k3s_cfg: Option<&K3sConfig>,
653    network: Option<&NetworkConfig>,
654) -> Vec<String> {
655    let mut values: Vec<String> = Vec::new();
656
657    if let Some(k3s_cfg) = k3s_cfg {
658        for component in &k3s_cfg.disable {
659            values.push(format!("--disable={component}"));
660        }
661    }
662
663    if let Some(network) = network
664        && !network.cluster_domain.is_empty()
665    {
666        values.push(format!("--cluster-domain={}", network.cluster_domain));
667    }
668
669    if values.is_empty() {
670        return vec!["--k3s-arg".to_string(), String::new()];
671    }
672
673    let mut args = Vec::with_capacity(values.len() * 2);
674    for v in values {
675        args.push("--k3s-arg".to_string());
676        args.push(v);
677    }
678
679    args
680}
681
682/// Port of `colima.upstreamServerURL`.
683pub fn upstream_server_url(upstream: &str) -> String {
684    if upstream == "docker.io" {
685        "https://registry-1.docker.io".to_string()
686    } else {
687        format!("https://{upstream}")
688    }
689}
690
691/// Port of `colima.renderHostsTOML`.
692pub fn render_hosts_toml(upstream: &str, mirror_url: &str) -> String {
693    format!(
694        "server = \"{}\"\n\n[host.\"{mirror_url}\"]\ncapabilities = [\"pull\", \"resolve\"]\nskip_verify = true\n",
695        upstream_server_url(upstream)
696    )
697}
698
699const CONTAINERD_CERTS_DIR: &str = "/etc/containerd/certs.d";
700
701const COLIMA_CONTAINERD_CONFIG: &str = "[grpc]\ngid = 1000\n\n[plugins.'io.containerd.cri.v1.images'.registry]\nconfig_path = '/etc/containerd/certs.d'\n";
702
703/// Port of `(*colima.Driver).ensureContainerdMirror`.
704pub fn ensure_containerd_mirror(
705    driver: &Driver,
706    mirror: &RegistryMirrorConfig,
707) -> Result<(), ColimaError> {
708    let endpoint = mirror.host_port();
709    let mirror_url = format!("http://{endpoint}");
710
711    for upstream in &mirror.upstreams {
712        if upstream == "." || upstream == ".." || upstream != &base_name(upstream) {
713            return Err(ColimaError::InvalidUpstream(upstream.clone()));
714        }
715    }
716
717    let certs_dir = certs_dir(&driver.profile_name)?;
718
719    tracing::info!(
720        profile = %driver.profile_name,
721        endpoint = %endpoint,
722        upstreams = ?mirror.upstreams,
723        certs_dir = %certs_dir.display(),
724        "ensuring containerd registry mirror for colima VM"
725    );
726
727    if certs_dir.exists() {
728        std::fs::remove_dir_all(&certs_dir)
729            .map_err(|e| ColimaError::Other(format!("clearing certs directory: {e}")))?;
730    }
731
732    for upstream in &mirror.upstreams {
733        let dir = certs_dir.join(upstream);
734
735        std::fs::create_dir_all(&dir)
736            .map_err(|e| ColimaError::Other(format!("creating {}: {e}", dir.display())))?;
737
738        std::fs::write(
739            dir.join("hosts.toml"),
740            render_hosts_toml(upstream, &mirror_url),
741        )
742        .map_err(|e| ColimaError::Other(format!("writing {}/hosts.toml: {e}", dir.display())))?;
743    }
744
745    symlink_certs_dir(driver, &certs_dir.to_string_lossy())
746}
747
748fn base_name(path: &str) -> String {
749    std::path::Path::new(path)
750        .file_name()
751        .map(|n| n.to_string_lossy().into_owned())
752        .unwrap_or_default()
753}
754
755/// Resolves the certs.d directory for `profile`, mirroring
756/// `datadir.CertsDir` but taking `$HOME` (respecting `colima_home_dir`'s
757/// sudo-aware resolution) directly rather than depending on the
758/// out-of-scope `xianyun-executor::datadir` module's ambient-HOME entry
759/// point.
760fn certs_dir(profile: &str) -> Result<PathBuf, ColimaError> {
761    let home = std::env::var("HOME")
762        .map_err(|_| ColimaError::Other("getting home directory: $HOME is not set".to_string()))?;
763
764    let dir = PathBuf::from(home)
765        .join(".local/share/caproni")
766        .join(profile)
767        .join("certs.d");
768
769    std::fs::create_dir_all(&dir)
770        .map_err(|e| ColimaError::Other(format!("creating certs directory: {e}")))?;
771
772    Ok(dir)
773}
774
775/// Port of `(*colima.Driver).colimaConfigDir`.
776pub fn colima_config_dir(driver: &Driver) -> Result<String, ColimaError> {
777    if let Ok(dir) = std::env::var("COLIMA_HOME")
778        && !dir.is_empty()
779        && std::path::Path::new(&dir).exists()
780    {
781        return Ok(dir);
782    }
783
784    let home = colima_home_dir()?;
785
786    let legacy_dir = PathBuf::from(&home).join(".colima");
787    if legacy_dir.exists() {
788        return Ok(legacy_dir.to_string_lossy().into_owned());
789    }
790
791    if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME")
792        && !xdg.is_empty()
793    {
794        return Ok(PathBuf::from(xdg)
795            .join("colima")
796            .to_string_lossy()
797            .into_owned());
798    }
799
800    if driver.goos == "darwin" {
801        return Ok(legacy_dir.to_string_lossy().into_owned());
802    }
803
804    Ok(PathBuf::from(&home)
805        .join(".config")
806        .join("colima")
807        .to_string_lossy()
808        .into_owned())
809}
810
811fn colima_containerd_config_path(driver: &Driver) -> Result<PathBuf, ColimaError> {
812    let dir = colima_config_dir(driver)?;
813    Ok(PathBuf::from(dir)
814        .join(&driver.profile_name)
815        .join("containerd")
816        .join("config.toml"))
817}
818
819/// Port of `(*colima.Driver).ensureColimaContainerdConfig`.
820pub fn ensure_colima_containerd_config(driver: &Driver) -> Result<(), ColimaError> {
821    let path = colima_containerd_config_path(driver)?;
822
823    let parent = path.parent().ok_or_else(|| {
824        ColimaError::Other("colima containerd config path has no parent".to_string())
825    })?;
826
827    std::fs::create_dir_all(parent).map_err(|e| {
828        ColimaError::Other(format!("creating colima containerd config directory: {e}"))
829    })?;
830
831    std::fs::write(&path, COLIMA_CONTAINERD_CONFIG)
832        .map_err(|e| ColimaError::Other(format!("writing colima containerd config: {e}")))?;
833
834    tracing::info!(
835        profile = %driver.profile_name,
836        path = %path.display(),
837        "wrote colima containerd config with registry config_path"
838    );
839
840    Ok(())
841}
842
843/// Port of `(*colima.Driver).registryMirrorConfigActive`.
844pub fn registry_mirror_config_active(driver: &Driver) -> bool {
845    let out = match read_colima_stdout(
846        driver,
847        &[
848            &profile_flag(),
849            &driver.profile_name,
850            "ssh",
851            "--",
852            "sudo",
853            "containerd",
854            "config",
855            "dump",
856        ],
857    ) {
858        Ok(out) => out,
859        Err(_) => return true,
860    };
861
862    out.contains(CONTAINERD_CERTS_DIR)
863}
864
865const K3S_MANIFESTS_DIR_PATH: &str = "/var/lib/rancher/k3s/server/manifests";
866
867/// Port of `(*colima.Driver).checkK3sReady`.
868pub fn check_k3s_ready(driver: &Driver) -> Result<(), ColimaError> {
869    read_colima_stdout(
870        driver,
871        &[
872            &profile_flag(),
873            &driver.profile_name,
874            "ssh",
875            "--",
876            "sudo",
877            "test",
878            "-d",
879            K3S_MANIFESTS_DIR_PATH,
880        ],
881    )
882    .map(|_| ())
883    .map_err(|_| {
884        ColimaError::Other(format!(
885            "{}: run `colima delete --profile {}` and try again",
886            ColimaError::K3sNotReady,
887            driver.profile_name
888        ))
889    })
890}
891
892/// Port of `(*colima.Driver).symlinkCertsDir`.
893pub fn symlink_certs_dir(driver: &Driver, certs_dir: &str) -> Result<(), ColimaError> {
894    read_colima_stdout(
895        driver,
896        &[
897            &profile_flag(),
898            &driver.profile_name,
899            "ssh",
900            "--",
901            "sudo",
902            "rm",
903            "-rf",
904            CONTAINERD_CERTS_DIR,
905        ],
906    )
907    .map_err(|e| ColimaError::Other(format!("clearing containerd certs.d in colima VM: {e}")))?;
908
909    read_colima_stdout(
910        driver,
911        &[
912            &profile_flag(),
913            &driver.profile_name,
914            "ssh",
915            "--",
916            "sudo",
917            "ln",
918            "-sfn",
919            certs_dir,
920            CONTAINERD_CERTS_DIR,
921        ],
922    )
923    .map_err(|e| ColimaError::Other(format!("symlinking containerd certs.d in colima VM: {e}")))?;
924
925    tracing::info!(
926        target = certs_dir,
927        link = CONTAINERD_CERTS_DIR,
928        "containerd certs.d symlinked in VM"
929    );
930
931    Ok(())
932}