Skip to main content

xianyun_cluster/kubeconfig/
file.rs

1//! Port of `internal/kubeconfig/file.go`: resolves, reads, and atomically
2//! rewrites a kubeconfig file on disk, keeping a rolling backup.
3
4use thiserror::Error;
5
6/// Port of the `backupSuffix`, `fileMode`, `dirMode` constants.
7pub const BACKUP_SUFFIX: &str = ".caproni.bak";
8pub const FILE_MODE: u32 = 0o600;
9pub const DIR_MODE: u32 = 0o700;
10
11/// Errors from resolving, reading, or writing a kubeconfig file.
12#[derive(Debug, Error)]
13pub enum FileError {
14    #[error("getting home dir: {0}")]
15    HomeDir(String),
16    #[error("reading kubeconfig {0}: {1}")]
17    Read(String, String),
18    #[error("creating kubeconfig directory {0}: {1}")]
19    CreateDir(String, String),
20    #[error("creating kubeconfig tempfile: {0}")]
21    CreateTempfile(String),
22    #[error("writing kubeconfig tempfile: {0}")]
23    WriteTempfile(String),
24    #[error("closing kubeconfig tempfile: {0}")]
25    CloseTempfile(String),
26    #[error("setting kubeconfig permissions: {0}")]
27    SetPermissions(String),
28    #[error("replacing kubeconfig {0}: {1}")]
29    Replace(String, String),
30    #[error("writing kubeconfig backup {0}: {1}")]
31    WriteBackup(String, String),
32}
33
34/// Port of `kubeconfig.TargetPath`. The precedence mirrors
35/// `kubectl config set-*`: an explicit `override` wins, otherwise the first
36/// entry of `KUBECONFIG` (kubectl writes to the first file in the list, not
37/// all of them), otherwise `~/.kube/config`.
38pub fn target_path(override_path: &str) -> Result<String, FileError> {
39    if !override_path.is_empty() {
40        return Ok(override_path.to_string());
41    }
42
43    let sep = if cfg!(windows) { ';' } else { ':' };
44    if let Ok(kubeconfig) = std::env::var("KUBECONFIG") {
45        for path in kubeconfig.split(sep) {
46            if !path.is_empty() {
47                return Ok(path.to_string());
48            }
49        }
50    }
51
52    let home_dir = std::env::var_os("HOME")
53        .or_else(|| std::env::var_os("USERPROFILE"))
54        .map(std::path::PathBuf::from)
55        .ok_or_else(|| FileError::HomeDir("could not determine home directory".to_string()))?;
56
57    Ok(home_dir
58        .join(".kube")
59        .join("config")
60        .to_string_lossy()
61        .into_owned())
62}
63
64/// Port of `kubeconfig.Read`. Returns `Ok(None)` when the file does not
65/// exist yet, matching Go's `nil, nil` return.
66pub fn read(path: &str) -> Result<Option<Vec<u8>>, FileError> {
67    match std::fs::read(path) {
68        Ok(data) => Ok(Some(data)),
69        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
70        Err(e) => Err(FileError::Read(path.to_string(), e.to_string())),
71    }
72}
73
74/// Port of `kubeconfig.Write`. Returns the backup path, empty when there
75/// was nothing to back up.
76pub fn write(path: &str, content: &[u8]) -> Result<String, FileError> {
77    let path_buf = std::path::Path::new(path);
78    let dir = path_buf
79        .parent()
80        .unwrap_or_else(|| std::path::Path::new("."));
81
82    std::fs::create_dir_all(dir)
83        .map_err(|e| FileError::CreateDir(dir.to_string_lossy().into_owned(), e.to_string()))?;
84
85    let backup_path = backup(path)?;
86
87    write_atomic(path, content, target_mode(path))?;
88
89    Ok(backup_path)
90}
91
92/// Port of `kubeconfig.targetMode`.
93fn target_mode(path: &str) -> u32 {
94    use std::os::unix::fs::PermissionsExt;
95
96    std::fs::metadata(path)
97        .map(|m| m.permissions().mode() & 0o777)
98        .unwrap_or(FILE_MODE)
99}
100
101/// Port of `kubeconfig.writeAtomic`.
102fn write_atomic(path: &str, content: &[u8], mode: u32) -> Result<(), FileError> {
103    use std::io::Write;
104    use std::os::unix::fs::PermissionsExt;
105
106    let path_buf = std::path::Path::new(path);
107    let dir = path_buf
108        .parent()
109        .unwrap_or_else(|| std::path::Path::new("."));
110
111    let mut tmp = tempfile::Builder::new()
112        .prefix(".caproni-kubeconfig-")
113        .tempfile_in(dir)
114        .map_err(|e| FileError::CreateTempfile(e.to_string()))?;
115
116    tmp.write_all(content)
117        .map_err(|e| FileError::WriteTempfile(e.to_string()))?;
118
119    tmp.as_file()
120        .set_permissions(std::fs::Permissions::from_mode(mode))
121        .map_err(|e| FileError::SetPermissions(e.to_string()))?;
122
123    tmp.persist(path_buf)
124        .map_err(|e| FileError::Replace(path.to_string(), e.to_string()))?;
125
126    Ok(())
127}
128
129/// Port of `kubeconfig.backup`.
130fn backup(path: &str) -> Result<String, FileError> {
131    let Some(data) = read(path)? else {
132        return Ok(String::new());
133    };
134
135    let backup_path = format!("{path}{BACKUP_SUFFIX}");
136
137    std::fs::write(&backup_path, &data)
138        .map_err(|e| FileError::WriteBackup(backup_path.clone(), e.to_string()))?;
139
140    Ok(backup_path)
141}