diff --git a/Cargo.lock b/Cargo.lock index d838a89..d8e67de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,6 +376,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -730,12 +731,6 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -1249,7 +1244,8 @@ dependencies = [ "lazy_static", "libc", "nom 8.0.0", - "oci-distribution", + "oci-client", + "oci-spec", "open", "openidconnect", "openssl", @@ -1258,7 +1254,7 @@ dependencies = [ "rand 0.9.2", "ratatui", "regex", - "reqwest", + "reqwest 0.12.24", "rstest", "rust_supervisor", "serde", @@ -1385,6 +1381,26 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -2442,7 +2458,7 @@ dependencies = [ "eyre", "oas3-gen-support", "regex", - "reqwest", + "reqwest 0.12.24", "serde", "serde_json", "serde_with", @@ -2693,6 +2709,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "gimli" version = "0.32.3" @@ -4403,10 +4431,10 @@ dependencies = [ "pkcs8 0.11.0-rc.8", "portmapper 0.11.0", "rand 0.9.2", - "reqwest", + "reqwest 0.12.24", "rustls 0.23.35", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.5.3", "rustls-webpki 0.103.7", "serde", "smallvec", @@ -4459,10 +4487,10 @@ dependencies = [ "pkcs8 0.11.0-rc.8", "portmapper 0.12.0", "rand 0.9.2", - "reqwest", + "reqwest 0.12.24", "rustls 0.23.35", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.5.3", "rustls-webpki 0.103.7", "serde", "smallvec", @@ -4654,7 +4682,7 @@ dependencies = [ "pkarr", "postcard", "rand 0.9.2", - "reqwest", + "reqwest 0.12.24", "rustls 0.23.35", "rustls-pki-types", "serde", @@ -4702,7 +4730,7 @@ dependencies = [ "pkarr", "postcard", "rand 0.9.2", - "reqwest", + "reqwest 0.12.24", "rustls 0.23.35", "rustls-pki-types", "serde", @@ -4908,18 +4936,18 @@ dependencies = [ ] [[package]] -name = "jwt" -version = "0.16.0" +name = "jsonwebtoken" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ - "base64 0.13.1", - "crypto-common 0.1.6", - "digest 0.10.7", - "hmac", + "aws-lc-rs", + "base64 0.22.1", + "getrandom 0.2.16", + "js-sys", "serde", "serde_json", - "sha2 0.10.9", + "signature 2.2.0", ] [[package]] @@ -5715,7 +5743,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "reqwest", + "reqwest 0.12.24", "serde", "serde_json", "serde_path_to_error", @@ -5738,7 +5766,7 @@ dependencies = [ "getrandom 0.2.16", "http 1.4.0", "rand 0.8.5", - "reqwest", + "reqwest 0.12.24", "serde", "serde_json", "serde_path_to_error", @@ -5776,30 +5804,48 @@ dependencies = [ ] [[package]] -name = "oci-distribution" -version = "0.11.0" +name = "oci-client" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95a2c51531af0cb93761f66094044ca6ea879320bccd35ab747ff3fcab3f422" +checksum = "407c69ed8de3ceeef4662d8fc2d830769b66f0b31ac23fdc9e9f59ac5f995f62" dependencies = [ "bytes", "chrono", "futures-util", "http 1.4.0", "http-auth", - "jwt", + "jsonwebtoken", "lazy_static", + "oci-spec", "olpc-cjson", "regex", - "reqwest", + "reqwest 0.13.1", "serde", "serde_json", "sha2 0.10.9", - "thiserror 1.0.69", + "thiserror 2.0.17", "tokio", "tracing", "unicase", ] +[[package]] +name = "oci-spec" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3da52b83ce3258fbf29f66ac784b279453c2ac3c22c5805371b921ede0d308" +dependencies = [ + "const_format", + "derive_builder", + "getset", + "regex", + "serde", + "serde_json", + "strum 0.27.2", + "strum_macros 0.27.2", + "thiserror 2.0.17", +] + [[package]] name = "olpc-cjson" version = "0.1.4" @@ -6307,7 +6353,7 @@ dependencies = [ "log", "lru 0.13.0", "ntimestamp", - "reqwest", + "reqwest 0.12.24", "self_cell", "serde", "sha1_smol", @@ -6651,6 +6697,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.3", "lru-slab", @@ -6905,6 +6952,47 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-rustls 0.27.7", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.35", + "rustls-pki-types", + "rustls-platform-verifier 0.6.2", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "resolv-conf" version = "0.7.6" @@ -6942,7 +7030,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -7190,6 +7278,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.35", + "rustls-native-certs 0.8.2", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.7", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs 1.0.4", + "windows-sys 0.61.1", +] + [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" @@ -7203,7 +7312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -7215,7 +7324,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -7301,7 +7410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -8537,9 +8646,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", @@ -8830,6 +8939,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/coman/.config/config.toml b/coman/.config/config.toml index 6f0fb76..098257d 100644 --- a/coman/.config/config.toml +++ b/coman/.config/config.toml @@ -23,7 +23,7 @@ sbatch_script_template = """ #!/bin/bash #SBATCH --job-name={{name}} #SBATCH --ntasks=1 -#SBATCH --time=1:00:00 +#SBATCH --time=24:00:00 srun {% if environment_file %}--environment={{environment_file}}{% endif %} {% if coman_squash %}/coman/coman exec {% endif %}{{command}} """ diff --git a/coman/Cargo.toml b/coman/Cargo.toml index efaeaad..dd21c42 100644 --- a/coman/Cargo.toml +++ b/coman/Cargo.toml @@ -66,7 +66,8 @@ tabled = { version = "0.20.0", features = ["macros"] } nom = "8.0.0" tera = "1.20.1" inquire = "0.9.1" -oci-distribution = "0.11.0" +oci-client = "0.16.0" +oci-spec = "0.8.4" docker_credential = "1.3.2" chrono = "0.4.42" openssl = { version = "0.10.75", features = ["vendored"] } diff --git a/coman/src/cscs/handlers.rs b/coman/src/cscs/handlers.rs index bd90313..8bbde55 100644 --- a/coman/src/cscs/handlers.rs +++ b/coman/src/cscs/handlers.rs @@ -48,7 +48,10 @@ use crate::{ start_cscs_device_login, }, }, - util::keyring::{Secret, get_secret, store_secret}, + util::{ + keyring::{Secret, get_secret, store_secret}, + types::{DockerImageMeta, DockerImageUrl}, + }, }; const CSCS_MAX_DIRECT_SIZE: usize = 5242880; @@ -623,6 +626,7 @@ async fn handle_edf( iroh_secret: &Option, workdir: &str, options: &JobStartOptions, + image_meta: &Option, ) -> Result { let config = Config::new().unwrap(); let environment_path = base_path.join("environment.toml"); @@ -642,47 +646,33 @@ async fn handle_edf( let mut context = tera::Context::new(); // check and validate image if set - let docker_image = if let Some(image) = options.image.clone() { - Some(image) - } else if let Some(image) = config.values.cscs.image { - let image = image.try_into()?; - Some(image) - } else { - None - }; - if let Some(docker_image) = docker_image { - match docker_image.inspect().await { - Ok(meta) => { - if let Some(system_info) = config.values.cscs.systems.get(current_system) { - let mut compatible = false; - for sys_platform in system_info.architecture.iter() { - if meta.platforms.contains(&sys_platform.clone().into()) { - compatible = true; - } - } - - if !compatible { - return Err(eyre!( - "System {} only supports images with architecture(s) '{}' but the supplied image is for architecture(s) '{}'", - current_system, - system_info.architecture.join(","), - meta.platforms - .iter() - .map(|p| p.to_string()) - .collect::>() - .join(",") - )); - } - } - } - Err(e) => { - println!("couldn't get image information, skipping checks: {e:?}"); - } + if let Some(meta) = image_meta { + if let Some(system_info) = config.values.cscs.systems.get(current_system) + && !meta + .platforms + .iter() + .any(|p| system_info.architecture.contains(&p.to_string())) + { + return Err(eyre!( + "System {} only supports images with architecture(s) '{}' but the supplied image is for architecture(s) '{}'", + current_system, + system_info.architecture.join(","), + meta.platforms + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(",") + )); } - - context.insert("edf_image", &docker_image.to_edf()); + } else { + println!("warn: no docker image metadata found, skipping validation"); + } + if let Some(image) = options.image.clone() { + context.insert("edf_image", &image.to_edf()); + } else if let Some(image) = config.values.cscs.image { + let image: DockerImageUrl = image.try_into()?; + context.insert("edf_image", &image.to_edf()); } - if !options.port_forward.is_empty() { let port_forward = options.port_forward.iter().map(|f| f.to_string()).join(","); context.insert("port_forward", &port_forward); @@ -721,6 +711,7 @@ async fn handle_script( coman_squash: Option, workdir: &str, options: &JobStartOptions, + image_meta: &Option, ) -> Result { let config = Config::new().unwrap(); let script_path = base_path.join("script.sh"); @@ -734,10 +725,22 @@ async fn handle_script( tera.add_raw_template("script.sh", &script_template)?; let mut context = tera::Context::new(); context.insert("name", &job_name); - context.insert( - "command", - &options.command.clone().unwrap_or(config.values.cscs.command).join(" "), - ); + let command = match &options.command { + Some(cmd) => cmd.clone(), + None => { + if !config.values.cscs.command.is_empty() { + config.values.cscs.command.clone() + } else { + // use default entrypoint + if let Some(meta) = image_meta { + meta.clone().entrypoint.unwrap_or_default() + } else { + vec![] + } + } + } + }; + context.insert("command", &command.join(" ")); context.insert("environment_file", &environment_path.to_path_buf()); context.insert("container_workdir", &workdir); if let Some(path) = coman_squash { @@ -807,6 +810,27 @@ pub async fn cscs_job_start( if coman_squash.is_none() { println!("Warning: coman squash wasn't templated and is needed for ssh through coman to work"); } + // check and validate image if set + let docker_image = if let Some(image) = options.image.clone() { + Some(image) + } else if let Some(image) = config.values.cscs.image { + let image = image.try_into()?; + Some(image) + } else { + None + }; + let image_meta = if let Some(docker_image) = docker_image { + match docker_image.inspect().await { + Ok(meta) => Some(meta), + Err(e) => { + println!("couldn't get image information: {e:?}"); + None + } + } + } else { + None + }; + let environment_path = handle_edf( &api_client, &base_path, @@ -817,6 +841,7 @@ pub async fn cscs_job_start( &secret_key, &container_workdir, &options, + &image_meta, ) .await?; @@ -829,6 +854,7 @@ pub async fn cscs_job_start( coman_squash, &container_workdir, &options, + &image_meta, ) .await?; diff --git a/coman/src/util/types.rs b/coman/src/util/types.rs index 2f58517..ede336f 100644 --- a/coman/src/util/types.rs +++ b/coman/src/util/types.rs @@ -12,12 +12,14 @@ use nom::{ multi::{many_m_n, many1, separated_list0, separated_list1}, sequence::{preceded, separated_pair, terminated}, }; -use oci_distribution::{ +use oci_client::{ Client, Reference, client::{ClientConfig, ClientProtocol}, + config::ConfigFile, manifest::OciManifest, secrets::RegistryAuth, }; +use oci_spec::image::Arch; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash, strum::Display)] pub enum OciPlatform { @@ -38,9 +40,21 @@ impl From for OciPlatform { } } +impl From for OciPlatform { + fn from(value: Arch) -> Self { + match value { + Arch::ARM64 => Self::arm64, + Arch::Amd64 => Self::amd64, + _ => Self::Other, + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd)] pub struct DockerImageMeta { pub platforms: Vec, + pub entrypoint: Option>, + pub working_dir: Option, } #[derive(Debug, Clone, PartialEq, PartialOrd)] pub struct DockerImageUrl { @@ -76,25 +90,33 @@ impl DockerImageUrl { .pull_manifest(&reference, &auth) .await .wrap_err(format!("Couldn't get image manifest for image {reference}"))?; + let (_, _, config) = client + .pull_manifest_and_config(&reference, &auth) + .await + .wrap_err(format!("Couldn't get image config for image {reference}"))?; + + let config: ConfigFile = serde_json::from_str(&config).unwrap(); match manifest { - OciManifest::Image(oci_image_manifest) => { - // it's not clear what is returned in this case, I never hit this in my testing. - // leaving the dbg statement so if a user ever hits this, we can ask for logs and figure it out. - let _ = dbg!(oci_image_manifest); - Err(eyre!( - "didn't get image index for image, plain manifest does not contain platform data" - )) + OciManifest::Image(_) => { + //Image does not contain platform, read it from config instead (no multi-arch image) + Ok(DockerImageMeta { + platforms: vec![config.clone().architecture.into()], + entrypoint: config.clone().config.and_then(|c| c.entrypoint), + working_dir: config.config.and_then(|c| c.working_dir), + }) } OciManifest::ImageIndex(oci_image_index) => { let mut platforms: HashSet = HashSet::new(); - platforms.extend( - oci_image_index - .manifests - .into_iter() - .map(|m| m.platform.map(|p| p.architecture).unwrap_or("".to_owned()).into()), - ); + platforms.extend(oci_image_index.manifests.into_iter().map(|m| { + m.platform + .map(|p| p.architecture) + .unwrap_or(Arch::Other("".to_owned())) + .into() + })); Ok(DockerImageMeta { platforms: platforms.into_iter().collect(), + entrypoint: config.clone().config.and_then(|c| c.entrypoint), + working_dir: config.config.and_then(|c| c.working_dir), }) } }