1use 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#[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
59pub 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 #[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 #[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
212fn 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
436pub 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
456fn 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
490pub 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#[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
518pub 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
546pub 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
560fn 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
575pub 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
604pub 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 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
650pub 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
682pub 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
691pub 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
703pub 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
755fn 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
775pub 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
819pub 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
843pub 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
867pub 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
892pub 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}