1#[allow(unused_imports)] use std::collections::HashMap;
7use std::fmt::Debug;
8
9use crate::{Api, Error, Result};
10use kube_core::{Resource, params::PostParams};
11use serde::{Serialize, de::DeserializeOwned};
12
13impl<K: Resource + Clone + DeserializeOwned + Debug> Api<K> {
14 pub async fn entry<'a>(&'a self, name: &'a str) -> Result<Entry<'a, K>> {
49 Ok(match self.get_opt(name).await? {
50 Some(object) => Entry::Occupied(OccupiedEntry {
51 api: self,
52 dirtiness: Dirtiness::Clean,
53 name,
54 object,
55 }),
56 None => Entry::Vacant(VacantEntry { api: self, name }),
57 })
58 }
59}
60
61#[derive(Debug)]
62pub enum Entry<'a, K> {
66 Occupied(OccupiedEntry<'a, K>),
68 Vacant(VacantEntry<'a, K>),
70}
71
72impl<'a, K> Entry<'a, K> {
73 pub fn get(&self) -> Option<&K> {
75 match self {
76 Entry::Occupied(entry) => Some(entry.get()),
77 Entry::Vacant(_) => None,
78 }
79 }
80
81 pub fn get_mut(&mut self) -> Option<&mut K> {
85 match self {
86 Entry::Occupied(entry) => Some(entry.get_mut()),
87 Entry::Vacant(_) => None,
88 }
89 }
90
91 pub fn and_modify(self, f: impl FnOnce(&mut K)) -> Self {
95 match self {
96 Entry::Occupied(entry) => Entry::Occupied(entry.and_modify(f)),
97 entry @ Entry::Vacant(_) => entry,
98 }
99 }
100
101 pub fn or_insert(self, default: impl FnOnce() -> K) -> OccupiedEntry<'a, K>
105 where
106 K: Resource,
107 {
108 match self {
109 Entry::Occupied(entry) => entry,
110 Entry::Vacant(entry) => entry.insert(default()),
111 }
112 }
113}
114
115#[derive(Debug)]
120pub struct OccupiedEntry<'a, K> {
121 api: &'a Api<K>,
122 dirtiness: Dirtiness,
123 name: &'a str,
124 object: K,
125}
126
127#[derive(Debug)]
128enum Dirtiness {
129 Clean,
131 Dirty,
133 New,
135}
136
137impl<K> OccupiedEntry<'_, K> {
138 pub fn get(&self) -> &K {
140 &self.object
141 }
142
143 pub fn get_mut(&mut self) -> &mut K {
147 self.dirtiness = match self.dirtiness {
148 Dirtiness::Clean => Dirtiness::Dirty,
149 Dirtiness::Dirty => Dirtiness::Dirty,
150 Dirtiness::New => Dirtiness::New,
151 };
152 &mut self.object
153 }
154
155 pub fn and_modify(mut self, f: impl FnOnce(&mut K)) -> Self {
159 f(self.get_mut());
160 self
161 }
162
163 pub fn into_object(self) -> K {
165 self.object
166 }
167
168 #[tracing::instrument(skip(self))]
182 pub async fn commit(&mut self, pp: &PostParams) -> Result<(), CommitError>
183 where
184 K: Resource + DeserializeOwned + Serialize + Clone + Debug,
185 {
186 self.prepare_for_commit()?;
187 match self.dirtiness {
188 Dirtiness::New => {
189 self.object = self
190 .api
191 .create(pp, &self.object)
192 .await
193 .map_err(CommitError::Save)?
194 }
195 Dirtiness::Dirty => {
196 self.object = self
197 .api
198 .replace(self.name, pp, &self.object)
199 .await
200 .map_err(CommitError::Save)?;
201 }
202 Dirtiness::Clean => (),
203 };
204 if !pp.dry_run {
205 self.dirtiness = Dirtiness::Clean;
206 }
207 Ok(())
208 }
209
210 fn prepare_for_commit(&mut self) -> Result<(), CommitValidationError>
214 where
215 K: Resource,
216 {
217 let meta = self.object.meta_mut();
219 match &mut meta.name {
220 name @ None => *name = Some(self.name.to_string()),
221 Some(name) if name != self.name => {
222 return Err(CommitValidationError::NameMismatch {
223 object_name: name.clone(),
224 expected: self.name.to_string(),
225 });
226 }
227 Some(_) => (),
228 }
229 match &mut meta.namespace {
230 ns @ None => ns.clone_from(&self.api.namespace),
231 Some(ns) if Some(ns.as_str()) != self.api.namespace.as_deref() => {
232 return Err(CommitValidationError::NamespaceMismatch {
233 object_namespace: Some(ns.clone()),
234 expected: self.api.namespace.clone(),
235 });
236 }
237 Some(_) => (),
238 }
239 if let Some(generate_name) = &meta.generate_name {
240 return Err(CommitValidationError::GenerateName {
241 object_generate_name: generate_name.clone(),
242 });
243 }
244 Ok(())
245 }
246}
247
248#[derive(Debug, thiserror::Error)]
249pub enum CommitError {
251 #[error("failed to validate object for saving")]
253 Validate(#[from] CommitValidationError),
254 #[error("failed to save object")]
256 Save(#[source] Error),
257}
258
259#[derive(Debug, thiserror::Error)]
260pub enum CommitValidationError {
262 #[error(
264 ".metadata.name does not match the name passed to Api::entry (got: {object_name:?}, expected: {expected:?})"
265 )]
266 NameMismatch {
267 object_name: String,
269 expected: String,
271 },
272 #[error(
274 ".metadata.namespace does not match the namespace of the Api (got: {object_namespace:?}, expected: {expected:?})"
275 )]
276 NamespaceMismatch {
277 object_namespace: Option<String>,
279 expected: Option<String>,
281 },
282 #[error(".metadata.generate_name must not be set (got: {object_generate_name:?})")]
284 GenerateName {
285 object_generate_name: String,
287 },
288}
289
290#[derive(Debug)]
294pub struct VacantEntry<'a, K> {
295 api: &'a Api<K>,
296 name: &'a str,
297}
298
299impl<'a, K> VacantEntry<'a, K> {
300 #[tracing::instrument(skip(self, object))]
304 pub fn insert(self, object: K) -> OccupiedEntry<'a, K>
305 where
306 K: Resource,
307 {
308 OccupiedEntry {
309 api: self.api,
310 dirtiness: Dirtiness::New,
311 name: self.name,
312 object,
313 }
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use std::collections::BTreeMap;
320
321 use k8s_openapi::api::core::v1::ConfigMap;
322 use kube_core::{
323 ObjectMeta,
324 params::{DeleteParams, PostParams},
325 };
326
327 use crate::{
328 Api, Client, Error,
329 api::entry::{CommitError, Entry},
330 };
331
332 #[tokio::test]
333 #[ignore = "needs cluster (gets and writes cms)"]
334 async fn entry_create_missing_object() -> Result<(), Box<dyn std::error::Error>> {
335 let client = Client::try_default().await?;
336 let api = Api::<ConfigMap>::default_namespaced(client);
337
338 let object_name = "entry-missing-cm";
339 if api.get_opt(object_name).await?.is_some() {
340 api.delete(object_name, &DeleteParams::default()).await?;
341 }
342
343 let entry = api.entry(object_name).await?;
344 let entry2 = api.entry(object_name).await?;
345 assert_eq!(entry.get(), None);
346 assert_eq!(entry2.get(), None);
347
348 let mut entry = entry.or_insert(|| ConfigMap {
350 data: Some([("key".to_string(), "value".to_string())].into()),
351 ..ConfigMap::default()
352 });
353 entry.commit(&PostParams::default()).await?;
354 assert_eq!(
355 entry
356 .get()
357 .data
358 .as_ref()
359 .and_then(|data| data.get("key"))
360 .map(String::as_str),
361 Some("value")
362 );
363 let fetched_obj = api.get(object_name).await?;
364 assert_eq!(
365 fetched_obj
366 .data
367 .as_ref()
368 .and_then(|data| data.get("key"))
369 .map(String::as_str),
370 Some("value")
371 );
372
373 entry
375 .get_mut()
376 .data
377 .get_or_insert_with(BTreeMap::default)
378 .insert("key".to_string(), "value2".to_string());
379 entry.commit(&PostParams::default()).await?;
380 assert_eq!(
381 entry
382 .get()
383 .data
384 .as_ref()
385 .and_then(|data| data.get("key"))
386 .map(String::as_str),
387 Some("value2")
388 );
389 let fetched_obj = api.get(object_name).await?;
390 assert_eq!(
391 fetched_obj
392 .data
393 .as_ref()
394 .and_then(|data| data.get("key"))
395 .map(String::as_str),
396 Some("value2")
397 );
398
399 let mut entry2 = entry2.or_insert(|| ConfigMap {
401 data: Some([("key".to_string(), "value3".to_string())].into()),
402 ..ConfigMap::default()
403 });
404 assert!(
405 matches!(dbg!(entry2.commit(&PostParams::default()).await), Err(CommitError::Save(Error::Api(status))) if status.is_already_exists())
406 );
407
408 api.delete(object_name, &DeleteParams::default()).await?;
410 Ok(())
411 }
412
413 #[tokio::test]
414 #[ignore = "needs cluster (gets and writes cms)"]
415 async fn entry_update_existing_object() -> Result<(), Box<dyn std::error::Error>> {
416 let client = Client::try_default().await?;
417 let api = Api::<ConfigMap>::default_namespaced(client);
418
419 let object_name = "entry-existing-cm";
420 if api.get_opt(object_name).await?.is_some() {
421 api.delete(object_name, &DeleteParams::default()).await?;
422 }
423 api.create(&PostParams::default(), &ConfigMap {
424 metadata: ObjectMeta {
425 namespace: api.namespace.clone(),
426 name: Some(object_name.to_string()),
427 ..ObjectMeta::default()
428 },
429 data: Some([("key".to_string(), "value".to_string())].into()),
430 ..ConfigMap::default()
431 })
432 .await?;
433
434 let mut entry = match api.entry(object_name).await? {
435 Entry::Occupied(entry) => entry,
436 entry => panic!("entry for existing object must be occupied: {entry:?}"),
437 };
438 let mut entry2 = match api.entry(object_name).await? {
439 Entry::Occupied(entry) => entry,
440 entry => panic!("entry for existing object must be occupied: {entry:?}"),
441 };
442
443 entry
445 .get_mut()
446 .data
447 .get_or_insert_with(BTreeMap::default)
448 .insert("key".to_string(), "value2".to_string());
449 entry.commit(&PostParams::default()).await?;
450 assert_eq!(
451 entry
452 .get()
453 .data
454 .as_ref()
455 .and_then(|data| data.get("key"))
456 .map(String::as_str),
457 Some("value2")
458 );
459 let fetched_obj = api.get(object_name).await?;
460 assert_eq!(
461 fetched_obj
462 .data
463 .as_ref()
464 .and_then(|data| data.get("key"))
465 .map(String::as_str),
466 Some("value2")
467 );
468
469 entry2
471 .get_mut()
472 .data
473 .get_or_insert_with(BTreeMap::default)
474 .insert("key".to_string(), "value3".to_string());
475 assert!(
476 matches!(entry2.commit(&PostParams::default()).await, Err(CommitError::Save(Error::Api(status))) if status.is_conflict())
477 );
478
479 api.delete(object_name, &DeleteParams::default()).await?;
481 Ok(())
482 }
483
484 #[tokio::test]
485 #[ignore = "needs cluster (gets and writes cms)"]
486 async fn entry_create_dry_run() -> Result<(), Box<dyn std::error::Error>> {
487 let client = Client::try_default().await?;
488 let api = Api::<ConfigMap>::default_namespaced(client);
489
490 let object_name = "entry-cm-dry";
491 if api.get_opt(object_name).await?.is_some() {
492 api.delete(object_name, &DeleteParams::default()).await?;
493 }
494
495 let pp_dry = PostParams {
496 dry_run: true,
497 ..Default::default()
498 };
499
500 let entry = api.entry(object_name).await?;
501 assert_eq!(entry.get(), None);
502
503 let mut entry = entry.or_insert(|| ConfigMap {
505 data: Some([("key".to_string(), "value".to_string())].into()),
506 ..ConfigMap::default()
507 });
508 entry.commit(&pp_dry).await?;
509 assert_eq!(
510 entry
511 .get()
512 .data
513 .as_ref()
514 .and_then(|data| data.get("key"))
515 .map(String::as_str),
516 Some("value")
517 );
518 let fetched_obj = api.get_opt(object_name).await?;
519 assert_eq!(fetched_obj, None);
520
521 entry.commit(&PostParams::default()).await?;
523 assert_eq!(
524 entry
525 .get()
526 .data
527 .as_ref()
528 .and_then(|data| data.get("key"))
529 .map(String::as_str),
530 Some("value")
531 );
532 let fetched_obj = api.get(object_name).await?;
533 assert_eq!(
534 fetched_obj
535 .data
536 .as_ref()
537 .and_then(|data| data.get("key"))
538 .map(String::as_str),
539 Some("value")
540 );
541
542 entry
544 .get_mut()
545 .data
546 .get_or_insert_with(BTreeMap::default)
547 .insert("key".to_string(), "value2".to_string());
548 entry.commit(&pp_dry).await?;
549 assert_eq!(
550 entry
551 .get()
552 .data
553 .as_ref()
554 .and_then(|data| data.get("key"))
555 .map(String::as_str),
556 Some("value2")
557 );
558 let fetched_obj = api.get(object_name).await?;
559 assert_eq!(
560 fetched_obj
561 .data
562 .as_ref()
563 .and_then(|data| data.get("key"))
564 .map(String::as_str),
565 Some("value")
566 );
567
568 entry.commit(&PostParams::default()).await?;
570 assert_eq!(
571 entry
572 .get()
573 .data
574 .as_ref()
575 .and_then(|data| data.get("key"))
576 .map(String::as_str),
577 Some("value2")
578 );
579 let fetched_obj = api.get(object_name).await?;
580 assert_eq!(
581 fetched_obj
582 .data
583 .as_ref()
584 .and_then(|data| data.get("key"))
585 .map(String::as_str),
586 Some("value2")
587 );
588
589 api.delete(object_name, &DeleteParams::default()).await?;
591 Ok(())
592 }
593}