Skip to main content

kbolt_core/engine/
schedule_status_ops.rs

1use crate::error::CoreError;
2use crate::schedule_backend::{current_schedule_backend, inspect_schedule_backend};
3use crate::schedule_state_store::ScheduleRunStateStore;
4use crate::schedule_store::ScheduleCatalog;
5use crate::schedule_support::schedule_id_sort_key;
6use crate::Result;
7use kbolt_types::{
8    KboltError, ScheduleOrphan, ScheduleScope, ScheduleState, ScheduleStatusEntry,
9    ScheduleStatusResponse,
10};
11
12use super::Engine;
13
14impl Engine {
15    pub fn schedule_status(&self) -> Result<ScheduleStatusResponse> {
16        let backend = current_schedule_backend()?;
17        let mut schedules = ScheduleCatalog::load(&self.config.config_dir)?.schedules;
18        schedules.sort_by_key(|schedule| schedule_id_sort_key(&schedule.id));
19
20        let inspection =
21            inspect_schedule_backend(&self.config.config_dir, &self.config.cache_dir, &schedules)?;
22        let mut entries = Vec::with_capacity(schedules.len());
23
24        for schedule in schedules {
25            let state = if self.schedule_targets_missing(&schedule.scope)? {
26                ScheduleState::TargetMissing
27            } else if inspection.drifted_ids.contains(&schedule.id) {
28                ScheduleState::Drifted
29            } else {
30                ScheduleState::Installed
31            };
32
33            let run_state = ScheduleRunStateStore::load(&self.config.cache_dir, &schedule.id)?;
34            entries.push(ScheduleStatusEntry {
35                schedule,
36                backend,
37                state,
38                run_state,
39            });
40        }
41
42        let orphans = inspection
43            .orphan_ids
44            .into_iter()
45            .map(|id| ScheduleOrphan { id, backend })
46            .collect();
47
48        Ok(ScheduleStatusResponse {
49            schedules: entries,
50            orphans,
51        })
52    }
53
54    fn schedule_targets_missing(&self, scope: &ScheduleScope) -> Result<bool> {
55        match scope {
56            ScheduleScope::All => Ok(false),
57            ScheduleScope::Space { space } => match self.storage.get_space(space) {
58                Ok(_) => Ok(false),
59                Err(CoreError::Domain(KboltError::SpaceNotFound { .. })) => Ok(true),
60                Err(err) => Err(err),
61            },
62            ScheduleScope::Collections { space, collections } => {
63                let resolved_space = match self.storage.get_space(space) {
64                    Ok(space) => space,
65                    Err(CoreError::Domain(KboltError::SpaceNotFound { .. })) => return Ok(true),
66                    Err(err) => return Err(err),
67                };
68
69                for collection in collections {
70                    match self.storage.get_collection(resolved_space.id, collection) {
71                        Ok(_) => {}
72                        Err(CoreError::Domain(KboltError::CollectionNotFound { .. })) => {
73                            return Ok(true)
74                        }
75                        Err(err) => return Err(err),
76                    }
77                }
78
79                Ok(false)
80            }
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use fs2::FileExt;
88    use std::fs::OpenOptions;
89    use std::mem;
90
91    use tempfile::tempdir;
92
93    use super::Engine;
94    use crate::config::{ChunkingConfig, Config, RankingConfig, ReapingConfig};
95    use crate::storage::Storage;
96    use kbolt_types::{AddScheduleRequest, ScheduleScope, ScheduleTrigger};
97
98    #[test]
99    fn schedule_status_succeeds_while_global_lock_is_held() {
100        let engine = test_engine();
101        engine
102            .add_schedule(AddScheduleRequest {
103                trigger: ScheduleTrigger::Daily {
104                    time: "09:00".to_string(),
105                },
106                scope: ScheduleScope::All,
107            })
108            .expect("add schedule");
109
110        let lock_path = engine.config().cache_dir.join("kbolt.lock");
111        std::fs::create_dir_all(&engine.config().cache_dir).expect("create cache dir");
112        let holder = OpenOptions::new()
113            .read(true)
114            .write(true)
115            .create(true)
116            .truncate(false)
117            .open(&lock_path)
118            .expect("open lock file");
119        FileExt::try_lock_exclusive(&holder).expect("acquire global lock");
120
121        let status = engine.schedule_status().expect("load schedule status");
122        assert_eq!(status.schedules.len(), 1);
123        assert!(status.orphans.is_empty());
124    }
125
126    fn test_engine() -> Engine {
127        let root = tempdir().expect("create temp root");
128        let root_path = root.path().to_path_buf();
129        mem::forget(root);
130        let config_dir = root_path.join("config");
131        let cache_dir = root_path.join("cache");
132        let storage = Storage::new(&cache_dir).expect("create storage");
133        let config = Config {
134            config_dir,
135            cache_dir,
136            default_space: None,
137            providers: std::collections::HashMap::new(),
138            roles: crate::config::RoleBindingsConfig::default(),
139            reaping: ReapingConfig { days: 7 },
140            chunking: ChunkingConfig::default(),
141            ranking: RankingConfig::default(),
142        };
143        Engine::from_parts(storage, config)
144    }
145}