diff --git a/.github/workflows/test-bundle.yml b/.github/workflows/test-bundle.yml index 82fac05..03d514a 100644 --- a/.github/workflows/test-bundle.yml +++ b/.github/workflows/test-bundle.yml @@ -50,17 +50,26 @@ jobs: - name: Verify resources run: | awslocal s3 ls | grep bundle-test-artifacts + awslocal s3 ls | grep bundle-test-logs awslocal sqs list-queues | grep bundle-test-events + awslocal sqs list-queues | grep bundle-test-events-dlq awslocal ssm get-parameter --name /bundle-test/version --query Parameter.Value --output text + awslocal ssm get-parameter --name /bundle-test/config --with-decryption --query Parameter.Value --output text + awslocal dynamodb describe-table --table-name bundle-test-jobs --query Table.TableStatus --output text + awslocal lambda get-function --function-name bundle-test-processor --query Configuration.State --output text + awslocal iam get-role --role-name bundle-test-lambda-exec --query Role.RoleName --output text - name: Submit proof bundle run: | curl -fsSL "https://raw.githubusercontent.com/whummer/localstack-utils/refs/heads/main/cli-extensions/bin/ls-bundle" -o ls-bundle && chmod +x ls-bundle ./ls-bundle submit --project test-whu1 \ --summary "Terraform infra deployed and verified" \ - --assertion "S3 bucket created=pass" \ - --assertion "SQS queue created=pass" \ - --assertion "SSM parameter created=pass" + --assertion "S3 buckets created=pass" \ + --assertion "SQS queue + DLQ created=pass" \ + --assertion "SSM parameters created=pass" \ + --assertion "DynamoDB table created=pass" \ + --assertion "Lambda function deployed=pass" \ + --assertion "IAM role and policies created=pass" - name: Print LocalStack logs if: always() diff --git a/cli-extensions/bundle-test/main.tf b/cli-extensions/bundle-test/main.tf index 6579136..d67032a 100644 --- a/cli-extensions/bundle-test/main.tf +++ b/cli-extensions/bundle-test/main.tf @@ -16,16 +16,175 @@ provider "aws" { skip_requesting_account_id = true } +# ── S3 ──────────────────────────────────────────────────────────────────────── + resource "aws_s3_bucket" "artifacts" { bucket = "bundle-test-artifacts" } +resource "aws_s3_bucket" "logs" { + bucket = "bundle-test-logs" +} + +resource "aws_s3_bucket_versioning" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + versioning_configuration { + status = "Enabled" + } +} + +# ── SQS ─────────────────────────────────────────────────────────────────────── + resource "aws_sqs_queue" "events" { - name = "bundle-test-events" + name = "bundle-test-events" + visibility_timeout_seconds = 30 + message_retention_seconds = 86400 +} + +resource "aws_sqs_queue" "events_dlq" { + name = "bundle-test-events-dlq" } +resource "aws_sqs_queue_redrive_policy" "events" { + queue_url = aws_sqs_queue.events.id + redrive_policy = jsonencode({ + deadLetterTargetArn = aws_sqs_queue.events_dlq.arn + maxReceiveCount = 3 + }) +} + +# ── SSM ─────────────────────────────────────────────────────────────────────── + resource "aws_ssm_parameter" "version" { name = "/bundle-test/version" type = "String" value = "1.0.0" } + +resource "aws_ssm_parameter" "config" { + name = "/bundle-test/config" + type = "SecureString" + value = jsonencode({ env = "ci", debug = false }) +} + +# ── DynamoDB ────────────────────────────────────────────────────────────────── + +resource "aws_dynamodb_table" "jobs" { + name = "bundle-test-jobs" + billing_mode = "PAY_PER_REQUEST" + hash_key = "jobId" + range_key = "createdAt" + + attribute { + name = "jobId" + type = "S" + } + + attribute { + name = "createdAt" + type = "S" + } + + ttl { + attribute_name = "expiresAt" + enabled = true + } +} + +# ── Lambda ──────────────────────────────────────────────────────────────────── + +data "archive_file" "handler" { + type = "zip" + output_path = "${path.module}/handler.zip" + source { + content = "def handler(event, context): return {'statusCode': 200}" + filename = "handler.py" + } +} + +resource "aws_lambda_function" "processor" { + function_name = "bundle-test-processor" + role = aws_iam_role.lambda_exec.arn + runtime = "python3.11" + handler = "handler.handler" + filename = data.archive_file.handler.output_path + source_code_hash = data.archive_file.handler.output_base64sha256 +} + +resource "aws_lambda_event_source_mapping" "sqs_trigger" { + event_source_arn = aws_sqs_queue.events.arn + function_name = aws_lambda_function.processor.arn + batch_size = 5 +} + +# ── IAM ─────────────────────────────────────────────────────────────────────── + +resource "aws_iam_role" "lambda_exec" { + name = "bundle-test-lambda-exec" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { Service = "lambda.amazonaws.com" } + Action = "sts:AssumeRole" + }] + }) +} + +resource "aws_iam_policy" "lambda_sqs" { + name = "bundle-test-lambda-sqs" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"] + Resource = aws_sqs_queue.events.arn + }, + { + Effect = "Allow" + Action = ["sqs:SendMessage"] + Resource = aws_sqs_queue.events_dlq.arn + } + ] + }) +} + +resource "aws_iam_policy" "lambda_dynamo" { + name = "bundle-test-lambda-dynamo" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = ["dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:UpdateItem", "dynamodb:Query"] + Resource = aws_dynamodb_table.jobs.arn + }] + }) +} + +resource "aws_iam_policy" "lambda_s3" { + name = "bundle-test-lambda-s3" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = ["s3:PutObject", "s3:GetObject"] + Resource = "${aws_s3_bucket.artifacts.arn}/*" + }] + }) +} + +resource "aws_iam_role_policy_attachment" "lambda_sqs" { + role = aws_iam_role.lambda_exec.name + policy_arn = aws_iam_policy.lambda_sqs.arn +} + +resource "aws_iam_role_policy_attachment" "lambda_dynamo" { + role = aws_iam_role.lambda_exec.name + policy_arn = aws_iam_policy.lambda_dynamo.arn +} + +resource "aws_iam_role_policy_attachment" "lambda_s3" { + role = aws_iam_role.lambda_exec.name + policy_arn = aws_iam_policy.lambda_s3.arn +}