// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! KMS Fault Recovery and Error Handling Tests //! //! This test suite validates KMS behavior under failure conditions: //! - KMS service unavailability //! - Network interruptions during multipart uploads //! - Disk space limitations //! - Corrupted key files //! - Recovery from transient failures use super::common::LocalKMSTestEnvironment; use crate::common::{TEST_BUCKET, init_logging}; use aws_sdk_s3::types::ServerSideEncryption; use std::fs; use std::time::Duration; use tokio::time::sleep; use tracing::{info, warn}; /// Test KMS behavior when key directory is temporarily unavailable #[tokio::test] async fn test_kms_key_directory_unavailable() -> Result<(), Box> { init_logging(); info!("๐Ÿงช Testing KMS behavior with unavailable key directory"); let mut kms_env = LocalKMSTestEnvironment::new().await?; let _default_key_id = kms_env.start_rustfs_for_local_kms().await?; tokio::time::sleep(Duration::from_secs(3)).await; let s3_client = kms_env.base_env.create_s3_client(); kms_env.base_env.create_test_bucket(TEST_BUCKET).await?; // First, upload a normal encrypted file to verify KMS is working info!("๐Ÿ“ค Uploading test file with KMS encryption"); let test_data = b"Test data before key directory issue"; let object_key = "test-before-key-issue"; let put_response = s3_client .put_object() .bucket(TEST_BUCKET) .key(object_key) .body(aws_sdk_s3::primitives::ByteStream::from(test_data.to_vec())) .server_side_encryption(ServerSideEncryption::Aes256) .send() .await?; assert_eq!(put_response.server_side_encryption(), Some(&ServerSideEncryption::Aes256)); // Temporarily rename the key directory to simulate unavailability info!("๐Ÿ”ง Simulating key directory unavailability"); let backup_dir = format!("{}.backup", kms_env.kms_keys_dir); fs::rename(&kms_env.kms_keys_dir, &backup_dir)?; // Try to upload another file - this should fail gracefully info!("๐Ÿ“ค Attempting upload with unavailable key directory"); let test_data2 = b"Test data during key directory issue"; let object_key2 = "test-during-key-issue"; let put_result2 = s3_client .put_object() .bucket(TEST_BUCKET) .key(object_key2) .body(aws_sdk_s3::primitives::ByteStream::from(test_data2.to_vec())) .server_side_encryption(ServerSideEncryption::Aes256) .send() .await; // This should fail, but the server should still be responsive if put_result2.is_err() { info!("โœ… Upload correctly failed when key directory unavailable"); } else { warn!("โš ๏ธ Upload succeeded despite unavailable key directory (may be using cached keys)"); } // Restore the key directory info!("๐Ÿ”ง Restoring key directory"); fs::rename(&backup_dir, &kms_env.kms_keys_dir)?; // Wait a moment for KMS to detect the restored directory sleep(Duration::from_secs(2)).await; // Try uploading again - this should work info!("๐Ÿ“ค Uploading after key directory restoration"); let test_data3 = b"Test data after key directory restoration"; let object_key3 = "test-after-key-restoration"; let put_response3 = s3_client .put_object() .bucket(TEST_BUCKET) .key(object_key3) .body(aws_sdk_s3::primitives::ByteStream::from(test_data3.to_vec())) .server_side_encryption(ServerSideEncryption::Aes256) .send() .await?; assert_eq!(put_response3.server_side_encryption(), Some(&ServerSideEncryption::Aes256)); // Verify we can still access the original file info!("๐Ÿ“ฅ Verifying access to original encrypted file"); let get_response = s3_client.get_object().bucket(TEST_BUCKET).key(object_key).send().await?; let downloaded_data = get_response.body.collect().await?.into_bytes(); assert_eq!(downloaded_data.as_ref(), test_data); kms_env.base_env.delete_test_bucket(TEST_BUCKET).await?; info!("โœ… Key directory unavailability test completed successfully"); Ok(()) } /// Test handling of corrupted key files #[tokio::test] async fn test_kms_corrupted_key_files() -> Result<(), Box> { init_logging(); info!("๐Ÿงช Testing KMS behavior with corrupted key files"); let mut kms_env = LocalKMSTestEnvironment::new().await?; let default_key_id = kms_env.start_rustfs_for_local_kms().await?; tokio::time::sleep(Duration::from_secs(3)).await; let s3_client = kms_env.base_env.create_s3_client(); kms_env.base_env.create_test_bucket(TEST_BUCKET).await?; // Upload a file with valid key info!("๐Ÿ“ค Uploading file with valid key"); let test_data = b"Test data before key corruption"; let object_key = "test-before-corruption"; s3_client .put_object() .bucket(TEST_BUCKET) .key(object_key) .body(aws_sdk_s3::primitives::ByteStream::from(test_data.to_vec())) .server_side_encryption(ServerSideEncryption::Aes256) .send() .await?; // Corrupt the default key file info!("๐Ÿ”ง Corrupting default key file"); let key_file_path = format!("{}/{}.key", kms_env.kms_keys_dir, default_key_id); let backup_key_path = format!("{key_file_path}.backup"); // Backup the original key file fs::copy(&key_file_path, &backup_key_path)?; // Write corrupted data to the key file fs::write(&key_file_path, b"corrupted key data")?; // Wait for potential key cache to expire sleep(Duration::from_secs(1)).await; // Try to upload with corrupted key - this should fail info!("๐Ÿ“ค Attempting upload with corrupted key"); let test_data2 = b"Test data with corrupted key"; let object_key2 = "test-with-corrupted-key"; let put_result2 = s3_client .put_object() .bucket(TEST_BUCKET) .key(object_key2) .body(aws_sdk_s3::primitives::ByteStream::from(test_data2.to_vec())) .server_side_encryption(ServerSideEncryption::Aes256) .send() .await; // This might succeed if KMS uses cached keys, but should eventually fail if put_result2.is_err() { info!("โœ… Upload correctly failed with corrupted key"); } else { warn!("โš ๏ธ Upload succeeded despite corrupted key (likely using cached key)"); } // Restore the original key file info!("๐Ÿ”ง Restoring original key file"); fs::copy(&backup_key_path, &key_file_path)?; fs::remove_file(&backup_key_path)?; // Wait for KMS to detect the restored key sleep(Duration::from_secs(2)).await; // Try uploading again - this should work info!("๐Ÿ“ค Uploading after key restoration"); let test_data3 = b"Test data after key restoration"; let object_key3 = "test-after-key-restoration"; let put_response3 = s3_client .put_object() .bucket(TEST_BUCKET) .key(object_key3) .body(aws_sdk_s3::primitives::ByteStream::from(test_data3.to_vec())) .server_side_encryption(ServerSideEncryption::Aes256) .send() .await?; assert_eq!(put_response3.server_side_encryption(), Some(&ServerSideEncryption::Aes256)); kms_env.base_env.delete_test_bucket(TEST_BUCKET).await?; info!("โœ… Corrupted key files test completed successfully"); Ok(()) } /// Test multipart upload interruption and recovery #[tokio::test] async fn test_kms_multipart_upload_interruption() -> Result<(), Box> { init_logging(); info!("๐Ÿงช Testing KMS multipart upload interruption and recovery"); let mut kms_env = LocalKMSTestEnvironment::new().await?; let _default_key_id = kms_env.start_rustfs_for_local_kms().await?; tokio::time::sleep(Duration::from_secs(3)).await; let s3_client = kms_env.base_env.create_s3_client(); kms_env.base_env.create_test_bucket(TEST_BUCKET).await?; // Test data for multipart upload let part_size = 5 * 1024 * 1024; // 5MB per part let total_parts = 3; let total_size = part_size * total_parts; let test_data: Vec = (0..total_size).map(|i| (i % 256) as u8).collect(); let object_key = "multipart-interruption-test"; info!("๐Ÿ“ค Starting multipart upload with encryption"); // Initiate multipart upload let create_multipart_output = s3_client .create_multipart_upload() .bucket(TEST_BUCKET) .key(object_key) .server_side_encryption(ServerSideEncryption::Aes256) .send() .await?; let upload_id = create_multipart_output.upload_id().unwrap(); info!("โœ… Multipart upload initiated with ID: {}", upload_id); // Upload first part successfully info!("๐Ÿ“ค Uploading part 1"); let part1_data = &test_data[0..part_size]; let upload_part1_output = s3_client .upload_part() .bucket(TEST_BUCKET) .key(object_key) .upload_id(upload_id) .part_number(1) .body(aws_sdk_s3::primitives::ByteStream::from(part1_data.to_vec())) .send() .await?; let part1_etag = upload_part1_output.e_tag().unwrap().to_string(); info!("โœ… Part 1 uploaded successfully"); // Upload second part successfully info!("๐Ÿ“ค Uploading part 2"); let part2_data = &test_data[part_size..part_size * 2]; let upload_part2_output = s3_client .upload_part() .bucket(TEST_BUCKET) .key(object_key) .upload_id(upload_id) .part_number(2) .body(aws_sdk_s3::primitives::ByteStream::from(part2_data.to_vec())) .send() .await?; let part2_etag = upload_part2_output.e_tag().unwrap().to_string(); info!("โœ… Part 2 uploaded successfully"); // Simulate interruption - we'll NOT upload part 3 and instead abort the upload info!("๐Ÿ”ง Simulating upload interruption"); // Abort the multipart upload let abort_result = s3_client .abort_multipart_upload() .bucket(TEST_BUCKET) .key(object_key) .upload_id(upload_id) .send() .await; match abort_result { Ok(_) => info!("โœ… Multipart upload aborted successfully"), Err(e) => warn!("โš ๏ธ Failed to abort multipart upload: {}", e), } // Try to complete the aborted upload - this should fail info!("๐Ÿ” Attempting to complete aborted upload"); let completed_parts = vec![ aws_sdk_s3::types::CompletedPart::builder() .part_number(1) .e_tag(&part1_etag) .build(), aws_sdk_s3::types::CompletedPart::builder() .part_number(2) .e_tag(&part2_etag) .build(), ]; let completed_multipart_upload = aws_sdk_s3::types::CompletedMultipartUpload::builder() .set_parts(Some(completed_parts)) .build(); let complete_result = s3_client .complete_multipart_upload() .bucket(TEST_BUCKET) .key(object_key) .upload_id(upload_id) .multipart_upload(completed_multipart_upload) .send() .await; assert!(complete_result.is_err(), "Should not be able to complete aborted upload"); info!("โœ… Correctly failed to complete aborted upload"); // Start a new multipart upload and complete it successfully info!("๐Ÿ“ค Starting new multipart upload"); let create_multipart_output2 = s3_client .create_multipart_upload() .bucket(TEST_BUCKET) .key(object_key) .server_side_encryption(ServerSideEncryption::Aes256) .send() .await?; let upload_id2 = create_multipart_output2.upload_id().unwrap(); // Upload all parts for the new upload let mut completed_parts2 = Vec::new(); for part_number in 1..=total_parts { let start = (part_number - 1) * part_size; let end = std::cmp::min(start + part_size, total_size); let part_data = &test_data[start..end]; let upload_part_output = s3_client .upload_part() .bucket(TEST_BUCKET) .key(object_key) .upload_id(upload_id2) .part_number(part_number as i32) .body(aws_sdk_s3::primitives::ByteStream::from(part_data.to_vec())) .send() .await?; let etag = upload_part_output.e_tag().unwrap().to_string(); completed_parts2.push( aws_sdk_s3::types::CompletedPart::builder() .part_number(part_number as i32) .e_tag(&etag) .build(), ); info!("โœ… Part {} uploaded successfully", part_number); } // Complete the new multipart upload let completed_multipart_upload2 = aws_sdk_s3::types::CompletedMultipartUpload::builder() .set_parts(Some(completed_parts2)) .build(); let _complete_output2 = s3_client .complete_multipart_upload() .bucket(TEST_BUCKET) .key(object_key) .upload_id(upload_id2) .multipart_upload(completed_multipart_upload2) .send() .await?; info!("โœ… New multipart upload completed successfully"); // Verify the completed upload let get_response = s3_client.get_object().bucket(TEST_BUCKET).key(object_key).send().await?; assert_eq!(get_response.server_side_encryption(), Some(&ServerSideEncryption::Aes256)); let downloaded_data = get_response.body.collect().await?.into_bytes(); assert_eq!(downloaded_data.len(), total_size); assert_eq!(&downloaded_data[..], &test_data[..]); info!("โœ… Downloaded data matches original test data"); kms_env.base_env.delete_test_bucket(TEST_BUCKET).await?; info!("โœ… Multipart upload interruption test completed successfully"); Ok(()) } /// Test KMS resilience to temporary resource constraints #[tokio::test] async fn test_kms_resource_constraints() -> Result<(), Box> { init_logging(); info!("๐Ÿงช Testing KMS behavior under resource constraints"); let mut kms_env = LocalKMSTestEnvironment::new().await?; let _default_key_id = kms_env.start_rustfs_for_local_kms().await?; tokio::time::sleep(Duration::from_secs(3)).await; let s3_client = kms_env.base_env.create_s3_client(); kms_env.base_env.create_test_bucket(TEST_BUCKET).await?; // Test multiple rapid encryption requests info!("๐Ÿ“ค Testing rapid successive encryption requests"); let mut upload_tasks = Vec::new(); for i in 0..10 { let client = s3_client.clone(); let test_data = format!("Rapid test data {i}").into_bytes(); let object_key = format!("rapid-test-{i}"); let task = tokio::spawn(async move { let result = client .put_object() .bucket(TEST_BUCKET) .key(&object_key) .body(aws_sdk_s3::primitives::ByteStream::from(test_data)) .server_side_encryption(ServerSideEncryption::Aes256) .send() .await; (object_key, result) }); upload_tasks.push(task); } // Wait for all uploads to complete let mut successful_uploads = 0; let mut failed_uploads = 0; for task in upload_tasks { let (object_key, result) = task.await.unwrap(); match result { Ok(_) => { successful_uploads += 1; info!("โœ… Rapid upload {} succeeded", object_key); } Err(e) => { failed_uploads += 1; warn!("โŒ Rapid upload {} failed: {}", object_key, e); } } } info!("๐Ÿ“Š Rapid upload results: {} succeeded, {} failed", successful_uploads, failed_uploads); // We expect most uploads to succeed even under load assert!(successful_uploads >= 7, "Expected at least 7/10 rapid uploads to succeed"); kms_env.base_env.delete_test_bucket(TEST_BUCKET).await?; info!("โœ… Resource constraints test completed successfully"); Ok(()) }