From 1ed99cbe076a94c7bcbde230f60c584c18f6229a Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Wed, 15 Oct 2025 14:46:18 -0400 Subject: [PATCH] implement resolve and apply Signed-off-by: Jason Hall --- .pre-commit-config.yaml | 1 + CLAUDE.md | 55 +++++- Cargo.toml | 2 + README.md | 104 +++++++++++ example/deployment.yaml | 19 ++ example/k8s/deployment.yaml | 17 ++ example/k8s/service.yaml | 10 ++ example/multi-deployment.yaml | 38 ++++ src/cli/mod.rs | 38 ++++ src/lib.rs | 1 + src/main.rs | 204 ++++++++++++++++++++++ src/registry/mod.rs | 9 +- src/resolve/mod.rs | 198 +++++++++++++++++++++ tests/testdata/apply_basic.txt | 32 ++++ tests/testdata/resolve_basic.txt | 32 ++++ tests/testdata/resolve_dedup.txt | 38 ++++ tests/testdata/resolve_directory.txt | 43 +++++ tests/testdata/resolve_multi_document.txt | 47 +++++ 18 files changed, 877 insertions(+), 11 deletions(-) create mode 100644 example/deployment.yaml create mode 100644 example/k8s/deployment.yaml create mode 100644 example/k8s/service.yaml create mode 100644 example/multi-deployment.yaml create mode 100644 src/resolve/mod.rs create mode 100644 tests/testdata/apply_basic.txt create mode 100644 tests/testdata/resolve_basic.txt create mode 100644 tests/testdata/resolve_dedup.txt create mode 100644 tests/testdata/resolve_directory.txt create mode 100644 tests/testdata/resolve_multi_document.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7829e76..9e972de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,4 +43,5 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml + args: [--allow-multiple-documents] - id: check-added-large-files diff --git a/CLAUDE.md b/CLAUDE.md index dd9e624..a63c180 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,6 +85,11 @@ GAR has special handling for blob uploads that differs from the standard OCI spe - Switched to `reqwest` for cleaner API and better redirect handling - Disabled automatic redirects to manually handle GAR's upload flow +5. **Memory Efficiency**: + - Blob downloads return `bytes::Bytes` instead of `Vec` to avoid unnecessary copies + - Blob uploads require `.to_vec()` due to reqwest's `'static` requirement for request bodies + - This is acceptable as reqwest streams the data internally + ### Cross-Compilation on macOS For Linux targets from macOS, you need: @@ -149,16 +154,54 @@ The iterative development process: 4. Fix issues discovered during real usage 5. Refine UX based on actual workflows +## Features Implemented + +### YAML Resolution (`krust resolve`) + +Inspired by ko's Kubernetes integration, krust can resolve `krust://` references in YAML files: + +1. **Reference Syntax**: Use `krust://path/to/project` in YAML (e.g., `image: krust://./example/hello-krust`) +2. **Deduplication**: Multiple references to the same path are deduplicated - builds only once +3. **Multi-document support**: Handles YAML files with multiple `---` separated documents +4. **Directory support**: Can process entire directories of YAML files with `-f ./k8s/` +5. **Output**: Resolved YAML to stdout with all `krust://` replaced by digests + +**Implementation details**: +- Uses `serde_yml` (maintained fork of deprecated serde_yaml) for parsing and serialization +- Recursively walks YAML tree to find all string values with `krust://` prefix +- Builds each unique path once, stores digest mapping +- Second pass replaces all references with digests +- Preserves YAML structure and formatting + +**Usage**: +- `krust resolve -f deployment.yaml | kubectl apply -f -` +- Or use the convenience command: `krust apply -f deployment.yaml` + +### Apply Command (`krust apply`) + +Convenience wrapper that combines `resolve` with `kubectl apply`: +- Resolves `krust://` references +- Pipes resolved YAML directly to `kubectl apply -f -` +- Exits with kubectl's exit code +- Requires `kubectl` to be installed and configured + +**Usage**: `krust apply -f deployment.yaml` + ## Future Improvements Potential enhancements identified: 1. ~~Registry authentication support~~ ✓ Implemented (supports Docker credential helpers) -2. Multi-platform image manifests -3. Build caching -4. Image layer optimization -5. Support for custom Dockerfile-like configs -6. SBOM (Software Bill of Materials) generation -7. Optimize blob uploads (check if blob exists before uploading) +2. ~~YAML resolution for Kubernetes deployments~~ ✓ Implemented (`krust resolve`) +3. Multi-platform image manifests +4. Build caching +5. Image layer optimization +6. Support for custom Dockerfile-like configs +7. SBOM (Software Bill of Materials) generation +8. ~~Optimize blob uploads (check if blob exists before uploading)~~ ✓ Implemented +9. Stream uploads from disk instead of buffering in memory + - Currently buffers tar and compressed layer in memory + - Could write to temp file, calculate diff_id, then stream upload + - Would reduce memory usage for large binaries ## Useful Commands diff --git a/Cargo.toml b/Cargo.toml index 9d0ef24..f5f229c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ chrono = "0.4" tar = "0.4" flate2 = "1.0" reqwest = { version = "0.12", features = ["json", "stream"] } +bytes = "1.0" +serde_yml = "0.0.12" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } dirs = "6.0" diff --git a/README.md b/README.md index 9092af9..b885c26 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,8 @@ Note: [ttl.sh](https://ttl.sh) is a free, temporary container registry perfect f ## CLI Reference +### Build Command + ``` krust build [OPTIONS] [DIRECTORY] [-- ...] @@ -343,11 +345,113 @@ Options: -i, --image Target image reference (overrides KRUST_REPO) --platform Target platform [default: linux/amd64] --no-push Skip pushing the image to registry + --tag Tag to apply to the image (e.g., latest, v1.0.0) --repo Repository prefix (uses KRUST_REPO env var) -v, --verbose Enable verbose logging -h, --help Print help ``` +### Resolve Command + +The `resolve` command scans YAML files for `krust://` references, builds the referenced images, and outputs resolved YAML with concrete image digests. + +``` +krust resolve -f [OPTIONS] + +Arguments: + -f, --filename Path to YAML file or directory (can be repeated) + +Options: + --platform Target platforms (comma-separated) + --repo Repository prefix (uses KRUST_REPO env var) + --tag Tag to apply to built images + -v, --verbose Enable verbose logging + -h, --help Print help +``` + +#### Usage Examples + +```bash +# Resolve a single file +export KRUST_REPO=ttl.sh/myuser +krust resolve -f deployment.yaml > resolved.yaml + +# Resolve multiple files +krust resolve -f deployment.yaml -f service.yaml + +# Resolve all YAML files in a directory +krust resolve -f ./k8s/ + +# Pipe directly to kubectl +krust resolve -f deployment.yaml | kubectl apply -f - + +# Build for multiple platforms +krust resolve -f deployment.yaml --platform linux/amd64,linux/arm64 +``` + +#### YAML Reference Syntax + +Use `krust://` prefix followed by the path to the Rust project: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + template: + spec: + containers: + - name: app + image: krust://./path/to/rust/project +``` + +The `resolve` command will: +1. Find all `krust://` references (deduplicates automatically) +2. Build each unique project once +3. Push images to the registry +4. Replace references with concrete digests (e.g., `ttl.sh/user/app@sha256:...`) +5. Output resolved YAML to stdout + +**Note**: Multiple references to the same path are deduplicated - the image is built only once and all references are updated with the same digest. + +### Apply Command + +The `apply` command combines `resolve` with `kubectl apply` for a seamless deployment workflow: + +``` +krust apply -f [OPTIONS] + +Arguments: + -f, --filename Path to YAML file or directory (can be repeated) + +Options: + --platform Target platforms (comma-separated) + --repo Repository prefix (uses KRUST_REPO env var) + --tag Tag to apply to built images + -v, --verbose Enable verbose logging + -h, --help Print help +``` + +#### Usage Examples + +```bash +# Build and deploy in one command +export KRUST_REPO=ttl.sh/myuser +krust apply -f deployment.yaml + +# Apply entire directory +krust apply -f ./k8s/ + +# Build for multiple platforms and deploy +krust apply -f deployment.yaml --platform linux/amd64,linux/arm64 +``` + +The `apply` command is equivalent to: +```bash +krust resolve -f deployment.yaml | kubectl apply -f - +``` + ## Troubleshooting ### macOS: "linking with `cc` failed" diff --git a/example/deployment.yaml b/example/deployment.yaml new file mode 100644 index 0000000..f90dbcd --- /dev/null +++ b/example/deployment.yaml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-krust +spec: + replicas: 3 + selector: + matchLabels: + app: hello-krust + template: + metadata: + labels: + app: hello-krust + spec: + containers: + - name: hello + image: krust://./example/hello-krust + ports: + - containerPort: 8080 diff --git a/example/k8s/deployment.yaml b/example/k8s/deployment.yaml new file mode 100644 index 0000000..1448782 --- /dev/null +++ b/example/k8s/deployment.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-krust +spec: + selector: + matchLabels: + app: hello-krust + replicas: 1 + template: + metadata: + labels: + app: hello-krust + spec: + containers: + - name: app + image: krust://./example/hello-krust diff --git a/example/k8s/service.yaml b/example/k8s/service.yaml new file mode 100644 index 0000000..acd5745 --- /dev/null +++ b/example/k8s/service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: hello-krust-svc +spec: + selector: + app: hello-krust + ports: + - port: 80 + targetPort: 8080 diff --git a/example/multi-deployment.yaml b/example/multi-deployment.yaml new file mode 100644 index 0000000..34292f7 --- /dev/null +++ b/example/multi-deployment.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + setting: "production" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-krust +spec: + replicas: 3 + selector: + matchLabels: + app: hello-krust + template: + metadata: + labels: + app: hello-krust + spec: + containers: + - name: app + image: krust://./example/hello-krust + initContainers: + - name: init + image: krust://./example/hello-krust +--- +apiVersion: v1 +kind: Service +metadata: + name: hello-krust-svc +spec: + selector: + app: hello-krust + ports: + - port: 80 + targetPort: 8080 diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 28fe2f2..e621015 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -50,6 +50,44 @@ pub enum Commands { image: String, }, + /// Resolve krust:// references in YAML files + Resolve { + /// Path to YAML file or directory containing YAML files + #[arg(short = 'f', long = "filename", required = true)] + filenames: Vec, + + /// Target platforms (e.g., linux/amd64, linux/arm64) + #[arg(long, value_delimiter = ',')] + platform: Option>, + + /// Repository prefix (e.g., ghcr.io/username) + #[arg(env = "KRUST_REPO")] + repo: Option, + + /// Tag to apply to the images (e.g., latest, v1.0.0) + #[arg(long)] + tag: Option, + }, + + /// Build images and apply resolved YAML with kubectl + Apply { + /// Path to YAML file or directory containing YAML files + #[arg(short = 'f', long = "filename", required = true)] + filenames: Vec, + + /// Target platforms (e.g., linux/amd64, linux/arm64) + #[arg(long, value_delimiter = ',')] + platform: Option>, + + /// Repository prefix (e.g., ghcr.io/username) + #[arg(env = "KRUST_REPO")] + repo: Option, + + /// Tag to apply to the images (e.g., latest, v1.0.0) + #[arg(long)] + tag: Option, + }, + /// Show version information Version, } diff --git a/src/lib.rs b/src/lib.rs index 2d1bbb8..b2b6000 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,5 +5,6 @@ pub mod config; pub mod image; pub mod manifest; pub mod registry; +pub mod resolve; pub use anyhow::Result; diff --git a/src/main.rs b/src/main.rs index 4871980..8714d8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,9 @@ use krust::{ image::ImageBuilder, manifest::{ManifestDescriptor, Platform}, registry::RegistryClient, + resolve::{find_krust_references, read_yaml_files, replace_krust_references}, }; +use std::collections::HashMap; use std::path::{Path, PathBuf}; use tracing::{error, info}; use tracing_subscriber::EnvFilter; @@ -218,6 +220,55 @@ async fn main() -> Result<()> { error!("Push command not yet implemented"); std::process::exit(1); } + Commands::Resolve { + filenames, + platform, + repo, + tag, + } => { + let resolved_yaml = resolve_yaml_files(filenames, platform, repo, tag).await?; + + // Output all documents separated by --- + for (i, doc) in resolved_yaml.iter().enumerate() { + if i > 0 { + println!("---"); + } + print!("{}", doc); + } + } + Commands::Apply { + filenames, + platform, + repo, + tag, + } => { + let resolved_yaml = resolve_yaml_files(filenames, platform, repo, tag).await?; + + // Combine all documents and pipe to kubectl + let combined_yaml = resolved_yaml.join("---\n"); + + // Execute kubectl apply + let mut kubectl = std::process::Command::new("kubectl") + .args(["apply", "-f", "-"]) + .stdin(std::process::Stdio::piped()) + .spawn() + .context("Failed to execute kubectl - is it installed?")?; + + // Write YAML to kubectl's stdin + if let Some(mut stdin) = kubectl.stdin.take() { + use std::io::Write; + stdin + .write_all(combined_yaml.as_bytes()) + .context("Failed to write to kubectl stdin")?; + } + + // Wait for kubectl to finish + let status = kubectl.wait()?; + + if !status.success() { + std::process::exit(status.code().unwrap_or(1)); + } + } Commands::Version => { println!("krust {}", env!("CARGO_PKG_VERSION")); } @@ -226,6 +277,159 @@ async fn main() -> Result<()> { Ok(()) } +/// Resolve krust:// references in YAML files +async fn resolve_yaml_files( + filenames: Vec, + platform: Option>, + repo: Option, + tag: Option, +) -> Result> { + let repo = repo.context("KRUST_REPO must be set")?; + let config = Config::load()?; + + // Collect all YAML content and find all krust:// references + let mut all_yaml_files = Vec::new(); + let mut all_references = std::collections::HashSet::new(); + + for path in &filenames { + let yaml_files = read_yaml_files(path)?; + for (filename, content) in &yaml_files { + let refs = find_krust_references(content)?; + all_references.extend(refs); + all_yaml_files.push((filename.clone(), content.clone())); + } + } + + info!( + "Found {} unique krust:// reference(s)", + all_references.len() + ); + + // Build and push images for each unique reference + let mut replacements = HashMap::new(); + let mut registry_client = RegistryClient::new()?; + + for krust_path in all_references { + info!("Building image for: krust://{}", krust_path); + + // Resolve the path (could be relative like ./example/hello-krust) + let project_path = PathBuf::from(&krust_path); + + if !project_path.exists() { + anyhow::bail!("Path does not exist: {}", krust_path); + } + + // Get project name + let project_name = get_project_name(&project_path)?; + let target_repo = format!("{}/{}", repo, project_name); + + // Load project config + let project_config = Config::load_project_config(&project_path)?; + let base_image = project_config + .base_image + .unwrap_or(config.base_image.clone()); + + // Determine platforms + let platforms = if let Some(ref platforms) = platform { + platforms.clone() + } else { + // Default to linux/amd64 for resolve + vec!["linux/amd64".to_string()] + }; + + // Build for each platform + let mut manifest_descriptors = Vec::new(); + + for platform_str in &platforms { + info!("Building {} for platform: {}", krust_path, platform_str); + + let target = get_rust_target_triple(platform_str)?; + let builder = RustBuilder::new(&project_path, &target); + let build_result = builder.build()?; + + let image_builder = ImageBuilder::new( + build_result.binary_path, + base_image.clone(), + platform_str.clone(), + ); + + let base_auth = resolve_auth(&base_image)?; + let (config_data, layer_data, manifest) = image_builder + .build(&mut registry_client, &base_auth) + .await?; + + // Push the image + let push_auth = resolve_auth(&target_repo)?; + let app_layer_media_type = manifest + .layers + .last() + .map(|l| l.media_type.clone()) + .unwrap_or_else(|| "application/vnd.docker.image.rootfs.diff.tar.gzip".to_string()); + + let (digest_ref, manifest_size) = registry_client + .push_layered_image( + &target_repo, + config_data, + layer_data, + app_layer_media_type, + &manifest, + &push_auth, + &base_image, + &base_auth, + ) + .await?; + + // Parse platform + let parts: Vec<&str> = platform_str.split('/').collect(); + let (os, arch) = if parts.len() >= 2 { + (parts[0].to_string(), parts[1].to_string()) + } else { + return Err(anyhow::anyhow!("Invalid platform format: {}", platform_str)); + }; + + let digest = digest_ref.split('@').next_back().unwrap_or("").to_string(); + + manifest_descriptors.push(ManifestDescriptor { + media_type: "application/vnd.oci.image.manifest.v1+json".to_string(), + size: manifest_size as i64, + digest, + platform: Platform { + architecture: arch, + os, + variant: None, + }, + }); + } + + // Push manifest list + let has_tag = tag.is_some(); + let manifest_target = if let Some(tag_name) = &tag { + format!("{}:{}", target_repo, tag_name) + } else { + target_repo.clone() + }; + + let final_auth = resolve_auth(&manifest_target)?; + let image_ref = registry_client + .push_manifest_list(&manifest_target, manifest_descriptors, &final_auth, has_tag) + .await?; + + info!("Resolved krust://{} -> {}", krust_path, image_ref); + replacements.insert(krust_path, image_ref); + } + + // Replace references in all YAML files and return resolved docs + let mut output_docs = Vec::new(); + + for (filename, content) in &all_yaml_files { + info!("Resolving references in: {}", filename); + let resolved = replace_krust_references(content, &replacements)?; + output_docs.push(resolved); + } + + Ok(output_docs) +} + fn get_project_name(project_path: &Path) -> Result { let cargo_toml_path = project_path.join("Cargo.toml"); let content = std::fs::read_to_string(&cargo_toml_path).context("Failed to read Cargo.toml")?; diff --git a/src/registry/mod.rs b/src/registry/mod.rs index f49e037..8176833 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use base64::Engine; +use bytes::Bytes; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -556,7 +557,7 @@ impl RegistryClient { image_ref: &str, descriptor: &OciDescriptor, auth: &RegistryAuth, - ) -> Result> { + ) -> Result { let reference = ImageReference::parse(image_ref)?; let token = self .authenticate(&reference.registry, &reference.repository, auth) @@ -589,8 +590,7 @@ impl RegistryClient { redirect_response.status() ); } - let body = redirect_response.bytes().await?; - return Ok(body.to_vec()); + return Ok(redirect_response.bytes().await?); } } @@ -602,8 +602,7 @@ impl RegistryClient { ); } - let body = response.bytes().await?; - Ok(body.to_vec()) + Ok(response.bytes().await?) } // Push a blob to the registry diff --git a/src/resolve/mod.rs b/src/resolve/mod.rs new file mode 100644 index 0000000..be3ddbf --- /dev/null +++ b/src/resolve/mod.rs @@ -0,0 +1,198 @@ +use anyhow::{Context, Result}; +use serde::Deserialize; +use std::collections::{HashMap, HashSet}; +use std::path::Path; + +const KRUST_PREFIX: &str = "krust://"; + +/// Find all krust:// references in YAML documents +pub fn find_krust_references(yaml_content: &str) -> Result> { + let mut references = HashSet::new(); + + // Parse YAML documents (handle multiple --- separated docs) + for doc in serde_yml::Deserializer::from_str(yaml_content) { + let value = serde_yml::Value::deserialize(doc)?; + find_references_in_value(&value, &mut references); + } + + Ok(references) +} + +/// Recursively search for krust:// references in a YAML value +fn find_references_in_value(value: &serde_yml::Value, references: &mut HashSet) { + match value { + serde_yml::Value::String(s) => { + if let Some(path) = s.strip_prefix(KRUST_PREFIX) { + references.insert(path.to_string()); + } + } + serde_yml::Value::Sequence(seq) => { + for item in seq { + find_references_in_value(item, references); + } + } + serde_yml::Value::Mapping(map) => { + for (_key, val) in map { + find_references_in_value(val, references); + } + } + _ => {} + } +} + +/// Replace all krust:// references with resolved image digests +pub fn replace_krust_references( + yaml_content: &str, + replacements: &HashMap, +) -> Result { + let mut result = Vec::new(); + + // Parse and process each YAML document + let docs: Vec = serde_yml::Deserializer::from_str(yaml_content) + .map(serde_yml::Value::deserialize) + .collect::, _>>()?; + + for (i, mut value) in docs.into_iter().enumerate() { + replace_in_value(&mut value, replacements); + + // Serialize back to YAML + let yaml_str = serde_yml::to_string(&value)?; + + // Add document separator if not the first document + if i > 0 { + result.push("---\n".to_string()); + } + result.push(yaml_str); + } + + Ok(result.join("")) +} + +/// Recursively replace krust:// references in a YAML value +fn replace_in_value(value: &mut serde_yml::Value, replacements: &HashMap) { + match value { + serde_yml::Value::String(s) => { + if let Some(path) = s.strip_prefix(KRUST_PREFIX) { + if let Some(replacement) = replacements.get(path) { + *s = replacement.clone(); + } + } + } + serde_yml::Value::Sequence(seq) => { + for item in seq { + replace_in_value(item, replacements); + } + } + serde_yml::Value::Mapping(map) => { + for (_key, val) in map { + replace_in_value(val, replacements); + } + } + _ => {} + } +} + +/// Read YAML files from a path (file or directory) +pub fn read_yaml_files(path: &Path) -> Result> { + let mut files = Vec::new(); + + if path.is_file() { + let content = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read file: {}", path.display()))?; + files.push((path.display().to_string(), content)); + } else if path.is_dir() { + // Read all .yaml and .yml files in the directory + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let entry_path = entry.path(); + + if entry_path.is_file() { + if let Some(ext) = entry_path.extension() { + if ext == "yaml" || ext == "yml" { + let content = std::fs::read_to_string(&entry_path)?; + files.push((entry_path.display().to_string(), content)); + } + } + } + } + + if files.is_empty() { + anyhow::bail!("No YAML files found in directory: {}", path.display()); + } + } else { + anyhow::bail!("Path does not exist: {}", path.display()); + } + + Ok(files) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_find_krust_references() { + let yaml = r#" +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test +spec: + template: + spec: + containers: + - name: app + image: krust://./example/hello-krust + - name: sidecar + image: krust://./example/hello-krust +"#; + + let refs = find_krust_references(yaml).unwrap(); + assert_eq!(refs.len(), 1); // Should deduplicate + assert!(refs.contains("./example/hello-krust")); + } + + #[test] + fn test_find_multiple_unique_references() { + let yaml = r#" +containers: +- image: krust://./app1 +- image: krust://./app2 +- image: regular-image:latest +"#; + + let refs = find_krust_references(yaml).unwrap(); + assert_eq!(refs.len(), 2); + assert!(refs.contains("./app1")); + assert!(refs.contains("./app2")); + } + + #[test] + fn test_replace_krust_references() { + let yaml = r#"image: krust://./example/hello-krust"#; + + let mut replacements = HashMap::new(); + replacements.insert( + "./example/hello-krust".to_string(), + "registry.io/repo@sha256:abc123".to_string(), + ); + + let result = replace_krust_references(yaml, &replacements).unwrap(); + assert!(result.contains("registry.io/repo@sha256:abc123")); + assert!(!result.contains("krust://")); + } + + #[test] + fn test_multi_document_yaml() { + let yaml = r#" +image: krust://./app1 +--- +image: krust://./app2 +"#; + + let refs = find_krust_references(yaml).unwrap(); + assert_eq!(refs.len(), 2); + assert!(refs.contains("./app1")); + assert!(refs.contains("./app2")); + } +} diff --git a/tests/testdata/apply_basic.txt b/tests/testdata/apply_basic.txt new file mode 100644 index 0000000..0af4a73 --- /dev/null +++ b/tests/testdata/apply_basic.txt @@ -0,0 +1,32 @@ +# Test basic apply command (requires kubectl) + +[!exec:kubectl] skip 'kubectl not available' + +-- Cargo.toml -- +[package] +name = "test-app" +version = "0.1.0" +edition = "2021" + +[dependencies] + +-- src/main.rs -- +fn main() { + println!("Hello from test-app!"); +} + +-- deployment.yaml -- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config +data: + key: value + +# Apply should resolve and apply with kubectl +# Note: This test only verifies the command runs, not the actual k8s deployment +# since we don't have a test cluster +env KRUST_REPO=ttl.sh/test +! exec ./krust apply -f deployment.yaml +# kubectl will fail with connection error in test environment, but that's expected +stderr 'kubectl' diff --git a/tests/testdata/resolve_basic.txt b/tests/testdata/resolve_basic.txt new file mode 100644 index 0000000..c55db80 --- /dev/null +++ b/tests/testdata/resolve_basic.txt @@ -0,0 +1,32 @@ +# Test basic resolve with single YAML file and single krust:// reference + +-- Cargo.toml -- +[package] +name = "test-app" +version = "0.1.0" +edition = "2021" + +[dependencies] + +-- src/main.rs -- +fn main() { + println!("Hello from test-app!"); +} + +-- deployment.yaml -- +apiVersion: v1 +kind: Pod +metadata: + name: test-pod +spec: + containers: + - name: app + image: krust://. + +# Resolve should build and replace reference +env KRUST_REPO=ttl.sh/test +exec ./krust resolve -f deployment.yaml +stdout 'ttl.sh/test/test-app@sha256:' +! stdout 'krust://' +stderr 'Found 1 unique krust:// reference' +stderr 'Building image for: krust://.' diff --git a/tests/testdata/resolve_dedup.txt b/tests/testdata/resolve_dedup.txt new file mode 100644 index 0000000..31457f2 --- /dev/null +++ b/tests/testdata/resolve_dedup.txt @@ -0,0 +1,38 @@ +# Test that multiple references to the same path are deduplicated + +-- Cargo.toml -- +[package] +name = "test-app" +version = "0.1.0" +edition = "2021" + +[dependencies] + +-- src/main.rs -- +fn main() { + println!("Hello!"); +} + +-- deployment.yaml -- +apiVersion: v1 +kind: Pod +metadata: + name: test-pod +spec: + containers: + - name: app1 + image: krust://. + - name: app2 + image: krust://. + initContainers: + - name: init + image: krust://. + +# Should find only 1 unique reference and build once +env KRUST_REPO=ttl.sh/test +exec ./krust resolve -f deployment.yaml +stdout 'ttl.sh/test/test-app@sha256:' +stderr 'Found 1 unique krust:// reference' +! stderr 'Found 3' +# All three references should be replaced with same digest +stdout -count=3 'ttl.sh/test/test-app@sha256:' diff --git a/tests/testdata/resolve_directory.txt b/tests/testdata/resolve_directory.txt new file mode 100644 index 0000000..4a6ff7b --- /dev/null +++ b/tests/testdata/resolve_directory.txt @@ -0,0 +1,43 @@ +# Test resolving references in a directory of YAML files + +-- Cargo.toml -- +[package] +name = "test-app" +version = "0.1.0" +edition = "2021" + +[dependencies] + +-- src/main.rs -- +fn main() { + println!("Hello!"); +} + +-- k8s/deployment.yaml -- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app +spec: + template: + spec: + containers: + - image: krust://. + +-- k8s/service.yaml -- +apiVersion: v1 +kind: Service +metadata: + name: app-service +spec: + selector: + app: test + +# Resolve should process all YAML files in directory +env KRUST_REPO=ttl.sh/test +exec ./krust resolve -f k8s +stdout 'kind: Deployment' +stdout 'kind: Service' +stdout 'ttl.sh/test/test-app@sha256:' +! stdout 'krust://' +stderr 'Found 1 unique krust:// reference' diff --git a/tests/testdata/resolve_multi_document.txt b/tests/testdata/resolve_multi_document.txt new file mode 100644 index 0000000..a857746 --- /dev/null +++ b/tests/testdata/resolve_multi_document.txt @@ -0,0 +1,47 @@ +# Test YAML file with multiple documents separated by --- + +-- Cargo.toml -- +[package] +name = "test-app" +version = "0.1.0" +edition = "2021" + +[dependencies] + +-- src/main.rs -- +fn main() { + println!("Hello!"); +} + +-- manifests.yaml -- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config +data: + key: value +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod1 +spec: + containers: + - image: krust://. +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +spec: + containers: + - image: krust://. + +# Should handle multiple documents and preserve structure +env KRUST_REPO=ttl.sh/test +exec ./krust resolve -f manifests.yaml +stdout 'kind: ConfigMap' +stdout 'kind: Pod' +stdout -count=2 'ttl.sh/test/test-app@sha256:' +stdout -count=2 '---' +stderr 'Found 1 unique krust:// reference'