xianyun_cluster/kubeconfig/
merge.rs1use thiserror::Error;
23
24pub const KEY_CLUSTERS: &str = "clusters";
26pub const KEY_CONTEXTS: &str = "contexts";
27pub const KEY_USERS: &str = "users";
28pub const KEY_CURRENT_CTX: &str = "current-context";
29pub const KEY_NAME: &str = "name";
30pub const KEY_CONTEXT: &str = "context";
31pub const KEY_CONTEXT_USER: &str = "user";
32pub const KEY_CONTEXT_CLUSTER: &str = "cluster";
33
34#[derive(Debug, Error, PartialEq, Eq)]
36pub enum MergeError {
37 #[error("kubeconfig contains no context: {0}")]
38 NoContext(String),
39 #[error("kubeconfig has multiple contexts and no current-context")]
40 AmbiguousContext,
41 #[error("kubeconfig is not a YAML mapping")]
42 NotMapping,
43}
44
45pub struct InstallEntries {
50 pub cluster: Vec<String>,
51 pub context: Vec<String>,
52 pub user: Vec<String>,
53}
54
55pub fn install(
67 dst: &[u8],
68 src: &[u8],
69 name: &str,
70 set_current: bool,
71) -> Result<Vec<u8>, MergeError> {
72 let entries = normalize_entries(src, name)?;
73
74 validate_mapping(dst)?;
75
76 let mut lines = to_lines(dst);
77
78 upsert_entry(&mut lines, KEY_CLUSTERS, name, entries.cluster);
79 upsert_entry(&mut lines, KEY_CONTEXTS, name, entries.context);
80 upsert_entry(&mut lines, KEY_USERS, name, entries.user);
81
82 if set_current {
83 set_scalar(&mut lines, KEY_CURRENT_CTX, name);
84 }
85
86 Ok(from_lines(&lines))
87}
88
89pub fn normalize(src: &[u8], name: &str) -> Result<Vec<u8>, MergeError> {
93 install(b"apiVersion: v1\nkind: Config\n", src, name, true)
94}
95
96pub fn uninstall(dst: &[u8], name: &str) -> Result<Vec<u8>, MergeError> {
103 if dst.is_empty() {
104 return Ok(Vec::new());
105 }
106
107 validate_mapping(dst)?;
108
109 let mut lines = to_lines(dst);
110 let mut changed = false;
111
112 for key in [KEY_CLUSTERS, KEY_CONTEXTS, KEY_USERS] {
113 changed |= remove_entry(&mut lines, key, name);
114 }
115
116 if get_scalar(&lines, KEY_CURRENT_CTX).as_deref() == Some(name) {
117 changed |= remove_scalar(&mut lines, KEY_CURRENT_CTX);
118 }
119
120 if !changed {
121 return Ok(dst.to_vec());
122 }
123
124 Ok(from_lines(&lines))
125}
126
127fn normalize_entries(src: &[u8], name: &str) -> Result<InstallEntries, MergeError> {
131 let root: serde_yaml::Value =
132 serde_yaml::from_slice(src).map_err(|e| MergeError::NoContext(e.to_string()))?;
133
134 let mapping = as_mapping_or_empty(&root)?;
135
136 let contexts: Vec<serde_yaml::Value> = mapping
137 .get(KEY_CONTEXTS)
138 .and_then(serde_yaml::Value::as_sequence)
139 .cloned()
140 .unwrap_or_default();
141
142 let current = mapping
143 .get(KEY_CURRENT_CTX)
144 .and_then(serde_yaml::Value::as_str)
145 .unwrap_or("")
146 .to_string();
147
148 let context_value = active_context(&contexts, ¤t)?;
149 let mut context_entry = context_value
150 .as_mapping()
151 .cloned()
152 .ok_or_else(|| MergeError::NoContext(format!("context {name:?} is not a mapping")))?;
153
154 let inner = context_entry
155 .get(KEY_CONTEXT)
156 .and_then(serde_yaml::Value::as_mapping)
157 .cloned()
158 .ok_or_else(|| MergeError::NoContext(format!("context {name:?} has no context block")))?;
159
160 let cluster_ref = inner
161 .get(KEY_CONTEXT_CLUSTER)
162 .and_then(serde_yaml::Value::as_str)
163 .unwrap_or("")
164 .to_string();
165 let user_ref = inner
166 .get(KEY_CONTEXT_USER)
167 .and_then(serde_yaml::Value::as_str)
168 .unwrap_or("")
169 .to_string();
170
171 let clusters: Vec<serde_yaml::Value> = mapping
172 .get(KEY_CLUSTERS)
173 .and_then(serde_yaml::Value::as_sequence)
174 .cloned()
175 .unwrap_or_default();
176 let users: Vec<serde_yaml::Value> = mapping
177 .get(KEY_USERS)
178 .and_then(serde_yaml::Value::as_sequence)
179 .cloned()
180 .unwrap_or_default();
181
182 let mut cluster_entry = find_entry(&clusters, &cluster_ref)
183 .and_then(|v| v.as_mapping().cloned())
184 .ok_or_else(|| MergeError::NoContext(format!("no cluster named {cluster_ref:?}")))?;
185
186 let mut user_entry = find_entry(&users, &user_ref)
189 .and_then(|v| v.as_mapping().cloned())
190 .unwrap_or_default();
191
192 let mut inner_renamed = inner;
194 inner_renamed.insert(
195 serde_yaml::Value::String(KEY_CONTEXT_CLUSTER.to_string()),
196 serde_yaml::Value::String(name.to_string()),
197 );
198 inner_renamed.insert(
199 serde_yaml::Value::String(KEY_CONTEXT_USER.to_string()),
200 serde_yaml::Value::String(name.to_string()),
201 );
202 context_entry.insert(
203 serde_yaml::Value::String(KEY_CONTEXT.to_string()),
204 serde_yaml::Value::Mapping(inner_renamed),
205 );
206 context_entry.insert(
207 serde_yaml::Value::String(KEY_NAME.to_string()),
208 serde_yaml::Value::String(name.to_string()),
209 );
210 cluster_entry.insert(
211 serde_yaml::Value::String(KEY_NAME.to_string()),
212 serde_yaml::Value::String(name.to_string()),
213 );
214 user_entry.insert(
215 serde_yaml::Value::String(KEY_NAME.to_string()),
216 serde_yaml::Value::String(name.to_string()),
217 );
218
219 Ok(InstallEntries {
220 cluster: render_entry_lines(&serde_yaml::Value::Mapping(cluster_entry)),
221 context: render_entry_lines(&serde_yaml::Value::Mapping(context_entry)),
222 user: render_entry_lines(&serde_yaml::Value::Mapping(user_entry)),
223 })
224}
225
226fn active_context(
230 contexts: &[serde_yaml::Value],
231 current: &str,
232) -> Result<serde_yaml::Value, MergeError> {
233 if contexts.is_empty() {
234 return Err(MergeError::NoContext(String::new()));
235 }
236
237 if !current.is_empty() {
238 return find_entry(contexts, current).ok_or_else(|| {
239 MergeError::NoContext(format!("current-context {current:?} is not defined"))
240 });
241 }
242
243 if contexts.len() > 1 {
244 return Err(MergeError::AmbiguousContext);
245 }
246
247 Ok(contexts[0].clone())
248}
249
250fn find_entry(seq: &[serde_yaml::Value], name: &str) -> Option<serde_yaml::Value> {
253 if name.is_empty() {
254 return None;
255 }
256
257 seq.iter()
258 .find(|entry| {
259 entry
260 .as_mapping()
261 .and_then(|m| m.get(KEY_NAME))
262 .and_then(serde_yaml::Value::as_str)
263 == Some(name)
264 })
265 .cloned()
266}
267
268fn as_mapping_or_empty(value: &serde_yaml::Value) -> Result<serde_yaml::Mapping, MergeError> {
271 match value {
272 serde_yaml::Value::Mapping(m) => Ok(m.clone()),
273 serde_yaml::Value::Null => Ok(serde_yaml::Mapping::new()),
274 _ => Err(MergeError::NotMapping),
275 }
276}
277
278fn validate_mapping(data: &[u8]) -> Result<(), MergeError> {
281 if data.iter().all(u8::is_ascii_whitespace) {
282 return Ok(());
283 }
284
285 let value: serde_yaml::Value =
286 serde_yaml::from_slice(data).map_err(|_| MergeError::NotMapping)?;
287 as_mapping_or_empty(&value).map(|_| ())
288}
289
290fn render_entry_lines(entry: &serde_yaml::Value) -> Vec<String> {
294 let text = serde_yaml::to_string(entry).expect("serializing a kubeconfig entry cannot fail");
295 let mut out = Vec::new();
296
297 for (i, line) in text.lines().enumerate() {
298 if line.is_empty() {
299 continue;
300 }
301
302 if i == 0 {
303 out.push(format!("- {line}"));
304 } else {
305 out.push(format!(" {line}"));
306 }
307 }
308
309 out
310}
311
312fn to_lines(data: &[u8]) -> Vec<String> {
315 let text = String::from_utf8_lossy(data);
316 if text.is_empty() {
317 return Vec::new();
318 }
319
320 text.strip_suffix('\n')
321 .unwrap_or(&text)
322 .split('\n')
323 .map(std::string::ToString::to_string)
324 .collect()
325}
326
327fn from_lines(lines: &[String]) -> Vec<u8> {
330 if lines.is_empty() {
331 return Vec::new();
332 }
333
334 let mut out = lines.join("\n");
335 out.push('\n');
336 out.into_bytes()
337}
338
339fn find_top_level_key(lines: &[String], key: &str) -> Option<usize> {
342 let target = format!("{key}:");
343 lines.iter().position(|line| line == &target)
344}
345
346fn section_end(lines: &[String], body_start: usize) -> usize {
351 for (offset, line) in lines[body_start..].iter().enumerate() {
352 if line.is_empty() {
353 continue;
354 }
355
356 if !line.starts_with(' ') && !line.starts_with('-') {
357 return body_start + offset;
358 }
359 }
360
361 lines.len()
362}
363
364fn entry_starts(lines: &[String], start: usize, end: usize) -> Vec<usize> {
367 (start..end)
368 .filter(|&i| lines[i].starts_with("- "))
369 .collect()
370}
371
372fn entry_end(starts: &[usize], entry_start: usize, section_end: usize) -> usize {
375 starts
376 .iter()
377 .copied()
378 .find(|&s| s > entry_start)
379 .unwrap_or(section_end)
380}
381
382fn entry_name(lines: &[String], entry_start: usize, entry_stop: usize) -> Option<String> {
386 for line in &lines[entry_start..entry_stop] {
387 if let Some(rest) = line.strip_prefix("- name: ") {
388 return Some(rest.trim().to_string());
389 }
390
391 if let Some(rest) = line.strip_prefix(" name: ") {
392 return Some(rest.trim().to_string());
393 }
394 }
395
396 None
397}
398
399fn upsert_entry(lines: &mut Vec<String>, key: &str, name: &str, entry: Vec<String>) {
403 let Some(key_idx) = find_top_level_key(lines, key) else {
404 lines.push(format!("{key}:"));
405 lines.extend(entry);
406 return;
407 };
408
409 let body_start = key_idx + 1;
410 let end = section_end(lines, body_start);
411 let starts = entry_starts(lines, body_start, end);
412
413 for &start in &starts {
414 let stop = entry_end(&starts, start, end);
415 if entry_name(lines, start, stop).as_deref() == Some(name) {
416 lines.splice(start..stop, entry);
417 return;
418 }
419 }
420
421 lines.splice(end..end, entry);
422}
423
424fn remove_entry(lines: &mut Vec<String>, key: &str, name: &str) -> bool {
427 let Some(key_idx) = find_top_level_key(lines, key) else {
428 return false;
429 };
430
431 let body_start = key_idx + 1;
432 let end = section_end(lines, body_start);
433 let starts = entry_starts(lines, body_start, end);
434
435 for &start in &starts {
436 let stop = entry_end(&starts, start, end);
437 if entry_name(lines, start, stop).as_deref() == Some(name) {
438 lines.drain(start..stop);
439 return true;
440 }
441 }
442
443 false
444}
445
446fn get_scalar(lines: &[String], key: &str) -> Option<String> {
448 let prefix = format!("{key}: ");
449 lines.iter().find_map(|line| {
450 line.strip_prefix(&prefix)
451 .map(std::string::ToString::to_string)
452 })
453}
454
455fn set_scalar(lines: &mut Vec<String>, key: &str, value: &str) {
458 let prefix = format!("{key}: ");
459 if let Some(idx) = lines.iter().position(|line| line.starts_with(&prefix)) {
460 lines[idx] = format!("{key}: {value}");
461 return;
462 }
463
464 lines.push(format!("{key}: {value}"));
465}
466
467fn remove_scalar(lines: &mut Vec<String>, key: &str) -> bool {
470 let prefix = format!("{key}: ");
471 if let Some(idx) = lines.iter().position(|line| line.starts_with(&prefix)) {
472 lines.remove(idx);
473 return true;
474 }
475
476 false
477}