talos_api_rs/testkit/
mod.rs1use base64::prelude::*;
4use serde::Deserialize;
5use std::env;
6use std::fs;
7use std::path::PathBuf;
8use std::process::Command;
9
10#[derive(Deserialize, Debug)]
11struct TalosConfig {
12 contexts: std::collections::HashMap<String, ContextConfig>,
13}
14
15#[derive(Deserialize, Debug)]
16struct ContextConfig {
17 endpoints: Vec<String>,
18 ca: String,
19 crt: String,
20 key: String,
21}
22
23pub struct TalosCluster {
24 pub name: String,
25 pub endpoint: String,
26 pub talosconfig_path: PathBuf,
27 _temp_dir: tempfile::TempDir,
29 pub ca_path: PathBuf,
30 pub crt_path: PathBuf,
31 pub key_path: PathBuf,
32}
33
34impl TalosCluster {
35 pub fn create(name: &str) -> Option<Self> {
38 if env::var("TALOS_DEV_TESTS").is_err() {
39 println!("Skipping integration test: TALOS_DEV_TESTS not set");
40 return None;
41 }
42
43 if Command::new("talosctl").arg("version").output().is_err() {
45 eprintln!("talosctl not found");
46 return None;
47 }
48
49 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
51 let talosconfig_path = temp_dir.path().join("talosconfig");
52
53 println!(
54 "Creating Talos cluster '{}' with config at {:?} ...",
55 name, talosconfig_path
56 );
57
58 let output = Command::new("talosctl")
61 .args([
62 "cluster",
63 "create",
64 "docker",
65 "--name",
66 name,
67 "--talosconfig-destination",
68 talosconfig_path.to_str().unwrap(),
69 ])
70 .output()
71 .expect("Failed to execute talosctl");
72
73 if !output.status.success() {
74 let stderr = String::from_utf8_lossy(&output.stderr);
75 if stderr.contains("Pool overlaps") {
76 eprintln!("\n\n!!! ERROR: Docker network overlap detected !!!");
77 eprintln!("A local Docker network is colliding with the Talos test subnet.");
78 eprintln!("Please clean up existing networks with:");
79 eprintln!(" docker network prune");
80 eprintln!("\nFull error: {}\n", stderr);
81 } else {
82 eprintln!("talosctl error: {}", stderr);
83 }
84 panic!("Failed to create cluster");
85 }
86
87 let config_str = fs::read_to_string(&talosconfig_path).expect("Failed to read talosconfig");
89 let config: TalosConfig =
90 serde_yaml::from_str(&config_str).expect("Failed to parse talosconfig");
91
92 let (_, ctx) = config
93 .contexts
94 .iter()
95 .next()
96 .expect("No context in talosconfig");
97
98 let decode_and_write = |fname: &str, content: &str| -> PathBuf {
100 let bytes = BASE64_STANDARD
101 .decode(content)
102 .or_else(|_| BASE64_STANDARD.decode(content.replace('\n', "")))
103 .expect("Failed to decode cert");
104 let path = temp_dir.path().join(fname);
105 fs::write(&path, bytes).expect("Failed to write cert file");
106 path
107 };
108
109 let ca_path = decode_and_write("ca.crt", &ctx.ca);
110 let crt_path = decode_and_write("client.crt", &ctx.crt);
111 let key_path = decode_and_write("client.key", &ctx.key);
112
113 let first_endpoint = ctx.endpoints.first().expect("No endpoints in talosconfig");
115 let endpoint = if first_endpoint.contains("://") {
116 first_endpoint.clone()
117 } else {
118 format!("https://{}", first_endpoint)
119 };
120
121 Some(Self {
122 name: name.to_string(),
123 endpoint,
124 talosconfig_path,
125 _temp_dir: temp_dir,
126 ca_path,
127 crt_path,
128 key_path,
129 })
130 }
131}
132
133impl Drop for TalosCluster {
134 fn drop(&mut self) {
135 if env::var("TALOS_DEV_TESTS").is_err() {
136 return;
137 }
138 println!("Destroying Talos cluster '{}'...", self.name);
139 let _ = Command::new("talosctl")
140 .args(["cluster", "destroy", "--name", &self.name])
141 .status();
142 }
143}