1use std::collections::BTreeMap;
4
5use super::error::StateError;
6use super::row::Row;
7use super::store::Store;
8use crate::types::DnsName;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum InstanceStatus {
12 Active,
13 Tombstoned,
14}
15
16impl InstanceStatus {
17 pub(super) fn from_sql(s: &str) -> Result<Self, StateError> {
18 match s {
19 "active" => Ok(Self::Active),
20 "tombstoned" => Ok(Self::Tombstoned),
21 other => Err(StateError::RowDecode {
22 column: 2,
23 detail: format!("unknown instance status {other:?}"),
24 }),
25 }
26 }
27}
28
29#[derive(Debug, Clone)]
30pub struct InstanceRecord {
31 pub name: DnsName,
32 pub substrate: DnsName,
33 pub status: InstanceStatus,
34 pub definition: String,
36 pub source_overrides: BTreeMap<String, String>,
38 pub dirty: bool,
41 pub definition_dir: String,
44 pub created_at: i64,
45 pub tombstoned_at: Option<i64>,
46}
47
48const SELECT_COLUMNS: &str = "name, substrate, status, definition, source_overrides, created_at, tombstoned_at, definition_dir, dirty";
49
50impl TryFrom<&Row> for InstanceRecord {
51 type Error = StateError;
52
53 fn try_from(row: &Row) -> Result<Self, Self::Error> {
54 let status = row.get_string(2)?;
55 let overrides_json = row.get_string(4)?;
56 let name = row.get_string(0)?;
57 let substrate = row.get_string(1)?;
58 Ok(Self {
59 name: DnsName::try_new(&name).map_err(|err| StateError::RowDecode {
60 column: 0,
61 detail: err.to_string(),
62 })?,
63 substrate: DnsName::try_new(&substrate).map_err(|err| StateError::RowDecode {
64 column: 1,
65 detail: err.to_string(),
66 })?,
67 status: InstanceStatus::from_sql(&status)?,
68 definition: row.get_string(3)?,
69 source_overrides: serde_json::from_str(&overrides_json).unwrap_or_default(),
70 created_at: row.get_i64(5)?,
71 tombstoned_at: row.get_opt_i64(6)?,
72 definition_dir: row.get_string(7)?,
73 dirty: row.get_i64(8)? != 0,
74 })
75 }
76}
77
78impl Store {
79 pub fn create_instance(
84 &self,
85 name: &str,
86 substrate: &str,
87 definition: &str,
88 source_overrides: &BTreeMap<String, String>,
89 definition_dir: &str,
90 dirty: bool,
91 ) -> Result<InstanceRecord, StateError> {
92 let overrides_json =
93 serde_json::to_string(source_overrides).unwrap_or_else(|_| "{}".into());
94 let result = self.execute(
95 "INSERT INTO instances (name, substrate, status, definition, source_overrides, created_at, definition_dir, dirty)
96 VALUES (?1, ?2, 'active', ?3, ?4, ?5, ?6, ?7)",
97 &[
98 name.into(),
99 substrate.into(),
100 definition.into(),
101 overrides_json.into(),
102 Self::now().into(),
103 definition_dir.into(),
104 i64::from(dirty).into(),
105 ],
106 );
107 match result {
108 Ok(_) => self
109 .instance(name)?
110 .ok_or_else(|| StateError::InstanceNotFound { name: name.into() }),
111 Err(err) => {
112 if let Some(existing) = self.instance(name)? {
117 Err(StateError::InstanceExists {
118 name: name.into(),
119 existing_substrate: existing.substrate.as_str().to_owned(),
120 })
121 } else {
122 Err(err)
123 }
124 }
125 }
126 }
127
128 pub fn instance(&self, name: &str) -> Result<Option<InstanceRecord>, StateError> {
129 self.query_row(
130 &format!("SELECT {SELECT_COLUMNS} FROM instances WHERE name = ?1"),
131 &[name.into()],
132 |row| InstanceRecord::try_from(row),
133 )
134 }
135
136 pub fn instances(&self) -> Result<Vec<InstanceRecord>, StateError> {
137 self.query_map(
138 &format!("SELECT {SELECT_COLUMNS} FROM instances ORDER BY name"),
139 &[],
140 |row| InstanceRecord::try_from(row),
141 )
142 }
143
144 pub fn tombstone_instance(&self, name: &str) -> Result<(), StateError> {
146 let changed = self.execute(
147 "UPDATE instances SET status = 'tombstoned', tombstoned_at = ?2 WHERE name = ?1",
148 &[name.into(), Self::now().into()],
149 )?;
150 if changed == 0 {
151 return Err(StateError::InstanceNotFound { name: name.into() });
152 }
153 Ok(())
154 }
155
156 pub fn revive_instance(
159 &self,
160 name: &str,
161 definition: &str,
162 source_overrides: &BTreeMap<String, String>,
163 dirty: bool,
164 ) -> Result<(), StateError> {
165 let overrides_json =
166 serde_json::to_string(source_overrides).unwrap_or_else(|_| "{}".into());
167 let changed = self.execute(
168 "UPDATE instances SET status = 'active', definition = ?2, source_overrides = ?3,
169 created_at = ?4, tombstoned_at = NULL, dirty = ?5 WHERE name = ?1",
170 &[
171 name.into(),
172 definition.into(),
173 overrides_json.into(),
174 Self::now().into(),
175 i64::from(dirty).into(),
176 ],
177 )?;
178 if changed == 0 {
179 return Err(StateError::InstanceNotFound { name: name.into() });
180 }
181 self.execute(
182 "DELETE FROM checkpoints WHERE instance = ?1",
183 &[name.into()],
184 )?;
185 Ok(())
186 }
187
188 pub fn update_source_overrides(
191 &self,
192 name: &str,
193 source_overrides: &BTreeMap<String, String>,
194 ) -> Result<(), StateError> {
195 let overrides_json =
196 serde_json::to_string(source_overrides).unwrap_or_else(|_| "{}".into());
197 self.execute(
198 "UPDATE instances SET source_overrides = ?2 WHERE name = ?1",
199 &[name.into(), overrides_json.into()],
200 )?;
201 Ok(())
202 }
203
204 pub fn update_dirty(&self, name: &str, dirty: bool) -> Result<(), StateError> {
206 self.execute(
207 "UPDATE instances SET dirty = ?2 WHERE name = ?1",
208 &[name.into(), i64::from(dirty).into()],
209 )?;
210 Ok(())
211 }
212}