diff --git a/Cargo.lock b/Cargo.lock index 32e24b9..f827457 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,45 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -368,6 +407,26 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.5" @@ -1240,6 +1299,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.1.0" @@ -1251,6 +1316,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1260,12 +1335,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1284,6 +1378,15 @@ dependencies = [ "libc", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1312,6 +1415,7 @@ dependencies = [ "futures", "k8s-openapi", "kube", + "rcgen", "rustls", "rustls-pemfile", "schemars", @@ -1321,6 +1425,7 @@ dependencies = [ "shadow-rs", "snafu", "strum", + "time", "tokio", "tracing", "tracing-subscriber", @@ -1500,6 +1605,20 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rcgen" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1572,6 +1691,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustls" version = "0.23.35" @@ -2626,6 +2754,33 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x509-parser" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index e45b4c7..680c771 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ rustls = { version = "0.23", default-features = false, features = ["ring"] } rustls-pemfile = "2.2.0" shadow-rs = "1.5.0" snafu = { version = "0.8.9", features = ["futures"] } +rcgen = "0.14.6" +time = "0.3.44" [dev-dependencies] @@ -37,4 +39,4 @@ unused_variables = "allow" [lints.clippy] unwrap_used = "deny" -expect_used = "deny" \ No newline at end of file +expect_used = "deny" diff --git a/src/reconcile.rs b/src/reconcile.rs index 77a00e6..da7bb2a 100644 --- a/src/reconcile.rs +++ b/src/reconcile.rs @@ -32,6 +32,9 @@ pub enum Error { #[snafu(transparent)] Types { source: types::error::Error }, + + #[snafu(transparent)] + Tls { source: crate::utils::tls::Error }, } pub async fn reconcile_rustfs(tenant: Arc, ctx: Arc) -> Result { @@ -104,6 +107,60 @@ pub async fn reconcile_rustfs(tenant: Arc, ctx: Arc) -> Result< } } + // 1.5. Managed TLS: Generate and inject certificate if requested + if latest_tenant.spec.request_auto_cert.unwrap_or(false) { + let secret_name = format!("{}-tls", latest_tenant.name()); + // Try to get existing secret + if ctx.get::(&secret_name, &ns).await.is_err() { + debug!( + "TLS secret {} not found, generating self-signed certificate", + secret_name + ); + + let cn = format!("{}.{}.svc", latest_tenant.name(), ns); + let sans = vec![ + cn.clone(), + format!("*.{}", cn), + format!("{}.{}.svc.cluster.local", latest_tenant.name(), ns), + ]; + + let (cert_pem, key_pem) = crate::utils::tls::generate_self_signed_cert(&cn, sans)?; + + let mut data: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + use k8s_openapi::ByteString; + data.insert("tls.crt".to_string(), ByteString(cert_pem.into_bytes())); + data.insert("tls.key".to_string(), ByteString(key_pem.into_bytes())); + + let secret = corev1::Secret { + metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { + name: Some(secret_name.clone()), + namespace: Some(ns.clone()), + owner_references: Some(vec![latest_tenant.new_owner_ref()]), + labels: Some(latest_tenant.common_labels()), + ..Default::default() + }, + type_: Some("kubernetes.io/tls".to_string()), + data: Some(data), + ..Default::default() + }; + + ctx.create(&secret, &ns).await?; + + let _ = ctx + .record( + &latest_tenant, + EventType::Normal, + "TLSCertificateGenerated", + &format!( + "Generated self-signed certificate in secret {}", + secret_name + ), + ) + .await; + } + } + // 2. Create Services ctx.apply(&latest_tenant.new_io_service(), &ns).await?; ctx.apply(&latest_tenant.new_console_service(), &ns).await?; @@ -164,6 +221,13 @@ pub async fn reconcile_rustfs(tenant: Arc, ctx: Arc) -> Result< let mut ready_replicas = 0; for pool in &latest_tenant.spec.pools { + // Create or update PDB for the pool + ctx.apply(&latest_tenant.new_pdb(pool)?, &ns).await?; + + // Resize PVCs if needed (Storage Expansion) + // This handles cases where statefulset.volumeClaimTemplates are immutable but PVCs can be resized. + resize_pool_pvcs(&latest_tenant, pool, &ns, &ctx).await?; + let ss_name = format!("{}-{}", latest_tenant.name(), pool.name); // Try to get existing StatefulSet @@ -584,7 +648,105 @@ pub fn error_policy(_object: Arc, error: &Error, _ctx: Arc) -> // Other type errors - use moderate requeue _ => Action::requeue(Duration::from_secs(15)), }, + + Error::Tls { .. } => Action::requeue(Duration::from_secs(60)), + } +} + +/// Patches existing PVCs if the storage request has increased +async fn resize_pool_pvcs( + tenant: &Tenant, + pool: &crate::types::v1alpha1::pool::Pool, + namespace: &str, + ctx: &Context, +) -> Result<(), Error> { + // Get desired storage size from spec + let desired_storage = if let Some(ref template) = pool.persistence.volume_claim_template + && let Some(ref resources) = template.resources + && let Some(ref requests) = resources.requests + && let Some(qty) = requests.get("storage") + { + qty + } else { + // No storage request defined in spec or template, defaulting to 10Gi + // If the user hasn't defined it, we assume they don't want to resize or are using defaults. + // For simplicity, we skip resizing if not explicitly defined in the current spec template. + return Ok(()); + }; + + // Parse desired quantity to allow comparison + // Note: Parsing k8s Quantity exactly is complex. Here we use string comparison if units are same, + // or rely on k8s to handle the patch if different. + // A more robust way is to blindly patch if string differs, and let K8s/CSI reject if invalid (e.g. shrinking). + + // List all PVCs for this pool + // Labels: rustfs.tenant={tenant}, rustfs.pool={pool} + let selector = format!("rustfs.tenant={},rustfs.pool={}", tenant.name(), pool.name); + let pvcs_api: kube::Api = + kube::Api::namespaced(ctx.client.clone(), namespace); + let pvcs = pvcs_api + .list(&ListParams::default().labels(&selector)) + .await + .map_err(|source| Error::Context { + source: context::Error::Kube { source }, + })?; + + for pvc in pvcs.items { + let pvc_name = pvc.metadata.name.as_deref().unwrap_or_default(); + + // Check current request + if let Some(ref spec) = pvc.spec + && let Some(ref resources) = spec.resources + && let Some(ref requests) = resources.requests + && let Some(current_qty) = requests.get("storage") + && current_qty != desired_storage + { + debug!( + "PVC {} storage varies: current={}, desired={}. Attempting resize.", + pvc_name, current_qty.0, desired_storage.0 + ); + + // Create patch to update storage request + let patch = serde_json::json!({ + "spec": { + "resources": { + "requests": { + "storage": desired_storage + } + } + } + }); + + match pvcs_api + .patch( + pvc_name, + &kube::api::PatchParams::apply("rustfs-operator"), + &kube::api::Patch::Merge(patch), + ) + .await + { + Ok(_) => { + let _ = ctx + .record( + tenant, + EventType::Normal, + "PVCResized", + &format!( + "Patched PVC {} storage request to {}", + pvc_name, desired_storage.0 + ), + ) + .await; + } + Err(e) => { + // Log but don't fail reconciliation completely, expanding storage might fail for valid reasons (unsupported SC) + warn!("Failed to patch PVC {} for resize: {}", pvc_name, e); + } + } + } } + + Ok(()) } #[cfg(test)] @@ -807,3 +969,5 @@ mod tests { )); } } + + diff --git a/src/tests.rs b/src/tests.rs index 87cc535..3ab0626 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![allow(clippy::unwrap_used)] + use k8s_openapi::apimachinery::pkg::apis::meta::v1 as metav1; use crate::types::v1alpha1::persistence::PersistenceConfig; @@ -47,3 +49,141 @@ pub fn create_test_tenant( status: None, } } + +#[cfg(test)] +mod unit_tests { + use super::*; + use k8s_openapi::api::core::v1 as corev1; + + #[test] + fn test_statefulset_generation_with_probes() { + let mut tenant = create_test_tenant(None, None); + // Add probes + tenant.spec.liveness = Some(corev1::Probe { + initial_delay_seconds: Some(10), + ..Default::default() + }); + + let ss = tenant.new_statefulset(&tenant.spec.pools[0]).unwrap(); + let container = &ss + .spec + .as_ref() + .unwrap() + .template + .spec + .as_ref() + .unwrap() + .containers[0]; + + assert!(container.liveness_probe.is_some()); + assert_eq!( + container + .liveness_probe + .as_ref() + .unwrap() + .initial_delay_seconds, + Some(10) + ); + } + + #[test] + fn test_pdb_generation() { + let tenant = create_test_tenant(None, None); + let pdb = tenant.new_pdb(&tenant.spec.pools[0]).unwrap(); + + assert_eq!(pdb.metadata.name.unwrap(), "test-tenant-pool-0"); + assert!(pdb.spec.unwrap().max_unavailable.is_some()); + } + + #[test] + fn test_statefulset_tls_config() { + let mut tenant = create_test_tenant(None, None); + tenant.spec.request_auto_cert = Some(true); + + let ss = tenant.new_statefulset(&tenant.spec.pools[0]).unwrap(); + let container = &ss + .spec + .as_ref() + .unwrap() + .template + .spec + .as_ref() + .unwrap() + .containers[0]; + + // Check Mounts + let has_tls_mount = container + .volume_mounts + .as_ref() + .unwrap() + .iter() + .any(|vm| vm.name == "tls-certs" && vm.mount_path == "/etc/rustfs-tls"); + assert!(has_tls_mount); + + // Check Env + let envs = container.env.as_ref().unwrap(); + assert!( + envs.iter() + .any(|e| e.name == "RUSTFS_TLS_ENABLE" && e.value == Some("true".to_string())) + ); + assert!(envs.iter().any(|e| e.name == "RUSTFS_TLS_CERT_FILE")); + + // Check Volumes + let volumes = ss + .spec + .as_ref() + .unwrap() + .template + .spec + .as_ref() + .unwrap() + .volumes + .as_ref() + .unwrap(); + let has_tls_vol = volumes + .iter() + .any(|v| v.name == "tls-certs" && v.secret.is_some()); + assert!(has_tls_vol); + } +} + +#[test] +fn test_volume_resize_detection_logic() { + use k8s_openapi::api::core::v1 as corev1; + use k8s_openapi::apimachinery::pkg::api::resource::Quantity; + + let mut tenant = create_test_tenant(None, None); + // Set initial validation + let mut template = corev1::PersistentVolumeClaimSpec::default(); + let mut requests = std::collections::BTreeMap::new(); + requests.insert("storage".to_string(), Quantity("10Gi".to_string())); + template.resources = Some(corev1::VolumeResourceRequirements { + requests: Some(requests), + ..Default::default() + }); + + tenant.spec.pools[0].persistence.volume_claim_template = Some(template); + + // Simulation logic similar to resize_pool_pvcs + let desired = tenant.spec.pools[0] + .persistence + .volume_claim_template + .as_ref() + .unwrap() + .resources + .as_ref() + .unwrap() + .requests + .as_ref() + .unwrap() + .get("storage") + .unwrap(); + + assert_eq!(desired.0, "10Gi"); + + // Mock current PVC state + let current_qty = Quantity("5Gi".to_string()); + + assert_ne!(¤t_qty, desired); + // Logic would trigger resize here +} diff --git a/src/types/v1alpha1/tenant.rs b/src/types/v1alpha1/tenant.rs index 541e837..7d8ce36 100644 --- a/src/types/v1alpha1/tenant.rs +++ b/src/types/v1alpha1/tenant.rs @@ -23,6 +23,7 @@ use snafu::OptionExt; // Submodules for resource factory methods mod helper; +mod pdb; mod rbac; mod services; mod workloads; @@ -80,20 +81,20 @@ pub struct TenantSpec { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub env: Vec, - // #[serde(default, skip_serializing_if = "Option::is_none")] - // pub request_auto_cert: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub request_auto_cert: Option, // // #[serde(default, skip_serializing_if = "Option::is_none")] // pub cert_expiry_alert_threshold: Option, // - // #[serde(default, skip_serializing_if = "Option::is_none")] - // pub liveness: Option, - // - // #[serde(default, skip_serializing_if = "Option::is_none")] - // pub readiness: Option, - // - // #[serde(default, skip_serializing_if = "Option::is_none")] - // pub startup: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub liveness: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub readiness: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub startup: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub lifecycle: Option, diff --git a/src/types/v1alpha1/tenant/pdb.rs b/src/types/v1alpha1/tenant/pdb.rs new file mode 100644 index 0000000..fac8f0e --- /dev/null +++ b/src/types/v1alpha1/tenant/pdb.rs @@ -0,0 +1,40 @@ +use super::Tenant; +use crate::types; +use crate::types::v1alpha1::pool::Pool; +use k8s_openapi::api::policy::v1 as policyv1; +use k8s_openapi::apimachinery::pkg::apis::meta::v1 as metav1; +use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString; + +impl Tenant { + pub fn new_pdb( + &self, + pool: &Pool, + ) -> Result { + let labels = self.pool_labels(pool); + let selector_labels = self.pool_selector_labels(pool); + let name = format!("{}-{}", self.name(), pool.name); + + // IntOrString::Int(1) means max 1 pod can be unavailable at a time. + // This is safe for most distributed storage upgrades/maintenance. + let max_unavailable = Some(IntOrString::Int(1)); + + Ok(policyv1::PodDisruptionBudget { + metadata: metav1::ObjectMeta { + name: Some(name), + namespace: self.namespace().ok(), + owner_references: Some(vec![self.new_owner_ref()]), + labels: Some(labels), + ..Default::default() + }, + spec: Some(policyv1::PodDisruptionBudgetSpec { + max_unavailable, + selector: Some(metav1::LabelSelector { + match_labels: Some(selector_labels), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }) + } +} diff --git a/src/types/v1alpha1/tenant/workloads.rs b/src/types/v1alpha1/tenant/workloads.rs index 9359b4a..e9461e8 100644 --- a/src/types/v1alpha1/tenant/workloads.rs +++ b/src/types/v1alpha1/tenant/workloads.rs @@ -210,6 +210,33 @@ impl Tenant { }); } + // Configure TLS if requested + if self.spec.request_auto_cert.unwrap_or(false) { + env_vars.push(corev1::EnvVar { + name: "RUSTFS_TLS_ENABLE".to_string(), + value: Some("true".to_string()), + ..Default::default() + }); + env_vars.push(corev1::EnvVar { + name: "RUSTFS_TLS_CERT_FILE".to_string(), + value: Some("/etc/rustfs-tls/tls.crt".to_string()), + ..Default::default() + }); + env_vars.push(corev1::EnvVar { + name: "RUSTFS_TLS_KEY_FILE".to_string(), + value: Some("/etc/rustfs-tls/tls.key".to_string()), + ..Default::default() + }); + + // Mount the certificate secret + volume_mounts.push(corev1::VolumeMount { + name: "tls-certs".to_string(), + mount_path: "/etc/rustfs-tls".to_string(), + read_only: Some(true), + ..Default::default() + }); + } + // Merge with user-provided environment variables // User-provided vars can override operator-managed ones for user_env in &self.spec.env { @@ -219,12 +246,25 @@ impl Tenant { } // Use an in-memory volume for logs to avoid permission issues on container filesystems - let pod_volumes = vec![corev1::Volume { + let mut pod_volumes = vec![corev1::Volume { name: LOG_VOLUME_NAME.to_string(), empty_dir: Some(corev1::EmptyDirVolumeSource::default()), ..Default::default() }]; + if self.spec.request_auto_cert.unwrap_or(false) { + let secret_name = format!("{}-tls", self.name()); + pod_volumes.push(corev1::Volume { + name: "tls-certs".to_string(), + secret: Some(corev1::SecretVolumeSource { + secret_name: Some(secret_name), + default_mode: Some(420), // 0644 + ..Default::default() + }), + ..Default::default() + }); + } + // Enforce non-root execution and make mounted volumes writable by RustFS user let pod_security_context = Some(corev1::PodSecurityContext { run_as_user: Some(DEFAULT_RUN_AS_USER), @@ -257,6 +297,9 @@ impl Tenant { }, ]), volume_mounts: Some(volume_mounts), + liveness_probe: self.spec.liveness.clone(), + readiness_probe: self.spec.readiness.clone(), + startup_probe: self.spec.startup.clone(), lifecycle: self.spec.lifecycle.clone(), // Apply pool-level resource requirements to container resources: pool.scheduling.resources.clone(), diff --git a/src/utils/tls.rs b/src/utils/tls.rs index 652447e..dc56808 100644 --- a/src/utils/tls.rs +++ b/src/utils/tls.rs @@ -44,6 +44,44 @@ pub enum Error { #[snafu(display("no supported pem type"))] NoSupportedPEMType, + + #[snafu(display("failed to generate certificate: {}", source))] + GenerateCert { source: rcgen::Error }, +} + +/// Generates a self-signed certificate and private key (RSA 2048) +/// Returns (cert_pem, key_pem) +#[allow(clippy::expect_used)] +pub fn generate_self_signed_cert( + common_name: &str, + san_dns_names: Vec, +) -> Result<(String, String), Error> { + let mut params = + rcgen::CertificateParams::new(vec![common_name.to_string()]).context(GenerateCertSnafu)?; + params.subject_alt_names = san_dns_names + .into_iter() + .map(|s| { + rcgen::SanType::DnsName( + s.try_into() + .map_err(|_| rcgen::Error::CouldNotParseCertificate) + .unwrap_or(rcgen::string::Ia5String::try_from("invalid").expect("invalid is ASCII")), + ) + }) + .collect(); + + // Set valid period to 10 years for simplicity in this version + // In production this should be shorter and updated via rotation + params.not_before = time::OffsetDateTime::now_utc().saturating_sub(time::Duration::days(1)); + params.not_after = + time::OffsetDateTime::now_utc().saturating_add(time::Duration::days(365 * 10)); + + let key_pair = rcgen::KeyPair::generate().context(GenerateCertSnafu)?; + let cert = params.self_signed(&key_pair).context(GenerateCertSnafu)?; + + let cert_pem = cert.pem(); + let key_pem = key_pair.serialize_pem(); + + Ok((cert_pem, key_pem)) } // load certificates from PEM file @@ -86,6 +124,7 @@ pub fn x509_key_pair>(cert_pem: T, key_pem: T) -> Result<(), Erro #[cfg(test)] mod tests { + #![allow(clippy::unwrap_used)] use super::*; #[test] @@ -241,4 +280,20 @@ do0DpyMVNy4vlS2yIvg6NmbMcDq6ugLh3A== assert!(x509_key_pair(cert_pem, key_pem).is_ok()); } + +#[test] +fn test_generate_self_signed_cert() { + let cn = "test-service.default.svc"; + let sans = vec![cn.to_string(), "localhost".to_string()]; + + let result = generate_self_signed_cert(cn, sans); + assert!(result.is_ok()); + + let (cert_pem, key_pem) = result.unwrap(); + assert!(cert_pem.contains("BEGIN CERTIFICATE")); + assert!(key_pem.contains("PRIVATE KEY")); + + // Verify key pair matches + assert!(x509_key_pair(&cert_pem, &key_pem).is_ok()); +} }