Skip to main content

talos_api_rs/testkit/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use 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 to hold certs
28    _temp_dir: tempfile::TempDir,
29    pub ca_path: PathBuf,
30    pub crt_path: PathBuf,
31    pub key_path: PathBuf,
32}
33
34impl TalosCluster {
35    /// Provisions a new local Talos cluster in Docker.
36    /// SKIPS if `TALOS_DEV_TESTS` is not set.
37    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        // check if talosctl exists
44        if Command::new("talosctl").arg("version").output().is_err() {
45            eprintln!("talosctl not found");
46            return None;
47        }
48
49        // Create temp dir for config and certs
50        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        // We use 'docker' provisioner explicitly via subcommand
59        // And --talosconfig-destination to save the config
60        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        // Parse talosconfig
88        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        // Helper to decode and write
99        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        // Format endpoint from first entry in endpoints array
114        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}