|
| 1 | +use std::collections::BTreeMap; |
| 2 | + |
1 | 3 | use jiff::Span; |
2 | 4 | use k8s_openapi::api::core::v1::{ |
3 | 5 | Affinity, LocalObjectReference, NodeAffinity, NodeSelector, NodeSelectorRequirement, |
4 | | - NodeSelectorTerm, SecretReference, |
| 6 | + NodeSelectorTerm, ResourceRequirements, SecretReference, |
5 | 7 | }; |
6 | 8 | use k8s_openapi::apimachinery::pkg::api::resource::Quantity; |
7 | 9 |
|
@@ -172,3 +174,169 @@ fn version_detect_job_has_ttl_seconds_after_finished() { |
172 | 174 | .expect("version-detect job must set ttlSecondsAfterFinished"); |
173 | 175 | assert!(ttl > 0, "ttlSecondsAfterFinished must be positive"); |
174 | 176 | } |
| 177 | + |
| 178 | +#[test] |
| 179 | +fn deployment_has_dshm_volume_with_default_resources() { |
| 180 | + let (mut restore, replica) = test_restore_and_replica(); |
| 181 | + restore.status = Some(PostgresPhysicalRestoreStatus { |
| 182 | + postgres_version: Some("16".to_string()), |
| 183 | + ..Default::default() |
| 184 | + }); |
| 185 | + |
| 186 | + let deploy = build_deployment(&restore, "test-restore", "default", &replica).unwrap(); |
| 187 | + let pod_spec = deploy.spec.unwrap().template.spec.unwrap(); |
| 188 | + |
| 189 | + let dshm_vol = pod_spec |
| 190 | + .volumes |
| 191 | + .as_ref() |
| 192 | + .unwrap() |
| 193 | + .iter() |
| 194 | + .find(|v| v.name == "dshm") |
| 195 | + .expect("dshm volume must exist"); |
| 196 | + |
| 197 | + let empty_dir = dshm_vol.empty_dir.as_ref().expect("dshm must be emptyDir"); |
| 198 | + assert_eq!(empty_dir.medium.as_deref(), Some("Memory")); |
| 199 | + // Default 1Gi request: min(512Mi, ceil(36% of 1Gi)) = min(512Mi, 369Mi) = 369Mi |
| 200 | + assert_eq!(empty_dir.size_limit.as_ref().unwrap().0, "369Mi"); |
| 201 | +} |
| 202 | + |
| 203 | +#[test] |
| 204 | +fn deployment_has_dshm_volume_with_custom_resources() { |
| 205 | + let (mut restore, mut replica) = test_restore_and_replica(); |
| 206 | + restore.status = Some(PostgresPhysicalRestoreStatus { |
| 207 | + postgres_version: Some("16".to_string()), |
| 208 | + ..Default::default() |
| 209 | + }); |
| 210 | + replica.spec.resources = Some(ResourceRequirements { |
| 211 | + requests: Some(BTreeMap::from([( |
| 212 | + "memory".to_string(), |
| 213 | + Quantity("4Gi".to_string()), |
| 214 | + )])), |
| 215 | + limits: Some(BTreeMap::from([( |
| 216 | + "memory".to_string(), |
| 217 | + Quantity("8Gi".to_string()), |
| 218 | + )])), |
| 219 | + ..Default::default() |
| 220 | + }); |
| 221 | + |
| 222 | + let deploy = build_deployment(&restore, "test-restore", "default", &replica).unwrap(); |
| 223 | + let pod_spec = deploy.spec.unwrap().template.spec.unwrap(); |
| 224 | + |
| 225 | + let dshm_vol = pod_spec |
| 226 | + .volumes |
| 227 | + .as_ref() |
| 228 | + .unwrap() |
| 229 | + .iter() |
| 230 | + .find(|v| v.name == "dshm") |
| 231 | + .expect("dshm volume must exist"); |
| 232 | + |
| 233 | + let empty_dir = dshm_vol.empty_dir.as_ref().expect("dshm must be emptyDir"); |
| 234 | + assert_eq!(empty_dir.medium.as_deref(), Some("Memory")); |
| 235 | + // 4Gi request, 8Gi limit: min(4Gi/2, ceil(36% of 8Gi)) = min(2048Mi, 2950Mi) = 2048Mi |
| 236 | + assert_eq!(empty_dir.size_limit.as_ref().unwrap().0, "2048Mi"); |
| 237 | +} |
| 238 | + |
| 239 | +#[test] |
| 240 | +fn deployment_mounts_dshm_on_postgres_and_setup_auth() { |
| 241 | + let (mut restore, replica) = test_restore_and_replica(); |
| 242 | + restore.status = Some(PostgresPhysicalRestoreStatus { |
| 243 | + postgres_version: Some("16".to_string()), |
| 244 | + ..Default::default() |
| 245 | + }); |
| 246 | + |
| 247 | + let deploy = build_deployment(&restore, "test-restore", "default", &replica).unwrap(); |
| 248 | + let pod_spec = deploy.spec.unwrap().template.spec.unwrap(); |
| 249 | + |
| 250 | + let postgres = &pod_spec.containers[0]; |
| 251 | + assert_eq!(postgres.name, "postgres"); |
| 252 | + let pg_mounts = postgres.volume_mounts.as_ref().unwrap(); |
| 253 | + let pg_shm = pg_mounts |
| 254 | + .iter() |
| 255 | + .find(|m| m.mount_path == "/dev/shm") |
| 256 | + .expect("postgres container must mount /dev/shm"); |
| 257 | + assert_eq!(pg_shm.name, "dshm"); |
| 258 | + |
| 259 | + let setup_auth = pod_spec |
| 260 | + .init_containers |
| 261 | + .as_ref() |
| 262 | + .unwrap() |
| 263 | + .iter() |
| 264 | + .find(|c| c.name == "setup-auth") |
| 265 | + .expect("setup-auth init container must exist"); |
| 266 | + let sa_mounts = setup_auth.volume_mounts.as_ref().unwrap(); |
| 267 | + assert!( |
| 268 | + !sa_mounts.iter().any(|m| m.mount_path == "/dev/shm"), |
| 269 | + "setup-auth must NOT mount /dev/shm (it never uses shared memory)" |
| 270 | + ); |
| 271 | +} |
| 272 | + |
| 273 | +#[test] |
| 274 | +fn deployment_init_script_sets_shared_buffers() { |
| 275 | + let (mut restore, replica) = test_restore_and_replica(); |
| 276 | + restore.status = Some(PostgresPhysicalRestoreStatus { |
| 277 | + postgres_version: Some("16".to_string()), |
| 278 | + ..Default::default() |
| 279 | + }); |
| 280 | + |
| 281 | + let deploy = build_deployment(&restore, "test-restore", "default", &replica).unwrap(); |
| 282 | + let pod_spec = deploy.spec.unwrap().template.spec.unwrap(); |
| 283 | + |
| 284 | + let setup_auth = pod_spec |
| 285 | + .init_containers |
| 286 | + .as_ref() |
| 287 | + .unwrap() |
| 288 | + .iter() |
| 289 | + .find(|c| c.name == "setup-auth") |
| 290 | + .expect("setup-auth init container must exist"); |
| 291 | + let script = &setup_auth.args.as_ref().unwrap()[0]; |
| 292 | + |
| 293 | + // Default 1Gi: SHM=369Mi, shared_buffers = floor(70% of 369) = 258MB |
| 294 | + assert!( |
| 295 | + script.contains("shared_buffers = 258MB"), |
| 296 | + "init script must set shared_buffers to computed value, got script containing: {}", |
| 297 | + script |
| 298 | + .lines() |
| 299 | + .find(|l| l.contains("shared_buffers")) |
| 300 | + .unwrap_or("<not found>") |
| 301 | + ); |
| 302 | + |
| 303 | + // The sed block must strip shared_buffers from source config |
| 304 | + assert!( |
| 305 | + script.contains("shared_buffers[[:space:]]*="), |
| 306 | + "sed must strip shared_buffers from source config" |
| 307 | + ); |
| 308 | +} |
| 309 | + |
| 310 | +#[test] |
| 311 | +fn deployment_shared_buffers_with_custom_resources() { |
| 312 | + let (mut restore, mut replica) = test_restore_and_replica(); |
| 313 | + restore.status = Some(PostgresPhysicalRestoreStatus { |
| 314 | + postgres_version: Some("16".to_string()), |
| 315 | + ..Default::default() |
| 316 | + }); |
| 317 | + replica.spec.resources = Some(ResourceRequirements { |
| 318 | + requests: Some(BTreeMap::from([( |
| 319 | + "memory".to_string(), |
| 320 | + Quantity("2Gi".to_string()), |
| 321 | + )])), |
| 322 | + ..Default::default() |
| 323 | + }); |
| 324 | + |
| 325 | + let deploy = build_deployment(&restore, "test-restore", "default", &replica).unwrap(); |
| 326 | + let pod_spec = deploy.spec.unwrap().template.spec.unwrap(); |
| 327 | + |
| 328 | + let setup_auth = pod_spec |
| 329 | + .init_containers |
| 330 | + .as_ref() |
| 331 | + .unwrap() |
| 332 | + .iter() |
| 333 | + .find(|c| c.name == "setup-auth") |
| 334 | + .expect("setup-auth init container must exist"); |
| 335 | + let script = &setup_auth.args.as_ref().unwrap()[0]; |
| 336 | + |
| 337 | + // 2Gi request: SHM=738Mi, shared_buffers = floor(70% of 738) = 516MB |
| 338 | + assert!( |
| 339 | + script.contains("shared_buffers = 516MB"), |
| 340 | + "init script must set shared_buffers for 2Gi request" |
| 341 | + ); |
| 342 | +} |
0 commit comments