husako_openapi/
kubeconfig.rs1use crate::OpenApiError;
2
3#[derive(Debug, Clone)]
5pub struct ClusterCredentials {
6 pub server: String,
7 pub bearer_token: String,
8}
9
10pub fn resolve_credentials(server_url: &str) -> Result<ClusterCredentials, OpenApiError> {
15 let kube_dir = dirs_kube();
16 resolve_credentials_from_dir(&kube_dir, server_url)
17}
18
19pub fn resolve_credentials_from_dir(
21 kube_dir: &std::path::Path,
22 server_url: &str,
23) -> Result<ClusterCredentials, OpenApiError> {
24 let entries = std::fs::read_dir(kube_dir).map_err(|e| {
25 OpenApiError::Kubeconfig(format!("cannot read {}: {e}", kube_dir.display()))
26 })?;
27
28 let normalized_target = normalize_url(server_url);
29
30 for entry in entries {
31 let entry = entry.map_err(|e| OpenApiError::Kubeconfig(format!("read entry: {e}")))?;
32 let path = entry.path();
33
34 if !path.is_file() {
36 continue;
37 }
38
39 let content = match std::fs::read_to_string(&path) {
40 Ok(c) => c,
41 Err(_) => continue, };
43
44 let config: KubeConfig = match serde_yaml_ng::from_str(&content) {
45 Ok(c) => c,
46 Err(_) => continue, };
48
49 if let Some(creds) = find_credentials(&config, &normalized_target) {
50 return Ok(creds);
51 }
52 }
53
54 Err(OpenApiError::Kubeconfig(format!(
55 "no kubeconfig found for server '{server_url}' in {}",
56 kube_dir.display()
57 )))
58}
59
60fn find_credentials(config: &KubeConfig, normalized_target: &str) -> Option<ClusterCredentials> {
61 let cluster_entry = config.clusters.iter().find(|c| {
63 let normalized = normalize_url(&c.cluster.server);
64 normalized == normalized_target
65 })?;
66
67 let cluster_name = &cluster_entry.name;
68
69 let context_entry = config
71 .contexts
72 .iter()
73 .find(|ctx| &ctx.context.cluster == cluster_name)?;
74
75 let user_name = &context_entry.context.user;
76
77 let user_entry = config.users.iter().find(|u| &u.name == user_name)?;
79
80 let token = user_entry.user.token.as_ref()?;
82
83 Some(ClusterCredentials {
84 server: cluster_entry.cluster.server.clone(),
85 bearer_token: token.clone(),
86 })
87}
88
89fn normalize_url(url: &str) -> String {
91 url.trim_end_matches('/').to_string()
92}
93
94fn dirs_kube() -> std::path::PathBuf {
95 dirs_home().join(".kube")
96}
97
98fn dirs_home() -> std::path::PathBuf {
99 std::env::var("HOME")
100 .map(std::path::PathBuf::from)
101 .unwrap_or_else(|_| std::path::PathBuf::from("/root"))
102}
103
104#[derive(Debug, serde::Deserialize)]
107struct KubeConfig {
108 #[serde(default)]
109 clusters: Vec<NamedCluster>,
110 #[serde(default)]
111 contexts: Vec<NamedContext>,
112 #[serde(default)]
113 users: Vec<NamedUser>,
114}
115
116#[derive(Debug, serde::Deserialize)]
117struct NamedCluster {
118 name: String,
119 cluster: ClusterInfo,
120}
121
122#[derive(Debug, serde::Deserialize)]
123struct ClusterInfo {
124 server: String,
125}
126
127#[derive(Debug, serde::Deserialize)]
128struct NamedContext {
129 #[allow(dead_code)]
130 name: String,
131 context: ContextInfo,
132}
133
134#[derive(Debug, serde::Deserialize)]
135struct ContextInfo {
136 cluster: String,
137 user: String,
138}
139
140#[derive(Debug, serde::Deserialize)]
141struct NamedUser {
142 name: String,
143 user: UserInfo,
144}
145
146#[derive(Debug, serde::Deserialize)]
147struct UserInfo {
148 #[serde(default)]
149 token: Option<String>,
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn write_kubeconfig(dir: &std::path::Path, filename: &str, content: &str) {
157 std::fs::write(dir.join(filename), content).unwrap();
158 }
159
160 const STANDARD_KUBECONFIG: &str = r#"
161apiVersion: v1
162kind: Config
163clusters:
164 - name: my-cluster
165 cluster:
166 server: https://10.0.0.1:6443
167contexts:
168 - name: my-context
169 context:
170 cluster: my-cluster
171 user: my-user
172users:
173 - name: my-user
174 user:
175 token: my-bearer-token-123
176"#;
177
178 #[test]
179 fn resolve_standard_bearer_token() {
180 let tmp = tempfile::tempdir().unwrap();
181 write_kubeconfig(tmp.path(), "config", STANDARD_KUBECONFIG);
182
183 let creds = resolve_credentials_from_dir(tmp.path(), "https://10.0.0.1:6443").unwrap();
184 assert_eq!(creds.bearer_token, "my-bearer-token-123");
185 assert_eq!(creds.server, "https://10.0.0.1:6443");
186 }
187
188 #[test]
189 fn url_normalization_trailing_slash() {
190 let tmp = tempfile::tempdir().unwrap();
191 write_kubeconfig(tmp.path(), "config", STANDARD_KUBECONFIG);
192
193 let creds = resolve_credentials_from_dir(tmp.path(), "https://10.0.0.1:6443/").unwrap();
195 assert_eq!(creds.bearer_token, "my-bearer-token-123");
196 }
197
198 #[test]
199 fn no_match_returns_error() {
200 let tmp = tempfile::tempdir().unwrap();
201 write_kubeconfig(tmp.path(), "config", STANDARD_KUBECONFIG);
202
203 let err =
204 resolve_credentials_from_dir(tmp.path(), "https://other-server:6443").unwrap_err();
205 assert!(err.to_string().contains("no kubeconfig found"));
206 }
207
208 #[test]
209 fn skip_non_yaml_files() {
210 let tmp = tempfile::tempdir().unwrap();
211 write_kubeconfig(tmp.path(), "config", STANDARD_KUBECONFIG);
212 std::fs::write(tmp.path().join("binary.dat"), [0xFF, 0xFE, 0x00]).unwrap();
213 std::fs::write(tmp.path().join("readme.txt"), "not yaml").unwrap();
214
215 let creds = resolve_credentials_from_dir(tmp.path(), "https://10.0.0.1:6443").unwrap();
216 assert_eq!(creds.bearer_token, "my-bearer-token-123");
217 }
218
219 #[test]
220 fn multiple_configs_first_match_wins() {
221 let tmp = tempfile::tempdir().unwrap();
222
223 write_kubeconfig(
224 tmp.path(),
225 "config-a",
226 r#"
227apiVersion: v1
228kind: Config
229clusters:
230 - name: cluster-a
231 cluster:
232 server: https://a:6443
233contexts:
234 - name: ctx-a
235 context:
236 cluster: cluster-a
237 user: user-a
238users:
239 - name: user-a
240 user:
241 token: token-a
242"#,
243 );
244
245 write_kubeconfig(
246 tmp.path(),
247 "config-b",
248 r#"
249apiVersion: v1
250kind: Config
251clusters:
252 - name: cluster-b
253 cluster:
254 server: https://b:6443
255contexts:
256 - name: ctx-b
257 context:
258 cluster: cluster-b
259 user: user-b
260users:
261 - name: user-b
262 user:
263 token: token-b
264"#,
265 );
266
267 let creds = resolve_credentials_from_dir(tmp.path(), "https://b:6443").unwrap();
268 assert_eq!(creds.bearer_token, "token-b");
269 }
270
271 #[test]
272 fn no_token_user_skipped() {
273 let tmp = tempfile::tempdir().unwrap();
274 write_kubeconfig(
275 tmp.path(),
276 "config",
277 r#"
278apiVersion: v1
279kind: Config
280clusters:
281 - name: cl
282 cluster:
283 server: https://10.0.0.1:6443
284contexts:
285 - name: ctx
286 context:
287 cluster: cl
288 user: usr
289users:
290 - name: usr
291 user: {}
292"#,
293 );
294
295 let err = resolve_credentials_from_dir(tmp.path(), "https://10.0.0.1:6443").unwrap_err();
296 assert!(err.to_string().contains("no kubeconfig found"));
297 }
298}