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: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ coverage/
.tmp/
# generated by hyperi-ci's osv-scanner stage from quality.ignore (mirrors deny.toml)
osv-scanner.toml
# written by `dfe-loader --emit-helm`; the release assembles the chart from the contract
/chart/

# Rust / cargo build artefacts
/target
Expand Down
2 changes: 1 addition & 1 deletion .hyperi-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ release:
helm:
enabled: true
contract: emit
library: "2.14.3"
library: "2.14.6"
container:
enabled: true
dockerfile: Dockerfile
Expand Down
12 changes: 6 additions & 6 deletions Cargo.lock

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

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ required-features = ["pgo-driver"]
tokio = { version = ">=1.53.2, <2", features = ["full"] }
tokio-util = { version = ">=0.7.19, <0.8", features = ["rt"] }

# Shared library with transport abstraction. The floor is the first release whose
# contract carries no vendor defaults, so the app names its own.
scalo = { version = "2.14.3", features = ["config-schema", "transport-kafka", "transport-grpc", "dlq-kafka", "config", "config-reload", "deployment", "version-check", "scaling", "cli-service", "top", "logger", "metrics", "service-metrics", "expression", "memory", "worker-batch", "governor", "secrets", "sink-stack"] }
# Shared library with transport abstraction. The floor moves with
# `release.helm.library` in .hyperi-ci.yaml: the library renders only the
# contract version its scalo release writes.
scalo = { version = "2.14.6", features = ["config-schema", "transport-kafka", "transport-grpc", "dlq-kafka", "config", "config-reload", "deployment", "version-check", "scaling", "cli-service", "top", "logger", "metrics", "service-metrics", "expression", "memory", "worker-batch", "governor", "secrets", "sink-stack"] }

# Steps scalo's jittered retry schedule (sink_stack backoff). Range MUST track
# scalo's: BackoffBuilder only applies to the ExponentialBuilder scalo returns.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Inbound, declared in `dfe-infra/suite.yaml`:
- **scalo-rs -> dfe-loader** (`generated-file`, lockstep) -- `Dockerfile` is emitted by `scalo::deployment::generate_dockerfile()` at schema version 4. Regenerate and commit the diff.
Outbound, so what a change here can break:

- **dfe-loader -> dfe-infra** (`image-pin`, lockstep) -- `dfe-infra/helm/charts/dfe-loader/Chart.yaml:6` pins this image as tag plus digest.
- **dfe-loader -> dfe-infra** (`image-pin`, lockstep) -- `dfe-infra/versions.yaml` pins this image and the thin chart released here, and Argo deploys that published chart.
- **dfe-loader -> dfe-engine** (`mirrored-logic`) -- dfe-engine hand-reimplements this repo's config validation in `plugins_builtin/loader.py` to catch errors when it authors a config. Nothing is copied, so no script compares them.

Runtime, not build edges: dfe-engine's `dfe-schema` entry point creates every destination table before the loader can insert, and producers reach the loader over Kafka (`*_land` / `*_load` topics) or direct gRPC (default topic `main_land`) -- in the suite that producer is dfe-receiver.
13 changes: 0 additions & 13 deletions config/defaults.yaml

This file was deleted.

8 changes: 5 additions & 3 deletions src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,13 +575,14 @@ impl Config {
/// Build a deployment contract from loader config defaults.
///
/// Apps provide ~20% customisation; scalo generates ~80% boilerplate
/// (Dockerfile, Helm chart, Compose fragment).
/// (Dockerfile, Compose fragment), and the release assembles the thin
/// chart from this contract.
pub fn deployment_contract() -> scalo::deployment::DeploymentContract {
use scalo::deployment::{
CONTRACT_SCHEMA_VERSION, DeploymentContract, HealthContract, ImageProfile,
NativeDepsContract, OciLabels, PortCondition, PortContract, ResourceList,
ResourcesContract, SecurityContract, WritablePath, base_image_from_cascade,
image_registry_from_cascade,
ResourcesContract, SecurityContract, ServiceAccount, WritablePath,
base_image_from_cascade, image_registry_from_cascade,
};

// A deployment.* cascade key (or env) wins over the base image and registry
Expand Down Expand Up @@ -683,6 +684,7 @@ impl Config {
},
security: SecurityContract::default(),
singleton: false,
service_account: ServiceAccount::Own,
}
}

Expand Down
14 changes: 0 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ struct App {
#[command(subcommand)]
command: Option<StandardCommand>,

/// Generate Helm chart from deployment contract and exit (default output: ./chart)
#[arg(long, value_name = "DIR", default_missing_value = "chart", num_args = 0..=1)]
emit_helm: Option<PathBuf>,

/// Generate Dockerfile from deployment contract and exit (default output: ./Dockerfile)
#[arg(long, value_name = "FILE", default_missing_value = "Dockerfile", num_args = 0..=1)]
emit_dockerfile: Option<PathBuf>,
Expand Down Expand Up @@ -315,16 +311,6 @@ async fn main() {

let app = App::parse();

if let Some(output) = &app.emit_helm {
let contract = Config::deployment_contract();
if let Err(e) = scalo::deployment::generate_chart(&contract, output, None) {
eprintln!("fatal: {e}");
std::process::exit(1);
}
println!("Helm chart generated at {}", output.display());
return;
}

if let Some(output) = &app.emit_dockerfile {
let contract = Config::deployment_contract();
let content = scalo::deployment::generate_dockerfile(&contract, None);
Expand Down
126 changes: 21 additions & 105 deletions tests/integration/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@

//! Deployment artifact generation tests
//!
//! Verifies that generated Dockerfile and Helm chart match the contract values.
//! Verifies that the generated Dockerfile and compose fragment match the
//! contract values.

use std::path::Path;

use dfe_loader::config::Config;
use scalo::deployment::{
generate_chart, generate_compose_fragment, generate_dockerfile, validate_dockerfile,
validate_helm_values,
};
use scalo::deployment::{generate_compose_fragment, generate_dockerfile, validate_dockerfile};

fn contract() -> scalo::deployment::DeploymentContract {
Config::deployment_contract()
Expand Down Expand Up @@ -53,89 +51,26 @@ fn test_generated_dockerfile_matches_existing() {
);
}

/// The library chart injects each secret env var the contract declares, and
/// `Config::load` reads exactly these names.
#[test]
fn test_generated_chart_structure() {
fn test_contract_declares_secret_env_vars() {
let contract = contract();
let dir = tempfile::tempdir().unwrap();
generate_chart(&contract, dir.path(), None).unwrap();

// Chart.yaml
let chart = std::fs::read_to_string(dir.path().join("Chart.yaml")).unwrap();
assert!(chart.contains("name: dfe-loader"));
assert!(chart.contains("description: High-performance Kafka to ClickHouse data loader"));

// values.yaml
let values = std::fs::read_to_string(dir.path().join("values.yaml")).unwrap();
assert!(values.contains("repository: ghcr.io/hyperi-io/dfe-loader"));
assert!(values.contains("port: 9090"));

// Secret sections
assert!(
values.contains("kafka:"),
"Should have kafka secret section"
);
assert!(
values.contains("clickhouse:"),
"Should have clickhouse secret section"
);
assert!(
values.contains("existingSecret:"),
"Should support existing secrets"
);

// KEDA section
assert!(values.contains("keda:"), "Should have KEDA section");

// templates/
let templates = dir.path().join("templates");
assert!(templates.join("_helpers.tpl").exists());
assert!(templates.join("deployment.yaml").exists());
assert!(templates.join("service.yaml").exists());
assert!(templates.join("serviceaccount.yaml").exists());
assert!(templates.join("configmap.yaml").exists());
assert!(templates.join("secret.yaml").exists());
assert!(templates.join("hpa.yaml").exists());
assert!(templates.join("keda-scaledobject.yaml").exists());
assert!(templates.join("keda-triggerauth.yaml").exists());
assert!(templates.join("NOTES.txt").exists());
}

#[test]
fn test_generated_chart_helpers() {
let contract = contract();
let dir = tempfile::tempdir().unwrap();
generate_chart(&contract, dir.path(), None).unwrap();

let helpers = std::fs::read_to_string(dir.path().join("templates/_helpers.tpl")).unwrap();
assert!(
helpers.contains("kafkaSecretName"),
"Should have kafka secret helper"
);
assert!(
helpers.contains("clickhouseSecretName"),
"Should have clickhouse secret helper"
);
}

#[test]
fn test_generated_deployment_has_env_vars() {
let contract = contract();
let dir = tempfile::tempdir().unwrap();
generate_chart(&contract, dir.path(), None).unwrap();

let deploy = std::fs::read_to_string(dir.path().join("templates/deployment.yaml")).unwrap();
assert!(
deploy.contains("DFE_LOADER__KAFKA__SASL__USERNAME"),
"Should inject kafka username env"
);
assert!(
deploy.contains("DFE_LOADER__KAFKA__SASL__PASSWORD"),
"Should inject kafka password env"
);
assert!(
deploy.contains("DFE_LOADER__CLICKHOUSE__PASSWORD"),
"Should inject clickhouse password env"
);
let env_vars: Vec<&str> = contract
.secrets
.iter()
.flat_map(|group| group.env_vars.iter().map(|env| env.env_var.as_str()))
.collect();
for name in [
"DFE_LOADER__KAFKA__SASL__USERNAME",
"DFE_LOADER__KAFKA__SASL__PASSWORD",
"DFE_LOADER__CLICKHOUSE__PASSWORD",
] {
assert!(
env_vars.contains(&name),
"contract should declare {name}: {env_vars:?}"
);
}
}

#[test]
Expand Down Expand Up @@ -168,20 +103,6 @@ fn test_validate_generated_dockerfile() {
);
}

#[test]
fn test_validate_generated_chart() {
let contract = contract();
let dir = tempfile::tempdir().unwrap();
generate_chart(&contract, dir.path(), None).unwrap();

// Generated chart should pass validation against its own contract
let result = validate_helm_values(&contract, dir.path().to_str().unwrap());
assert!(
result.is_ok(),
"Generated chart should validate: {result:?}"
);
}

/// Write generated artifacts to .tmp/ for manual comparison.
/// Run with: cargo test --test deployment -- `write_artifacts` --ignored --nocapture
#[test]
Expand All @@ -199,10 +120,5 @@ fn write_artifacts_to_tmp() {
let compose = generate_compose_fragment(&contract);
std::fs::write(base.join("compose.service.yaml"), &compose).unwrap();

// Helm chart
let chart_dir = base.join("chart");
let _ = std::fs::remove_dir_all(&chart_dir);
generate_chart(&contract, &chart_dir, None).unwrap();

eprintln!("Generated artifacts written to: {}", base.display());
}
12 changes: 0 additions & 12 deletions tests/smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,6 @@ fn smoke_deployment_contract() {
);
}

/// Helm chart generates from contract without panicking.
#[test]
fn smoke_helm_generation() {
let contract = Config::deployment_contract();
let tmp = tempfile::tempdir().expect("tempdir");
let result = scalo::deployment::generate_chart(&contract, tmp.path(), None);
assert!(
result.is_ok(),
"chart generation should succeed: {result:?}"
);
}

/// Dockerfile generates from contract without panicking.
#[test]
fn smoke_dockerfile_generation() {
Expand Down
Loading