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
2 changes: 1 addition & 1 deletion .github/workflows/merge_queue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- lint
- test-aws
- test-gcp
- test-azure
#- test-azure
steps:
- uses: technote-space/workflow-conclusion-action@v3
- name: Verify all checks passed
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/test-azure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ jobs:
working-directory: test
env:
MATERIALIZE_LICENSE_KEY: ${{ secrets.MATERIALIZE_LICENSE_KEY }}
# Use OIDC directly with the AzureRM Terraform provider so it
# can request fresh tokens from GitHub's OIDC endpoint as needed,
# avoiding the short-lived Azure CLI token expiring mid-run.
ARM_USE_OIDC: "true"
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
run: |
cargo run -- run --destroy-on-failure azure \
--owner "github-actions" \
Expand Down
9 changes: 4 additions & 5 deletions test/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,11 @@ pub struct CommonInitArgs {
/// Path to a file containing the Materialize license key (conflicts with --license-key).
#[arg(long, conflicts_with = "license_key")]
pub license_key_file: Option<PathBuf>,
/// Path to Helm chart for the operator.
/// Path to a local orchestratord Helm chart directory. When set, automatically
/// injects helm_chart / use_local_chart into the operator module, creates
/// dev_variables.tf, and sets the corresponding tfvars values.
#[arg(long)]
pub helm_chart: Option<String>,
/// Use a local Helm chart instead of a registry chart.
#[arg(long)]
pub use_local_chart: bool,
pub local_chart_path: Option<PathBuf>,
/// Orchestratord image version.
#[arg(long)]
pub orchestratord_version: Option<String>,
Expand Down
171 changes: 165 additions & 6 deletions test/src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ pub async fn phase_init(provider_args: &InitProvider) -> Result<PathBuf> {
println!("\nCopying terraform files...");
copy_example_files(&src, &dest, provider).await?;

// When any dev overrides are provided (--local-chart-path,
// --orchestratord-version, --environmentd-version), create
// dev_variables.tf and inject the corresponding variables into
// the relevant module blocks in main.tf.
let common = provider_args.common();
let overrides = DevOverrides {
local_chart: common.local_chart_path.is_some(),
orchestratord_version: common.orchestratord_version.is_some(),
environmentd_version: common.environmentd_version.is_some(),
};
if overrides.any() {
println!("\nApplying dev overrides...");
write_dev_variables_tf(&dest).await?;
inject_dev_overrides(&dest, &overrides).await?;
}

println!("\nBuilding terraform.tfvars.json...");
let tfvars = build_tfvars(provider_args, &test_run_id)?;
let tfvars_path = dest.join("terraform.tfvars.json");
Expand Down Expand Up @@ -138,16 +154,21 @@ fn to_gcp_label(s: &str) -> String {

fn build_tfvars(provider_args: &InitProvider, test_run_id: &str) -> Result<TfVars> {
let common = provider_args.common();

let (helm_chart, use_local_chart) = if let Some(chart_path) = &common.local_chart_path {
let canonical =
std::fs::canonicalize(chart_path).context("Failed to resolve --local-chart-path")?;
(Some(canonical.to_string_lossy().into_owned()), Some(true))
} else {
(None, None)
};

let common_tf = CommonTfVars {
name_prefix: test_run_id.to_string(),
license_key: common.resolve_license_key()?,
internal_load_balancer: false,
helm_chart: common.helm_chart.clone(),
use_local_chart: if common.use_local_chart {
Some(true)
} else {
None
},
helm_chart,
use_local_chart,
orchestratord_version: common.orchestratord_version.clone(),
environmentd_version: common.environmentd_version.clone(),
};
Expand Down Expand Up @@ -200,3 +221,141 @@ fn build_tfvars(provider_args: &InitProvider, test_run_id: &str) -> Result<TfVar
},
})
}

/// Writes a `dev_variables.tf` file into the test run directory, defining
/// optional override variables for local development.
pub(crate) async fn write_dev_variables_tf(dest: &Path) -> Result<()> {
let content = r#"variable "helm_chart" {
description = "Chart name from repository or local path to chart. For local charts, set the path to the chart directory."
type = string
default = null
}

variable "use_local_chart" {
description = "Whether to use a local chart instead of one from a repository"
type = bool
default = null
}

variable "orchestratord_version" {
description = "Version of the Materialize orchestrator to install"
type = string
default = null
}

variable "environmentd_version" {
description = "Version of environmentd to use"
type = string
default = null
}
"#;
let path = dest.join("dev_variables.tf");
tokio::fs::write(&path, content).await?;
println!(" Wrote dev_variables.tf");
Ok(())
}

/// Flags that control which dev-override variables to inject into `main.tf`.
pub(crate) struct DevOverrides {
pub local_chart: bool,
pub orchestratord_version: bool,
pub environmentd_version: bool,
}

impl DevOverrides {
/// Returns `true` if any override is active.
pub fn any(&self) -> bool {
self.local_chart || self.orchestratord_version || self.environmentd_version
}
}

/// Injects dev-override variable references into the appropriate module
/// blocks in `main.tf`. Each injection is skipped if the variable reference
/// 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)
.await
.context("Failed to read 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",
),
]
.iter()
.filter(|(needed, var)| *needed && !content.contains(*var))
.map(|(_, var)| *var)
.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");
}
}

// 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 changed {
tokio::fs::write(&main_tf_path, &content).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;
}
}

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()));
}

Ok(lines.join("\n") + "\n")
}
17 changes: 16 additions & 1 deletion test/src/commands/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use std::path::Path;

use anyhow::Result;

use crate::commands::init::copy_example_files;
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};

/// Re-copies example .tf files into an existing test run directory,
Expand All @@ -12,6 +14,7 @@ pub async fn phase_sync(dir: &Path) -> Result<()> {
ci_log_group("Sync", || async {
let tfvars = read_tfvars(dir)?;
let provider = tfvars.cloud_provider();
let common = tfvars.common();
let src = example_dir(provider)?;
let root = project_root()?;

Expand All @@ -31,6 +34,18 @@ pub async fn phase_sync(dir: &Path) -> Result<()> {
println!("\nCopying terraform files...");
copy_example_files(&src, dir, provider).await?;

// Re-apply dev overrides that were set during init.
let overrides = DevOverrides {
local_chart: common.helm_chart.is_some() && common.use_local_chart == Some(true),
orchestratord_version: common.orchestratord_version.is_some(),
environmentd_version: common.environmentd_version.is_some(),
};
if overrides.any() {
println!("\nRe-applying dev overrides...");
write_dev_variables_tf(dir).await?;
inject_dev_overrides(dir, &overrides).await?;
}

println!("\nSync completed successfully.");
Ok(())
})
Expand Down
8 changes: 8 additions & 0 deletions test/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ impl TfVars {
}
}

pub fn common(&self) -> &CommonTfVars {
match self {
TfVars::Aws { common, .. }
| TfVars::Azure { common, .. }
| TfVars::Gcp { common, .. } => common,
}
}

pub fn common_mut(&mut self) -> &mut CommonTfVars {
match self {
TfVars::Aws { common, .. }
Expand Down
Loading