1use std::collections::BTreeMap;
14use std::path::{Path, PathBuf};
15
16use isoloom_manifest::{Driver, SetBy};
17
18use crate::Result;
19use crate::error::Error;
20use crate::manifest::{LoadedManifest, load_manifest};
21use crate::name::RunName;
22use crate::runner::Runner;
23use crate::state::RunState;
24use crate::store::RunRecord;
25
26const STATE: &str = "terraform.tfstate";
28
29const MOVED: [&str; 2] = [STATE, "terraform.tfstate.backup"];
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34#[non_exhaustive]
35pub struct Adoption {
36 pub name: RunName,
38 pub state_from: PathBuf,
40 pub params: BTreeMap<String, String>,
43 pub host_label: Option<String>,
45 pub expires_at: Option<u64>,
47 pub started_at: Option<String>,
49 pub slot: Option<u8>,
52}
53
54impl Adoption {
55 #[must_use]
57 pub fn new(name: RunName, state_from: impl Into<PathBuf>) -> Adoption {
58 Adoption {
59 name,
60 state_from: state_from.into(),
61 params: BTreeMap::new(),
62 host_label: None,
63 expires_at: None,
64 started_at: None,
65 slot: None,
66 }
67 }
68
69 #[must_use]
71 pub fn with_params(mut self, params: impl IntoIterator<Item = (String, String)>) -> Adoption {
72 self.params.extend(params);
73 self
74 }
75
76 #[must_use]
78 pub fn with_host_label(mut self, label: impl Into<String>) -> Adoption {
79 self.host_label = Some(label.into());
80 self
81 }
82
83 #[must_use]
85 pub fn with_expires_at(mut self, unix_seconds: u64) -> Adoption {
86 self.expires_at = Some(unix_seconds);
87 self
88 }
89
90 #[must_use]
92 pub fn with_started_at(mut self, rfc3339: impl Into<String>) -> Adoption {
93 self.started_at = Some(rfc3339.into());
94 self
95 }
96
97 #[must_use]
99 pub fn with_slot(mut self, slot: u8) -> Adoption {
100 self.slot = Some(slot);
101 self
102 }
103}
104
105impl Runner {
106 pub async fn adopt(&self, manifest: &Path, adoption: Adoption) -> Result<RunRecord> {
122 let loaded = load_manifest(manifest)?;
123 let name = adoption.name.clone();
124 let refuse = |reason: String| Error::Adopt { run: name.to_string(), reason };
125 if loaded.manifest.driver != Driver::Terraform {
126 return Err(refuse(format!("its manifest's driver is {}, not terraform", loaded.manifest.driver.id())));
127 }
128 let to = self.store.run_dir(&name)?;
129 let _lock = self.locks.acquire(&name, &self.store.lock_path(&name)).await?;
130 if self.store.record(&name)?.is_some_and(|r| r.state != RunState::Absent) {
132 return Err(refuse("the run has a record already".into()));
133 }
134 if to.join(STATE).exists() {
135 return Err(refuse("the run has a Terraform state already".into()));
136 }
137 if !adoption.state_from.join(STATE).is_file() {
138 return Err(refuse(format!("{} holds no {STATE}", adoption.state_from.display())));
139 }
140 let from = adoption.state_from.clone();
141 let record = self.adopted(loaded, adoption)?;
142 let moved = move_files(&from, &to)?;
143 if let Err(e) = self.store.save(&record) {
144 put_back(&moved, &from, &to);
145 return Err(e);
146 }
147 Ok(record)
148 }
149}
150
151impl Runner {
152 fn adopted(&self, loaded: LoadedManifest, adoption: Adoption) -> Result<RunRecord> {
155 let manifest = &loaded.manifest;
156 let mut record = RunRecord::fresh(adoption.name, loaded.path, loaded.sha256, manifest.target, manifest.driver);
157 record.params = adoption
158 .params
159 .into_iter()
160 .filter(|(k, _)| manifest.params.iter().any(|p| p.name.as_str() == k && p.set_by == SetBy::Embedder))
161 .collect();
162 if crate::slot::needed(manifest) {
163 let place = adoption.host_label.as_deref();
164 let taken = crate::slot::taken(&self.store.records()?, &record.name, place);
165 record.slot = Some(crate::slot::pick(adoption.slot, &taken, place)?);
166 }
167 record.cloud = manifest.cloud;
168 record.host_label = adoption.host_label;
169 record.expires_at = adoption.expires_at;
170 record.started_at = Some(adoption.started_at.unwrap_or_else(|| crate::clock::rfc3339(self.config.clock.now())));
172 record.state = RunState::Ready;
173 Ok(record)
174 }
175}
176
177fn move_files(from: &Path, to: &Path) -> Result<Vec<&'static str>> {
180 let mut moved = Vec::new();
181 for name in MOVED {
182 let source = from.join(name);
183 if !source.is_file() {
184 continue;
185 }
186 if let Err(e) = move_file(&source, &to.join(name)) {
187 put_back(&moved, from, to);
188 return Err(Error::io(&source, "moved")(e));
189 }
190 moved.push(name);
191 }
192 Ok(moved)
193}
194
195fn move_file(from: &Path, to: &Path) -> std::io::Result<()> {
197 if std::fs::rename(from, to).is_ok() {
198 return Ok(());
199 }
200 std::fs::copy(from, to)?;
201 std::fs::remove_file(from)
202}
203
204fn put_back(names: &[&str], from: &Path, to: &Path) {
206 for name in names {
207 if let Err(e) = move_file(&to.join(name), &from.join(name)) {
208 tracing::warn!(error = %e, file = name, "a taken-over state couldn't be put back");
209 }
210 }
211}
212
213#[cfg(test)]
214mod tests;