From bf1c9385922e26691cbdb6416c969c696d26a9bb Mon Sep 17 00:00:00 2001 From: Federico Maleh Date: Wed, 4 Feb 2026 12:28:26 -0300 Subject: [PATCH] Add logging format and tests for k8s/backup module - Update backup_templates with standardized logging format - Update s3 script with detailed error messages and fix suggestions - Add comprehensive bats tests for backup_templates - Add comprehensive bats tests for s3 operations --- CHANGELOG.md | 2 + k8s/backup/backup_templates | 13 +- k8s/backup/s3 | 55 ++++- k8s/backup/tests/backup_templates.bats | 174 ++++++++++++++ k8s/backup/tests/s3.bats | 299 +++++++++++++++++++++++++ 5 files changed, 528 insertions(+), 15 deletions(-) create mode 100644 k8s/backup/tests/backup_templates.bats create mode 100644 k8s/backup/tests/s3.bats diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a7caa1..9322a328 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Add unit testing support - Add scope configuration +- Improve **k8s/backup** logging format with detailed error messages and fix suggestions +- Add unit tests for **k8s/backup** module (backup_templates and s3 operations) ## [1.10.0] - 2026-01-14 - Add support to configure the traffic manager nginx through a configmap. diff --git a/k8s/backup/backup_templates b/k8s/backup/backup_templates index 26642f0c..1393b173 100644 --- a/k8s/backup/backup_templates +++ b/k8s/backup/backup_templates @@ -6,7 +6,7 @@ BACKUP_ENABLED=$(echo "$MANIFEST_BACKUP" | jq -r .ENABLED) TYPE=$(echo "$MANIFEST_BACKUP" | jq -r .TYPE) if [[ "$BACKUP_ENABLED" == "false" || "$BACKUP_ENABLED" == "null" ]]; then - echo "No manifest backup enabled. Skipping manifest backup" + echo "📋 Manifest backup is disabled, skipping" return fi @@ -40,7 +40,14 @@ case "$TYPE" in source "$SERVICE_PATH/backup/s3" --action="$ACTION" --files "${FILES[@]}" ;; *) - echo "Error: Unsupported manifest backup type type '$TYPE'" + echo "❌ Unsupported manifest backup type: '$TYPE'" + echo "" + echo "💡 Possible causes:" + echo " The MANIFEST_BACKUP.TYPE configuration is invalid" + echo "" + echo "🔧 How to fix:" + echo " • Set MANIFEST_BACKUP.TYPE to 's3' in values.yaml" + echo "" exit 1 ;; -esac \ No newline at end of file +esac diff --git a/k8s/backup/s3 b/k8s/backup/s3 index 8435804e..74ec4558 100644 --- a/k8s/backup/s3 +++ b/k8s/backup/s3 @@ -26,11 +26,16 @@ done BUCKET=$(echo "$MANIFEST_BACKUP" | jq -r .BUCKET) PREFIX=$(echo "$MANIFEST_BACKUP" | jq -r .PREFIX) -echo "[INFO] Initializing S3 manifest backup operation - Action: $ACTION | Bucket: $BUCKET | Prefix: $PREFIX | Files: ${#FILES[@]}" +echo "📝 Starting S3 manifest backup..." +echo "📋 Action: $ACTION" +echo "📋 Bucket: $BUCKET" +echo "📋 Prefix: $PREFIX" +echo "📋 Files: ${#FILES[@]}" +echo "" # Now you can iterate over the files for file in "${FILES[@]}"; do - echo "[DEBUG] Processing manifest file: $file" + echo "📝 Processing: $(basename "$file")" # Extract the path after 'output/' and remove the action folder (apply/delete) # Example: /root/.np/services/k8s/output/1862688057-34121609/apply/secret-1862688057-34121609.yaml @@ -54,34 +59,60 @@ for file in "${FILES[@]}"; do if [[ "$ACTION" == "apply" ]]; then - echo "[INFO] Uploading manifest to S3: s3://$BUCKET/$s3_key" + echo " 📡 Uploading to s3://$BUCKET/$s3_key" # Upload to S3 - if aws s3 cp --region "$REGION" "$file" "s3://$BUCKET/$s3_key"; then - echo "[SUCCESS] Manifest upload completed successfully: $file" + if aws s3 cp --region "$REGION" "$file" "s3://$BUCKET/$s3_key" >/dev/null; then + echo " ✅ Upload successful" else - echo "[ERROR] Manifest upload failed: $file" >&2 + echo " ❌ Upload failed" + echo "" + echo "💡 Possible causes:" + echo " • S3 bucket does not exist or is not accessible" + echo " • IAM permissions are missing for s3:PutObject" + echo "" + echo "🔧 How to fix:" + echo " • Verify bucket '$BUCKET' exists and is accessible" + echo " • Check IAM permissions for the agent" + echo "" exit 1 fi elif [[ "$ACTION" == "delete" ]]; then - echo "[INFO] Removing manifest from S3: s3://$BUCKET/$s3_key" + echo " 📡 Deleting s3://$BUCKET/$s3_key" # Delete from S3 with error handling aws_output=$(aws s3 rm --region "$REGION" "s3://$BUCKET/$s3_key" 2>&1) aws_exit_code=$? if [[ $aws_exit_code -eq 0 ]]; then - echo "[SUCCESS] Manifest deletion completed successfully: s3://$BUCKET/$s3_key" + echo " ✅ Deletion successful" elif [[ "$aws_output" == *"NoSuchKey"* ]] || [[ "$aws_output" == *"Not Found"* ]]; then - echo "[WARN] Manifest not found in S3, skipping deletion: s3://$BUCKET/$s3_key" + echo " 📋 File not found in S3, skipping" else - echo "[ERROR] Manifest deletion failed: s3://$BUCKET/$s3_key - $aws_output" >&2 + echo " ❌ Deletion failed" + echo "📋 AWS Error: $aws_output" + echo "" + echo "💡 Possible causes:" + echo " • S3 bucket does not exist or is not accessible" + echo " • IAM permissions are missing for s3:DeleteObject" + echo "" + echo "🔧 How to fix:" + echo " • Verify bucket '$BUCKET' exists and is accessible" + echo " • Check IAM permissions for the agent" + echo "" exit 1 fi else - echo "[ERROR] Invalid action specified: $ACTION" >&2 + echo "❌ Invalid action: '$ACTION'" + echo "" + echo "💡 Possible causes:" + echo " The action parameter must be 'apply' or 'delete'" + echo "" exit 1 fi -done \ No newline at end of file +done + +echo "" +echo "✨ S3 backup operation completed successfully" diff --git a/k8s/backup/tests/backup_templates.bats b/k8s/backup/tests/backup_templates.bats new file mode 100644 index 00000000..8619dbc9 --- /dev/null +++ b/k8s/backup/tests/backup_templates.bats @@ -0,0 +1,174 @@ +#!/usr/bin/env bats +# ============================================================================= +# Unit tests for backup/backup_templates - manifest backup orchestration +# ============================================================================= + +setup() { + # Get project root directory + export PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" + + # Source assertions + source "$PROJECT_ROOT/testing/assertions.sh" + + # Set required environment variables + export SERVICE_PATH="$PROJECT_ROOT/k8s" +} + +teardown() { + unset MANIFEST_BACKUP + unset SERVICE_PATH +} + +# ============================================================================= +# Test: Skips when backup is disabled (false) +# ============================================================================= +@test "backup_templates: skips when BACKUP_ENABLED is false" { + export MANIFEST_BACKUP='{"ENABLED":"false","TYPE":"s3"}' + + # Use a subshell to capture the return statement behavior + run bash -c ' + source "$SERVICE_PATH/backup/backup_templates" --action=apply --files /tmp/test.yaml + ' + + assert_equal "$status" "0" + assert_equal "$output" "📋 Manifest backup is disabled, skipping" +} + +# ============================================================================= +# Test: Skips when backup is disabled (null) +# ============================================================================= +@test "backup_templates: skips when BACKUP_ENABLED is null" { + export MANIFEST_BACKUP='{"TYPE":"s3"}' + + run bash -c ' + source "$SERVICE_PATH/backup/backup_templates" --action=apply --files /tmp/test.yaml + ' + + assert_equal "$status" "0" + assert_equal "$output" "📋 Manifest backup is disabled, skipping" +} + +# ============================================================================= +# Test: Skips when MANIFEST_BACKUP is empty +# ============================================================================= +@test "backup_templates: skips when MANIFEST_BACKUP is empty" { + export MANIFEST_BACKUP='{}' + + run bash -c ' + source "$SERVICE_PATH/backup/backup_templates" --action=apply --files /tmp/test.yaml + ' + + assert_equal "$status" "0" + assert_equal "$output" "📋 Manifest backup is disabled, skipping" +} + +# ============================================================================= +# Test: Fails with unsupported backup type - Error message +# ============================================================================= +@test "backup_templates: fails with unsupported backup type error" { + export MANIFEST_BACKUP='{"ENABLED":"true","TYPE":"gcs"}' + + run bash "$SERVICE_PATH/backup/backup_templates" --action=apply --files /tmp/test.yaml + + assert_equal "$status" "1" + assert_contains "$output" "❌ Unsupported manifest backup type: 'gcs'" + assert_contains "$output" "💡 Possible causes:" + assert_contains "$output" "MANIFEST_BACKUP.TYPE configuration is invalid" + assert_contains "$output" "🔧 How to fix:" + assert_contains "$output" "• Set MANIFEST_BACKUP.TYPE to 's3' in values.yaml" +} + +# ============================================================================= +# Test: Parses action argument correctly +# ============================================================================= +@test "backup_templates: parses action argument" { + export MANIFEST_BACKUP='{"ENABLED":"true","TYPE":"s3","BUCKET":"test","PREFIX":"manifests"}' + + # Mock aws to avoid actual calls + aws() { + return 0 + } + export -f aws + export REGION="us-east-1" + + run bash -c ' + source "$SERVICE_PATH/backup/backup_templates" --action=apply --files /tmp/output/123/apply/test.yaml + ' + + assert_contains "$output" "📋 Action: apply" +} + +# ============================================================================= +# Test: Parses files argument correctly +# ============================================================================= +@test "backup_templates: parses files argument" { + export MANIFEST_BACKUP='{"ENABLED":"true","TYPE":"s3","BUCKET":"test","PREFIX":"manifests"}' + + # Mock aws to avoid actual calls + aws() { + return 0 + } + export -f aws + export REGION="us-east-1" + + run bash -c ' + source "$SERVICE_PATH/backup/backup_templates" --action=apply --files /tmp/output/123/apply/file1.yaml /tmp/output/123/apply/file2.yaml + ' + + assert_contains "$output" "📋 Files: 2" +} + +# ============================================================================= +# Test: Calls s3 backup for s3 type +# ============================================================================= +@test "backup_templates: calls s3 backup for s3 type" { + export MANIFEST_BACKUP='{"ENABLED":"true","TYPE":"s3","BUCKET":"my-bucket","PREFIX":"backups"}' + + # Mock aws to avoid actual calls + aws() { + return 0 + } + export -f aws + export REGION="us-east-1" + + run bash -c ' + source "$SERVICE_PATH/backup/backup_templates" --action=apply --files /tmp/output/123/apply/test.yaml + ' + + assert_equal "$status" "0" + assert_contains "$output" "📝 Starting S3 manifest backup..." +} + +@test "backup_templates: shows bucket name when calling s3" { + export MANIFEST_BACKUP='{"ENABLED":"true","TYPE":"s3","BUCKET":"my-bucket","PREFIX":"backups"}' + + aws() { + return 0 + } + export -f aws + export REGION="us-east-1" + + run bash -c ' + source "$SERVICE_PATH/backup/backup_templates" --action=apply --files /tmp/output/123/apply/test.yaml + ' + + assert_equal "$status" "0" + assert_contains "$output" "📋 Bucket: my-bucket" +} + +@test "backup_templates: shows prefix when calling s3" { + export MANIFEST_BACKUP='{"ENABLED":"true","TYPE":"s3","BUCKET":"my-bucket","PREFIX":"backups"}' + + aws() { + return 0 + } + export -f aws + export REGION="us-east-1" + + run bash -c ' + source "$SERVICE_PATH/backup/backup_templates" --action=apply --files /tmp/output/123/apply/test.yaml + ' + + assert_equal "$status" "0" + assert_contains "$output" "📋 Prefix: backups" +} diff --git a/k8s/backup/tests/s3.bats b/k8s/backup/tests/s3.bats new file mode 100644 index 00000000..be9d58c3 --- /dev/null +++ b/k8s/backup/tests/s3.bats @@ -0,0 +1,299 @@ +#!/usr/bin/env bats +# ============================================================================= +# Unit tests for backup/s3 - S3 manifest backup operations +# ============================================================================= + +setup() { + # Get project root directory + export PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" + + # Source assertions + source "$PROJECT_ROOT/testing/assertions.sh" + + # Set required environment variables + export SERVICE_PATH="$PROJECT_ROOT/k8s" + export REGION="us-east-1" + export MANIFEST_BACKUP='{"ENABLED":"true","TYPE":"s3","BUCKET":"test-bucket","PREFIX":"manifests"}' + + # Create temp files for testing + export TEST_DIR="$(mktemp -d)" + mkdir -p "$TEST_DIR/output/scope-123/apply" + echo "test content" > "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + # Mock aws CLI by default (success) + aws() { + return 0 + } + export -f aws +} + +teardown() { + rm -rf "$TEST_DIR" + unset MANIFEST_BACKUP + unset SERVICE_PATH + unset REGION + unset -f aws +} + +# ============================================================================= +# Test: Displays starting message +# ============================================================================= +@test "s3: displays starting message with emoji" { + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + assert_equal "$status" "0" + assert_contains "$output" "📝 Starting S3 manifest backup..." +} + +# ============================================================================= +# Test: Extracts bucket from MANIFEST_BACKUP +# ============================================================================= +@test "s3: extracts bucket from MANIFEST_BACKUP" { + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + assert_contains "$output" "📋 Bucket: test-bucket" +} + +# ============================================================================= +# Test: Extracts prefix from MANIFEST_BACKUP +# ============================================================================= +@test "s3: extracts prefix from MANIFEST_BACKUP" { + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + assert_contains "$output" "📋 Prefix: manifests" +} + +# ============================================================================= +# Test: Shows file count +# ============================================================================= +@test "s3: shows file count" { + echo "test" > "$TEST_DIR/output/scope-123/apply/service.yaml" + + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" "$TEST_DIR/output/scope-123/apply/service.yaml" + + assert_contains "$output" "📋 Files: 2" +} + +# ============================================================================= +# Test: Shows action +# ============================================================================= +@test "s3: shows action with emoji" { + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + assert_contains "$output" "📋 Action: apply" +} + +# ============================================================================= +# Test: Uploads file on apply action +# ============================================================================= +@test "s3: uploads file on apply action" { + local aws_called=false + aws() { + if [[ "$1" == "s3" && "$2" == "cp" ]]; then + aws_called=true + fi + return 0 + } + export -f aws + + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + [ "$status" -eq 0 ] + assert_contains "$output" "📝 Processing:" + assert_contains "$output" "📡 Uploading to" + assert_contains "$output" "✅ Upload successful" +} + +# ============================================================================= +# Test: Deletes file on delete action +# ============================================================================= +@test "s3: deletes file on delete action" { + mkdir -p "$TEST_DIR/output/scope-123/delete" + echo "test" > "$TEST_DIR/output/scope-123/delete/deployment.yaml" + + aws() { + if [[ "$1" == "s3" && "$2" == "rm" ]]; then + return 0 + fi + return 0 + } + export -f aws + + run bash "$SERVICE_PATH/backup/s3" --action=delete --files "$TEST_DIR/output/scope-123/delete/deployment.yaml" + + [ "$status" -eq 0 ] + assert_contains "$output" "📡 Deleting" + assert_contains "$output" "✅ Deletion successful" +} + +# ============================================================================= +# Test: Handles NoSuchKey error gracefully on delete +# ============================================================================= +@test "s3: handles NoSuchKey error gracefully on delete" { + mkdir -p "$TEST_DIR/output/scope-123/delete" + echo "test" > "$TEST_DIR/output/scope-123/delete/deployment.yaml" + + aws() { + if [[ "$1" == "s3" && "$2" == "rm" ]]; then + echo "An error occurred (NoSuchKey) when calling the DeleteObject operation" + return 1 + fi + return 0 + } + export -f aws + + run bash "$SERVICE_PATH/backup/s3" --action=delete --files "$TEST_DIR/output/scope-123/delete/deployment.yaml" + + [ "$status" -eq 0 ] + assert_contains "$output" "📋 File not found in S3, skipping" +} + +# ============================================================================= +# Test: Handles Not Found error gracefully on delete +# ============================================================================= +@test "s3: handles Not Found error gracefully on delete" { + mkdir -p "$TEST_DIR/output/scope-123/delete" + echo "test" > "$TEST_DIR/output/scope-123/delete/deployment.yaml" + + aws() { + if [[ "$1" == "s3" && "$2" == "rm" ]]; then + echo "Not Found" + return 1 + fi + return 0 + } + export -f aws + + run bash "$SERVICE_PATH/backup/s3" --action=delete --files "$TEST_DIR/output/scope-123/delete/deployment.yaml" + + [ "$status" -eq 0 ] + assert_contains "$output" "📋 File not found in S3, skipping" +} + +# ============================================================================= +# Test: Fails on upload error - Error message +# ============================================================================= +@test "s3: fails on upload error with error message" { + aws() { + if [[ "$1" == "s3" && "$2" == "cp" ]]; then + return 1 + fi + return 0 + } + export -f aws + + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + [ "$status" -eq 1 ] + + assert_contains "$output" "❌ Upload failed" + assert_contains "$output" "💡 Possible causes:" + assert_contains "$output" "• S3 bucket does not exist or is not accessible" + assert_contains "$output" "• IAM permissions are missing for s3:PutObject" + assert_contains "$output" "🔧 How to fix:" + assert_contains "$output" "• Verify bucket 'test-bucket' exists and is accessible" + assert_contains "$output" "• Check IAM permissions for the agent" +} + +# ============================================================================= +# Test: Fails on delete error (non-NoSuchKey) - Error message +# ============================================================================= +@test "s3: fails on delete error with error message" { + mkdir -p "$TEST_DIR/output/scope-123/delete" + echo "test" > "$TEST_DIR/output/scope-123/delete/deployment.yaml" + + aws() { + if [[ "$1" == "s3" && "$2" == "rm" ]]; then + echo "Access Denied" + return 1 + fi + return 0 + } + export -f aws + + run bash "$SERVICE_PATH/backup/s3" --action=delete --files "$TEST_DIR/output/scope-123/delete/deployment.yaml" + + [ "$status" -eq 1 ] + assert_contains "$output" "❌ Deletion failed" + assert_contains "$output" "💡 Possible causes:" + assert_contains "$output" "• S3 bucket does not exist or is not accessible" + assert_contains "$output" "• IAM permissions are missing for s3:DeleteObject" + assert_contains "$output" "🔧 How to fix:" + assert_contains "$output" "• Verify bucket 'test-bucket' exists and is accessible" + assert_contains "$output" "• Check IAM permissions for the agent" +} + +# ============================================================================= +# Test: Fails on invalid action - Error message +# ============================================================================= +@test "s3: fails on invalid action with error message" { + run bash "$SERVICE_PATH/backup/s3" --action=invalid --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + [ "$status" -eq 1 ] + assert_contains "$output" "❌ Invalid action: 'invalid'" + assert_contains "$output" "💡 Possible causes:" + assert_contains "$output" "The action parameter must be 'apply' or 'delete'" +} + +# ============================================================================= +# Test: Constructs correct S3 path +# ============================================================================= +@test "s3: constructs correct S3 path from file path" { + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + # S3 path should be: manifests/scope-123/deployment.yaml + assert_contains "$output" "manifests/scope-123/deployment.yaml" +} + +# ============================================================================= +# Test: Shows success summary +# ============================================================================= +@test "s3: shows success summary" { + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + [ "$status" -eq 0 ] + assert_contains "$output" "✨ S3 backup operation completed successfully" +} + +# ============================================================================= +# Test: Processes multiple files +# ============================================================================= +@test "s3: processes multiple files" { + echo "test" > "$TEST_DIR/output/scope-123/apply/service.yaml" + echo "test" > "$TEST_DIR/output/scope-123/apply/secret.yaml" + + local upload_count=0 + aws() { + if [[ "$1" == "s3" && "$2" == "cp" ]]; then + upload_count=$((upload_count + 1)) + fi + return 0 + } + export -f aws + + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" "$TEST_DIR/output/scope-123/apply/service.yaml" "$TEST_DIR/output/scope-123/apply/secret.yaml" + + [ "$status" -eq 0 ] + assert_contains "$output" "📋 Files: 3" +} + + +# ============================================================================= +# Test: Uses REGION environment variable +# ============================================================================= +@test "s3: uses REGION environment variable" { + local region_used="" + aws() { + for arg in "$@"; do + if [[ "$arg" == "us-east-1" ]]; then + region_used="us-east-1" + fi + done + return 0 + } + export -f aws + + run bash "$SERVICE_PATH/backup/s3" --action=apply --files "$TEST_DIR/output/scope-123/apply/deployment.yaml" + + [ "$status" -eq 0 ] +}