diff --git a/example/hello-krust/src/main.rs b/example/hello-krust/src/main.rs index 24f646a..b79e197 100644 --- a/example/hello-krust/src/main.rs +++ b/example/hello-krust/src/main.rs @@ -1,4 +1,17 @@ fn main() { println!("Hello from krust example!"); - println!("This is running inside a container built with krust."); + println!("Current architecture: {}", std::env::consts::ARCH); + + // Check for SSL_CERT_FILE environment variable (common in distroless images) + match std::env::var("SSL_CERT_FILE") { + Ok(value) => println!("✓ SSL_CERT_FILE found: {}", value), + Err(_) => panic!("✗ SSL_CERT_FILE not found (base image env not preserved)"), + } + + // Check for /etc/os-release file (common in most Linux base images) + if std::path::Path::new("/etc/os-release").exists() { + println!("✓ /etc/os-release found (base image layers preserved)"); + } else { + panic!("✗ /etc/os-release not found (base image layers not preserved)"); + } } diff --git a/src/image/mod.rs b/src/image/mod.rs index ce3221c..de8ee91 100644 --- a/src/image/mod.rs +++ b/src/image/mod.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use flate2::write::GzEncoder; use flate2::Compression; +use oci_distribution::secrets::RegistryAuth; use serde::{Deserialize, Serialize}; use sha256::digest; use std::fs::File; @@ -8,27 +9,25 @@ use std::io::Write; use tar::Builder; use tracing::{debug, info}; -#[cfg(test)] -mod tests; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImageConfig { pub architecture: String, pub os: String, pub config: Config, pub rootfs: RootFs, + #[serde(default)] pub history: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { - #[serde(rename = "Env")] + #[serde(rename = "Env", default)] pub env: Vec, #[serde(rename = "Cmd")] pub cmd: Option>, - #[serde(rename = "WorkingDir")] + #[serde(rename = "WorkingDir", default)] pub working_dir: String, - #[serde(rename = "User")] + #[serde(rename = "User", default)] pub user: String, } @@ -43,7 +42,9 @@ pub struct RootFs { pub struct History { pub created: String, pub created_by: String, + #[serde(default)] pub comment: String, + #[serde(default)] pub empty_layer: bool, } @@ -83,18 +84,49 @@ impl ImageBuilder { } } - pub fn build(&self) -> Result<(Vec, Vec, Manifest)> { + pub async fn build( + &self, + registry_client: &mut crate::registry::RegistryClient, + auth: &RegistryAuth, + ) -> Result<(Vec, Vec, Manifest)> { info!("Building container image"); - let (os, arch) = self.parse_platform()?; + let (_os, _arch) = self.parse_platform()?; + + // Fetch base image data + info!( + "Fetching base image: {} for platform: {}", + self.base_image, self.platform + ); + let (base_manifest, base_config) = registry_client + .fetch_image_data(&self.base_image, &self.platform, auth) + .await + .context("Failed to fetch base image data")?; // Create application layer - let (layer_data, diff_id) = self.create_layer()?; - let layer_digest = format!("sha256:{}", digest(&layer_data)); - let layer_size = layer_data.len() as i64; + let (app_layer_data, app_diff_id) = self.create_layer()?; + let app_layer_digest = format!("sha256:{}", digest(&app_layer_data)); + let app_layer_size = app_layer_data.len() as i64; - // Create image config - let config = self.create_config(&os, &arch, &diff_id)?; + // Combine base image layers with application layer + let mut all_layers = Vec::new(); + for layer in &base_manifest.layers { + all_layers.push(Descriptor { + media_type: layer.media_type.clone(), + size: layer.size, + digest: layer.digest.clone(), + }); + } + + // Add the application layer + all_layers.push(Descriptor { + media_type: "application/vnd.docker.image.rootfs.diff.tar.gzip".to_string(), + size: app_layer_size, + digest: app_layer_digest, + }); + + // Create merged config + let config = self.create_layered_config(&base_config, &app_diff_id)?; let config_data = serde_json::to_vec_pretty(&config)?; let config_digest = format!("sha256:{}", digest(&config_data)); let config_size = config_data.len() as i64; @@ -108,14 +140,10 @@ impl ImageBuilder { size: config_size, digest: config_digest, }, - layers: vec![Descriptor { - media_type: "application/vnd.docker.image.rootfs.diff.tar.gzip".to_string(), - size: layer_size, - digest: layer_digest, - }], + layers: all_layers, }; - Ok((config_data, layer_data, manifest)) + Ok((config_data, app_layer_data, manifest)) } fn parse_platform(&self) -> Result<(String, String)> { @@ -163,7 +191,11 @@ impl ImageBuilder { Ok((compressed, diff_id)) } - fn create_config(&self, os: &str, arch: &str, layer_digest: &str) -> Result { + fn create_layered_config( + &self, + base_config: &ImageConfig, + app_diff_id: &str, + ) -> Result { let binary_name = self .binary_path .file_name() @@ -171,27 +203,225 @@ impl ImageBuilder { .to_str() .context("Invalid UTF-8 in binary name")?; + // Merge environment variables (preserve base + add our own) + let mut merged_env = base_config.config.env.clone(); + + // Add PATH if not present + if !merged_env.iter().any(|env| env.starts_with("PATH=")) { + merged_env.push( + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(), + ); + } + + // Combine diff_ids (base layers + app layer) + let mut merged_diff_ids = base_config.rootfs.diff_ids.clone(); + merged_diff_ids.push(app_diff_id.to_string()); + + // Combine history (base history + app history) + let mut merged_history = base_config.history.clone(); + merged_history.push(History { + created: chrono::Utc::now().to_rfc3339(), + created_by: "krust".to_string(), + comment: "Built with krust".to_string(), + empty_layer: false, + }); + Ok(ImageConfig { - architecture: arch.to_string(), - os: os.to_string(), + architecture: base_config.architecture.clone(), + os: base_config.os.clone(), config: Config { - env: vec![ - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(), - ], + env: merged_env, cmd: Some(vec![format!("/app/{}", binary_name)]), - working_dir: "/".to_string(), - user: "65532:65532".to_string(), // nonroot user + working_dir: base_config.config.working_dir.clone(), + user: base_config.config.user.clone(), }, rootfs: RootFs { fs_type: "layers".to_string(), - diff_ids: vec![layer_digest.to_string()], + diff_ids: merged_diff_ids, }, - history: vec![History { - created: chrono::Utc::now().to_rfc3339(), - created_by: "krust".to_string(), - comment: "Built with krust".to_string(), - empty_layer: false, - }], + history: merged_history, }) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use std::path::PathBuf; + use tempfile::NamedTempFile; + + fn create_test_binary() -> PathBuf { + let mut temp_file = NamedTempFile::new().unwrap(); + temp_file.write_all(b"fake binary content").unwrap(); + let path = temp_file.path().to_path_buf(); + // Keep the file alive by converting to a regular file + std::fs::copy(&path, &path).unwrap(); + path + } + + fn create_base_image_config() -> ImageConfig { + ImageConfig { + architecture: "amd64".to_string(), + os: "linux".to_string(), + config: Config { + env: vec![ + "PATH=/usr/local/bin:/usr/bin:/bin".to_string(), + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt".to_string(), + ], + cmd: None, + working_dir: "/".to_string(), + user: "nonroot:nonroot".to_string(), + }, + rootfs: RootFs { + fs_type: "layers".to_string(), + diff_ids: vec![ + "sha256:base_layer_1".to_string(), + "sha256:base_layer_2".to_string(), + ], + }, + history: vec![ + History { + created: "2023-01-01T00:00:00Z".to_string(), + created_by: "base-image-builder".to_string(), + comment: "Base layer 1".to_string(), + empty_layer: false, + }, + History { + created: "2023-01-01T00:01:00Z".to_string(), + created_by: "base-image-builder".to_string(), + comment: "Base layer 2".to_string(), + empty_layer: false, + }, + ], + } + } + + #[test] + fn test_parse_platform() { + let builder = ImageBuilder::new( + PathBuf::from("/tmp/test"), + "test-base".to_string(), + "linux/amd64".to_string(), + ); + + let (os, arch) = builder.parse_platform().unwrap(); + assert_eq!(os, "linux"); + assert_eq!(arch, "amd64"); + } + + #[test] + fn test_create_layered_config_preserves_base_environment() { + let binary_path = create_test_binary(); + let builder = ImageBuilder::new( + binary_path, + "test-base".to_string(), + "linux/amd64".to_string(), + ); + + let base_config = create_base_image_config(); + let app_diff_id = "sha256:app_layer_diff_id"; + + let result = builder + .create_layered_config(&base_config, app_diff_id) + .unwrap(); + + // Check that base environment variables are preserved + assert!(result + .config + .env + .contains(&"SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt".to_string())); + assert!(result.config.env.iter().any(|env| env.starts_with("PATH="))); + + // Check that working directory is preserved + assert_eq!(result.config.working_dir, "/"); + + // Check that user is preserved + assert_eq!(result.config.user, "nonroot:nonroot"); + + // Check that architecture and OS are preserved + assert_eq!(result.architecture, "amd64"); + assert_eq!(result.os, "linux"); + } + + #[test] + fn test_create_layered_config_combines_diff_ids() { + let binary_path = create_test_binary(); + let builder = ImageBuilder::new( + binary_path, + "test-base".to_string(), + "linux/amd64".to_string(), + ); + + let base_config = create_base_image_config(); + let app_diff_id = "sha256:app_layer_diff_id"; + + let result = builder + .create_layered_config(&base_config, app_diff_id) + .unwrap(); + + // Check that base diff_ids are preserved and app diff_id is appended + assert_eq!(result.rootfs.diff_ids.len(), 3); + assert_eq!(result.rootfs.diff_ids[0], "sha256:base_layer_1"); + assert_eq!(result.rootfs.diff_ids[1], "sha256:base_layer_2"); + assert_eq!(result.rootfs.diff_ids[2], "sha256:app_layer_diff_id"); + } + + #[test] + fn test_create_layered_config_combines_history() { + let binary_path = create_test_binary(); + let builder = ImageBuilder::new( + binary_path, + "test-base".to_string(), + "linux/amd64".to_string(), + ); + + let base_config = create_base_image_config(); + let app_diff_id = "sha256:app_layer_diff_id"; + + let result = builder + .create_layered_config(&base_config, app_diff_id) + .unwrap(); + + // Check that base history is preserved and app history is appended + assert_eq!(result.history.len(), 3); + assert_eq!(result.history[0].created_by, "base-image-builder"); + assert_eq!(result.history[1].created_by, "base-image-builder"); + assert_eq!(result.history[2].created_by, "krust"); + assert_eq!(result.history[2].comment, "Built with krust"); + assert!(!result.history[2].empty_layer); + } + + #[test] + fn test_image_config_serialization_compatibility() { + // Test that our ImageConfig can deserialize from a realistic base image config + let json_config = r#"{ + "architecture": "amd64", + "os": "linux", + "config": { + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt" + ], + "WorkingDir": "/", + "User": "nonroot:nonroot" + }, + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:b49b96bfa4b2477b73b0b8fe5e4ce383aa1d0026c1e845fb5d43c1f0b8bdb6ac" + ] + } + }"#; + + let parsed: ImageConfig = serde_json::from_str(json_config).unwrap(); + + assert_eq!(parsed.architecture, "amd64"); + assert_eq!(parsed.os, "linux"); + assert_eq!(parsed.config.env.len(), 2); + assert_eq!(parsed.config.working_dir, "/"); + assert_eq!(parsed.config.user, "nonroot:nonroot"); + assert_eq!(parsed.rootfs.diff_ids.len(), 1); + assert_eq!(parsed.history.len(), 0); // Default empty for missing field + } +} diff --git a/src/image/tests.rs b/src/image/tests.rs deleted file mode 100644 index b029471..0000000 --- a/src/image/tests.rs +++ /dev/null @@ -1,29 +0,0 @@ -#[cfg(test)] -mod tests { - use super::super::*; - use std::path::PathBuf; - - #[test] - fn test_parse_platform() { - let builder = ImageBuilder::new( - PathBuf::from("/tmp/test"), - "test-base".to_string(), - "linux/amd64".to_string(), - ); - - let (os, arch) = builder.parse_platform().unwrap(); - assert_eq!(os, "linux"); - assert_eq!(arch, "amd64"); - } - - #[test] - fn test_parse_platform_invalid() { - let builder = ImageBuilder::new( - PathBuf::from("/tmp/test"), - "test-base".to_string(), - "invalid-platform".to_string(), - ); - - assert!(builder.parse_platform().is_err()); - } -} diff --git a/src/main.rs b/src/main.rs index aac2ba6..1cacb67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,20 +120,40 @@ async fn main() -> Result<()> { platform_str.clone(), ); - let (config_data, layer_data, manifest) = image_builder.build()?; + // Always use layered approach - registry layer will handle cross-registry blob copying + let base_auth = resolve_auth(&base_image)?; + let (config_data, layer_data, manifest) = image_builder + .build(&mut registry_client, &base_auth) + .await?; // Push platform-specific image if not --no-push if !no_push { info!("Pushing image for platform: {}", platform_str); - let layers = vec![(layer_data, manifest.layers[0].media_type.clone())]; - // Get auth for the target registry let push_auth = resolve_auth(&target_repo)?; - // Push platform image by digest only (no tags) + // Get the media type of the application layer (last layer in manifest) + 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() + }); + + // Push layered image (copy base layers if needed + push app layer + manifest) let (digest_ref, manifest_size) = registry_client - .push_image_by_digest(&target_repo, config_data, layers, &push_auth) + .push_layered_image( + &target_repo, + config_data, + layer_data, + app_layer_media_type, + &manifest, + &push_auth, + &base_image, + &base_auth, + ) .await?; // Parse platform string diff --git a/src/registry/mod.rs b/src/registry/mod.rs index ef492fa..2405211 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -5,9 +5,6 @@ use oci_distribution::{Client, Reference}; use std::str::FromStr; use tracing::{debug, info}; -#[cfg(test)] -mod tests; - pub struct RegistryClient { client: Client, } @@ -269,6 +266,244 @@ impl RegistryClient { Ok(image_ref) } + /// Push a layered image where only the top layer is new + #[allow(clippy::too_many_arguments)] + pub async fn push_layered_image( + &mut self, + repository: &str, + config_data: Vec, + new_layer_data: Vec, + _new_layer_media_type: String, + manifest: &crate::image::Manifest, + auth: &RegistryAuth, + base_image_ref: &str, + base_auth: &RegistryAuth, + ) -> Result<(String, usize)> { + // Create a temporary reference for authentication + let temp_ref = format!("{}:temp", repository); + let reference: Reference = temp_ref + .parse() + .context("Failed to parse repository reference")?; + + info!("Pushing layered image to {} (digest only)", repository); + + // Authenticate with the registry + self.client + .auth(&reference, auth, oci_distribution::RegistryOperation::Push) + .await + .context("Failed to authenticate with registry")?; + + // Push config blob + let config_digest = format!("sha256:{}", sha256::digest(&config_data)); + debug!("Pushing config blob: {}", config_digest); + + self.client + .push_blob(&reference, &config_data, &config_digest) + .await + .context("Failed to push config blob")?; + + // Copy base image layers if they don't exist in target registry + let base_reference: Reference = base_image_ref + .parse() + .context("Failed to parse base image reference")?; + + // Check if we need to copy base layers (cross-registry scenario) + let base_registry = base_reference.registry(); + let target_registry = reference.registry(); + let need_copy_layers = base_registry != target_registry; + + if need_copy_layers { + info!( + "Copying base image layers from {} to {}", + base_registry, target_registry + ); + + // Authenticate with base image registry + let base_client = Client::new(oci_distribution::client::ClientConfig::default()); + base_client + .auth( + &base_reference, + base_auth, + oci_distribution::RegistryOperation::Pull, + ) + .await + .context("Failed to authenticate with base registry")?; + + // Copy each base layer (all except the last one which is our app layer) + for layer in &manifest.layers[..manifest.layers.len().saturating_sub(1)] { + debug!("Copying base layer: {}", layer.digest); + + // Pull the layer from base registry + let mut layer_data = Vec::new(); + let layer_descriptor = oci_distribution::manifest::OciDescriptor { + media_type: layer.media_type.clone(), + digest: layer.digest.clone(), + size: layer.size, + urls: None, + annotations: None, + }; + + base_client + .pull_blob(&base_reference, &layer_descriptor, &mut layer_data) + .await + .context("Failed to pull base layer")?; + + // Push the layer to target registry + self.client + .push_blob(&reference, &layer_data, &layer.digest) + .await + .context("Failed to push copied base layer")?; + } + } + + // Push the new application layer + let new_layer_digest = format!("sha256:{}", sha256::digest(&new_layer_data)); + debug!("Pushing new application layer: {}", new_layer_digest); + + self.client + .push_blob(&reference, &new_layer_data, &new_layer_digest) + .await + .context("Failed to push new layer")?; + + // Create manifest with all layers (base + new) + let mut manifest_layers = Vec::new(); + for layer in &manifest.layers { + manifest_layers.push(oci_distribution::manifest::OciDescriptor { + media_type: layer.media_type.clone(), + digest: layer.digest.clone(), + size: layer.size, + urls: None, + annotations: None, + }); + } + + // Create and push manifest + let image_manifest = oci_distribution::manifest::OciImageManifest { + schema_version: 2, + media_type: Some("application/vnd.oci.image.manifest.v1+json".to_string()), + artifact_type: None, + config: oci_distribution::manifest::OciDescriptor { + media_type: "application/vnd.oci.image.config.v1+json".to_string(), + digest: config_digest, + size: config_data.len() as i64, + urls: None, + annotations: None, + }, + layers: manifest_layers, + annotations: None, + }; + + // Wrap in OciManifest enum + let oci_manifest = oci_distribution::manifest::OciManifest::Image(image_manifest); + + debug!("Pushing manifest for layered image"); + let (manifest_url, digest) = self + .client + .push_manifest_and_get_digest(&reference, &oci_manifest) + .await + .context("Failed to push manifest")?; + + info!( + "Successfully pushed layered image to {} (digest: {})", + manifest_url, digest + ); + + // Build the full image reference with digest only + let registry = reference.registry(); + let repository = reference.repository(); + let digest_ref = format!("{}/{}@{}", registry, repository, digest); + + // Return both the digest ref and the actual manifest size + let manifest_size = serde_json::to_vec(&oci_manifest)?.len(); + Ok((digest_ref, manifest_size)) + } + + /// Fetch the manifest and config for a specific platform of an image + pub async fn fetch_image_data( + &mut self, + image_ref: &str, + platform: &str, + auth: &RegistryAuth, + ) -> Result<( + oci_distribution::manifest::OciImageManifest, + crate::image::ImageConfig, + )> { + let reference: Reference = image_ref + .parse() + .context("Failed to parse image reference")?; + + debug!( + "Fetching image data for {} platform {}", + reference, platform + ); + + // Authenticate with the registry + self.client + .auth(&reference, auth, oci_distribution::RegistryOperation::Pull) + .await + .context("Failed to authenticate with registry")?; + + // Pull the manifest for the specific platform + let (manifest, _digest) = self + .client + .pull_manifest(&reference, auth) + .await + .context("Failed to pull manifest")?; + + // Handle different manifest types + let image_manifest = match manifest { + oci_distribution::manifest::OciManifest::Image(img_manifest) => img_manifest, + oci_distribution::manifest::OciManifest::ImageIndex(index) => { + // Find the manifest for the requested platform + let (os, arch) = platform + .split_once('/') + .ok_or_else(|| anyhow::anyhow!("Invalid platform format: {}", platform))?; + + let platform_manifest = index + .manifests + .iter() + .find(|m| { + m.platform + .as_ref() + .is_some_and(|p| p.os == os && p.architecture == arch) + }) + .ok_or_else(|| { + anyhow::anyhow!("Platform {} not found in image index", platform) + })?; + + // Pull the platform-specific manifest + let platform_ref = format!("{}@{}", image_ref, platform_manifest.digest); + let platform_reference: Reference = platform_ref + .parse() + .context("Failed to parse platform reference")?; + + let (platform_manifest, _) = self + .client + .pull_manifest(&platform_reference, auth) + .await + .context("Failed to pull platform manifest")?; + + match platform_manifest { + oci_distribution::manifest::OciManifest::Image(img_manifest) => img_manifest, + _ => anyhow::bail!("Expected image manifest, got index"), + } + } + }; + + // Pull the config blob + let mut config_data = Vec::new(); + self.client + .pull_blob(&reference, &image_manifest.config, &mut config_data) + .await + .context("Failed to pull config blob")?; + + // Parse the config + let config: crate::image::ImageConfig = + serde_json::from_slice(&config_data).context("Failed to parse image config")?; + + Ok((image_manifest, config)) + } + /// Fetch the manifest for an image and extract available platforms pub async fn get_image_platforms( &mut self, @@ -341,3 +576,91 @@ pub fn parse_image_reference(image: &str) -> Result<(String, String, String)> { Ok((registry, repository, tag)) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::image::{Config, History, ImageConfig, RootFs}; + + #[test] + fn test_parse_image_reference() { + let (registry, repo, tag) = + parse_image_reference("docker.io/library/hello-world:latest").unwrap(); + assert_eq!(registry, "docker.io"); + assert_eq!(repo, "library/hello-world"); + assert_eq!(tag, "latest"); + } + + #[test] + fn test_parse_image_reference_no_tag() { + let (_, _, tag) = parse_image_reference("docker.io/library/hello-world").unwrap(); + assert_eq!(tag, "latest"); + } + + #[test] + fn test_layered_config_structure() { + // Test layered config creation + let config = ImageConfig { + architecture: "amd64".to_string(), + os: "linux".to_string(), + config: Config { + env: vec![ + "PATH=/usr/local/bin:/usr/bin:/bin".to_string(), + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt".to_string(), + ], + cmd: Some(vec!["/app/test-binary".to_string()]), + working_dir: "/".to_string(), + user: "nonroot:nonroot".to_string(), + }, + rootfs: RootFs { + fs_type: "layers".to_string(), + diff_ids: vec![ + "sha256:base_layer_1".to_string(), + "sha256:base_layer_2".to_string(), + "sha256:app_layer".to_string(), + ], + }, + history: vec![ + History { + created: "2023-01-01T00:00:00Z".to_string(), + created_by: "base-image-builder".to_string(), + comment: "Base layer 1".to_string(), + empty_layer: false, + }, + History { + created: "2023-01-01T00:01:00Z".to_string(), + created_by: "base-image-builder".to_string(), + comment: "Base layer 2".to_string(), + empty_layer: false, + }, + History { + created: "2023-01-01T00:02:00Z".to_string(), + created_by: "krust".to_string(), + comment: "Built with krust".to_string(), + empty_layer: false, + }, + ], + }; + + // Test that layered configs preserve base image properties + assert_eq!(config.rootfs.diff_ids.len(), 3); + assert_eq!(config.history.len(), 3); + + // Verify diff_ids match expected order + assert_eq!(config.rootfs.diff_ids[0], "sha256:base_layer_1"); + assert_eq!(config.rootfs.diff_ids[1], "sha256:base_layer_2"); + assert_eq!(config.rootfs.diff_ids[2], "sha256:app_layer"); + + // Verify history is preserved and extended + assert_eq!(config.history[0].created_by, "base-image-builder"); + assert_eq!(config.history[1].created_by, "base-image-builder"); + assert_eq!(config.history[2].created_by, "krust"); + + // Verify base image environment is preserved + assert!(config + .config + .env + .contains(&"SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt".to_string())); + assert!(config.config.env.iter().any(|env| env.starts_with("PATH="))); + } +} diff --git a/src/registry/tests.rs b/src/registry/tests.rs deleted file mode 100644 index 2a93475..0000000 --- a/src/registry/tests.rs +++ /dev/null @@ -1,27 +0,0 @@ -#[cfg(test)] -mod tests { - use super::super::*; - - #[test] - fn test_parse_image_reference() { - let (registry, repo, tag) = - parse_image_reference("docker.io/library/hello-world:latest").unwrap(); - assert_eq!(registry, "docker.io"); - assert_eq!(repo, "library/hello-world"); - assert_eq!(tag, "latest"); - } - - #[test] - fn test_parse_image_reference_no_tag() { - let (_, _, tag) = parse_image_reference("docker.io/library/hello-world").unwrap(); - assert_eq!(tag, "latest"); - } - - #[test] - fn test_parse_image_reference_with_port() { - let (registry, repo, tag) = parse_image_reference("localhost:5000/myapp:v1.0").unwrap(); - assert_eq!(registry, "localhost:5000"); - assert_eq!(repo, "myapp"); - assert_eq!(tag, "v1.0"); - } -}