project.rs
1 //! Project specification. 2 3 use std::{ 4 collections::HashMap, 5 fs::{read, write}, 6 io::Write, 7 path::{Path, PathBuf}, 8 }; 9 10 use clingwrap::tildepathbuf::TildePathBuf; 11 use serde::{Deserialize, Serialize}; 12 13 use crate::{ 14 action::{PostPlanAction, PrePlanAction, UnsafeAction}, 15 util::mkdir, 16 }; 17 18 /// A list of projects. 19 #[derive(Debug, Deserialize, Clone)] 20 #[serde(deny_unknown_fields)] 21 pub struct Projects { 22 projects: HashMap<String, Project>, 23 } 24 25 impl Projects { 26 /// Load from a file. 27 pub fn from_file(filename: &Path) -> Result<Self, ProjectError> { 28 let dirname = if let Some(parent) = filename.parent() { 29 parent.to_path_buf() 30 } else { 31 return Err(ProjectError::Parent(filename.into())); 32 }; 33 34 let yaml = read(filename).map_err(|e| ProjectError::Read(filename.into(), e))?; 35 let mut projects: Self = 36 serde_norway::from_slice(&yaml).map_err(|e| ProjectError::Yaml(filename.into(), e))?; 37 38 for (name, p) in projects.projects.iter_mut() { 39 p.expand_tilde(&dirname)?; 40 if !p.expanded_source.is_dir() { 41 return Err(ProjectError::NotADirectory( 42 name.into(), 43 p.expanded_source.clone(), 44 )); 45 } 46 } 47 48 Ok(projects) 49 } 50 51 /// Look up project by name. 52 pub fn get(&self, name: &str) -> Option<&Project> { 53 self.projects.get(name) 54 } 55 56 /// Iterator over projects. 57 pub fn iter(&self) -> impl Iterator<Item = (&str, &Project)> { 58 self.projects.iter().map(|(k, v)| (k.as_str(), v)) 59 } 60 } 61 62 /// Specification of one CI project. 63 #[derive(Debug, Deserialize, Clone)] 64 #[serde(deny_unknown_fields)] 65 pub struct Project { 66 /// Source directory. 67 pub source: TildePathBuf, 68 69 #[serde(skip)] 70 expanded_source: PathBuf, 71 72 /// Virtual machine image to use. 73 pub image: TildePathBuf, 74 75 #[serde(skip)] 76 expanded_image: PathBuf, 77 78 /// Pre-plan actions. 79 pub pre_plan: Option<Vec<PrePlanAction>>, 80 81 /// Plan actions. 82 pub plan: Option<Vec<UnsafeAction>>, 83 84 /// Post-plan actions. 85 pub post_plan: Option<Vec<PostPlanAction>>, 86 87 /// Maximum size of artifacts directory for this project, in bytes. 88 pub artifact_max_size: Option<u64>, 89 90 /// Maximum size of cache directory for this project, in bytes. 91 pub cache_max_size: Option<u64>, 92 } 93 94 impl Project { 95 fn expand_tilde(&mut self, basedir: &Path) -> Result<(), ProjectError> { 96 self.expanded_source = Self::abspath(basedir.join(self.source.path()))?; 97 self.expanded_image = Self::abspath(basedir.join(self.image.path()))?; 98 Ok(()) 99 } 100 101 /// Load from file. 102 pub fn from_file(filename: &Path) -> Result<Self, ProjectError> { 103 let dirname = if let Some(parent) = filename.parent() { 104 parent.to_path_buf() 105 } else { 106 return Err(ProjectError::Parent(filename.into())); 107 }; 108 109 let yaml = read(filename).map_err(|e| ProjectError::Read(filename.into(), e))?; 110 let mut project: Project = 111 serde_norway::from_slice(&yaml).map_err(|e| ProjectError::Yaml(filename.into(), e))?; 112 113 project.expand_tilde(&dirname)?; 114 if !project.expanded_source.is_dir() { 115 return Err(ProjectError::NotADirectory( 116 filename.to_string_lossy().to_string(), 117 project.expanded_source, 118 )); 119 } 120 121 Ok(project) 122 } 123 124 fn abspath(path: PathBuf) -> Result<PathBuf, ProjectError> { 125 path.canonicalize() 126 .map_err(|e| ProjectError::Canonicalize(path, e)) 127 } 128 129 /// Source directory. 130 pub fn source(&self) -> &Path { 131 &self.expanded_source 132 } 133 134 /// Virtual machine image file. 135 pub fn image(&self) -> &Path { 136 &self.expanded_image 137 } 138 139 /// Maximum size of artifacts directory, in bytes. 140 pub fn artifact_max_size(&self) -> Option<u64> { 141 self.artifact_max_size 142 } 143 144 /// Maximum size of cache directory, in bytes. 145 pub fn cache_max_size(&self) -> Option<u64> { 146 self.cache_max_size 147 } 148 149 /// List of pre-plan actions. 150 pub fn pre_plan(&self) -> &[PrePlanAction] { 151 if let Some(plan) = &self.pre_plan { 152 plan.as_slice() 153 } else { 154 &[] 155 } 156 } 157 158 /// List of plan actions. 159 pub fn plan(&self) -> &[UnsafeAction] { 160 if let Some(plan) = &self.plan { 161 plan.as_slice() 162 } else { 163 &[] 164 } 165 } 166 167 /// List of post-plan actions. 168 pub fn post_plan(&self) -> &[PostPlanAction] { 169 if let Some(plan) = &self.post_plan { 170 plan.as_slice() 171 } else { 172 &[] 173 } 174 } 175 } 176 177 /// Persistent project state. 178 #[derive(Debug, Clone, Deserialize, Serialize)] 179 #[allow(dead_code)] 180 pub struct State { 181 // File where this state is stored, if it's stored. 182 #[serde(skip)] 183 filename: PathBuf, 184 185 // Where persistent state is stored for this project. 186 #[serde(skip)] 187 statedir: PathBuf, 188 189 /// Latest commit that CI has run on, if any. 190 pub latest_commit: Option<String>, 191 } 192 193 impl State { 194 /// Load state for a project from a file, if it's present. If it's 195 /// not present, return an empty state. 196 pub fn from_file(statedir: &Path, project: &str) -> Result<Self, ProjectError> { 197 let statedir = statedir.join(project); 198 let filename = statedir.join("meta.yaml"); 199 let state = if filename.exists() { 200 let yaml = read(&filename).map_err(|e| ProjectError::ReadState(filename.clone(), e))?; 201 let mut state: Self = serde_norway::from_slice(&yaml) 202 .map_err(|e| ProjectError::ParseState(filename.clone(), e))?; 203 state.filename = filename; 204 state.statedir = statedir; 205 state 206 } else { 207 Self { 208 filename, 209 statedir, 210 latest_commit: None, 211 } 212 }; 213 214 mkdir(&state.artifactsdir())?; 215 mkdir(&state.cachedir())?; 216 mkdir(&state.dependenciesdir())?; 217 218 Ok(state) 219 } 220 221 /// Write project state. 222 pub fn write_to_file(&self) -> Result<(), ProjectError> { 223 let yaml = serde_norway::to_string(&self) 224 .map_err(|e| ProjectError::SerializeState(self.clone(), e))?; 225 if !self.statedir.exists() { 226 std::fs::create_dir(&self.statedir) 227 .map_err(|e| ProjectError::CreateState(self.statedir.clone(), e))?; 228 } 229 write(&self.filename, yaml) 230 .map_err(|e| ProjectError::WriteState(self.filename.clone(), e))?; 231 Ok(()) 232 } 233 234 /// Return state directory. 235 pub fn statedir(&self) -> &Path { 236 &self.statedir 237 } 238 239 /// Return artifacts directory for project. 240 pub fn artifactsdir(&self) -> PathBuf { 241 self.statedir.join("artifacts") 242 } 243 244 /// Return cache directory for project. 245 pub fn cachedir(&self) -> PathBuf { 246 self.statedir.join("cache") 247 } 248 249 /// Return dependencies directory for a project. 250 pub fn dependenciesdir(&self) -> PathBuf { 251 self.statedir.join("dependencies") 252 } 253 254 /// Return latest commit that CI has run on. 255 pub fn latest_commit(&self) -> Option<&str> { 256 self.latest_commit.as_deref() 257 } 258 259 /// Set latest commit. 260 pub fn set_latest_commot(&mut self, commit: Option<&str>) { 261 self.latest_commit = commit.map(|s| s.into()); 262 } 263 264 /// Path to console log. 265 pub fn console_log_filename(&self) -> PathBuf { 266 self.statedir.join("console.log") 267 } 268 269 /// Remove any existing console log. 270 pub fn remove_console_log(&self) -> Result<(), ProjectError> { 271 let filename = self.console_log_filename(); 272 if filename.exists() { 273 std::fs::remove_file(&filename) 274 .map_err(|err| ProjectError::RemoveConsoleLog(filename, err))?; 275 } 276 Ok(()) 277 } 278 279 /// Create empty console log file. Return its filename. 280 pub fn create_console_log(&self) -> Result<PathBuf, ProjectError> { 281 let filename = self.console_log_filename(); 282 std::fs::OpenOptions::new() 283 .create(true) 284 .write(true) 285 .truncate(true) 286 .open(&filename) 287 .map_err(|err| ProjectError::CreateConsoleLog(filename.clone(), err))?; 288 Ok(filename) 289 } 290 291 /// Append data to console log. The file must already exist. 292 pub fn append_to_console_log(&self, data: &[u8]) -> Result<(), ProjectError> { 293 let filename = self.console_log_filename(); 294 let mut file = std::fs::OpenOptions::new() 295 .append(true) 296 .open(&filename) 297 .map_err(|err| ProjectError::CreateConsoleLog(filename.clone(), err))?; 298 299 file.write_all(data) 300 .map_err(|err| ProjectError::AppendToConsoleLog(filename, err))?; 301 302 Ok(()) 303 } 304 305 /// Return contents of console log. 306 pub fn read_console_log(&self) -> Result<Vec<u8>, ProjectError> { 307 let filename = self.run_log_filename(); 308 let data = 309 std::fs::read(&filename).map_err(|err| ProjectError::ReadConsoleLog(filename, err))?; 310 Ok(data) 311 } 312 313 /// Path to run log. 314 pub fn run_log_filename(&self) -> PathBuf { 315 self.statedir.join("run.log") 316 } 317 318 /// Remove any existing run log. 319 pub fn remove_run_log(&self) -> Result<(), ProjectError> { 320 let filename = self.run_log_filename(); 321 if filename.exists() { 322 std::fs::remove_file(&filename) 323 .map_err(|err| ProjectError::RemoveRunLog(filename, err))?; 324 } 325 Ok(()) 326 } 327 328 /// Create empty run log file. Return its filename. 329 pub fn create_run_log(&self) -> Result<PathBuf, ProjectError> { 330 let filename = self.run_log_filename(); 331 std::fs::OpenOptions::new() 332 .create(true) 333 .write(true) 334 .truncate(true) 335 .open(&filename) 336 .map_err(|err| ProjectError::CreateRunLog(filename.clone(), err))?; 337 Ok(filename) 338 } 339 340 /// Create empty raw log file. Return its filename. 341 pub fn create_raw_log(&self) -> Result<PathBuf, ProjectError> { 342 let filename = self.raw_log_filename(); 343 std::fs::OpenOptions::new() 344 .create(true) 345 .write(true) 346 .truncate(true) 347 .open(&filename) 348 .map_err(|err| ProjectError::CreateRunLog(filename.clone(), err))?; 349 Ok(filename) 350 } 351 352 /// Remove any existing raw log. 353 pub fn remove_raw_log(&self) -> Result<(), ProjectError> { 354 let filename = self.raw_log_filename(); 355 if filename.exists() { 356 std::fs::remove_file(&filename) 357 .map_err(|err| ProjectError::RemoveRawLog(filename, err))?; 358 } 359 Ok(()) 360 } 361 362 /// Path to raw log. This is where the output from virtual machine goes. 363 pub fn raw_log_filename(&self) -> PathBuf { 364 self.statedir.join("raw.log") 365 } 366 367 /// Append data to run log. The file must already exist. 368 pub fn append_to_run_log(&self, data: &[u8]) -> Result<(), ProjectError> { 369 let filename = self.run_log_filename(); 370 let mut file = std::fs::OpenOptions::new() 371 .append(true) 372 .open(&filename) 373 .map_err(|err| ProjectError::CreateRunLog(filename.clone(), err))?; 374 375 file.write_all(data) 376 .map_err(|err| ProjectError::AppendToRunLog(filename, err))?; 377 378 Ok(()) 379 } 380 381 /// Return contents of run log. 382 pub fn read_run_log(&self) -> Result<Vec<u8>, ProjectError> { 383 let filename = self.run_log_filename(); 384 let data = 385 std::fs::read(&filename).map_err(|err| ProjectError::ReadRunLog(filename, err))?; 386 Ok(data) 387 } 388 } 389 390 /// Errors from handling project specifications. 391 #[derive(Debug, thiserror::Error)] 392 pub enum ProjectError { 393 /// Can't find parent directory. 394 #[error("failed to determine directory containing project file {0}")] 395 Parent(PathBuf), 396 397 /// Can't make filename absolute. 398 #[error("failed to make filename absolute: {0}")] 399 Canonicalize(PathBuf, #[source] std::io::Error), 400 401 /// Can't read projects file. 402 #[error("failed top read project file {0}")] 403 Read(PathBuf, #[source] std::io::Error), 404 405 /// Can't parse projects file as YAML. 406 #[error("failed to parse project file as YAML: {0}")] 407 Yaml(PathBuf, #[source] serde_norway::Error), 408 409 /// Can't serialize project state as YAML. 410 #[error("failed to serialize project state as YAML: {0:#?}")] 411 SerializeState(State, #[source] serde_norway::Error), 412 413 /// Can't write project state to file. 414 #[error("failed to write project state to file {0}")] 415 WriteState(PathBuf, #[source] std::io::Error), 416 417 /// Can't read project state from file. 418 #[error("failed to read project state from file {0}")] 419 ReadState(PathBuf, #[source] std::io::Error), 420 421 /// Can't parse project state as YAML. 422 #[error("failed to parse project state file as YAML: {0}")] 423 ParseState(PathBuf, #[source] serde_norway::Error), 424 425 /// Can't create project state directory. 426 #[error("failed to create project state directory {0}")] 427 CreateState(PathBuf, #[source] std::io::Error), 428 429 /// Can't remove run log file. 430 #[error("failed to remove run log file {0}")] 431 RemoveRunLog(PathBuf, #[source] std::io::Error), 432 433 /// Can't remove raw log file. 434 #[error("failed to remove raw log file {0}")] 435 RemoveRawLog(PathBuf, #[source] std::io::Error), 436 437 /// Can't create run log file. 438 #[error("failed to create run log file {0}")] 439 CreateRunLog(PathBuf, #[source] std::io::Error), 440 441 /// Can't append to run log file. 442 #[error("failed to append to run log file {0}")] 443 AppendToRunLog(PathBuf, #[source] std::io::Error), 444 445 /// Can't read run log file. 446 #[error("failed to read run log file {0}")] 447 ReadRunLog(PathBuf, #[source] std::io::Error), 448 449 /// Can't remove console log file. 450 #[error("failed to remove console log file {0}")] 451 RemoveConsoleLog(PathBuf, #[source] std::io::Error), 452 453 /// Can't create consolelog file. 454 #[error("failed to create consolelog file {0}")] 455 CreateConsoleLog(PathBuf, #[source] std::io::Error), 456 457 /// Can't append to console log file. 458 #[error("failed to append to console log file {0}")] 459 AppendToConsoleLog(PathBuf, #[source] std::io::Error), 460 461 /// Can't read console log file. 462 #[error("failed to read console log file {0}")] 463 ReadConsoleLog(PathBuf, #[source] std::io::Error), 464 465 /// Can't create directory. 466 #[error(transparent)] 467 MKdir(#[from] crate::util::UtilError), 468 469 /// Source directory isn't a directory. 470 #[error("project {0} source directory is not a directory: {1}")] 471 NotADirectory(String, PathBuf), 472 }