Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions test/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ publish = false
anyhow = "1"
chrono = "0.4"
clap = { version = "4.5.36", features = ["derive", "env"] }
hcl-edit = "0.9"
rand = "0.9"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
3 changes: 2 additions & 1 deletion test/src/commands/destroy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::Path;
use anyhow::{Context, Result};
use tokio::process::Command;

use crate::helpers::{ci_log_group, run_cmd, write_lifecycle};
use crate::helpers::{ci_log_group, delete_backend_state, run_cmd, write_lifecycle};

const MAX_DESTROY_ATTEMPTS: u32 = 3;

Expand Down Expand Up @@ -38,6 +38,7 @@ pub async fn phase_destroy(dir: &Path, rm: bool) -> Result<()> {
}

if rm {
delete_backend_state(dir).await?;
tokio::fs::remove_dir_all(dir).await?;
println!(
"\nDestroy completed successfully. Removed {}",
Expand Down
149 changes: 81 additions & 68 deletions test/src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use tokio::process::Command;
use crate::cli::InitProvider;
use crate::helpers::{
ci_log_group, example_dir, generate_test_run_id, project_root, run_cmd, runs_dir,
write_lifecycle,
upload_tfvars_to_backend, write_lifecycle,
};
use crate::types::{CloudProvider, CommonTfVars, TfVars};

Expand Down Expand Up @@ -71,6 +71,8 @@ pub async fn phase_init(provider_args: &InitProvider) -> Result<PathBuf> {
println!("{backend_tf}");
}

upload_tfvars_to_backend(&dest).await?;

println!("\nRunning terraform init...");
run_cmd(Command::new("terraform").arg("init").current_dir(&dest))
.await
Expand Down Expand Up @@ -110,7 +112,7 @@ pub(crate) async fn copy_example_files(
let file_type = entry.file_type().await?;
if file_type.is_file() {
let content = tokio::fs::read_to_string(entry.path()).await?;
let rewritten = rewrite_module_sources(&content, provider);
let rewritten = rewrite_module_sources(&content, provider)?;
let dest_file = dest.join(&name);
tokio::fs::write(&dest_file, rewritten).await?;
println!(" Copied {}", name_str);
Expand All @@ -121,15 +123,31 @@ pub(crate) async fn copy_example_files(

/// Rewrites module source paths from the example directory layout to the
/// test/runs/{id}/ layout.
fn rewrite_module_sources(content: &str, provider: CloudProvider) -> String {
fn rewrite_module_sources(content: &str, provider: CloudProvider) -> Result<String> {
use hcl_edit::expr::Expression;

let provider_dir = provider.dir_name();
// Provider-specific modules: ../../modules/ → ../../../{provider}/modules/
// (the original goes up 2 levels to {provider}/, we need up 3 to root then into {provider}/)
content.replace(
"\"../../modules/",
&format!("\"../../../{provider_dir}/modules/"),
)
// Kubernetes modules: ../../../kubernetes/ stays the same (already 3 levels up to root)
let old_prefix = "../../modules/";
let new_prefix = format!("../../../{provider_dir}/modules/");

let mut body: hcl_edit::structure::Body =
content.parse().context("Failed to parse terraform file")?;

for block in body.get_blocks_mut("module") {
if let Some(mut attr) = block.body.get_attribute_mut("source") {
let new_val = attr
.get()
.value
.as_str()
.filter(|s| s.starts_with(old_prefix))
.map(|s| s.replacen(old_prefix, &new_prefix, 1));
if let Some(new_val) = new_val {
*attr.value_mut() = Expression::from(new_val);
}
}
}

Ok(body.to_string())
}

/// Converts a string to a valid GCP label value: lowercase, replacing
Expand Down Expand Up @@ -274,88 +292,83 @@ impl DevOverrides {
/// is already present in the file.
pub(crate) async fn inject_dev_overrides(dest: &Path, overrides: &DevOverrides) -> Result<()> {
let main_tf_path = dest.join("main.tf");
let mut content = tokio::fs::read_to_string(&main_tf_path)
let content = tokio::fs::read_to_string(&main_tf_path)
.await
.context("Failed to read main.tf")?;

let mut body: hcl_edit::structure::Body = content.parse().context("Failed to parse main.tf")?;

let mut changed = false;

// Operator module: helm_chart, use_local_chart, orchestratord_version
let operator_vars: Vec<&str> = [
(overrides.local_chart, "helm_chart = var.helm_chart"),
(
overrides.local_chart,
"use_local_chart = var.use_local_chart",
),
(
overrides.orchestratord_version,
"orchestratord_version = var.orchestratord_version",
),
(overrides.local_chart, "helm_chart"),
(overrides.local_chart, "use_local_chart"),
(overrides.orchestratord_version, "orchestratord_version"),
]
.iter()
.filter(|(needed, var)| *needed && !content.contains(*var))
.map(|(_, var)| *var)
.filter(|(needed, _)| *needed)
.map(|(_, key)| *key)
.collect();

if !operator_vars.is_empty() {
content = inject_into_module(&content, "operator", &operator_vars)?;
changed = true;
for var in &operator_vars {
let name = var.split('=').next().unwrap().trim();
println!(" Injected {name} into operator module in main.tf");
let module = find_module_mut(&mut body, "operator")?;
for &key in &operator_vars {
if !module.body.has_attribute(key) {
module.body.push(module_var_attr(key));
changed = true;
println!(" Injected {key} into operator module in main.tf");
}
}
}

// Materialize instance module: environmentd_version
if overrides.environmentd_version
&& !content.contains("environmentd_version = var.environmentd_version")
{
content = inject_into_module(
&content,
"materialize_instance",
&["environmentd_version = var.environmentd_version"],
)?;
changed = true;
println!(" Injected environmentd_version into materialize_instance module in main.tf");
if overrides.environmentd_version {
let module = find_module_mut(&mut body, "materialize_instance")?;
if !module.body.has_attribute("environmentd_version") {
module.body.push(module_var_attr("environmentd_version"));
changed = true;
println!(" Injected environmentd_version into materialize_instance module in main.tf");
}
}

if changed {
tokio::fs::write(&main_tf_path, &content).await?;
tokio::fs::write(&main_tf_path, body.to_string()).await?;
}

Ok(())
}

/// Finds `module "<name>"` in the content and injects the given lines after
/// the `source = ` line inside that block.
fn inject_into_module(content: &str, module_name: &str, vars: &[&str]) -> Result<String> {
let target = format!("module \"{module_name}\"");
let mut lines: Vec<&str> = content.lines().collect();
let mut in_module = false;
let mut insert_after = None;

for (i, line) in lines.iter().enumerate() {
if line.contains(&target) {
in_module = true;
}
if in_module && line.trim_start().starts_with("source") {
insert_after = Some(i);
break;
}
}
/// Finds a `module "<name>"` block in the body, returning a mutable reference.
fn find_module_mut<'a>(
body: &'a mut hcl_edit::structure::Body,
name: &str,
) -> Result<&'a mut hcl_edit::structure::Block> {
body.get_blocks_mut("module")
.find(|b| b.has_labels(&[name]))
.with_context(|| format!("could not find module \"{name}\" in tf file"))
}

let idx = insert_after.ok_or_else(|| {
anyhow::anyhow!("could not find `source` line in module \"{module_name}\" in main.tf")
})?;

let mut offset = 1;
lines.insert(idx + offset, "");
for var in vars {
offset += 1;
let line = format!(" {var}");
// Leak is fine here – this runs once during init.
lines.insert(idx + offset, Box::leak(line.into_boxed_str()));
}
/// Builds an `Attribute` like `key = var.key` with 2-space indentation to
/// match the surrounding module block.
fn module_var_attr(name: &str) -> hcl_edit::structure::Attribute {
use hcl_edit::Decorate;

Ok(lines.join("\n") + "\n")
let mut attr = hcl_edit::structure::Attribute::new(hcl_edit::Ident::new(name), var_ref(name));
attr.decor_mut().set_prefix(" ");
attr
}

/// Builds a `var.<name>` traversal expression.
fn var_ref(name: &str) -> hcl_edit::expr::Expression {
use hcl_edit::Decorated;
use hcl_edit::expr::{Traversal, TraversalOperator};

Traversal::new(
hcl_edit::Ident::new("var"),
vec![Decorated::new(TraversalOperator::GetAttr(Decorated::new(
hcl_edit::Ident::new(name),
)))],
)
.into()
}
6 changes: 5 additions & 1 deletion test/src/commands/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use anyhow::Result;
use crate::commands::init::{
DevOverrides, copy_example_files, inject_dev_overrides, write_dev_variables_tf,
};
use crate::helpers::{ci_log_group, example_dir, project_root, read_tfvars};
use crate::helpers::{
ci_log_group, example_dir, project_root, read_tfvars, upload_tfvars_to_backend,
};

/// Re-copies example .tf files into an existing test run directory,
/// overwriting the current versions. Useful for picking up local
Expand Down Expand Up @@ -46,6 +48,8 @@ pub async fn phase_sync(dir: &Path) -> Result<()> {
inject_dev_overrides(dir, &overrides).await?;
}

upload_tfvars_to_backend(dir).await?;

println!("\nSync completed successfully.");
Ok(())
})
Expand Down
Loading
Loading