1pub use microvms_domain::names::*;
17
18use crate::error::{Error, ErrorKind};
19use crate::region::Region;
20use crate::session::Session;
21
22pub trait NameStore: Send + Sync {
24 fn get(&self, name: &str) -> Result<Option<NameRecord>, Error>;
26 fn put(&self, record: &NameRecord) -> Result<(), Error>;
28 fn delete(&self, name: &str) -> Result<bool, Error>;
30 fn list(&self) -> Result<Vec<NameRecord>, Error>;
32
33 fn describe(&self) -> String {
35 "the name store".to_string()
36 }
37
38 fn release_by_vm(&self, microvm_id: &str) -> Result<Vec<String>, Error> {
44 let mut released = Vec::new();
45 for record in self.list()? {
46 if record.microvm_id == microvm_id && self.delete(&record.name)? {
47 released.push(record.name);
48 }
49 }
50 released.sort();
51 Ok(released)
52 }
53}
54
55pub fn resolve(
59 store: &dyn NameStore,
60 name: &str,
61 expected_region: Option<&Region>,
62) -> Result<NameRecord, Error> {
63 resolve_record(name, store.get(name)?, &store.describe(), expected_region)
64}
65
66pub const IMPORT_PROBE_PATH: &str = "/dev/null";
76
77#[derive(Clone, Debug, Eq, PartialEq)]
79pub enum NameHolder {
80 Free,
82 Same,
84 Other(Option<String>),
87}
88
89pub fn holder(store: &dyn NameStore, record: &NameRecord) -> NameHolder {
91 match store.get(&record.name) {
92 Ok(None) => NameHolder::Free,
93 Ok(Some(existing)) if existing.microvm_id == record.microvm_id => NameHolder::Same,
94 Ok(Some(existing)) => NameHolder::Other(Some(existing.microvm_id)),
95 Err(_) => NameHolder::Other(None),
96 }
97}
98
99pub struct Probed {
104 microvm_id: String,
105 endpoint: String,
106 agent_token: String,
107}
108
109impl std::fmt::Debug for Probed {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("Probed")
113 .field("microvm_id", &self.microvm_id)
114 .field("endpoint", &self.endpoint)
115 .finish_non_exhaustive()
116 }
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub struct Imported {
122 pub replaced: bool,
124}
125
126pub async fn probe(session: &Session, record: &NameRecord) -> Result<Probed, Error> {
134 if session.endpoint() != record.endpoint || session.agent_token() != record.agent_token {
135 return Err(Error::invalid_arg(format!(
136 "the session isn't attached with the record for {:?}: a probe proves the token it's sent with, so attach with the record's own endpoint and token",
137 record.name
138 )));
139 }
140 session.file_exists(IMPORT_PROBE_PATH).await?;
141 Ok(Probed {
142 microvm_id: record.microvm_id.clone(),
143 endpoint: record.endpoint.clone(),
144 agent_token: record.agent_token.clone(),
145 })
146}
147
148pub fn import_probed(
155 store: &dyn NameStore,
156 record: &NameRecord,
157 probed: Probed,
158) -> Result<Imported, Error> {
159 if probed.microvm_id != record.microvm_id
160 || probed.endpoint != record.endpoint
161 || probed.agent_token != record.agent_token
162 {
163 return Err(Error::invalid_arg(format!(
164 "the probe was of {}, not the record for {:?}: an import writes only the triple that answered",
165 probed.microvm_id, record.name
166 )));
167 }
168 let record = NameRecord::from_json(record.to_json())?;
169 let replaced = match holder(store, &record) {
170 NameHolder::Free => false,
171 NameHolder::Same => true,
172 NameHolder::Other(holder) => return Err(taken(store, &record, holder)),
173 };
174 store.put(&record)?;
175 Ok(Imported { replaced })
176}
177
178pub async fn import(
186 store: &dyn NameStore,
187 record: &NameRecord,
188 session: &Session,
189) -> Result<Imported, Error> {
190 if let NameHolder::Other(holder) = holder(store, record) {
191 return Err(taken(store, record, holder));
192 }
193 let probed = probe(session, record).await?;
194 import_probed(store, record, probed)
195}
196
197fn taken(store: &dyn NameStore, record: &NameRecord, holder: Option<String>) -> Error {
198 Error::new(
199 ErrorKind::Precondition,
200 format!(
201 "{:?} is already registered in {} to {}, and this import names {}: a name is a promise about which VM answers to it, and nothing overwrites one silently",
202 record.name,
203 store.describe(),
204 holder.as_deref().unwrap_or("an unreadable record"),
205 record.microvm_id,
206 ),
207 )
208}
209
210#[cfg(test)]
211mod tests {
212 use std::collections::BTreeMap;
213 use std::sync::{Arc, Mutex};
214
215 use super::*;
216 use crate::session::testing::{Recorder, Reply, session_with};
217
218 #[derive(Default)]
220 struct Memory {
221 records: Mutex<BTreeMap<String, NameRecord>>,
222 torn: Vec<String>,
223 }
224
225 impl NameStore for Memory {
226 fn get(&self, name: &str) -> Result<Option<NameRecord>, Error> {
227 if self.torn.iter().any(|torn| torn == name) {
228 return Err(Error::new(ErrorKind::Precondition, "torn"));
229 }
230 Ok(self.records.lock().expect("unpoisoned").get(name).cloned())
231 }
232 fn put(&self, record: &NameRecord) -> Result<(), Error> {
233 self.records
234 .lock()
235 .expect("unpoisoned")
236 .insert(record.name.clone(), record.clone());
237 Ok(())
238 }
239 fn delete(&self, name: &str) -> Result<bool, Error> {
240 Ok(self
241 .records
242 .lock()
243 .expect("unpoisoned")
244 .remove(name)
245 .is_some())
246 }
247 fn list(&self) -> Result<Vec<NameRecord>, Error> {
248 Ok(self
249 .records
250 .lock()
251 .expect("unpoisoned")
252 .values()
253 .cloned()
254 .collect())
255 }
256 }
257
258 fn record(name: &str, microvm_id: &str) -> NameRecord {
260 NameRecord::new_at(
261 name,
262 microvm_id,
263 "https://vm.example",
264 "agent-token-abcdef",
265 "us-east-1",
266 1,
267 )
268 .expect("a record")
269 }
270
271 fn answered(status: u16) -> Arc<Recorder> {
272 Recorder::with([Reply::Body(status, Vec::new())])
273 }
274
275 #[tokio::test]
282 async fn an_import_writes_only_after_the_vm_answers_with_the_records_token() {
283 let store = Memory::default();
284 let (refusing, _, _) = session_with(answered(401));
285 let error = import(&store, &record("ci", "mvm-1"), &refusing)
286 .await
287 .expect_err("a refused token");
288 assert_eq!(error.kind(), ErrorKind::Credentials, "{error}");
289 assert!(store.get("ci").expect("reads").is_none(), "nothing written");
290
291 let (answering, _, _) = session_with(answered(200));
292 let imported = import(&store, &record("ci", "mvm-1"), &answering)
293 .await
294 .expect("an answered probe");
295 assert_eq!(imported, Imported { replaced: false });
296 assert_eq!(
297 store.get("ci").expect("reads").map(|r| r.microvm_id),
298 Some("mvm-1".into())
299 );
300
301 let (again, _, _) = session_with(answered(404));
302 let refreshed = import(&store, &record("ci", "mvm-1"), &again)
303 .await
304 .expect("absence answers behind the same token check");
305 assert_eq!(refreshed, Imported { replaced: true });
306 }
307
308 #[tokio::test]
311 async fn an_import_refuses_a_taken_name_before_any_request() {
312 let store = Memory {
313 torn: vec!["torn".into()],
314 ..Memory::default()
315 };
316 store.put(&record("ci", "mvm-holder")).expect("seeded");
317 for name in ["ci", "torn"] {
318 let recorder = Recorder::with([]);
319 let (session, _, _) = session_with(Arc::clone(&recorder));
320 let error = import(&store, &record(name, "mvm-new"), &session)
321 .await
322 .expect_err("a taken name");
323 assert_eq!(error.kind(), ErrorKind::Precondition, "{name}: {error}");
324 assert!(recorder.requests().is_empty(), "{name}: no request");
325 }
326 assert_eq!(
327 store.get("ci").expect("reads").map(|r| r.microvm_id),
328 Some("mvm-holder".into())
329 );
330 }
331
332 #[tokio::test]
335 async fn a_probe_is_of_one_record_and_its_write_takes_only_that_one() {
336 let (session, _, _) = session_with(answered(200));
337 let mut stranger = record("ci", "mvm-1");
338 stranger.agent_token = "another-token".into();
339 let error = probe(&session, &stranger).await.expect_err("another token");
340 assert_eq!(error.kind(), ErrorKind::InvalidArg, "{error}");
341
342 let mut other_endpoint = record("ci", "mvm-1");
344 other_endpoint.endpoint = "https://other.example".into();
345 let mut other_token = record("ci", "mvm-1");
346 other_token.agent_token = "another-token".into();
347 for (row, other) in [
348 ("the probe of another VM", record("ci", "mvm-2")),
349 ("another endpoint", other_endpoint),
350 ("another token", other_token),
351 ] {
352 let (answering, _, _) = session_with(answered(200));
353 let probed = probe(&answering, &record("ci", "mvm-1"))
354 .await
355 .expect("probed");
356 let store = Memory::default();
357 let error = import_probed(&store, &other, probed).expect_err(row);
358 assert_eq!(error.kind(), ErrorKind::InvalidArg, "{row}: {error}");
359 assert!(
360 store.get("ci").expect("reads").is_none(),
361 "{row}: nothing written"
362 );
363 }
364 }
365
366 #[tokio::test]
368 async fn a_probes_debug_names_the_vm_and_redacts_the_token() {
369 let (session, _, _) = session_with(answered(200));
370 let probed = probe(&session, &record("ci", "mvm-1"))
371 .await
372 .expect("probed");
373 let shown = format!("{probed:?}");
374 assert!(shown.contains("mvm-1"), "{shown}");
375 assert!(!shown.contains("agent-token-abcdef"), "{shown}");
376 }
377}