use std::{
path::{Path, PathBuf},
sync::Arc,
};
use crate::{
eval::{CallCx, FuncError, HclFunc},
ir::Value,
util::paths::{self, SymlinkPolicy},
};
#[derive(Debug)]
pub(super) struct TgState {
pub workspace_root: Arc<Path>,
pub component_dir: Arc<Path>,
pub active_include: std::sync::Mutex<Option<Arc<Path>>>,
}
impl TgState {
pub(super) fn new(workspace_root: Arc<Path>, component_dir: Arc<Path>) -> Self {
Self {
workspace_root,
component_dir,
active_include: std::sync::Mutex::new(None),
}
}
}
#[derive(Debug)]
pub(super) struct GetTerragruntDirFn {
pub state: Arc<TgState>,
}
impl HclFunc for GetTerragruntDirFn {
fn call(&self, args: &[Value], _cx: &CallCx<'_>) -> Result<Value, FuncError> {
if !args.is_empty() {
return Err(FuncError::Arity {
name: Arc::from("get_terragrunt_dir"),
expected: 0,
got: args.len(),
});
}
Ok(Value::Str(Arc::from(
self.state.component_dir.to_string_lossy().as_ref(),
)))
}
}
#[derive(Debug)]
pub(super) struct GetRepoRootFn {
pub state: Arc<TgState>,
}
impl HclFunc for GetRepoRootFn {
fn call(&self, args: &[Value], _cx: &CallCx<'_>) -> Result<Value, FuncError> {
if !args.is_empty() {
return Err(FuncError::Arity {
name: Arc::from("get_repo_root"),
expected: 0,
got: args.len(),
});
}
Ok(Value::Str(Arc::from(
self.state.workspace_root.to_string_lossy().as_ref(),
)))
}
}
#[derive(Debug)]
pub(super) struct GetParentTerragruntDirFn {
pub state: Arc<TgState>,
}
impl HclFunc for GetParentTerragruntDirFn {
fn call(&self, args: &[Value], _cx: &CallCx<'_>) -> Result<Value, FuncError> {
if !args.is_empty() {
return Err(FuncError::Arity {
name: Arc::from("get_parent_terragrunt_dir"),
expected: 0,
got: args.len(),
});
}
let path = match self.state.active_include.lock() {
Ok(guard) => guard
.as_ref()
.and_then(|p| p.parent().map(Path::to_path_buf)),
Err(poisoned) => poisoned
.into_inner()
.as_ref()
.and_then(|p| p.parent().map(Path::to_path_buf)),
};
let display = path.unwrap_or_else(|| self.state.component_dir.to_path_buf());
Ok(Value::Str(Arc::from(display.to_string_lossy().as_ref())))
}
}
#[derive(Debug)]
pub(super) struct FindInParentFoldersFn {
pub state: Arc<TgState>,
}
impl HclFunc for FindInParentFoldersFn {
fn call(&self, args: &[Value], _cx: &CallCx<'_>) -> Result<Value, FuncError> {
let name: &str = match args.first() {
Some(Value::Str(s)) => s.as_ref(),
None => "terragrunt.hcl",
Some(other) => {
return Err(FuncError::Type {
name: Arc::from("find_in_parent_folders"),
index: 0,
expected: "string",
got: type_name(other),
});
}
};
let fallback: Option<&str> = match args.get(1) {
Some(Value::Str(s)) => Some(s.as_ref()),
_ => None,
};
match find_in_parents(&self.state.component_dir, name, &self.state.workspace_root) {
Some(path) => Ok(Value::Str(Arc::from(path.to_string_lossy().as_ref()))),
None => match fallback {
Some(fb) => Ok(Value::Str(Arc::from(fb))),
None => Err(FuncError::Other {
name: Arc::from("find_in_parent_folders"),
message: Arc::from(format!(
"no `{name}` found above `{}`",
self.state.component_dir.display()
)),
}),
},
}
}
}
#[derive(Debug)]
pub(super) struct FindInParentFoldersFromFn {
pub state: Arc<TgState>,
}
impl HclFunc for FindInParentFoldersFromFn {
fn call(&self, args: &[Value], _cx: &CallCx<'_>) -> Result<Value, FuncError> {
if args.is_empty() {
return Err(FuncError::Arity {
name: Arc::from("find_in_parent_folders_from"),
expected: 1,
got: 0,
});
}
let start = match args.first() {
Some(Value::Str(s)) => Path::new(s.as_ref()).to_path_buf(),
Some(other) => {
return Err(FuncError::Type {
name: Arc::from("find_in_parent_folders_from"),
index: 0,
expected: "string",
got: type_name(other),
});
}
None => {
return Err(FuncError::Arity {
name: Arc::from("find_in_parent_folders_from"),
expected: 1,
got: 0,
});
}
};
let name: &str = match args.get(1) {
Some(Value::Str(s)) => s.as_ref(),
None => "terragrunt.hcl",
Some(other) => {
return Err(FuncError::Type {
name: Arc::from("find_in_parent_folders_from"),
index: 1,
expected: "string",
got: type_name(other),
});
}
};
match find_in_parents(&start, name, &self.state.workspace_root) {
Some(path) => Ok(Value::Str(Arc::from(path.to_string_lossy().as_ref()))),
None => Err(FuncError::Other {
name: Arc::from("find_in_parent_folders_from"),
message: Arc::from(format!("no `{name}` found above `{}`", start.display())),
}),
}
}
}
#[derive(Debug)]
pub(super) struct PathRelativeToIncludeFn {
pub state: Arc<TgState>,
}
impl HclFunc for PathRelativeToIncludeFn {
fn call(&self, args: &[Value], _cx: &CallCx<'_>) -> Result<Value, FuncError> {
if !args.is_empty() {
return Err(FuncError::Arity {
name: Arc::from("path_relative_to_include"),
expected: 0,
got: args.len(),
});
}
let active = match self.state.active_include.lock() {
Ok(g) => g.clone(),
Err(p) => p.into_inner().clone(),
};
let active_dir = active
.and_then(|p| p.parent().map(Path::to_path_buf))
.unwrap_or_else(|| self.state.component_dir.to_path_buf());
let rel = match self.state.component_dir.strip_prefix(&active_dir) {
Ok(r) => r.to_path_buf(),
Err(_) => PathBuf::from("."),
};
let rendered = if rel.as_os_str().is_empty() {
".".to_string()
} else {
rel.to_string_lossy().into_owned()
};
Ok(Value::Str(Arc::from(rendered)))
}
}
#[derive(Debug)]
pub(super) struct PathRelativeFromIncludeFn {
pub state: Arc<TgState>,
}
impl HclFunc for PathRelativeFromIncludeFn {
fn call(&self, args: &[Value], _cx: &CallCx<'_>) -> Result<Value, FuncError> {
if !args.is_empty() {
return Err(FuncError::Arity {
name: Arc::from("path_relative_from_include"),
expected: 0,
got: args.len(),
});
}
let active = match self.state.active_include.lock() {
Ok(g) => g.clone(),
Err(p) => p.into_inner().clone(),
};
let active_dir = active
.and_then(|p| p.parent().map(Path::to_path_buf))
.unwrap_or_else(|| self.state.component_dir.to_path_buf());
let rel = match active_dir.strip_prefix(&*self.state.component_dir) {
Ok(r) => r.to_path_buf(),
Err(_) => PathBuf::from("."),
};
let rendered = if rel.as_os_str().is_empty() {
".".to_string()
} else {
rel.to_string_lossy().into_owned()
};
Ok(Value::Str(Arc::from(rendered)))
}
}
#[derive(Debug)]
pub(super) struct TryFn;
impl HclFunc for TryFn {
fn call(&self, args: &[Value], _cx: &CallCx<'_>) -> Result<Value, FuncError> {
match args.first() {
Some(v) => Ok(v.clone()),
None => match args.get(1) {
Some(fb) => Ok(fb.clone()),
None => Err(FuncError::Arity {
name: Arc::from("try"),
expected: 1,
got: 0,
}),
},
}
}
}
fn type_name(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Int(_) => "int",
Value::Number(_) => "number",
Value::Str(_) => "string",
Value::List(_) => "list",
Value::Map(_) => "map",
}
}
fn find_in_parents(start_dir: &Path, name: &str, workspace_root: &Path) -> Option<PathBuf> {
let abs_start = if start_dir.is_absolute() {
start_dir.to_path_buf()
} else {
workspace_root.join(start_dir)
};
let mut cursor: PathBuf = abs_start;
loop {
let candidate = cursor.join(name);
if candidate.exists() {
if let Ok(canonical) =
paths::canonicalize_inside(&candidate, workspace_root, SymlinkPolicy::Follow)
{
return Some(canonical);
}
}
if !paths::is_descendant(&cursor, workspace_root) {
return None;
}
if cursor == workspace_root {
return None;
}
if !cursor.pop() {
return None;
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::panic)]
mod tests {
use super::*;
use crate::eval::{EnvVarMode, EvalLimits};
fn cx<'a>(root: &'a Path, env: &'a EnvVarMode, limits: &'a EvalLimits) -> CallCx<'a> {
CallCx {
workspace_root: root,
env_vars: env,
limits,
}
}
#[test]
fn test_get_terragrunt_dir_returns_component_dir() {
let root: Arc<Path> = Arc::from(Path::new("/tmp/repo"));
let comp: Arc<Path> = Arc::from(Path::new("/tmp/repo/services/a"));
let state = Arc::new(TgState::new(Arc::clone(&root), Arc::clone(&comp)));
let fn_ = GetTerragruntDirFn {
state: Arc::clone(&state),
};
let env = EnvVarMode::default();
let limits = EvalLimits::default();
let v = fn_.call(&[], &cx(&root, &env, &limits)).unwrap();
assert_eq!(v, Value::Str(Arc::from("/tmp/repo/services/a")));
}
#[test]
fn test_get_repo_root_returns_workspace_root() {
let root: Arc<Path> = Arc::from(Path::new("/tmp/repo"));
let comp: Arc<Path> = Arc::from(Path::new("/tmp/repo/x"));
let state = Arc::new(TgState::new(Arc::clone(&root), comp));
let fn_ = GetRepoRootFn { state };
let env = EnvVarMode::default();
let limits = EvalLimits::default();
let v = fn_.call(&[], &cx(&root, &env, &limits)).unwrap();
assert_eq!(v, Value::Str(Arc::from("/tmp/repo")));
}
#[test]
fn test_find_in_parent_folders_returns_path() {
let tmp = tempfile::tempdir().unwrap();
let root: Arc<Path> = Arc::from(std::fs::canonicalize(tmp.path()).unwrap());
let nested = root.join("services/a");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(root.join("root.hcl"), "").unwrap();
let state = Arc::new(TgState::new(Arc::clone(&root), Arc::from(nested)));
let fn_ = FindInParentFoldersFn { state };
let env = EnvVarMode::default();
let limits = EvalLimits::default();
let v = fn_
.call(
&[Value::Str(Arc::from("root.hcl"))],
&cx(&root, &env, &limits),
)
.unwrap();
match v {
Value::Str(s) => assert!(s.ends_with("root.hcl"), "{s}"),
other => panic!("expected Str, got {other:?}"),
}
}
#[test]
fn test_find_in_parent_folders_fallback() {
let tmp = tempfile::tempdir().unwrap();
let root: Arc<Path> = Arc::from(std::fs::canonicalize(tmp.path()).unwrap());
let nested = root.join("services/a");
std::fs::create_dir_all(&nested).unwrap();
let state = Arc::new(TgState::new(Arc::clone(&root), Arc::from(nested)));
let fn_ = FindInParentFoldersFn { state };
let env = EnvVarMode::default();
let limits = EvalLimits::default();
let v = fn_
.call(
&[
Value::Str(Arc::from("missing.hcl")),
Value::Str(Arc::from("fallback.hcl")),
],
&cx(&root, &env, &limits),
)
.unwrap();
assert_eq!(v, Value::Str(Arc::from("fallback.hcl")));
}
#[test]
fn test_try_returns_first_arg() {
let fn_ = TryFn;
let env = EnvVarMode::default();
let limits = EvalLimits::default();
let root = Path::new("/");
let v = fn_
.call(&[Value::Int(42), Value::Int(0)], &cx(root, &env, &limits))
.unwrap();
assert_eq!(v, Value::Int(42));
}
}