Page MenuHomeDevCentral

D4201.id11019.diff
No OneTemporary

D4201.id11019.diff

diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -91,6 +91,10 @@
- init: called by `alkane init`
- update: called by `alkane update`
+If your update procedure is idempotent and can also initialize the site,
+you can provide only the update script. Alkane will use it when an init
+script isn't provided.
+
Several environment variables are available to those scripts:
| Variable | Description |
diff --git a/src/runner/store.rs b/src/runner/store.rs
--- a/src/runner/store.rs
+++ b/src/runner/store.rs
@@ -8,7 +8,7 @@
use std::collections::HashMap;
use std::path::Path;
-use log::error;
+use log::{error, info};
use crate::config::AlkaneConfig;
use crate::runner::run;
@@ -47,7 +47,7 @@
}
pub async fn run_recipe(&self, site: &Site, action: &str) -> RecipeStatus {
- let Some(command) = self.get_recipe_path(&site.name, action) else {
+ let Some(command) = self.resolve_recipe_path(&site.name, action) else {
error!(
"Refusing to run a recipe for invalid site name {:?}",
site.name
@@ -59,6 +59,34 @@
run(command, Vec::new(), environment).await
}
+ /// Resolve the recipe to run for a site and action.
+ ///
+ /// If the update recipe can also initialize a fresh site, a site can omit
+ /// its init recipe. In that case, init resolves to the update recipe.
+ fn resolve_recipe_path(&self, site_name: &str, action: &str) -> Option<String> {
+ let path = self.get_recipe_path(site_name, action)?;
+
+ let init_is_absent = action == "init"
+ && matches!(
+ std::fs::symlink_metadata(&path),
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound
+ );
+
+ if init_is_absent {
+ let fallback = self.get_recipe_path(site_name, "update")?;
+
+ if matches!(Path::new(&fallback).try_exists(), Ok(true)) {
+ info!(
+ "No init recipe for {}, falling back to the update recipe",
+ site_name
+ );
+ return Some(fallback);
+ }
+ }
+
+ Some(path)
+ }
+
fn get_environment(&self, site: &Site) -> HashMap<String, String> {
let mut map = HashMap::new();
@@ -82,7 +110,9 @@
#[cfg(test)]
mod tests {
+ use std::fs;
use std::path::MAIN_SEPARATOR_STR;
+ use std::path::PathBuf;
use super::*;
@@ -156,4 +186,126 @@
assert!(environment.contains_key("ALKANE_SITE_CONTEXT"));
assert_eq!("CH3-CH3", environment["ALKANE_SITE_CONTEXT"])
}
+
+ // -------------------------------------------------------------
+ // Recipe resolution helpers
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+ struct TempStore {
+ root: PathBuf,
+ store: RecipesStore,
+ }
+
+ impl TempStore {
+ fn new(name: &str) -> Self {
+ let root = std::env::temp_dir().join(format!(
+ "alkane-test-{}-{}",
+ std::process::id(),
+ name
+ ));
+
+ let _ = fs::remove_dir_all(&root);
+ fs::create_dir_all(&root).expect("Can't create temporary recipe store");
+
+ let store = RecipesStore::new(root.to_str().expect("Non-UTF8 temp path"));
+
+ Self { root, store }
+ }
+ }
+
+ impl Drop for TempStore {
+ fn drop(&mut self) {
+ let _ = fs::remove_dir_all(&self.root);
+ }
+ }
+
+ /// Create an empty recipe file for a site and action
+ fn add_recipe(root: &Path, site_name: &str, action: &str) {
+ let site_dir = root.join(site_name);
+ fs::create_dir_all(&site_dir).expect("Can't create site recipe directory");
+ fs::write(site_dir.join(action), "#!/bin/sh\n").expect("Can't write recipe");
+ }
+
+ // -------------------------------------------------------------
+ // Recipe resolution tests
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+ #[test]
+ pub fn test_resolve_recipe_path_prefers_init() {
+ let temp = TempStore::new("resolve-prefer-init");
+ add_recipe(&temp.root, "foo.acme.tld", "init");
+ add_recipe(&temp.root, "foo.acme.tld", "update");
+
+ let expected = temp.root.join("foo.acme.tld").join("init");
+
+ assert_eq!(
+ Some(expected.to_str().unwrap().to_string()),
+ temp.store.resolve_recipe_path("foo.acme.tld", "init")
+ );
+ }
+
+ #[test]
+ pub fn test_resolve_recipe_path_falls_back_to_update() {
+ let temp = TempStore::new("resolve-fallback");
+ add_recipe(&temp.root, "foo.acme.tld", "update");
+
+ let expected = temp.root.join("foo.acme.tld").join("update");
+
+ assert_eq!(
+ Some(expected.to_str().unwrap().to_string()),
+ temp.store.resolve_recipe_path("foo.acme.tld", "init")
+ );
+ }
+
+ #[cfg(unix)]
+ #[test]
+ pub fn test_resolve_recipe_path_keeps_dangling_init_symlink() {
+ let temp = TempStore::new("resolve-dangling-init");
+ add_recipe(&temp.root, "foo.acme.tld", "update");
+
+ let init = temp.root.join("foo.acme.tld").join("init");
+ std::os::unix::fs::symlink("missing-init-target", &init)
+ .expect("Can't create dangling init symlink");
+
+ assert_eq!(
+ Some(init.to_str().unwrap().to_string()),
+ temp.store.resolve_recipe_path("foo.acme.tld", "init")
+ );
+ }
+
+ #[test]
+ pub fn test_resolve_recipe_path_update_not_affected_by_fallback() {
+ let temp = TempStore::new("resolve-update");
+ add_recipe(&temp.root, "foo.acme.tld", "update");
+
+ let expected = temp.root.join("foo.acme.tld").join("update");
+
+ assert_eq!(
+ Some(expected.to_str().unwrap().to_string()),
+ temp.store.resolve_recipe_path("foo.acme.tld", "update")
+ );
+ }
+
+ #[test]
+ pub fn test_resolve_recipe_path_init_without_any_recipe() {
+ let temp = TempStore::new("resolve-no-recipe");
+ fs::create_dir_all(temp.root.join("foo.acme.tld")).expect("Can't create site recipe directory");
+
+ // No fallback available: the init path is returned as-is,
+ // and run() will report the missing recipe.
+ let expected = temp.root.join("foo.acme.tld").join("init");
+
+ assert_eq!(
+ Some(expected.to_str().unwrap().to_string()),
+ temp.store.resolve_recipe_path("foo.acme.tld", "init")
+ );
+ }
+
+ #[test]
+ pub fn test_resolve_recipe_path_rejects_traversal() {
+ let temp = TempStore::new("resolve-traversal");
+
+ assert_eq!(None, temp.store.resolve_recipe_path("../foo.acme.tld", "init"));
+ assert_eq!(None, temp.store.resolve_recipe_path("../foo.acme.tld", "update"));
+ }
}

File Metadata

Mime Type
text/plain
Expires
Mon, Aug 31, 23:09 (12 h, 5 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4050700
Default Alt Text
D4201.id11019.diff (6 KB)

Event Timeline