diff --git a/sources/Cargo.lock b/sources/Cargo.lock index c58ee6d2ea2..30160c2a0a8 100644 --- a/sources/Cargo.lock +++ b/sources/Cargo.lock @@ -3676,6 +3676,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "settings-extension-oci-defaults" +version = "0.1.0" +dependencies = [ + "bottlerocket-settings-sdk", + "env_logger", + "model-derive", + "modeled-types", + "serde", + "serde_json", + "toml", +] + [[package]] name = "settings-extension-oci-hooks" version = "0.1.0" diff --git a/sources/Cargo.toml b/sources/Cargo.toml index a31beb2e172..2adc968305a 100644 --- a/sources/Cargo.toml +++ b/sources/Cargo.toml @@ -79,6 +79,7 @@ members = [ "settings-extensions/motd", "settings-extensions/network", "settings-extensions/ntp", + "settings-extensions/oci-defaults", "settings-extensions/oci-hooks", "settings-extensions/pki", "settings-extensions/updates", diff --git a/sources/settings-extensions/oci-defaults/Cargo.toml b/sources/settings-extensions/oci-defaults/Cargo.toml new file mode 100644 index 00000000000..9324f4efceb --- /dev/null +++ b/sources/settings-extensions/oci-defaults/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "settings-extension-oci-defaults" +version = "0.1.0" +authors = ["Gaurav Sharma "] +license = "Apache-2.0 OR MIT" +edition = "2021" +publish = false + +[dependencies] +env_logger = "0.10" +modeled-types = { path = "../../models/modeled-types", version = "0.1" } +model-derive = { path = "../../models/model-derive", version = "0.1" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" + +[dependencies.bottlerocket-settings-sdk] +git = "https://github.com/bottlerocket-os/bottlerocket-settings-sdk" +tag = "bottlerocket-settings-sdk-v0.1.0-alpha.2" +version = "0.1.0-alpha" diff --git a/sources/settings-extensions/oci-defaults/oci-defaults.toml b/sources/settings-extensions/oci-defaults/oci-defaults.toml new file mode 100644 index 00000000000..727dfb274cd --- /dev/null +++ b/sources/settings-extensions/oci-defaults/oci-defaults.toml @@ -0,0 +1,13 @@ +[extension] +supported-versions = [ + "v1" +] +default-version = "v1" + +[v1] +[v1.validation.cross-validates] + +[v1.templating] +helpers = [] + +[v1.generation.requires] diff --git a/sources/settings-extensions/oci-defaults/src/de.rs b/sources/settings-extensions/oci-defaults/src/de.rs new file mode 100644 index 00000000000..7d4ec1c3ffb --- /dev/null +++ b/sources/settings-extensions/oci-defaults/src/de.rs @@ -0,0 +1,126 @@ +use serde::de::Error; +use serde::{Deserialize, Deserializer}; + +/// This specifies that any non negative i64 integer, -1, and "unlimited" +/// are the valid resource-limits. The hard-limit set to "unlimited" or -1 +/// and soft-limit set to "unlimited" or -1 are converted to u64::MAX in +/// the spec file for the container runtime which ultimately represents +/// unlimited for that resource +pub(crate) fn deserialize_limit<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrInt64 { + String(String), + Int(i64), + } + + match StringOrInt64::deserialize(deserializer)? { + StringOrInt64::String(s) => { + if s == "unlimited" { + Ok(-1) + } else { + Err(Error::custom(format!( + "Invalid rlimit {}, expected -1 to {} or \"unlimited\"", + s, + i64::MAX + ))) + } + } + StringOrInt64::Int(i) => { + if (-1..=i64::MAX).contains(&i) { + Ok(i) + } else { + Err(Error::custom(format!( + "Invalid rlimit {}, expected -1 to {} or \"unlimited\"", + i, + i64::MAX + ))) + } + } + } +} + +#[cfg(test)] +mod oci_default_resource_limit_tests { + use crate::OciDefaultsResourceLimitV1; + + #[test] + fn valid_any_integer_i_64() { + assert!(toml::from_str::( + r#" + hard-limit = 200000 + soft-limit = 10000 + "# + ) + .is_ok()); + } + + #[test] + fn valid_string_unlimited() { + assert!(toml::from_str::( + r#" + hard-limit = 'unlimited' + soft-limit = 10000 + "# + ) + .is_ok()); + } + + #[test] + fn valid_integer_i_64_max() { + assert!(toml::from_str::( + r#" + hard-limit = 9223372036854775807 + soft-limit = 10000 + "# + ) + .is_ok()); + } + + #[test] + fn valid_integer_minus_one() { + assert!(toml::from_str::( + r#" + hard-limit = -1 + soft-limit = 10000 + "# + ) + .is_ok()); + } + + #[test] + fn invalid_integer_greater_than_i_64_max() { + assert!(toml::from_str::( + r#" + hard-limit = 9223372036854775808 + soft-limit = 10000 + "# + ) + .is_err()); + } + + #[test] + fn invalid_minus_2() { + assert!(toml::from_str::( + r#" + hard-limit = -2 + soft-limit = 10000 + "# + ) + .is_err()); + } + + #[test] + fn invalid_string_abc() { + assert!(toml::from_str::( + r#" + hard-limit = 'abc' + soft-limit = 10000 + "# + ) + .is_err()); + } +} diff --git a/sources/settings-extensions/oci-defaults/src/lib.rs b/sources/settings-extensions/oci-defaults/src/lib.rs new file mode 100644 index 00000000000..2240b0cc4f7 --- /dev/null +++ b/sources/settings-extensions/oci-defaults/src/lib.rs @@ -0,0 +1,122 @@ +/// Settings related to orchestrated containers for overriding the OCI runtime spec defaults +mod de; + +use crate::de::deserialize_limit; +use bottlerocket_settings_sdk::{GenerateResult, SettingsModel}; +use model_derive::model; +use modeled_types::{OciDefaultsCapability, OciDefaultsResourceLimitType}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::convert::Infallible; + +///// OCI defaults specifies the default values that will be used in cri-base-json. +#[model(impl_default = true)] +struct OciDefaultsV1 { + capabilities: HashMap, + resource_limits: HashMap, +} + +///// The hard and soft limit values for an OCI defaults resource limit. +#[model(add_option = false)] +#[derive(Copy, Clone, Debug, Deserialize, Serialize, Eq, Ord, PartialOrd, PartialEq)] +struct OciDefaultsResourceLimitV1 { + #[serde(deserialize_with = "deserialize_limit")] + hard_limit: i64, + #[serde(deserialize_with = "deserialize_limit")] + soft_limit: i64, +} + +type Result = std::result::Result; + +impl SettingsModel for OciDefaultsV1 { + type PartialKind = Self; + type ErrorKind = Infallible; + + fn get_version() -> &'static str { + "v1" + } + + fn set(_current_value: Option, _target: Self) -> Result<()> { + // Set anything that can be parsed as OciDefaultsV1. + Ok(()) + } + + fn generate( + existing_partial: Option, + _dependent_settings: Option, + ) -> Result> { + Ok(GenerateResult::Complete( + existing_partial.unwrap_or_default(), + )) + } + + fn validate(_value: Self, _validated_settings: Option) -> Result<()> { + // OciDefaultsV1 is validated during deserialization. + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn test_generate_oci_defaults() { + assert_eq!( + OciDefaultsV1::generate(None, None), + Ok(GenerateResult::Complete(OciDefaultsV1 { + capabilities: None, + resource_limits: None, + })) + ) + } + + #[test] + fn test_serde_oci_defaults() { + let test_json = json!({ + "capabilities": { + "sys-admin": true, + "net-admin": false + }, + "resource-limits": { + "max-cpu-time": { + "hard-limit": 1000, + "soft-limit": 500 + } + } + }); + + let test_json_str = test_json.to_string(); + + let oci_defaults: OciDefaultsV1 = serde_json::from_str(&test_json_str).unwrap(); + + let mut expected_capabilities = HashMap::new(); + expected_capabilities.insert(OciDefaultsCapability::SysAdmin, true); + expected_capabilities.insert(OciDefaultsCapability::NetAdmin, false); + + let mut expected_resource_limits = HashMap::new(); + expected_resource_limits.insert( + OciDefaultsResourceLimitType::MaxCpuTime, + OciDefaultsResourceLimitV1 { + hard_limit: 1000, + soft_limit: 500, + }, + ); + + assert_eq!( + oci_defaults, + OciDefaultsV1 { + capabilities: Some(expected_capabilities), + resource_limits: Some(expected_resource_limits), + } + ); + + let serialized_json: serde_json::Value = serde_json::to_string(&oci_defaults) + .map(|s| serde_json::from_str(&s).unwrap()) + .unwrap(); + + assert_eq!(serialized_json, test_json); + } +} diff --git a/sources/settings-extensions/oci-defaults/src/main.rs b/sources/settings-extensions/oci-defaults/src/main.rs new file mode 100644 index 00000000000..f75ee51705c --- /dev/null +++ b/sources/settings-extensions/oci-defaults/src/main.rs @@ -0,0 +1,18 @@ +use bottlerocket_settings_sdk::{BottlerocketSetting, NullMigratorExtensionBuilder}; +use settings_extension_oci_defaults::OciDefaultsV1; +use std::process::ExitCode; + +fn main() -> ExitCode { + env_logger::init(); + + match NullMigratorExtensionBuilder::with_name("oci-defaults") + .with_models(vec![BottlerocketSetting::::model()]) + .build() + { + Ok(extension) => extension.run(), + Err(e) => { + println!("{}", e); + ExitCode::FAILURE + } + } +}