second first commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Keep host-side Terraform working state out of the image. CI builds from a fresh
|
||||
# checkout where none of this exists (.terraform/ and .terraform.lock.hcl are
|
||||
# gitignored), so copying a local run's leftovers in would make the image behave
|
||||
# differently from CI and quietly skew any timing comparison.
|
||||
.terraform
|
||||
.terraform.lock.hcl
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
errored.tfstate
|
||||
test-results
|
||||
large-object.bin
|
||||
@@ -0,0 +1,27 @@
|
||||
FROM hashicorp/terraform:1.14.7 AS terraform
|
||||
|
||||
FROM amazon/aws-cli:2.34.32
|
||||
|
||||
# Copy terraform binary from official Terraform image
|
||||
COPY --from=terraform /bin/terraform /usr/local/bin/terraform
|
||||
|
||||
RUN yum install -y git jq
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install bats and helpers
|
||||
RUN git clone --depth 1 https://github.com/bats-core/bats-core.git /opt/bats-core \
|
||||
&& git clone --depth 1 https://github.com/bats-core/bats-support.git /opt/bats-support \
|
||||
&& git clone --depth 1 https://github.com/bats-core/bats-assert.git /opt/bats-assert
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV FLOCI_ENDPOINT=http://floci:4566
|
||||
ENV AWS_ENDPOINT_URL=http://floci:4566
|
||||
ENV AWS_ACCESS_KEY_ID=test
|
||||
ENV AWS_SECRET_ACCESS_KEY=test
|
||||
ENV AWS_DEFAULT_REGION=us-east-1
|
||||
ENV BATS_LIB_PATH=/opt
|
||||
|
||||
RUN chmod +x /app/run-bats-in-container.sh && mkdir -p /results
|
||||
ENTRYPOINT ["./run-bats-in-container.sh"]
|
||||
@@ -0,0 +1,13 @@
|
||||
bucket = "tfstate"
|
||||
key = "floci-compat.tfstate"
|
||||
region = "us-east-1"
|
||||
|
||||
endpoint = "http://localhost:4566"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
skip_credentials_validation = true
|
||||
skip_region_validation = true
|
||||
use_path_style = true
|
||||
|
||||
dynamodb_endpoint = "http://localhost:4566"
|
||||
dynamodb_table = "tflock"
|
||||
@@ -0,0 +1,882 @@
|
||||
# NOTE: Keep resource definitions in sync with ../compat-opentofu/main.tf.
|
||||
# Enforced by .github/ci/compat-opentofu-drift-check.py -- a resource added
|
||||
# here and not there (or vice versa) fails CI unless listed in
|
||||
# .github/ci/compat-opentofu-allowlist-tf-only.txt as a deliberate gap.
|
||||
|
||||
# -- S3 Bucket ------------------------------------------------------------------
|
||||
resource "aws_s3_bucket" "app" {
|
||||
bucket = "floci-compat-app"
|
||||
force_destroy = true
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_versioning" "app" {
|
||||
bucket = aws_s3_bucket.app.id
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
# A 12 MiB file with a declared checksum algorithm: the provider uploads it through the SDK transfer
|
||||
# manager as a multipart upload (5 MiB parts), so S3 stores a COMPOSITE checksum and a multipart ETag.
|
||||
# The file is generated by the test setup (see test/*.bats) and is not committed.
|
||||
resource "aws_s3_object" "large" {
|
||||
bucket = aws_s3_bucket.app.id
|
||||
key = "large/multipart.bin"
|
||||
source = "${path.module}/large-object.bin"
|
||||
checksum_algorithm = "SHA256"
|
||||
|
||||
depends_on = [aws_s3_bucket_versioning.app]
|
||||
}
|
||||
|
||||
# -- SQS Queue -----------------------------------------------------------------
|
||||
resource "aws_sqs_queue" "jobs" {
|
||||
name = "floci-compat-jobs"
|
||||
visibility_timeout_seconds = 30
|
||||
message_retention_seconds = 86400
|
||||
}
|
||||
|
||||
resource "aws_sqs_queue" "jobs_dlq" {
|
||||
name = "floci-compat-jobs-dlq"
|
||||
}
|
||||
|
||||
resource "aws_sqs_queue_redrive_policy" "jobs" {
|
||||
queue_url = aws_sqs_queue.jobs.id
|
||||
redrive_policy = jsonencode({
|
||||
deadLetterTargetArn = aws_sqs_queue.jobs_dlq.arn
|
||||
maxReceiveCount = 3
|
||||
})
|
||||
}
|
||||
|
||||
# -- SNS Topic -----------------------------------------------------------------
|
||||
resource "aws_sns_topic" "events" {
|
||||
name = "floci-compat-events"
|
||||
}
|
||||
|
||||
resource "aws_sns_topic_subscription" "events_to_sqs" {
|
||||
topic_arn = aws_sns_topic.events.arn
|
||||
protocol = "sqs"
|
||||
endpoint = aws_sqs_queue.jobs.arn
|
||||
}
|
||||
|
||||
# -- DynamoDB Table -------------------------------------------------------------
|
||||
resource "aws_dynamodb_table" "items" {
|
||||
name = "floci-compat-items"
|
||||
billing_mode = "PAY_PER_REQUEST"
|
||||
hash_key = "pk"
|
||||
range_key = "sk"
|
||||
|
||||
attribute {
|
||||
name = "pk"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
attribute {
|
||||
name = "sk"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
ttl {
|
||||
attribute_name = "expires_at"
|
||||
enabled = true
|
||||
}
|
||||
|
||||
tags = {
|
||||
Environment = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
# -- IAM Role (for Lambda) -----------------------------------------------------
|
||||
data "aws_iam_policy_document" "lambda_assume_role" {
|
||||
statement {
|
||||
actions = ["sts:AssumeRole"]
|
||||
effect = "Allow"
|
||||
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["lambda.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "lambda_exec" {
|
||||
name = "floci-compat-lambda-exec"
|
||||
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
|
||||
}
|
||||
|
||||
# -- ECR Repository ------------------------------------------------------------
|
||||
resource "aws_ecr_repository" "app" {
|
||||
name = "floci-compat-app"
|
||||
image_tag_mutability = "MUTABLE"
|
||||
|
||||
image_scanning_configuration {
|
||||
scan_on_push = false
|
||||
}
|
||||
}
|
||||
|
||||
# -- ECS Cluster ---------------------------------------------------------------
|
||||
resource "aws_ecs_cluster" "app" {
|
||||
name = "floci-compat-cluster"
|
||||
}
|
||||
|
||||
# -- KMS Key -------------------------------------------------------------------
|
||||
resource "aws_kms_key" "compat" {
|
||||
description = "Floci Terraform compatibility key"
|
||||
deletion_window_in_days = 7
|
||||
enable_key_rotation = false
|
||||
}
|
||||
|
||||
# -- Kinesis Stream ------------------------------------------------------------
|
||||
resource "aws_kinesis_stream" "events" {
|
||||
name = "floci-compat-events"
|
||||
shard_count = 1
|
||||
retention_period = 24
|
||||
}
|
||||
|
||||
# -- CloudWatch Log Group ------------------------------------------------------
|
||||
resource "aws_cloudwatch_log_group" "app" {
|
||||
name = "/floci/compat/app"
|
||||
retention_in_days = 1
|
||||
|
||||
tags = {
|
||||
Environment = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
# -- EventBridge Event Bus -----------------------------------------------------
|
||||
resource "aws_cloudwatch_event_bus" "compat" {
|
||||
name = "floci-compat-bus"
|
||||
}
|
||||
|
||||
# -- Step Functions State Machine ----------------------------------------------
|
||||
resource "aws_sfn_state_machine" "compat" {
|
||||
name = "floci-compat-state-machine"
|
||||
role_arn = aws_iam_role.lambda_exec.arn
|
||||
definition = jsonencode({
|
||||
Comment = "Floci Terraform compatibility state machine"
|
||||
StartAt = "Pass"
|
||||
States = {
|
||||
Pass = {
|
||||
Type = "Pass"
|
||||
End = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
# -- CloudFormation Stack ------------------------------------------------------
|
||||
resource "aws_cloudformation_stack" "compat" {
|
||||
name = "floci-compat-stack"
|
||||
template_body = jsonencode({
|
||||
Resources = {
|
||||
CompatBucket = {
|
||||
Type = "AWS::S3::Bucket"
|
||||
Properties = {
|
||||
BucketName = "floci-compat-cfn-bucket"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
# -- EKS Cluster ---------------------------------------------------------------
|
||||
resource "aws_eks_cluster" "compat" {
|
||||
name = "floci-compat-eks"
|
||||
role_arn = aws_iam_role.lambda_exec.arn
|
||||
|
||||
vpc_config {
|
||||
subnet_ids = [aws_subnet.compat.id]
|
||||
}
|
||||
}
|
||||
|
||||
# -- API Gateway v2 ------------------------------------------------------------
|
||||
resource "aws_apigatewayv2_api" "compat" {
|
||||
name = "floci-compat-http-api"
|
||||
protocol_type = "HTTP"
|
||||
}
|
||||
|
||||
# -- AppConfig Application -----------------------------------------------------
|
||||
resource "aws_appconfig_application" "compat" {
|
||||
name = "floci-compat-appconfig"
|
||||
description = "Floci Terraform compatibility application"
|
||||
}
|
||||
|
||||
# -- AWS Backup Vault ----------------------------------------------------------
|
||||
resource "aws_backup_vault" "compat" {
|
||||
name = "floci-compat-vault"
|
||||
}
|
||||
|
||||
# -- Cloud Map Namespace -------------------------------------------------------
|
||||
resource "aws_service_discovery_private_dns_namespace" "compat" {
|
||||
name = "floci-compat.internal"
|
||||
description = "Floci Terraform compatibility namespace"
|
||||
vpc = aws_vpc.compat.id
|
||||
}
|
||||
|
||||
# -- CodeDeploy Application ----------------------------------------------------
|
||||
resource "aws_codedeploy_app" "compat" {
|
||||
name = "floci-compat-codedeploy"
|
||||
compute_platform = "Server"
|
||||
}
|
||||
|
||||
# -- ACM Certificate -----------------------------------------------------------
|
||||
resource "aws_acm_certificate" "compat" {
|
||||
domain_name = "floci-compat.internal"
|
||||
validation_method = "DNS"
|
||||
}
|
||||
|
||||
# -- SES Email Identity --------------------------------------------------------
|
||||
resource "aws_ses_email_identity" "compat" {
|
||||
email = "terraform@floci-compat.internal"
|
||||
}
|
||||
|
||||
# -- EventBridge Scheduler -----------------------------------------------------
|
||||
resource "aws_scheduler_schedule" "compat" {
|
||||
name = "floci-compat-schedule"
|
||||
schedule_expression = "rate(1 hour)"
|
||||
schedule_expression_timezone = "UTC"
|
||||
|
||||
flexible_time_window {
|
||||
mode = "OFF"
|
||||
}
|
||||
|
||||
target {
|
||||
arn = aws_sqs_queue.jobs.arn
|
||||
role_arn = aws_iam_role.lambda_exec.arn
|
||||
}
|
||||
}
|
||||
|
||||
# -- WAFv2 Web ACL -------------------------------------------------------------
|
||||
resource "aws_wafv2_web_acl" "compat" {
|
||||
name = "floci-compat-waf"
|
||||
scope = "REGIONAL"
|
||||
|
||||
default_action {
|
||||
allow {}
|
||||
}
|
||||
|
||||
visibility_config {
|
||||
cloudwatch_metrics_enabled = false
|
||||
metric_name = "flociCompatWaf"
|
||||
sampled_requests_enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
# -- Cost and Usage Report -----------------------------------------------------
|
||||
resource "aws_cur_report_definition" "compat" {
|
||||
report_name = "floci-compat-report"
|
||||
time_unit = "HOURLY"
|
||||
format = "Parquet"
|
||||
compression = "Parquet"
|
||||
additional_schema_elements = ["RESOURCES"]
|
||||
s3_bucket = aws_s3_bucket.app.bucket
|
||||
s3_prefix = "cur"
|
||||
s3_region = "us-east-1"
|
||||
}
|
||||
|
||||
# -- AppSync GraphQL API -------------------------------------------------------
|
||||
resource "aws_appsync_graphql_api" "compat" {
|
||||
name = "floci-compat-graphql"
|
||||
authentication_type = "API_KEY"
|
||||
schema = "type Query { health: String }"
|
||||
}
|
||||
|
||||
# -- ElastiCache Replication Group ----------------------------------------------
|
||||
resource "aws_elasticache_replication_group" "compat" {
|
||||
replication_group_id = "floci-compat-cache"
|
||||
description = "Floci Terraform compatibility cache"
|
||||
engine = "redis"
|
||||
node_type = "cache.t3.micro"
|
||||
num_cache_clusters = 1
|
||||
# Deliberately not 6379: that is the bottom of floci's ElastiCache proxy range and the
|
||||
# port it would hand out anyway, so pinning it asserts nothing. A non-default port is
|
||||
# what actually exercises CreateReplicationGroup's Port input, and a mismatch shows up
|
||||
# as permanent drift because Terraform treats the port as replacement-forcing.
|
||||
#
|
||||
# It must also stay above the block the cluster-mode fixture allocates: the bats files
|
||||
# run concurrently and that fixture takes one port per node from the base of the range.
|
||||
port = 6395
|
||||
}
|
||||
|
||||
# -- Firehose Delivery Stream --------------------------------------------------
|
||||
resource "aws_kinesis_firehose_delivery_stream" "compat" {
|
||||
name = "floci-compat-firehose-basic"
|
||||
destination = "extended_s3"
|
||||
|
||||
extended_s3_configuration {
|
||||
role_arn = aws_iam_role.lambda_exec.arn
|
||||
bucket_arn = aws_s3_bucket.app.arn
|
||||
buffering_size = 5
|
||||
buffering_interval = 60
|
||||
compression_format = "UNCOMPRESSED"
|
||||
}
|
||||
}
|
||||
|
||||
# -- EventBridge Pipe ----------------------------------------------------------
|
||||
resource "aws_pipes_pipe" "compat" {
|
||||
name = "floci-compat-pipe"
|
||||
role_arn = aws_iam_role.lambda_exec.arn
|
||||
source = aws_sqs_queue.jobs.arn
|
||||
target = aws_sns_topic.events.arn
|
||||
|
||||
source_parameters {
|
||||
sqs_queue_parameters {
|
||||
batch_size = 1
|
||||
maximum_batching_window_in_seconds = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -- SSM Parameters ------------------------------------------------------------
|
||||
resource "aws_ssm_parameter" "db_url" {
|
||||
name = "/floci-compat/db-url"
|
||||
type = "String"
|
||||
value = "jdbc:postgresql://localhost:5432/app"
|
||||
}
|
||||
|
||||
resource "aws_ssm_parameter" "api_key" {
|
||||
name = "/floci-compat/api-key"
|
||||
type = "SecureString"
|
||||
value = "super-secret-key"
|
||||
}
|
||||
|
||||
# -- Secrets Manager -----------------------------------------------------------
|
||||
resource "aws_secretsmanager_secret" "db_creds" {
|
||||
name = "floci-compat/db-creds"
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret_version" "db_creds" {
|
||||
secret_id = aws_secretsmanager_secret.db_creds.id
|
||||
secret_string = jsonencode({
|
||||
username = "admin"
|
||||
password = "s3cret"
|
||||
})
|
||||
}
|
||||
|
||||
# -- RDS DB Instance -----------------------------------------------------------
|
||||
resource "aws_db_instance" "app" {
|
||||
identifier = "floci-compat-db"
|
||||
engine = "postgres"
|
||||
engine_version = "15"
|
||||
instance_class = "db.t3.micro"
|
||||
allocated_storage = 20
|
||||
username = "admin"
|
||||
password = "Password1!"
|
||||
skip_final_snapshot = true
|
||||
}
|
||||
|
||||
# -- Outputs -------------------------------------------------------------------
|
||||
output "bucket_id" {
|
||||
value = aws_s3_bucket.app.id
|
||||
}
|
||||
|
||||
output "queue_url" {
|
||||
value = aws_sqs_queue.jobs.url
|
||||
}
|
||||
|
||||
output "topic_arn" {
|
||||
value = aws_sns_topic.events.arn
|
||||
}
|
||||
|
||||
output "table_name" {
|
||||
value = aws_dynamodb_table.items.name
|
||||
}
|
||||
|
||||
output "secret_arn" {
|
||||
value = aws_secretsmanager_secret.db_creds.arn
|
||||
}
|
||||
|
||||
# -- Cognito User Pool ---------------------------------------------------------
|
||||
resource "aws_cognito_user_pool" "pool" {
|
||||
name = "floci-compat-pool"
|
||||
|
||||
password_policy {
|
||||
minimum_length = 12
|
||||
require_lowercase = true
|
||||
require_numbers = true
|
||||
require_symbols = true
|
||||
require_uppercase = true
|
||||
}
|
||||
|
||||
auto_verified_attributes = ["email"]
|
||||
username_attributes = ["email"]
|
||||
|
||||
admin_create_user_config {
|
||||
allow_admin_create_user_only = false
|
||||
}
|
||||
|
||||
verification_message_template {
|
||||
default_email_option = "CONFIRM_WITH_CODE"
|
||||
email_message = "Your code is {####}"
|
||||
email_subject = "Verify your account"
|
||||
}
|
||||
|
||||
account_recovery_setting {
|
||||
recovery_mechanism {
|
||||
name = "verified_email"
|
||||
priority = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output "user_pool_id" {
|
||||
value = aws_cognito_user_pool.pool.id
|
||||
}
|
||||
|
||||
output "user_pool_arn" {
|
||||
value = aws_cognito_user_pool.pool.arn
|
||||
}
|
||||
|
||||
# -- Cognito User Pool Client --------------------------------------------------
|
||||
resource "aws_cognito_user_pool_client" "client" {
|
||||
name = "floci-compat-pool-client"
|
||||
user_pool_id = aws_cognito_user_pool.pool.id
|
||||
}
|
||||
|
||||
# -- CloudWatch Alarms ---------------------------------------------------------
|
||||
resource "aws_cloudwatch_metric_alarm" "cpu" {
|
||||
alarm_name = "floci-compat-cpu-alarm"
|
||||
comparison_operator = "GreaterThanThreshold"
|
||||
evaluation_periods = 1
|
||||
metric_name = "CPUUtilization"
|
||||
namespace = "AWS/EC2"
|
||||
period = 60
|
||||
statistic = "Average"
|
||||
threshold = 80
|
||||
alarm_description = "CPU alarm for compat test"
|
||||
|
||||
tags = {
|
||||
env = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
output "alarm_arn" {
|
||||
value = aws_cloudwatch_metric_alarm.cpu.arn
|
||||
}
|
||||
|
||||
# -- VPC networking (issues #468, #401: VpcAttribute, RouteTableAssociation, DescribeTags) ------
|
||||
resource "aws_vpc" "compat" {
|
||||
cidr_block = "10.0.0.0/16"
|
||||
enable_dns_support = false
|
||||
enable_dns_hostnames = false
|
||||
|
||||
tags = {
|
||||
Name = "floci-compat-vpc"
|
||||
Environment = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_internet_gateway" "compat" {
|
||||
vpc_id = aws_vpc.compat.id
|
||||
|
||||
tags = {
|
||||
Name = "floci-compat-igw"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_subnet" "compat" {
|
||||
vpc_id = aws_vpc.compat.id
|
||||
cidr_block = "10.0.1.0/24"
|
||||
availability_zone = "us-east-1a"
|
||||
|
||||
tags = {
|
||||
Name = "floci-compat-subnet"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table" "compat" {
|
||||
vpc_id = aws_vpc.compat.id
|
||||
|
||||
route {
|
||||
cidr_block = "0.0.0.0/0"
|
||||
gateway_id = aws_internet_gateway.compat.id
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "floci-compat-rt"
|
||||
}
|
||||
}
|
||||
|
||||
# Exercises AssociateRouteTable + DescribeRouteTables(association.route-table-association-id)
|
||||
resource "aws_route_table_association" "compat" {
|
||||
subnet_id = aws_subnet.compat.id
|
||||
route_table_id = aws_route_table.compat.id
|
||||
}
|
||||
|
||||
resource "aws_security_group" "compat" {
|
||||
name = "floci-compat-sg"
|
||||
description = "Compat test security group"
|
||||
vpc_id = aws_vpc.compat.id
|
||||
|
||||
ingress {
|
||||
from_port = 80
|
||||
to_port = 80
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "floci-compat-sg"
|
||||
}
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
value = aws_vpc.compat.id
|
||||
}
|
||||
|
||||
output "subnet_id" {
|
||||
value = aws_subnet.compat.id
|
||||
}
|
||||
|
||||
output "route_table_id" {
|
||||
value = aws_route_table.compat.id
|
||||
}
|
||||
|
||||
output "security_group_id" {
|
||||
value = aws_security_group.compat.id
|
||||
}
|
||||
|
||||
# -- Route53 -------------------------------------------------------------------
|
||||
resource "aws_route53_zone" "compat" {
|
||||
name = "floci-compat.internal"
|
||||
force_destroy = true
|
||||
|
||||
tags = {
|
||||
Environment = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "app" {
|
||||
zone_id = aws_route53_zone.compat.zone_id
|
||||
name = "app.floci-compat.internal"
|
||||
type = "A"
|
||||
ttl = 300
|
||||
records = ["10.0.1.10"]
|
||||
}
|
||||
|
||||
resource "aws_route53_health_check" "app" {
|
||||
fqdn = "app.floci-compat.internal"
|
||||
port = 80
|
||||
type = "HTTP"
|
||||
resource_path = "/health"
|
||||
failure_threshold = 3
|
||||
request_interval = 30
|
||||
|
||||
tags = {
|
||||
Environment = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
output "zone_id" {
|
||||
value = aws_route53_zone.compat.zone_id
|
||||
}
|
||||
|
||||
output "health_check_id" {
|
||||
value = aws_route53_health_check.app.id
|
||||
}
|
||||
|
||||
# -- Kinesis Firehose Delivery Stream (extended_s3, issue #1043) ---------------
|
||||
resource "aws_kinesis_firehose_delivery_stream" "events" {
|
||||
name = "floci-compat-firehose"
|
||||
destination = "extended_s3"
|
||||
|
||||
extended_s3_configuration {
|
||||
role_arn = aws_iam_role.lambda_exec.arn
|
||||
bucket_arn = aws_s3_bucket.app.arn
|
||||
prefix = "events/data/"
|
||||
error_output_prefix = "events/errors/"
|
||||
compression_format = "GZIP"
|
||||
buffering_size = 64
|
||||
buffering_interval = 120
|
||||
}
|
||||
|
||||
tags = {
|
||||
Environment = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
output "firehose_stream_arn" {
|
||||
value = aws_kinesis_firehose_delivery_stream.events.arn
|
||||
}
|
||||
|
||||
# -- Application Auto Scaling (scalable target + target-tracking policies) -----
|
||||
resource "aws_appautoscaling_target" "ecs_service" {
|
||||
max_capacity = 20
|
||||
min_capacity = 2
|
||||
resource_id = "service/floci-compat-cluster/floci-compat-service"
|
||||
scalable_dimension = "ecs:service:DesiredCount"
|
||||
service_namespace = "ecs"
|
||||
|
||||
tags = {
|
||||
Environment = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_appautoscaling_policy" "ecs_cpu" {
|
||||
name = "floci-compat-cpu-tracking"
|
||||
policy_type = "TargetTrackingScaling"
|
||||
resource_id = aws_appautoscaling_target.ecs_service.resource_id
|
||||
scalable_dimension = aws_appautoscaling_target.ecs_service.scalable_dimension
|
||||
service_namespace = aws_appautoscaling_target.ecs_service.service_namespace
|
||||
|
||||
target_tracking_scaling_policy_configuration {
|
||||
predefined_metric_specification {
|
||||
predefined_metric_type = "ECSServiceAverageCPUUtilization"
|
||||
}
|
||||
target_value = 65
|
||||
}
|
||||
}
|
||||
|
||||
# resource_label round-trips only if every nested field is echoed back, so this
|
||||
# resource is the canary for target-tracking drift.
|
||||
resource "aws_appautoscaling_policy" "ecs_alb_requests" {
|
||||
name = "floci-compat-alb-request-count"
|
||||
policy_type = "TargetTrackingScaling"
|
||||
resource_id = aws_appautoscaling_target.ecs_service.resource_id
|
||||
scalable_dimension = aws_appautoscaling_target.ecs_service.scalable_dimension
|
||||
service_namespace = aws_appautoscaling_target.ecs_service.service_namespace
|
||||
|
||||
target_tracking_scaling_policy_configuration {
|
||||
target_value = 1000
|
||||
scale_in_cooldown = 240
|
||||
scale_out_cooldown = 60
|
||||
|
||||
predefined_metric_specification {
|
||||
predefined_metric_type = "ALBRequestCountPerTarget"
|
||||
resource_label = "app/floci-compat-alb/abc123/targetgroup/floci-compat-tg/def456"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output "appautoscaling_target_arn" {
|
||||
value = aws_appautoscaling_target.ecs_service.arn
|
||||
}
|
||||
|
||||
output "appautoscaling_alb_policy_arn" {
|
||||
value = aws_appautoscaling_policy.ecs_alb_requests.arn
|
||||
}
|
||||
|
||||
# ── SES Receipt Rule Set ───────────────────────────────────────────────────
|
||||
# floci stores it inertly (no inbound-mail routing); the management API just round-trips.
|
||||
resource "aws_ses_receipt_rule_set" "compat" {
|
||||
rule_set_name = "floci-compat-rule-set"
|
||||
}
|
||||
|
||||
resource "aws_ses_active_receipt_rule_set" "compat" {
|
||||
rule_set_name = aws_ses_receipt_rule_set.compat.rule_set_name
|
||||
}
|
||||
|
||||
resource "aws_ses_receipt_filter" "compat" {
|
||||
name = "floci-compat-filter"
|
||||
cidr = "10.10.10.0/24"
|
||||
policy = "Block"
|
||||
}
|
||||
|
||||
resource "aws_ses_receipt_rule" "compat" {
|
||||
rule_set_name = aws_ses_receipt_rule_set.compat.rule_set_name
|
||||
name = "floci-compat-rule"
|
||||
enabled = true
|
||||
scan_enabled = true
|
||||
tls_policy = "Optional"
|
||||
recipients = ["compat@example.com"]
|
||||
|
||||
add_header_action {
|
||||
header_name = "X-Floci-Compat"
|
||||
header_value = "true"
|
||||
position = 1
|
||||
}
|
||||
|
||||
sns_action {
|
||||
topic_arn = aws_sns_topic.events.arn
|
||||
position = 2
|
||||
}
|
||||
|
||||
stop_action {
|
||||
scope = "RuleSet"
|
||||
position = 3
|
||||
}
|
||||
}
|
||||
|
||||
output "ses_rule_set_name" {
|
||||
value = aws_ses_receipt_rule_set.compat.rule_set_name
|
||||
}
|
||||
|
||||
output "ses_receipt_rule_name" {
|
||||
value = aws_ses_receipt_rule.compat.name
|
||||
}
|
||||
|
||||
# -- IAM managed-policy attachment ---------------------------------------------
|
||||
# These policies sit outside the small curated set Floci used to ship, so the
|
||||
# attachments below only succeed once the full AWS managed-policy catalog is seeded.
|
||||
resource "aws_iam_role" "managed_policy_attach" {
|
||||
name = "floci-compat-managed-policy-role"
|
||||
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "ec2_read_only" {
|
||||
role = aws_iam_role.managed_policy_attach.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "emr_service_role" {
|
||||
role = aws_iam_role.managed_policy_attach.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonElasticMapReduceRole"
|
||||
}
|
||||
|
||||
output "managed_policy_role_arn" {
|
||||
value = aws_iam_role.managed_policy_attach.arn
|
||||
}
|
||||
|
||||
# The provider reads tags off the GetRole/GetPolicy/GetFunction response rather than by calling
|
||||
# List*Tags, so a tagged resource that does not echo them back applies cleanly and then diffs on
|
||||
# every subsequent plan. The re-plan assertion below is what catches that.
|
||||
resource "aws_iam_role" "tagged" {
|
||||
name = "floci-compat-tagged-role"
|
||||
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
|
||||
|
||||
tags = {
|
||||
Environment = "compat"
|
||||
Owner = "floci"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "tagged" {
|
||||
name = "floci-compat-tagged-policy"
|
||||
description = "Tagged policy used to assert tags survive a round trip"
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Action = "s3:GetObject"
|
||||
Resource = "*"
|
||||
}]
|
||||
})
|
||||
|
||||
tags = {
|
||||
Environment = "compat"
|
||||
}
|
||||
}
|
||||
|
||||
output "tagged_role_arn" {
|
||||
value = aws_iam_role.tagged.arn
|
||||
}
|
||||
|
||||
output "tagged_policy_arn" {
|
||||
value = aws_iam_policy.tagged.arn
|
||||
}
|
||||
|
||||
# -- GuardDuty -----------------------------------------------------------------
|
||||
# Detector, per-feature configuration, and organization configuration mirror the
|
||||
# resource set an org security-baseline stack manages. additional_configuration
|
||||
# is an ordered list block: Floci must echo it back in submitted order or every
|
||||
# re-plan proposes a replacement.
|
||||
resource "aws_guardduty_detector" "compat" {
|
||||
enable = true
|
||||
finding_publishing_frequency = "SIX_HOURS"
|
||||
|
||||
tags = {
|
||||
Environment = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_guardduty_detector_feature" "runtime_monitoring" {
|
||||
detector_id = aws_guardduty_detector.compat.id
|
||||
name = "RUNTIME_MONITORING"
|
||||
status = "ENABLED"
|
||||
|
||||
additional_configuration {
|
||||
name = "ECS_FARGATE_AGENT_MANAGEMENT"
|
||||
status = "ENABLED"
|
||||
}
|
||||
|
||||
additional_configuration {
|
||||
name = "EC2_AGENT_MANAGEMENT"
|
||||
status = "ENABLED"
|
||||
}
|
||||
|
||||
additional_configuration {
|
||||
name = "EKS_ADDON_MANAGEMENT"
|
||||
status = "DISABLED"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_guardduty_organization_configuration" "compat" {
|
||||
detector_id = aws_guardduty_detector.compat.id
|
||||
auto_enable_organization_members = "ALL"
|
||||
}
|
||||
|
||||
resource "aws_guardduty_organization_configuration_feature" "runtime_monitoring" {
|
||||
detector_id = aws_guardduty_detector.compat.id
|
||||
name = "RUNTIME_MONITORING"
|
||||
auto_enable = "ALL"
|
||||
|
||||
additional_configuration {
|
||||
name = "ECS_FARGATE_AGENT_MANAGEMENT"
|
||||
auto_enable = "ALL"
|
||||
}
|
||||
|
||||
additional_configuration {
|
||||
name = "EC2_AGENT_MANAGEMENT"
|
||||
auto_enable = "ALL"
|
||||
}
|
||||
|
||||
additional_configuration {
|
||||
name = "EKS_ADDON_MANAGEMENT"
|
||||
auto_enable = "NONE"
|
||||
}
|
||||
|
||||
depends_on = [aws_guardduty_organization_configuration.compat]
|
||||
}
|
||||
|
||||
output "guardduty_detector_id" {
|
||||
value = aws_guardduty_detector.compat.id
|
||||
}
|
||||
|
||||
output "guardduty_detector_arn" {
|
||||
value = aws_guardduty_detector.compat.arn
|
||||
}
|
||||
|
||||
output "large_object_checksum_sha256" {
|
||||
value = aws_s3_object.large.checksum_sha256
|
||||
}
|
||||
|
||||
# -- Transfer Family Server (issue #2802) --------------------------------------
|
||||
# Never stopped, so destroy exercises DeleteServer against an ONLINE server.
|
||||
resource "aws_transfer_server" "compat" {
|
||||
protocols = ["SFTP"]
|
||||
|
||||
tags = {
|
||||
Environment = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
output "transfer_server_id" {
|
||||
value = aws_transfer_server.compat.id
|
||||
}
|
||||
|
||||
# -- CloudTrail Trail (issue #2800) --------------------------------------------
|
||||
# The provider sends tags inside CreateTrail and reads them back with ListTags on
|
||||
# every refresh, so this covers the post-create tag refresh path end to end.
|
||||
resource "aws_s3_bucket" "trail_logs" {
|
||||
bucket = "floci-compat-trail-logs"
|
||||
force_destroy = true
|
||||
}
|
||||
|
||||
resource "aws_cloudtrail" "compat" {
|
||||
name = "floci-compat-trail"
|
||||
s3_bucket_name = aws_s3_bucket.trail_logs.id
|
||||
|
||||
tags = {
|
||||
Environment = "compat-test"
|
||||
}
|
||||
}
|
||||
|
||||
output "cloudtrail_arn" {
|
||||
value = aws_cloudtrail.compat.arn
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
s3 = var.endpoint
|
||||
sqs = var.endpoint
|
||||
sns = var.endpoint
|
||||
dynamodb = var.endpoint
|
||||
lambda = var.endpoint
|
||||
iam = var.endpoint
|
||||
sts = var.endpoint
|
||||
ssm = var.endpoint
|
||||
secretsmanager = var.endpoint
|
||||
cognitoidp = var.endpoint
|
||||
rds = var.endpoint
|
||||
cloudwatch = var.endpoint
|
||||
ec2 = var.endpoint
|
||||
route53 = var.endpoint
|
||||
appautoscaling = var.endpoint
|
||||
guardduty = var.endpoint
|
||||
elasticache = var.endpoint
|
||||
ecr = var.endpoint
|
||||
ecs = var.endpoint
|
||||
kms = var.endpoint
|
||||
kinesis = var.endpoint
|
||||
logs = var.endpoint
|
||||
events = var.endpoint
|
||||
sfn = var.endpoint
|
||||
cloudformation = var.endpoint
|
||||
eks = var.endpoint
|
||||
codebuild = var.endpoint
|
||||
apigateway = var.endpoint
|
||||
apigatewayv2 = var.endpoint
|
||||
appconfig = var.endpoint
|
||||
appsync = var.endpoint
|
||||
athena = var.endpoint
|
||||
backup = var.endpoint
|
||||
servicediscovery = var.endpoint
|
||||
codedeploy = var.endpoint
|
||||
acm = var.endpoint
|
||||
ses = var.endpoint
|
||||
scheduler = var.endpoint
|
||||
transfer = var.endpoint
|
||||
wafv2 = var.endpoint
|
||||
cloudtrail = var.endpoint
|
||||
cur = var.endpoint
|
||||
glue = var.endpoint
|
||||
firehose = var.endpoint
|
||||
pipes = var.endpoint
|
||||
batch = var.endpoint
|
||||
neptune = var.endpoint
|
||||
opensearch = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs this suite's bats files, one process per file, concurrently by default.
|
||||
#
|
||||
# Each test/*.bats file is an independent root module: it creates its own state
|
||||
# (local state for test/*-tf/, the S3 backend for the main suite), uses distinct
|
||||
# resource identifiers, and cleans up in teardown_file. Nothing is shared, so the
|
||||
# only thing serialising them was running `bats test/` as a single process.
|
||||
#
|
||||
# bats' own --jobs needs GNU parallel or rush, neither of which is in this image,
|
||||
# so the files are forked here and reaped with wait.
|
||||
#
|
||||
# Environment:
|
||||
# RESULTS_DIR where junit-<file>.xml is written (default /results)
|
||||
# BATS_PARALLEL_FILES 0 to run the files sequentially (bisecting a flake)
|
||||
# IAC_BIN terraform | tofu (default: whichever is on PATH)
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
RESULTS_DIR="${RESULTS_DIR:-/results}"
|
||||
PARALLEL="${BATS_PARALLEL_FILES:-1}"
|
||||
# Bound one bats file so a hung fixture fails under its own name instead of the whole job
|
||||
# dying at the workflow's timeout-minutes with nothing reported. The workflow gives the
|
||||
# job 20 minutes and a healthy concurrent run finishes in about seven, so twelve leaves
|
||||
# room for a slow runner while still naming the culprit before the job limit.
|
||||
FILE_TIMEOUT="${BATS_FILE_TIMEOUT:-12m}"
|
||||
|
||||
if [ -n "${IAC_BIN:-}" ]; then
|
||||
:
|
||||
elif command -v terraform >/dev/null 2>&1; then
|
||||
IAC_BIN=terraform
|
||||
elif command -v tofu >/dev/null 2>&1; then
|
||||
IAC_BIN=tofu
|
||||
else
|
||||
echo "Error: neither terraform nor tofu is on PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Same resolution order as test/test_helper/common-setup.bash, so this script
|
||||
# behaves identically in the image (BATS_LIB_PATH=/opt) and from a local checkout.
|
||||
if [ -n "${BATS_LIB_PATH:-}" ] && [ -x "${BATS_LIB_PATH}/bats-core/bin/bats" ]; then
|
||||
BATS_BIN="${BATS_LIB_PATH}/bats-core/bin/bats"
|
||||
elif [ -x "${SCRIPT_DIR}/../lib/bats-core/bin/bats" ]; then
|
||||
BATS_BIN="${SCRIPT_DIR}/../lib/bats-core/bin/bats"
|
||||
else
|
||||
echo "Error: bats-core not found. Run 'just setup-bats' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILES=()
|
||||
for f in test/*.bats; do
|
||||
[ -e "$f" ] || continue
|
||||
FILES+=("$f")
|
||||
done
|
||||
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||
echo "Error: no test/*.bats files found in $SCRIPT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/bats-run-XXXXXX")"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
mkdir -p "$RESULTS_DIR"
|
||||
# Clear only the reports this run will write. A run that stops before rewriting every one
|
||||
# would otherwise leave the survivors beside the new results, and every consumer globs the
|
||||
# directory. Naming the files rather than globbing junit*.xml is deliberate:
|
||||
# docker/run-docker-tests.sh mounts one shared test-results directory for all eight suites, so
|
||||
# a glob here would delete compat-terraform's reports when compat-opentofu starts, and both
|
||||
# would delete the junit.xml that sdk-test-awscli and compat-cdk write.
|
||||
for report in "${FILES[@]}"; do
|
||||
rm -f "${RESULTS_DIR}/junit-$(basename "$report" .bats).xml"
|
||||
done
|
||||
|
||||
# One shared provider download instead of one per file. Every fixture deletes its
|
||||
# lock file in setup_file and re-resolves, so without this the concurrent inits
|
||||
# would each fetch the provider at the same time. Warmed sequentially here because
|
||||
# the plugin cache is only safe for concurrent readers, not concurrent writers.
|
||||
warm_provider_cache() {
|
||||
local constraint warm_dir
|
||||
constraint="$(sed -n 's/.*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' provider.tf | head -1)"
|
||||
if [ -z "$constraint" ]; then
|
||||
echo "warn: no provider version constraint found in provider.tf, skipping cache warm-up" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
export TF_PLUGIN_CACHE_DIR="${TF_PLUGIN_CACHE_DIR:-${WORK_DIR}/plugin-cache}"
|
||||
export TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=true
|
||||
mkdir -p "$TF_PLUGIN_CACHE_DIR"
|
||||
|
||||
warm_dir="${WORK_DIR}/warm"
|
||||
mkdir -p "$warm_dir"
|
||||
cat > "${warm_dir}/versions.tf" <<EOF
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "${constraint}"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "# --- warming provider cache (aws ${constraint}) into ${TF_PLUGIN_CACHE_DIR} ---"
|
||||
if ! (cd "$warm_dir" && "$IAC_BIN" init -backend=false -input=false -no-color >/dev/null 2>&1); then
|
||||
# Not fatal: every fixture still resolves the provider itself, just without
|
||||
# the shared cache. Losing the warm-up costs time, never correctness.
|
||||
echo "warn: provider cache warm-up failed; files will resolve the provider individually" >&2
|
||||
unset TF_PLUGIN_CACHE_DIR TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE
|
||||
fi
|
||||
}
|
||||
|
||||
run_one() {
|
||||
local file="$1" name="$2" started status
|
||||
started=$(date +%s)
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout -k 30s "$FILE_TIMEOUT" \
|
||||
"$BATS_BIN" --report-formatter junit -o "${WORK_DIR}/${name}" "$file" \
|
||||
> "${WORK_DIR}/${name}.log" 2>&1
|
||||
else
|
||||
"$BATS_BIN" --report-formatter junit -o "${WORK_DIR}/${name}" "$file" \
|
||||
> "${WORK_DIR}/${name}.log" 2>&1
|
||||
fi
|
||||
status=$?
|
||||
if [ "$status" -eq 124 ]; then
|
||||
echo "# ${name}.bats: timed out after ${FILE_TIMEOUT}; the TAP output above ends at the test that hung" \
|
||||
>> "${WORK_DIR}/${name}.log"
|
||||
fi
|
||||
echo "$status" > "${WORK_DIR}/${name}.status"
|
||||
# Publish the report as soon as this file is done rather than after every file has
|
||||
# finished: if the job is cancelled while another file hangs, the reports of the files
|
||||
# that completed are still uploaded, and the missing one names the hang.
|
||||
if [ -f "${WORK_DIR}/${name}/report.xml" ]; then
|
||||
mv "${WORK_DIR}/${name}/report.xml" "${RESULTS_DIR}/junit-${name}.xml"
|
||||
fi
|
||||
echo "# --- ${name}.bats finished in $(( $(date +%s) - started ))s (exit ${status}) ---"
|
||||
}
|
||||
|
||||
warm_provider_cache
|
||||
|
||||
NAMES=()
|
||||
for f in "${FILES[@]}"; do
|
||||
name="$(basename "$f" .bats)"
|
||||
NAMES+=("$name")
|
||||
mkdir -p "${WORK_DIR}/${name}"
|
||||
done
|
||||
|
||||
start=$(date +%s)
|
||||
if [ "$PARALLEL" = "0" ]; then
|
||||
echo "# --- running ${#FILES[@]} bats files sequentially ---"
|
||||
for i in "${!FILES[@]}"; do
|
||||
run_one "${FILES[$i]}" "${NAMES[$i]}"
|
||||
done
|
||||
else
|
||||
echo "# --- running ${#FILES[@]} bats files concurrently ---"
|
||||
pids=()
|
||||
for i in "${!FILES[@]}"; do
|
||||
run_one "${FILES[$i]}" "${NAMES[$i]}" &
|
||||
pids+=("$!")
|
||||
done
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid"
|
||||
done
|
||||
fi
|
||||
elapsed=$(( $(date +%s) - start ))
|
||||
|
||||
# Output is collected per file and replayed in a fixed order: six interleaved bats
|
||||
# streams are unreadable, and grouped blocks keep a failure next to its own TAP plan.
|
||||
status=0
|
||||
for name in "${NAMES[@]}"; do
|
||||
echo
|
||||
echo "=============== ${name}.bats ==============="
|
||||
cat "${WORK_DIR}/${name}.log" 2>/dev/null || true
|
||||
|
||||
file_status=1
|
||||
if [ -f "${WORK_DIR}/${name}.status" ]; then
|
||||
file_status="$(cat "${WORK_DIR}/${name}.status")"
|
||||
fi
|
||||
[ "$file_status" -eq 0 ] || status=1
|
||||
|
||||
if [ ! -f "${RESULTS_DIR}/junit-${name}.xml" ]; then
|
||||
echo "Error: no JUnit report produced for ${name}.bats" >&2
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "=============== summary ==============="
|
||||
for name in "${NAMES[@]}"; do
|
||||
file_status="$(cat "${WORK_DIR}/${name}.status" 2>/dev/null || echo 1)"
|
||||
if [ "$file_status" -eq 0 ]; then
|
||||
echo " PASS ${name}"
|
||||
else
|
||||
echo " FAIL ${name} (exit ${file_status})"
|
||||
fi
|
||||
done
|
||||
echo "total ${elapsed}s"
|
||||
|
||||
exit "$status"
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Runs the Terraform compatibility suite against a Floci already listening on
|
||||
# FLOCI_ENDPOINT. Delegates to run-bats-in-container.sh, the same entrypoint the
|
||||
# Docker image uses, so a local run and a CI run execute identical logic.
|
||||
|
||||
export AWS_REGION=us-east-1
|
||||
export AWS_DEFAULT_REGION=us-east-1
|
||||
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-test}"
|
||||
export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-test}"
|
||||
export FLOCI_ENDPOINT="${FLOCI_ENDPOINT:-http://localhost:4566}"
|
||||
export AWS_ENDPOINT_URL="$FLOCI_ENDPOINT"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
if [ ! -d "$REPO_ROOT/lib/bats-core" ]; then
|
||||
echo "Error: bats-core not found. Run 'just setup-bats' first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# BATS_JUNIT_XML used to name a single file; the suite now writes one report per
|
||||
# bats file, so the variable names the directory they land in.
|
||||
export RESULTS_DIR="${RESULTS_DIR:-$SCRIPT_DIR/test-results}"
|
||||
|
||||
exec "$SCRIPT_DIR/run-bats-in-container.sh"
|
||||
@@ -0,0 +1,104 @@
|
||||
# Verifies that a REST API gateway converges: a second plan reports no changes.
|
||||
#
|
||||
# GetMethod dropped requestParameters, GetRestApi dropped policy, and GetStage dropped
|
||||
# accessLogSettings, tracingEnabled and tags. Terraform planned the same three in-place
|
||||
# updates on every run, and because the deployment's redeployment trigger hashes the
|
||||
# method, the re-apply failed with "Provider produced inconsistent final plan".
|
||||
|
||||
variable "extra_request_parameter" {
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "stage_env" {
|
||||
type = string
|
||||
default = "local"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_rest_api" "api" {
|
||||
name = "floci-apigw-rest-readback"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_resource" "proxy" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
parent_id = aws_api_gateway_rest_api.api.root_resource_id
|
||||
path_part = "{proxy+}"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_method" "proxy" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.proxy.id
|
||||
http_method = "ANY"
|
||||
authorization = "NONE"
|
||||
|
||||
request_parameters = merge(
|
||||
{ "method.request.path.proxy" = true },
|
||||
var.extra_request_parameter ? { "method.request.header.X-Tenant" = false } : {},
|
||||
)
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration" "proxy" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.proxy.id
|
||||
http_method = aws_api_gateway_method.proxy.http_method
|
||||
type = "MOCK"
|
||||
|
||||
request_templates = {
|
||||
"application/json" = "{\"statusCode\": 200}"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_rest_api_policy" "api" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Principal = "*"
|
||||
Action = "execute-api:Invoke"
|
||||
Resource = "${aws_api_gateway_rest_api.api.execution_arn}/*"
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
# The common module pattern from the bug report: the trigger hashes the method, so a
|
||||
# method that reads back differently than it was written breaks the apply itself.
|
||||
resource "aws_api_gateway_deployment" "api" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
|
||||
triggers = {
|
||||
redeployment = sha1(jsonencode([
|
||||
aws_api_gateway_resource.proxy,
|
||||
aws_api_gateway_method.proxy,
|
||||
aws_api_gateway_integration.proxy,
|
||||
]))
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_stage" "local" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
deployment_id = aws_api_gateway_deployment.api.id
|
||||
stage_name = "local"
|
||||
xray_tracing_enabled = true
|
||||
|
||||
access_log_settings {
|
||||
destination_arn = "arn:aws:logs:us-east-1:000000000000:log-group:floci-apigw-rest-readback"
|
||||
format = "$context.requestId $context.status"
|
||||
}
|
||||
|
||||
tags = {
|
||||
env = var.stage_env
|
||||
}
|
||||
}
|
||||
|
||||
output "rest_api_id" {
|
||||
value = aws_api_gateway_rest_api.api.id
|
||||
}
|
||||
|
||||
output "resource_id" {
|
||||
value = aws_api_gateway_resource.proxy.id
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
apigateway = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bats
|
||||
# API Gateway REST Read-Back Compatibility Test
|
||||
#
|
||||
# GetMethod dropped requestParameters, GetRestApi dropped policy, and GetStage dropped
|
||||
# accessLogSettings, tracingEnabled and tags. Every write succeeded, but the reads came back
|
||||
# without the values, so each plan proposed the same three in-place updates forever. With a
|
||||
# deployment trigger that hashes the method, the re-apply failed outright with "Provider
|
||||
# produced inconsistent final plan". This verifies the module applies, reads back what it
|
||||
# wrote, converges on a re-apply, and survives an in-place method change.
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
APIGW_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/apigateway-rest-readback-tf" && pwd)"
|
||||
cd "$APIGW_TF_DIR"
|
||||
|
||||
echo "# === API Gateway REST Read-Back Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $APIGW_TF_DIR" >&3
|
||||
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
APIGW_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/apigateway-rest-readback-tf" && pwd)"
|
||||
cd "$APIGW_TF_DIR"
|
||||
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
APIGW_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/apigateway-rest-readback-tf" && pwd)"
|
||||
}
|
||||
|
||||
@test "API Gateway read-back: GetMethod returns requestParameters" {
|
||||
API_ID=$(terraform -chdir="$APIGW_TF_DIR" output -raw rest_api_id)
|
||||
RESOURCE_ID=$(terraform -chdir="$APIGW_TF_DIR" output -raw resource_id)
|
||||
run aws_cmd apigateway get-method --rest-api-id "$API_ID" --resource-id "$RESOURCE_ID" \
|
||||
--http-method ANY --query 'requestParameters."method.request.path.proxy"' --output text
|
||||
assert_success
|
||||
assert_output "True"
|
||||
}
|
||||
|
||||
@test "API Gateway read-back: GetRestApi returns the policy" {
|
||||
API_ID=$(terraform -chdir="$APIGW_TF_DIR" output -raw rest_api_id)
|
||||
run aws_cmd apigateway get-rest-api --rest-api-id "$API_ID" --query policy --output text
|
||||
assert_success
|
||||
assert_output --partial "execute-api:Invoke"
|
||||
}
|
||||
|
||||
@test "API Gateway read-back: GetStage returns access logs, tracing and tags" {
|
||||
API_ID=$(terraform -chdir="$APIGW_TF_DIR" output -raw rest_api_id)
|
||||
run aws_cmd apigateway get-stage --rest-api-id "$API_ID" --stage-name local \
|
||||
--query '[accessLogSettings.format, tracingEnabled, tags.env]' --output text
|
||||
assert_success
|
||||
assert_output "\$context.requestId \$context.status True local"
|
||||
}
|
||||
|
||||
# The failure from the bug report: the method read back without requestParameters, so it
|
||||
# differed between plan and apply, the deployment trigger hash moved mid-apply, and the
|
||||
# re-apply died with "Provider produced inconsistent final plan".
|
||||
#
|
||||
# The re-apply may still replace the deployment once, whatever the server returns:
|
||||
# aws_api_gateway_method's create does not read the method back, so unset collections
|
||||
# (request_models, authorization_scopes) stay null in state until the first refresh fills
|
||||
# them in as empty, and that moves the trigger hash a single time. The stage then follows the
|
||||
# new deployment id, so only the method and the policy must stay untouched here.
|
||||
@test "API Gateway read-back: re-apply succeeds" {
|
||||
cd "$APIGW_TF_DIR"
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
assert_success
|
||||
refute_output --partial "inconsistent final plan"
|
||||
refute_output --partial "aws_api_gateway_method.proxy: Modifying"
|
||||
refute_output --partial "aws_api_gateway_rest_api_policy.api: Modifying"
|
||||
}
|
||||
|
||||
# The critical assertion: before the fix every plan proposed the same three updates forever.
|
||||
@test "API Gateway read-back: plan after re-apply reports no changes" {
|
||||
cd "$APIGW_TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
|
||||
# Changing a method request parameter goes through UpdateMethod patch operations and changes
|
||||
# the deployment trigger hash. This is where "inconsistent final plan" used to surface.
|
||||
@test "API Gateway read-back: changing a request parameter applies and re-plans clean" {
|
||||
cd "$APIGW_TF_DIR"
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -var="extra_request_parameter=true" \
|
||||
-input=false -auto-approve -no-color
|
||||
assert_success
|
||||
refute_output --partial "inconsistent final plan"
|
||||
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -var="extra_request_parameter=true" \
|
||||
-input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
|
||||
# Stage tag changes go through TagResource on the stage ARN, which used to tag the REST API
|
||||
# instead, so the stage never showed the new value and the plan never settled.
|
||||
@test "API Gateway read-back: changing a stage tag lands on the stage and re-plans clean" {
|
||||
cd "$APIGW_TF_DIR"
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -var="extra_request_parameter=true" \
|
||||
-var="stage_env=staging" -input=false -auto-approve -no-color
|
||||
assert_success
|
||||
|
||||
API_ID=$(terraform -chdir="$APIGW_TF_DIR" output -raw rest_api_id)
|
||||
run aws_cmd apigateway get-stage --rest-api-id "$API_ID" --stage-name local \
|
||||
--query 'tags.env' --output text
|
||||
assert_success
|
||||
assert_output "staging"
|
||||
|
||||
run aws_cmd apigateway get-rest-api --rest-api-id "$API_ID" --query 'tags' --output text
|
||||
assert_success
|
||||
assert_output "None"
|
||||
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -var="extra_request_parameter=true" \
|
||||
-var="stage_env=staging" -input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# Verifies the CodeArtifact control plane: domain/repository CRUD, tags, upstream repositories,
|
||||
# and resource policies, all of which previously had no Floci support at all.
|
||||
|
||||
resource "aws_codeartifact_domain" "test" {
|
||||
domain = "floci-codeartifact-domain"
|
||||
|
||||
tags = {
|
||||
owner = "platform"
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "domain_policy" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["codeartifact:GetDomainPermissions"]
|
||||
resources = [aws_codeartifact_domain.test.arn]
|
||||
principals {
|
||||
type = "AWS"
|
||||
identifiers = ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_codeartifact_domain_permissions_policy" "test" {
|
||||
domain = aws_codeartifact_domain.test.domain
|
||||
policy_document = data.aws_iam_policy_document.domain_policy.json
|
||||
}
|
||||
|
||||
resource "aws_codeartifact_repository" "store" {
|
||||
domain = aws_codeartifact_domain.test.domain
|
||||
repository = "floci-codeartifact-store"
|
||||
}
|
||||
|
||||
resource "aws_codeartifact_repository" "consumer" {
|
||||
domain = aws_codeartifact_domain.test.domain
|
||||
repository = "floci-codeartifact-consumer"
|
||||
description = "consumes packages via the store repository"
|
||||
|
||||
upstream {
|
||||
repository_name = aws_codeartifact_repository.store.repository
|
||||
}
|
||||
|
||||
tags = {
|
||||
team = "data"
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "repository_policy" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["codeartifact:ReadFromRepository"]
|
||||
resources = [aws_codeartifact_repository.consumer.arn]
|
||||
principals {
|
||||
type = "AWS"
|
||||
identifiers = ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_codeartifact_repository_permissions_policy" "test" {
|
||||
domain = aws_codeartifact_domain.test.domain
|
||||
repository = aws_codeartifact_repository.consumer.repository
|
||||
policy_document = data.aws_iam_policy_document.repository_policy.json
|
||||
}
|
||||
|
||||
output "domain_arn" {
|
||||
value = aws_codeartifact_domain.test.arn
|
||||
}
|
||||
|
||||
output "domain_owner_tag" {
|
||||
value = aws_codeartifact_domain.test.tags["owner"]
|
||||
}
|
||||
|
||||
output "repository_arn" {
|
||||
value = aws_codeartifact_repository.consumer.arn
|
||||
}
|
||||
|
||||
output "repository_upstream" {
|
||||
value = aws_codeartifact_repository.consumer.upstream[0].repository_name
|
||||
}
|
||||
|
||||
output "repository_team_tag" {
|
||||
value = aws_codeartifact_repository.consumer.tags["team"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
codeartifact = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bats
|
||||
# CodeArtifact Control Plane Compatibility Test
|
||||
#
|
||||
# Floci previously had no CodeArtifact support at all. This verifies aws_codeartifact_domain,
|
||||
# aws_codeartifact_domain_permissions_policy, aws_codeartifact_repository (with tags and an
|
||||
# upstream), and aws_codeartifact_repository_permissions_policy can all apply, that a second
|
||||
# plan is clean, and that destroy tears everything down in dependency order.
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
CA_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/codeartifact-domain-repo-tf" && pwd)"
|
||||
cd "$CA_TF_DIR"
|
||||
|
||||
echo "# === CodeArtifact Domain/Repository Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $CA_TF_DIR" >&3
|
||||
|
||||
find .terraform -mindepth 1 -delete 2>/dev/null || true
|
||||
rm -f .terraform.lock.hcl terraform.tfstate terraform.tfstate.backup 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
CA_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/codeartifact-domain-repo-tf" && pwd)"
|
||||
cd "$CA_TF_DIR"
|
||||
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
find .terraform -mindepth 1 -delete 2>/dev/null || true
|
||||
rm -f .terraform.lock.hcl terraform.tfstate terraform.tfstate.backup 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
CA_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/codeartifact-domain-repo-tf" && pwd)"
|
||||
}
|
||||
|
||||
@test "CodeArtifact domain/repository: apply reports the domain and repository ARNs" {
|
||||
DOMAIN_ARN=$(terraform -chdir="$CA_TF_DIR" output -raw domain_arn)
|
||||
REPO_ARN=$(terraform -chdir="$CA_TF_DIR" output -raw repository_arn)
|
||||
assert_equal "$DOMAIN_ARN" "arn:aws:codeartifact:us-east-1:000000000000:domain/floci-codeartifact-domain"
|
||||
assert_equal "$REPO_ARN" "arn:aws:codeartifact:us-east-1:000000000000:repository/floci-codeartifact-domain/floci-codeartifact-consumer"
|
||||
}
|
||||
|
||||
@test "CodeArtifact domain/repository: tags and upstream round-trip" {
|
||||
OWNER_TAG=$(terraform -chdir="$CA_TF_DIR" output -raw domain_owner_tag)
|
||||
TEAM_TAG=$(terraform -chdir="$CA_TF_DIR" output -raw repository_team_tag)
|
||||
UPSTREAM=$(terraform -chdir="$CA_TF_DIR" output -raw repository_upstream)
|
||||
assert_equal "$OWNER_TAG" "platform"
|
||||
assert_equal "$TEAM_TAG" "data"
|
||||
assert_equal "$UPSTREAM" "floci-codeartifact-store"
|
||||
}
|
||||
|
||||
@test "CodeArtifact domain/repository: DescribeDomain reports the live repository count" {
|
||||
run aws_cmd codeartifact describe-domain --domain floci-codeartifact-domain \
|
||||
--query "domain.repositoryCount" --output text
|
||||
assert_success
|
||||
assert_output "2"
|
||||
}
|
||||
|
||||
@test "CodeArtifact domain/repository: second plan reports no changes" {
|
||||
cd "$CA_TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
|
||||
@test "CodeArtifact domain/repository: GetRepositoryEndpoint resolves for a real package format" {
|
||||
run aws_cmd codeartifact get-repository-endpoint --domain floci-codeartifact-domain \
|
||||
--repository floci-codeartifact-consumer --format npm --query repositoryEndpoint --output text
|
||||
assert_success
|
||||
[[ "$output" == *"/codeartifact/npm/floci-codeartifact-domain/floci-codeartifact-consumer/"* ]]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Verifies that a DynamoDB Kinesis streaming destination keeps the precision it was created with.
|
||||
#
|
||||
# EnableKinesisStreamingDestination ignored ApproximateCreationDateTimePrecision and
|
||||
# DescribeKinesisStreamingDestination always reported MILLISECOND. The attribute is ForceNew
|
||||
# in the provider, so a MICROSECOND destination was destroyed and recreated on every apply.
|
||||
#
|
||||
# The table deliberately leaves DynamoDB Streams off: enabling the destination used to switch
|
||||
# them on, which aws_dynamodb_table then planned to switch off again on every run.
|
||||
|
||||
variable "precision" {
|
||||
type = string
|
||||
default = "MICROSECOND"
|
||||
}
|
||||
|
||||
resource "aws_kinesis_stream" "events" {
|
||||
name = "floci-ddb-kinesis-precision"
|
||||
shard_count = 1
|
||||
retention_period = 24
|
||||
}
|
||||
|
||||
resource "aws_dynamodb_table" "items" {
|
||||
name = "floci-ddb-kinesis-precision"
|
||||
billing_mode = "PAY_PER_REQUEST"
|
||||
hash_key = "pk"
|
||||
|
||||
attribute {
|
||||
name = "pk"
|
||||
type = "S"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_dynamodb_kinesis_streaming_destination" "items" {
|
||||
stream_arn = aws_kinesis_stream.events.arn
|
||||
table_name = aws_dynamodb_table.items.name
|
||||
approximate_creation_date_time_precision = var.precision
|
||||
}
|
||||
|
||||
output "table_name" {
|
||||
value = aws_dynamodb_table.items.name
|
||||
}
|
||||
|
||||
output "stream_arn" {
|
||||
value = aws_kinesis_stream.events.arn
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
dynamodb = var.endpoint
|
||||
kinesis = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bats
|
||||
# DynamoDB Kinesis Streaming Destination Precision Compatibility Test
|
||||
#
|
||||
# EnableKinesisStreamingDestination ignored ApproximateCreationDateTimePrecision, and
|
||||
# DescribeKinesisStreamingDestination always reported MILLISECOND. The provider marks
|
||||
# approximate_creation_date_time_precision as ForceNew, so a MICROSECOND destination read back
|
||||
# as MILLISECOND and was destroyed and recreated on every apply. Enabling the destination also
|
||||
# switched on DynamoDB Streams for the table, so aws_dynamodb_table planned to turn it back off
|
||||
# on every run. This verifies the precision reads back as written, a second plan is clean for
|
||||
# both resources, and changing the precision replaces the destination once.
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
DDB_KINESIS_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/dynamodb-kinesis-precision-tf" && pwd)"
|
||||
cd "$DDB_KINESIS_TF_DIR"
|
||||
|
||||
echo "# === DynamoDB Kinesis Destination Precision Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $DDB_KINESIS_TF_DIR" >&3
|
||||
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply (MICROSECOND) ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
DDB_KINESIS_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/dynamodb-kinesis-precision-tf" && pwd)"
|
||||
cd "$DDB_KINESIS_TF_DIR"
|
||||
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
DDB_KINESIS_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/dynamodb-kinesis-precision-tf" && pwd)"
|
||||
}
|
||||
|
||||
describe_precision() {
|
||||
local table_name stream_arn
|
||||
table_name=$(terraform -chdir="$DDB_KINESIS_TF_DIR" output -raw table_name)
|
||||
stream_arn=$(terraform -chdir="$DDB_KINESIS_TF_DIR" output -raw stream_arn)
|
||||
aws_cmd dynamodb describe-kinesis-streaming-destination --table-name "$table_name" \
|
||||
--query "KinesisDataStreamDestinations[?StreamArn=='${stream_arn}'].ApproximateCreationDateTimePrecision | [0]" \
|
||||
--output text
|
||||
}
|
||||
|
||||
@test "DynamoDB Kinesis precision: destination reads back as MICROSECOND" {
|
||||
run describe_precision
|
||||
assert_success
|
||||
assert_output "MICROSECOND"
|
||||
}
|
||||
|
||||
# The critical assertion: before the fix the precision read back as MILLISECOND, and because
|
||||
# the attribute is ForceNew every plan proposed replacing the destination. The table here does
|
||||
# not enable streams, so this also catches stream_enabled drift on aws_dynamodb_table.
|
||||
@test "DynamoDB Kinesis precision: second plan reports no changes" {
|
||||
cd "$DDB_KINESIS_TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
|
||||
# A real precision change is still ForceNew: it must replace the destination once, then settle.
|
||||
@test "DynamoDB Kinesis precision: changing the precision replaces once and re-plans clean" {
|
||||
cd "$DDB_KINESIS_TF_DIR"
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -var="precision=MILLISECOND" \
|
||||
-input=false -auto-approve -no-color
|
||||
assert_success
|
||||
assert_output --partial "1 added, 0 changed, 1 destroyed"
|
||||
|
||||
run describe_precision
|
||||
assert_success
|
||||
assert_output "MILLISECOND"
|
||||
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -var="precision=MILLISECOND" \
|
||||
-input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
resource "aws_elasticache_replication_group" "cluster" {
|
||||
replication_group_id = "floci-tf-valkey-cluster"
|
||||
description = "floci terraform cluster-mode compatibility test"
|
||||
engine = "valkey"
|
||||
node_type = "cache.t4g.micro"
|
||||
parameter_group_name = "default.valkey8.cluster.on"
|
||||
automatic_failover_enabled = true
|
||||
num_node_groups = 2
|
||||
replicas_per_node_group = 1
|
||||
}
|
||||
|
||||
output "cluster_enabled" {
|
||||
value = aws_elasticache_replication_group.cluster.cluster_enabled
|
||||
}
|
||||
|
||||
output "configuration_endpoint_address" {
|
||||
value = aws_elasticache_replication_group.cluster.configuration_endpoint_address
|
||||
}
|
||||
|
||||
output "num_node_groups" {
|
||||
value = aws_elasticache_replication_group.cluster.num_node_groups
|
||||
}
|
||||
|
||||
output "member_clusters" {
|
||||
value = aws_elasticache_replication_group.cluster.member_clusters
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
elasticache = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bats
|
||||
# ElastiCache Cluster Mode Compatibility Test
|
||||
#
|
||||
# Applies an aws_elasticache_replication_group with num_node_groups /
|
||||
# replicas_per_node_group (Terraform's cluster-mode shape) against floci and
|
||||
# verifies both sides of the contract: the terraform read-back is honest
|
||||
# (cluster_enabled, member_clusters, a clean re-plan), and the data plane is a
|
||||
# genuine Valkey cluster (CLUSTER INFO over RESP through the auth proxy).
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
EC_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/elasticache-cluster-tf" && pwd)"
|
||||
cd "$EC_TF_DIR"
|
||||
|
||||
echo "# === ElastiCache Cluster Mode Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $EC_TF_DIR" >&3
|
||||
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply (2 shards, 1 replica each) ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
EC_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/elasticache-cluster-tf" && pwd)"
|
||||
cd "$EC_TF_DIR"
|
||||
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
EC_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/elasticache-cluster-tf" && pwd)"
|
||||
}
|
||||
|
||||
# Sends one RESP command over /dev/tcp and echoes whatever the server replies
|
||||
# within the read window. No redis-cli in this image, so raw RESP it is.
|
||||
valkey_command() {
|
||||
local host="$1" port="$2"
|
||||
shift 2
|
||||
local payload="*$#\r\n"
|
||||
local arg
|
||||
for arg in "$@"; do
|
||||
payload+="\$${#arg}\r\n${arg}\r\n"
|
||||
done
|
||||
exec 9<>"/dev/tcp/${host}/${port}" || return 1
|
||||
printf '%b' "$payload" >&9
|
||||
local reply="" chunk
|
||||
while IFS= read -r -t 3 -u 9 chunk; do
|
||||
reply+="${chunk}"$'\n'
|
||||
done
|
||||
exec 9<&- 9>&-
|
||||
printf '%s' "$reply"
|
||||
}
|
||||
|
||||
@test "ElastiCache cluster mode: terraform reads back cluster_enabled=true" {
|
||||
run terraform -chdir="$EC_TF_DIR" output -raw cluster_enabled
|
||||
assert_success
|
||||
assert_output "true"
|
||||
|
||||
run terraform -chdir="$EC_TF_DIR" output -raw num_node_groups
|
||||
assert_success
|
||||
assert_output "2"
|
||||
}
|
||||
|
||||
@test "ElastiCache cluster mode: describe reports the sharded topology" {
|
||||
run aws_cmd elasticache describe-replication-groups \
|
||||
--replication-group-id floci-tf-valkey-cluster
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.ReplicationGroups[0].ClusterEnabled')" = "true" ]
|
||||
[ "$(json_get "$output" '.ReplicationGroups[0].AutomaticFailover')" = "enabled" ]
|
||||
[ "$(json_get "$output" '.ReplicationGroups[0].Engine')" = "valkey" ]
|
||||
[ "$(json_get "$output" '.ReplicationGroups[0].NodeGroups | length')" = "2" ]
|
||||
[ "$(json_get "$output" '.ReplicationGroups[0].NodeGroups[0].Slots')" = "0-8191" ]
|
||||
[ "$(json_get "$output" '.ReplicationGroups[0].NodeGroups[1].Slots')" = "8192-16383" ]
|
||||
[ "$(json_get "$output" '.ReplicationGroups[0].MemberClusters | length')" = "4" ]
|
||||
}
|
||||
|
||||
@test "ElastiCache cluster mode: member cache clusters answer DescribeCacheClusters" {
|
||||
member=$(terraform -chdir="$EC_TF_DIR" output -json member_clusters | jq -r '.[0]')
|
||||
run aws_cmd elasticache describe-cache-clusters \
|
||||
--cache-cluster-id "$member" --show-cache-node-info
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.CacheClusters[0].ReplicationGroupId')" = "floci-tf-valkey-cluster" ]
|
||||
[ "$(json_get "$output" '.CacheClusters[0].Engine')" = "valkey" ]
|
||||
[ -n "$(json_get "$output" '.CacheClusters[0].CacheNodes[0].Endpoint.Port')" ]
|
||||
}
|
||||
|
||||
# The critical assertion: the endpoint terraform hands out fronts a Valkey
|
||||
# node that is actually running in cluster mode, in a formed, healthy cluster.
|
||||
@test "ElastiCache cluster mode: Valkey runs in cluster mode behind the configuration endpoint" {
|
||||
host=$(terraform -chdir="$EC_TF_DIR" output -raw configuration_endpoint_address)
|
||||
describe=$(aws_cmd elasticache describe-replication-groups \
|
||||
--replication-group-id floci-tf-valkey-cluster)
|
||||
port=$(json_get "$describe" '.ReplicationGroups[0].ConfigurationEndpoint.Port')
|
||||
|
||||
run valkey_command "$host" "$port" INFO cluster
|
||||
assert_success
|
||||
assert_output --partial "cluster_enabled:1"
|
||||
|
||||
run valkey_command "$host" "$port" CLUSTER INFO
|
||||
assert_success
|
||||
assert_output --partial "cluster_state:ok"
|
||||
assert_output --partial "cluster_size:2"
|
||||
assert_output --partial "cluster_known_nodes:4"
|
||||
}
|
||||
|
||||
# Guards against read-back drift (the class of bug in floci-io/floci#2481):
|
||||
# a second plan straight after apply must not want to change anything.
|
||||
@test "ElastiCache cluster mode: re-plan after apply is clean" {
|
||||
cd "$EC_TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# Minimal VPC + Subnet + Security Group to verify
|
||||
# terraform destroy succeeds after DescribeNetworkInterfaces fix.
|
||||
# Previously https://github.com/floci-io/floci/issues/1031
|
||||
|
||||
resource "aws_vpc" "test" {
|
||||
cidr_block = "10.0.0.0/16"
|
||||
|
||||
tags = {
|
||||
Name = "floci-eni-destroy-vpc"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_subnet" "test" {
|
||||
vpc_id = aws_vpc.test.id
|
||||
cidr_block = "10.0.1.0/24"
|
||||
|
||||
tags = {
|
||||
Name = "floci-eni-destroy-subnet"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_security_group" "test" {
|
||||
name = "floci-eni-destroy-sg"
|
||||
description = "ENI destroy test SG"
|
||||
vpc_id = aws_vpc.test.id
|
||||
|
||||
tags = {
|
||||
Name = "floci-eni-destroy-sg"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
ec2 = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bats
|
||||
# ENI Destroy Compatibility Test
|
||||
#
|
||||
# Reproduces https://github.com/floci-io/floci/issues/1031
|
||||
# Terraform destroy succeeds on VPC+subnet+SG because
|
||||
# DescribeNetworkInterfaces is now implemented.
|
||||
#
|
||||
# This test verifies that terraform destroy on a VPC with
|
||||
# subnets and security groups completes without error.
|
||||
# DescribeNetworkInterfaces returns an empty ENI list when
|
||||
# no instances exist, unblocking the destroy workflow.
|
||||
#
|
||||
# FIX VERIFIED: DescribeNetworkInterfaces implemented,
|
||||
# Test now PASSES. (Previously returned UnsupportedOperation.)
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
# Override TF_DIR to the minimal eni-destroy terraform config
|
||||
ENI_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/eni-destroy-tf" && pwd)"
|
||||
cd "$ENI_TF_DIR"
|
||||
|
||||
echo "# === ENI Destroy Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $ENI_TF_DIR" >&3
|
||||
|
||||
# Clean any previous state
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform validate ---" >&3
|
||||
run terraform validate -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform validate failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform plan ---" >&3
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform plan failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
ENI_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/eni-destroy-tf" && pwd)"
|
||||
cd "$ENI_TF_DIR"
|
||||
|
||||
# Clean up terraform state after test
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
ENI_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/eni-destroy-tf" && pwd)"
|
||||
}
|
||||
|
||||
# --- Spot checks: resources were created ---
|
||||
|
||||
@test "ENI Destroy: VPC was created" {
|
||||
run aws_cmd ec2 describe-vpcs \
|
||||
--filters "Name=tag:Name,Values=floci-eni-destroy-vpc"
|
||||
assert_success
|
||||
assert_output --partial "floci-eni-destroy-vpc"
|
||||
assert_output --partial "10.0.0.0/16"
|
||||
}
|
||||
|
||||
@test "ENI Destroy: Subnet was created" {
|
||||
run terraform -chdir="$ENI_TF_DIR" state show aws_subnet.test
|
||||
assert_success
|
||||
assert_output --partial "10.0.1.0/24"
|
||||
}
|
||||
|
||||
@test "ENI Destroy: Security Group was created" {
|
||||
run aws_cmd ec2 describe-security-groups \
|
||||
--group-names "floci-eni-destroy-sg"
|
||||
assert_success
|
||||
assert_output --partial "floci-eni-destroy-sg"
|
||||
}
|
||||
|
||||
# --- The critical assertion: terraform destroy must succeed ---
|
||||
#
|
||||
# Terraform destroy on a VPC with subnets/SGs calls
|
||||
# DescribeNetworkInterfaces to check for ENIs. With no
|
||||
# instances running, the response is an empty set,
|
||||
# allowing the destroy to proceed cleanly.
|
||||
|
||||
@test "ENI Destroy: terraform destroy succeeds" {
|
||||
cd "$ENI_TF_DIR"
|
||||
|
||||
echo "# --- terraform destroy ---" >&3
|
||||
run terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
|
||||
assert_success
|
||||
|
||||
# Verify resources are actually gone
|
||||
run aws_cmd ec2 describe-vpcs \
|
||||
--filters "Name=tag:Name,Values=floci-eni-destroy-vpc"
|
||||
assert_output --partial '"Vpcs": []' # empty result = VPC deleted
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# Verifies https://github.com/floci-io/floci/issues/2291
|
||||
#
|
||||
# The Terraform AWS provider treats target_key_id on aws_kms_alias as an
|
||||
# in-place update, not a replacement. Flipping which key the alias points at
|
||||
# must therefore call UpdateAlias, not CreateAlias/DeleteAlias.
|
||||
|
||||
resource "aws_kms_key" "a" {
|
||||
description = "floci-kms-update-alias-test key A"
|
||||
}
|
||||
|
||||
resource "aws_kms_key" "b" {
|
||||
description = "floci-kms-update-alias-test key B"
|
||||
}
|
||||
|
||||
variable "use_second_key" {
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
resource "aws_kms_alias" "test" {
|
||||
name = "alias/floci-kms-update-alias-test"
|
||||
target_key_id = var.use_second_key ? aws_kms_key.b.key_id : aws_kms_key.a.key_id
|
||||
}
|
||||
|
||||
output "key_a_id" {
|
||||
value = aws_kms_key.a.key_id
|
||||
}
|
||||
|
||||
output "key_b_id" {
|
||||
value = aws_kms_key.b.key_id
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
kms = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bats
|
||||
# KMS UpdateAlias Compatibility Test
|
||||
#
|
||||
# Reproduces https://github.com/floci-io/floci/issues/2291
|
||||
#
|
||||
# The Terraform AWS provider treats a target_key_id change on aws_kms_alias
|
||||
# as an in-place update (not a replacement), which requires the emulator to
|
||||
# support the UpdateAlias action. This verifies that re-applying with a
|
||||
# different target key updates the existing alias rather than erroring or
|
||||
# recreating it.
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
KMS_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/kms-update-alias-tf" && pwd)"
|
||||
cd "$KMS_TF_DIR"
|
||||
|
||||
echo "# === KMS UpdateAlias Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $KMS_TF_DIR" >&3
|
||||
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply (alias -> key A) ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
KMS_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/kms-update-alias-tf" && pwd)"
|
||||
cd "$KMS_TF_DIR"
|
||||
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
KMS_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/kms-update-alias-tf" && pwd)"
|
||||
}
|
||||
|
||||
@test "KMS UpdateAlias: alias initially targets key A" {
|
||||
KEY_A=$(terraform -chdir="$KMS_TF_DIR" output -raw key_a_id)
|
||||
run aws_cmd kms list-aliases --key-id "$KEY_A" \
|
||||
--query "Aliases[?AliasName=='alias/floci-kms-update-alias-test'].TargetKeyId | [0]" --output text
|
||||
assert_success
|
||||
assert_output "$KEY_A"
|
||||
}
|
||||
|
||||
# The critical assertion: re-applying with a different target_key_id must
|
||||
# update the existing alias in place, not destroy/recreate it and not fail.
|
||||
@test "KMS UpdateAlias: re-apply with a different target updates in place" {
|
||||
cd "$KMS_TF_DIR"
|
||||
|
||||
echo "# --- terraform apply (alias -> key B) ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -var="use_second_key=true" \
|
||||
-input=false -auto-approve -no-color
|
||||
assert_success
|
||||
assert_output --partial "1 changed"
|
||||
refute_output --partial "1 destroyed"
|
||||
|
||||
KEY_B=$(terraform -chdir="$KMS_TF_DIR" output -raw key_b_id)
|
||||
run aws_cmd kms list-aliases --key-id "$KEY_B" \
|
||||
--query "Aliases[?AliasName=='alias/floci-kms-update-alias-test'].TargetKeyId | [0]" --output text
|
||||
assert_success
|
||||
assert_output "$KEY_B"
|
||||
}
|
||||
|
||||
@test "KMS UpdateAlias: alias no longer resolves under key A" {
|
||||
KEY_A=$(terraform -chdir="$KMS_TF_DIR" output -raw key_a_id)
|
||||
run aws_cmd kms list-aliases --key-id "$KEY_A" \
|
||||
--query "Aliases[?AliasName=='alias/floci-kms-update-alias-test']" --output text
|
||||
assert_success
|
||||
[ -z "$output" ]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
resource "aws_neptune_cluster" "cluster" {
|
||||
cluster_identifier = "floci-tf-neptune-instances"
|
||||
engine = "neptune"
|
||||
port = 8183
|
||||
skip_final_snapshot = true
|
||||
apply_immediately = true
|
||||
}
|
||||
|
||||
resource "aws_neptune_cluster_instance" "writer" {
|
||||
identifier = "floci-tf-neptune-writer"
|
||||
cluster_identifier = aws_neptune_cluster.cluster.id
|
||||
instance_class = "db.r5.large"
|
||||
engine = "neptune"
|
||||
port = 8183
|
||||
apply_immediately = true
|
||||
|
||||
tags = {
|
||||
Name = "floci-tf-neptune-writer"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_neptune_cluster_instance" "reader" {
|
||||
identifier = "floci-tf-neptune-reader"
|
||||
cluster_identifier = aws_neptune_cluster.cluster.id
|
||||
instance_class = "db.r5.large"
|
||||
engine = "neptune"
|
||||
port = 8183
|
||||
promotion_tier = 2
|
||||
auto_minor_version_upgrade = false
|
||||
apply_immediately = true
|
||||
|
||||
depends_on = [aws_neptune_cluster_instance.writer]
|
||||
}
|
||||
|
||||
output "writer_arn" {
|
||||
value = aws_neptune_cluster_instance.writer.arn
|
||||
}
|
||||
|
||||
output "writer_endpoint" {
|
||||
value = aws_neptune_cluster_instance.writer.endpoint
|
||||
}
|
||||
|
||||
output "writer_port" {
|
||||
value = aws_neptune_cluster_instance.writer.port
|
||||
}
|
||||
|
||||
output "writer_is_writer" {
|
||||
value = aws_neptune_cluster_instance.writer.writer
|
||||
}
|
||||
|
||||
output "writer_storage_encrypted" {
|
||||
value = aws_neptune_cluster_instance.writer.storage_encrypted
|
||||
}
|
||||
|
||||
output "writer_parameter_group_name" {
|
||||
value = aws_neptune_cluster_instance.writer.neptune_parameter_group_name
|
||||
}
|
||||
|
||||
output "writer_auto_minor_version_upgrade" {
|
||||
value = aws_neptune_cluster_instance.writer.auto_minor_version_upgrade
|
||||
}
|
||||
|
||||
output "reader_is_writer" {
|
||||
value = aws_neptune_cluster_instance.reader.writer
|
||||
}
|
||||
|
||||
output "reader_promotion_tier" {
|
||||
value = aws_neptune_cluster_instance.reader.promotion_tier
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
neptune = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
resource "aws_neptune_cluster" "cluster" {
|
||||
cluster_identifier = "floci-tf-neptune"
|
||||
engine = "neptune"
|
||||
backup_retention_period = 5
|
||||
preferred_backup_window = "07:00-09:00"
|
||||
iam_database_authentication_enabled = true
|
||||
deletion_protection = false
|
||||
skip_final_snapshot = true
|
||||
apply_immediately = true
|
||||
|
||||
tags = {
|
||||
Name = "floci-tf-neptune"
|
||||
}
|
||||
}
|
||||
|
||||
output "cluster_arn" {
|
||||
value = aws_neptune_cluster.cluster.arn
|
||||
}
|
||||
|
||||
output "cluster_resource_id" {
|
||||
value = aws_neptune_cluster.cluster.cluster_resource_id
|
||||
}
|
||||
|
||||
output "endpoint" {
|
||||
value = aws_neptune_cluster.cluster.endpoint
|
||||
}
|
||||
|
||||
output "port" {
|
||||
value = aws_neptune_cluster.cluster.port
|
||||
}
|
||||
|
||||
output "storage_encrypted" {
|
||||
value = aws_neptune_cluster.cluster.storage_encrypted
|
||||
}
|
||||
|
||||
output "parameter_group_name" {
|
||||
value = aws_neptune_cluster.cluster.neptune_cluster_parameter_group_name
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
neptune = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bats
|
||||
# Neptune Cluster Compatibility Test
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
NEPTUNE_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/neptune-cluster-tf" && pwd)"
|
||||
cd "$NEPTUNE_TF_DIR"
|
||||
|
||||
echo "# === Neptune Cluster Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $NEPTUNE_TF_DIR" >&3
|
||||
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
NEPTUNE_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/neptune-cluster-tf" && pwd)"
|
||||
cd "$NEPTUNE_TF_DIR"
|
||||
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
NEPTUNE_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/neptune-cluster-tf" && pwd)"
|
||||
}
|
||||
|
||||
@test "Neptune cluster: terraform reads back the attributes it manages" {
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw storage_encrypted
|
||||
assert_success
|
||||
assert_output "false"
|
||||
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw parameter_group_name
|
||||
assert_success
|
||||
assert_output "default.neptune1.3"
|
||||
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw port
|
||||
assert_success
|
||||
assert_output "8182"
|
||||
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw cluster_arn
|
||||
assert_success
|
||||
assert_output --partial ":cluster:floci-tf-neptune"
|
||||
}
|
||||
|
||||
@test "Neptune cluster: describe reports the configured settings" {
|
||||
run aws_cmd neptune describe-db-clusters --db-cluster-identifier floci-tf-neptune
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.DBClusters[0].Engine')" = "neptune" ]
|
||||
[ "$(json_get "$output" '.DBClusters[0].Status')" = "available" ]
|
||||
[ "$(json_get "$output" '.DBClusters[0].BackupRetentionPeriod')" = "5" ]
|
||||
[ "$(json_get "$output" '.DBClusters[0].PreferredBackupWindow')" = "07:00-09:00" ]
|
||||
[ "$(json_get "$output" '.DBClusters[0].StorageEncrypted')" = "false" ]
|
||||
[ "$(json_get "$output" '.DBClusters[0].DeletionProtection')" = "false" ]
|
||||
[ "$(json_get "$output" '.DBClusters[0].IAMDatabaseAuthenticationEnabled')" = "true" ]
|
||||
[ "$(json_get "$output" '.DBClusters[0].DBClusterParameterGroup')" = "default.neptune1.3" ]
|
||||
[ "$(json_get "$output" '.DBClusters[0].AvailabilityZones | length')" = "1" ]
|
||||
}
|
||||
|
||||
@test "Neptune cluster: tags round-trip through ListTagsForResource" {
|
||||
arn=$(terraform -chdir="$NEPTUNE_TF_DIR" output -raw cluster_arn)
|
||||
run aws_cmd neptune list-tags-for-resource --resource-name "$arn"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.TagList[] | select(.Key == "Name") | .Value')" = "floci-tf-neptune" ]
|
||||
}
|
||||
|
||||
@test "Neptune cluster: re-plan after apply is clean" {
|
||||
cd "$NEPTUNE_TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bats
|
||||
# Neptune Cluster Instance Compatibility Test
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
NEPTUNE_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/neptune-cluster-instance-tf" && pwd)"
|
||||
cd "$NEPTUNE_TF_DIR"
|
||||
|
||||
echo "# === Neptune Cluster Instance Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $NEPTUNE_TF_DIR" >&3
|
||||
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
NEPTUNE_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/neptune-cluster-instance-tf" && pwd)"
|
||||
cd "$NEPTUNE_TF_DIR"
|
||||
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
NEPTUNE_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/neptune-cluster-instance-tf" && pwd)"
|
||||
}
|
||||
|
||||
@test "Neptune cluster instance: terraform reads back the attributes it manages" {
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw writer_is_writer
|
||||
assert_success
|
||||
assert_output "true"
|
||||
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw reader_is_writer
|
||||
assert_success
|
||||
assert_output "false"
|
||||
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw writer_storage_encrypted
|
||||
assert_success
|
||||
assert_output "false"
|
||||
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw writer_parameter_group_name
|
||||
assert_success
|
||||
assert_output "default.neptune1.3"
|
||||
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw writer_port
|
||||
assert_success
|
||||
assert_output "8183"
|
||||
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw writer_auto_minor_version_upgrade
|
||||
assert_success
|
||||
assert_output "true"
|
||||
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw reader_promotion_tier
|
||||
assert_success
|
||||
assert_output "2"
|
||||
|
||||
run terraform -chdir="$NEPTUNE_TF_DIR" output -raw writer_endpoint
|
||||
assert_success
|
||||
assert_output --partial ":8183"
|
||||
}
|
||||
|
||||
@test "Neptune cluster instance: describe reports the configured settings" {
|
||||
run aws_cmd neptune describe-db-instances --db-instance-identifier floci-tf-neptune-reader
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.DBInstances[0].DBClusterIdentifier')" = "floci-tf-neptune-instances" ]
|
||||
[ "$(json_get "$output" '.DBInstances[0].DBInstanceClass')" = "db.r5.large" ]
|
||||
[ "$(json_get "$output" '.DBInstances[0].DBInstanceStatus')" = "available" ]
|
||||
[ "$(json_get "$output" '.DBInstances[0].Endpoint.Port')" = "8183" ]
|
||||
[ "$(json_get "$output" '.DBInstances[0].PromotionTier')" = "2" ]
|
||||
[ "$(json_get "$output" '.DBInstances[0].AutoMinorVersionUpgrade')" = "false" ]
|
||||
[ "$(json_get "$output" '.DBInstances[0].PubliclyAccessible')" = "false" ]
|
||||
[ "$(json_get "$output" '.DBInstances[0].StorageEncrypted')" = "false" ]
|
||||
[ "$(json_get "$output" '.DBInstances[0].DBSubnetGroup.DBSubnetGroupName')" = "default" ]
|
||||
[ "$(json_get "$output" '.DBInstances[0].DBParameterGroups[0].DBParameterGroupName')" = "default.neptune1.3" ]
|
||||
}
|
||||
|
||||
@test "Neptune cluster instance: tags round-trip through ListTagsForResource" {
|
||||
arn=$(terraform -chdir="$NEPTUNE_TF_DIR" output -raw writer_arn)
|
||||
run aws_cmd neptune list-tags-for-resource --resource-name "$arn"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.TagList[] | select(.Key == "Name") | .Value')" = "floci-tf-neptune-writer" ]
|
||||
}
|
||||
|
||||
@test "Neptune cluster instance: the cluster reports exactly one writer" {
|
||||
run aws_cmd neptune describe-db-clusters --db-cluster-identifier floci-tf-neptune-instances
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.DBClusters[0].DBClusterMembers | length')" = "2" ]
|
||||
[ "$(json_get "$output" '[.DBClusters[0].DBClusterMembers[] | select(.IsClusterWriter)] | length')" = "1" ]
|
||||
[ "$(json_get "$output" '.DBClusters[0].DBClusterMembers[] | select(.IsClusterWriter) | .DBInstanceIdentifier')" = "floci-tf-neptune-writer" ]
|
||||
}
|
||||
|
||||
@test "Neptune cluster instance: re-plan after apply is clean" {
|
||||
cd "$NEPTUNE_TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
# Verifies aws_db_proxy's endpoint_network_type/target_connection_network_type round-trip
|
||||
# through CreateDBProxy/DescribeDBProxies instead of being rejected outright. AWS requires both
|
||||
# the VPC and every selected subnet to carry an IPv6 CIDR block for IPV6/DUAL (see
|
||||
# https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-network-prereqs.html), so this
|
||||
# VPC and its subnets are genuinely dual-stack; see rds_proxy_network_type.bats for the companion
|
||||
# negative case against an IPv4-only VPC.
|
||||
|
||||
resource "aws_vpc" "proxy" {
|
||||
cidr_block = "10.80.0.0/16"
|
||||
assign_generated_ipv6_cidr_block = true
|
||||
|
||||
tags = {
|
||||
Name = "floci-rds-proxy-network-type"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_subnet" "a" {
|
||||
vpc_id = aws_vpc.proxy.id
|
||||
cidr_block = "10.80.1.0/24"
|
||||
ipv6_cidr_block = cidrsubnet(aws_vpc.proxy.ipv6_cidr_block, 8, 0)
|
||||
availability_zone = "us-east-1a"
|
||||
}
|
||||
|
||||
resource "aws_subnet" "b" {
|
||||
vpc_id = aws_vpc.proxy.id
|
||||
cidr_block = "10.80.2.0/24"
|
||||
ipv6_cidr_block = cidrsubnet(aws_vpc.proxy.ipv6_cidr_block, 8, 1)
|
||||
availability_zone = "us-east-1b"
|
||||
}
|
||||
|
||||
resource "aws_security_group" "proxy" {
|
||||
name = "floci-rds-proxy-network-type"
|
||||
vpc_id = aws_vpc.proxy.id
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret" "db" {
|
||||
name = "floci-rds-proxy-network-type-secret"
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret_version" "db" {
|
||||
secret_id = aws_secretsmanager_secret.db.id
|
||||
secret_string = jsonencode({ username = "app", password = "changeme123!" })
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "proxy" {
|
||||
name = "floci-rds-proxy-network-type-role"
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Principal = { Service = "rds.amazonaws.com" }
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
# IPv4-only sibling VPC/subnets: the negative case in rds_proxy_network_type.bats asserts a
|
||||
# direct CreateDBProxy against these with EndpointNetworkType=DUAL is rejected, since AWS
|
||||
# requires the VPC to have an IPv6 CIDR block for that.
|
||||
resource "aws_vpc" "ipv4_only" {
|
||||
cidr_block = "10.90.0.0/16"
|
||||
|
||||
tags = {
|
||||
Name = "floci-rds-proxy-network-type-ipv4-only"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_subnet" "ipv4_only_a" {
|
||||
vpc_id = aws_vpc.ipv4_only.id
|
||||
cidr_block = "10.90.1.0/24"
|
||||
availability_zone = "us-east-1a"
|
||||
}
|
||||
|
||||
resource "aws_subnet" "ipv4_only_b" {
|
||||
vpc_id = aws_vpc.ipv4_only.id
|
||||
cidr_block = "10.90.2.0/24"
|
||||
availability_zone = "us-east-1b"
|
||||
}
|
||||
|
||||
# Dual-stack VPC whose subnets never got an IPv6 CIDR block: the negative case in
|
||||
# rds_proxy_network_type.bats asserts a direct CreateDBProxy against these with
|
||||
# EndpointNetworkType=DUAL is still rejected, since AWS requires every selected subnet -- not
|
||||
# just the VPC -- to carry an IPv6 CIDR block.
|
||||
resource "aws_vpc" "ipv6_subnets_missing" {
|
||||
cidr_block = "10.100.0.0/16"
|
||||
assign_generated_ipv6_cidr_block = true
|
||||
|
||||
tags = {
|
||||
Name = "floci-rds-proxy-network-type-ipv6-subnets-missing"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_subnet" "ipv6_subnets_missing_a" {
|
||||
vpc_id = aws_vpc.ipv6_subnets_missing.id
|
||||
cidr_block = "10.100.1.0/24"
|
||||
availability_zone = "us-east-1a"
|
||||
}
|
||||
|
||||
resource "aws_subnet" "ipv6_subnets_missing_b" {
|
||||
vpc_id = aws_vpc.ipv6_subnets_missing.id
|
||||
cidr_block = "10.100.2.0/24"
|
||||
availability_zone = "us-east-1b"
|
||||
}
|
||||
|
||||
resource "aws_db_proxy" "test" {
|
||||
name = "floci-rds-proxy-network-type"
|
||||
engine_family = "MYSQL"
|
||||
role_arn = aws_iam_role.proxy.arn
|
||||
vpc_subnet_ids = [aws_subnet.a.id, aws_subnet.b.id]
|
||||
vpc_security_group_ids = [aws_security_group.proxy.id]
|
||||
endpoint_network_type = "DUAL"
|
||||
target_connection_network_type = "IPV6"
|
||||
|
||||
auth {
|
||||
auth_scheme = "SECRETS"
|
||||
iam_auth = "DISABLED"
|
||||
secret_arn = aws_secretsmanager_secret.db.arn
|
||||
}
|
||||
}
|
||||
|
||||
output "proxy_arn" {
|
||||
value = aws_db_proxy.test.arn
|
||||
}
|
||||
|
||||
output "endpoint_network_type" {
|
||||
value = aws_db_proxy.test.endpoint_network_type
|
||||
}
|
||||
|
||||
output "target_connection_network_type" {
|
||||
value = aws_db_proxy.test.target_connection_network_type
|
||||
}
|
||||
|
||||
output "role_arn" {
|
||||
value = aws_iam_role.proxy.arn
|
||||
}
|
||||
|
||||
output "secret_arn" {
|
||||
value = aws_secretsmanager_secret.db.arn
|
||||
}
|
||||
|
||||
output "ipv4_only_subnet_ids" {
|
||||
value = [aws_subnet.ipv4_only_a.id, aws_subnet.ipv4_only_b.id]
|
||||
}
|
||||
|
||||
output "ipv6_subnets_missing_subnet_ids" {
|
||||
value = [aws_subnet.ipv6_subnets_missing_a.id, aws_subnet.ipv6_subnets_missing_b.id]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
rds = var.endpoint
|
||||
ec2 = var.endpoint
|
||||
iam = var.endpoint
|
||||
secretsmanager = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# Verifies aws_db_snapshot tags round-trip through CreateDBSnapshot/TagResource/DescribeDBSnapshots
|
||||
# instead of TagResource rejecting the snapshot ARN outright ("not yet implemented by Floci").
|
||||
|
||||
resource "aws_db_instance" "source" {
|
||||
identifier = "floci-rds-snapshot-tags-source"
|
||||
engine = "postgres"
|
||||
engine_version = "16"
|
||||
instance_class = "db.t3.micro"
|
||||
allocated_storage = 20
|
||||
username = "app"
|
||||
password = "changeme123!"
|
||||
skip_final_snapshot = true
|
||||
}
|
||||
|
||||
variable "owner_tag" {
|
||||
type = string
|
||||
default = "platform"
|
||||
}
|
||||
|
||||
resource "aws_db_snapshot" "test" {
|
||||
db_instance_identifier = aws_db_instance.source.identifier
|
||||
db_snapshot_identifier = "floci-rds-snapshot-tags-snap"
|
||||
|
||||
tags = {
|
||||
owner = var.owner_tag
|
||||
}
|
||||
}
|
||||
|
||||
output "db_snapshot_arn" {
|
||||
value = aws_db_snapshot.test.db_snapshot_arn
|
||||
}
|
||||
|
||||
output "owner_tag" {
|
||||
value = aws_db_snapshot.test.tags["owner"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_requesting_account_id = true
|
||||
s3_use_path_style = true
|
||||
|
||||
endpoints {
|
||||
rds = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bats
|
||||
# RDS Proxy EndpointNetworkType/TargetConnectionNetworkType Compatibility Test
|
||||
#
|
||||
# CreateDBProxy used to reject any EndpointNetworkType/TargetConnectionNetworkType other than
|
||||
# IPV4 outright. This verifies aws_db_proxy can apply with endpoint_network_type = "DUAL" and
|
||||
# target_connection_network_type = "IPV6", that DescribeDBProxies reads the values back, and
|
||||
# that a second plan is clean (no perpetual diff from a default the provider disagrees with).
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
PROXY_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/rds-proxy-network-type-tf" && pwd)"
|
||||
cd "$PROXY_TF_DIR"
|
||||
|
||||
echo "# === RDS Proxy Network Type Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $PROXY_TF_DIR" >&3
|
||||
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
PROXY_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/rds-proxy-network-type-tf" && pwd)"
|
||||
cd "$PROXY_TF_DIR"
|
||||
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
PROXY_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/rds-proxy-network-type-tf" && pwd)"
|
||||
}
|
||||
|
||||
@test "RDS Proxy network type: apply reports DUAL/IPV6 outputs" {
|
||||
ENDPOINT_TYPE=$(terraform -chdir="$PROXY_TF_DIR" output -raw endpoint_network_type)
|
||||
TARGET_TYPE=$(terraform -chdir="$PROXY_TF_DIR" output -raw target_connection_network_type)
|
||||
assert_equal "$ENDPOINT_TYPE" "DUAL"
|
||||
assert_equal "$TARGET_TYPE" "IPV6"
|
||||
}
|
||||
|
||||
@test "RDS Proxy network type: DescribeDBProxies reads the values back" {
|
||||
run aws_cmd rds describe-db-proxies --db-proxy-name floci-rds-proxy-network-type \
|
||||
--query "DBProxies[0].EndpointNetworkType" --output text
|
||||
assert_success
|
||||
assert_output "DUAL"
|
||||
|
||||
run aws_cmd rds describe-db-proxies --db-proxy-name floci-rds-proxy-network-type \
|
||||
--query "DBProxies[0].TargetConnectionNetworkType" --output text
|
||||
assert_success
|
||||
assert_output "IPV6"
|
||||
}
|
||||
|
||||
@test "RDS Proxy network type: second plan reports no changes" {
|
||||
cd "$PROXY_TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
|
||||
@test "RDS Proxy network type: CreateDBProxy rejects DUAL against an IPv4-only VPC" {
|
||||
ROLE_ARN=$(terraform -chdir="$PROXY_TF_DIR" output -raw role_arn)
|
||||
SECRET_ARN=$(terraform -chdir="$PROXY_TF_DIR" output -raw secret_arn)
|
||||
SUBNET_A=$(terraform -chdir="$PROXY_TF_DIR" output -json ipv4_only_subnet_ids | jq -r '.[0]')
|
||||
SUBNET_B=$(terraform -chdir="$PROXY_TF_DIR" output -json ipv4_only_subnet_ids | jq -r '.[1]')
|
||||
|
||||
run aws_cmd rds create-db-proxy \
|
||||
--db-proxy-name floci-rds-proxy-ipv4-only-rejected \
|
||||
--engine-family MYSQL \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--vpc-subnet-ids "$SUBNET_A" "$SUBNET_B" \
|
||||
--auth "AuthScheme=SECRETS,SecretArn=${SECRET_ARN},IAMAuth=DISABLED" \
|
||||
--endpoint-network-type DUAL
|
||||
assert_failure
|
||||
assert_output --partial "IPv6 CIDR block"
|
||||
|
||||
run aws_cmd rds describe-db-proxies --db-proxy-name floci-rds-proxy-ipv4-only-rejected
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "RDS Proxy network type: CreateDBProxy rejects DUAL when the VPC is dual-stack but the subnets are not" {
|
||||
ROLE_ARN=$(terraform -chdir="$PROXY_TF_DIR" output -raw role_arn)
|
||||
SECRET_ARN=$(terraform -chdir="$PROXY_TF_DIR" output -raw secret_arn)
|
||||
SUBNET_A=$(terraform -chdir="$PROXY_TF_DIR" output -json ipv6_subnets_missing_subnet_ids | jq -r '.[0]')
|
||||
SUBNET_B=$(terraform -chdir="$PROXY_TF_DIR" output -json ipv6_subnets_missing_subnet_ids | jq -r '.[1]')
|
||||
|
||||
run aws_cmd rds create-db-proxy \
|
||||
--db-proxy-name floci-rds-proxy-ipv6-subnets-missing-rejected \
|
||||
--engine-family MYSQL \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--vpc-subnet-ids "$SUBNET_A" "$SUBNET_B" \
|
||||
--auth "AuthScheme=SECRETS,SecretArn=${SECRET_ARN},IAMAuth=DISABLED" \
|
||||
--endpoint-network-type DUAL
|
||||
assert_failure
|
||||
assert_output --partial "VpcSubnetIds"
|
||||
assert_output --partial "IPv6 CIDR block"
|
||||
|
||||
run aws_cmd rds describe-db-proxies --db-proxy-name floci-rds-proxy-ipv6-subnets-missing-rejected
|
||||
assert_failure
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bats
|
||||
# RDS DB Snapshot Tagging Compatibility Test
|
||||
#
|
||||
# TagResource/UntagResource used to reject a snapshot ARN outright ("not yet implemented by
|
||||
# Floci"), and CreateDBSnapshot silently dropped its Tags parameter. This verifies aws_db_snapshot
|
||||
# can apply with tags, that DescribeDBSnapshots reads them back (both DBSnapshotArn and TagList),
|
||||
# that a second plan is clean, and that changing a tag updates in place rather than replacing.
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
SNAP_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/rds-snapshot-tags-tf" && pwd)"
|
||||
cd "$SNAP_TF_DIR"
|
||||
|
||||
echo "# === RDS Snapshot Tagging Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $SNAP_TF_DIR" >&3
|
||||
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
SNAP_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/rds-snapshot-tags-tf" && pwd)"
|
||||
cd "$SNAP_TF_DIR"
|
||||
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
SNAP_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/rds-snapshot-tags-tf" && pwd)"
|
||||
}
|
||||
|
||||
@test "RDS snapshot tags: apply reports a snapshot ARN pinned to the provider's region" {
|
||||
ARN=$(terraform -chdir="$SNAP_TF_DIR" output -raw db_snapshot_arn)
|
||||
OWNER=$(terraform -chdir="$SNAP_TF_DIR" output -raw owner_tag)
|
||||
# Exact match, not a wildcard: us-east-1 is also the default region, so a wildcard here would miss a regression.
|
||||
assert_equal "$ARN" "arn:aws:rds:us-east-1:000000000000:snapshot:floci-rds-snapshot-tags-snap"
|
||||
assert_equal "$OWNER" "platform"
|
||||
}
|
||||
|
||||
@test "RDS snapshot tags: DescribeDBSnapshots reads the tag back" {
|
||||
run aws_cmd rds describe-db-snapshots --db-snapshot-identifier floci-rds-snapshot-tags-snap \
|
||||
--query "DBSnapshots[0].TagList[?Key=='owner'].Value | [0]" --output text
|
||||
assert_success
|
||||
assert_output "platform"
|
||||
}
|
||||
|
||||
@test "RDS snapshot tags: second plan reports no changes" {
|
||||
cd "$SNAP_TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode
|
||||
assert_success
|
||||
assert_output --partial "No changes"
|
||||
}
|
||||
|
||||
@test "RDS snapshot tags: changing a tag updates in place, not a replacement" {
|
||||
cd "$SNAP_TF_DIR"
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -var="owner_tag=data-team" \
|
||||
-input=false -auto-approve -no-color
|
||||
assert_success
|
||||
assert_output --partial "1 changed"
|
||||
refute_output --partial "1 destroyed"
|
||||
|
||||
run aws_cmd rds describe-db-snapshots --db-snapshot-identifier floci-rds-snapshot-tags-snap \
|
||||
--query "DBSnapshots[0].TagList[?Key=='owner'].Value | [0]" --output text
|
||||
assert_success
|
||||
assert_output "data-team"
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
# Regression coverage for #2266 at the Terraform layer.
|
||||
#
|
||||
# "Allow traffic from security group X" is how one tier is wired to another -- an ALB
|
||||
# to its backend tasks, an application to its database, a cluster to itself. When
|
||||
# AuthorizeSecurityGroupIngress dropped the source, the provider's create-wait could
|
||||
# not find the rule it had just made and the apply failed with
|
||||
# "couldn't find resource"; a response that reported the rule but lost the reference
|
||||
# would instead apply cleanly and then show as drift on every subsequent plan.
|
||||
#
|
||||
# Both of the standard spellings are exercised, because they read the source back
|
||||
# through different APIs: aws_security_group_rule polls DescribeSecurityGroups, and
|
||||
# aws_vpc_security_group_ingress_rule polls DescribeSecurityGroupRules.
|
||||
|
||||
resource "aws_vpc" "sg_sources" {
|
||||
cidr_block = "10.71.0.0/16"
|
||||
|
||||
tags = {
|
||||
Name = "floci-sg-rule-sources"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_security_group" "alb" {
|
||||
name = "floci-sg-rule-sources-alb"
|
||||
description = "Front tier"
|
||||
vpc_id = aws_vpc.sg_sources.id
|
||||
}
|
||||
|
||||
resource "aws_security_group" "backend" {
|
||||
name = "floci-sg-rule-sources-backend"
|
||||
description = "Back tier, reachable only from the front tier"
|
||||
vpc_id = aws_vpc.sg_sources.id
|
||||
}
|
||||
|
||||
# The classic spelling: reads its source back off DescribeSecurityGroups.
|
||||
resource "aws_security_group_rule" "backend_from_alb" {
|
||||
type = "ingress"
|
||||
from_port = 8080
|
||||
to_port = 8080
|
||||
protocol = "tcp"
|
||||
security_group_id = aws_security_group.backend.id
|
||||
source_security_group_id = aws_security_group.alb.id
|
||||
description = "app traffic from the load balancer"
|
||||
}
|
||||
|
||||
# Self-reference: a cluster whose members talk to each other. This is the shape that
|
||||
# fails first, because the group is both the rule's owner and its source.
|
||||
resource "aws_security_group_rule" "backend_self" {
|
||||
type = "ingress"
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
security_group_id = aws_security_group.backend.id
|
||||
self = true
|
||||
description = "intra-cluster traffic"
|
||||
}
|
||||
|
||||
# The current spelling: reads its source back off DescribeSecurityGroupRules, and
|
||||
# needs referencedGroupInfo rather than a UserIdGroupPair.
|
||||
resource "aws_vpc_security_group_ingress_rule" "backend_from_alb_v2" {
|
||||
security_group_id = aws_security_group.backend.id
|
||||
referenced_security_group_id = aws_security_group.alb.id
|
||||
from_port = 9090
|
||||
to_port = 9090
|
||||
ip_protocol = "tcp"
|
||||
description = "admin traffic from the load balancer"
|
||||
}
|
||||
|
||||
# A CIDR rule alongside them: the reference must not be lost when a group carries
|
||||
# both kinds, and a CIDR rule must not sprout a phantom reference.
|
||||
resource "aws_vpc_security_group_ingress_rule" "backend_from_cidr" {
|
||||
security_group_id = aws_security_group.backend.id
|
||||
cidr_ipv4 = "10.71.0.0/16"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
ip_protocol = "tcp"
|
||||
description = "https from inside the vpc"
|
||||
}
|
||||
|
||||
output "alb_sg_id" {
|
||||
value = aws_security_group.alb.id
|
||||
}
|
||||
|
||||
output "backend_sg_id" {
|
||||
value = aws_security_group.backend.id
|
||||
}
|
||||
|
||||
output "backend_from_alb_v2_rule_id" {
|
||||
value = aws_vpc_security_group_ingress_rule.backend_from_alb_v2.security_group_rule_id
|
||||
}
|
||||
|
||||
output "backend_from_cidr_rule_id" {
|
||||
value = aws_vpc_security_group_ingress_rule.backend_from_cidr.security_group_rule_id
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "http://localhost:4566"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
s3_use_path_style = true
|
||||
|
||||
# Deliberately NOT skip_requesting_account_id. The provider renders a same-account
|
||||
# referenced_security_group_id as a bare group id and a cross-account one as
|
||||
# "<account>/<group>", and it tells them apart by comparing ReferencedGroupInfo.UserId
|
||||
# against its own resolved account. Skipping account resolution leaves that unknown,
|
||||
# so every same-account reference reads back as cross-account and the plan never
|
||||
# converges. Floci answers STS GetCallerIdentity, so there is nothing to skip.
|
||||
|
||||
endpoints {
|
||||
ec2 = var.endpoint
|
||||
sts = var.endpoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bats
|
||||
# Security Group Rule Sources Compatibility Test
|
||||
#
|
||||
# Terraform-layer regression coverage for
|
||||
# https://github.com/floci-io/floci/issues/2266
|
||||
#
|
||||
# The fix is already covered by Ec2SecurityGroupRuleSourcesIntegrationTest and by an
|
||||
# AWS CLI case in sdk-test-awscli. This adds the layer where the bug actually bit:
|
||||
# the Terraform provider polls for the rule it just created and fails the apply when
|
||||
# it cannot find it, which no API-level assertion reproduces. It also pins the
|
||||
# quieter half of the same bug -- a rule that comes back without its source applies
|
||||
# cleanly and then shows as drift on every later plan, which only an empty re-plan
|
||||
# catches.
|
||||
#
|
||||
# This config has its own provider.tf rather than reusing the module's, because it
|
||||
# must NOT set skip_requesting_account_id. See the comment there: skipping account
|
||||
# resolution makes every same-account group reference read back as cross-account, and
|
||||
# the re-plan assertion below fails for a reason that has nothing to do with #2266.
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
SG_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/security-group-rule-sources-tf" && pwd)"
|
||||
cd "$SG_TF_DIR"
|
||||
|
||||
echo "# === Security Group Rule Sources Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
echo "# Config: $SG_TF_DIR" >&3
|
||||
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
# This apply is itself the primary assertion. Before #2266 it failed with
|
||||
# "waiting for Security Group Rule create: couldn't find resource".
|
||||
echo "# --- terraform apply ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
SG_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/security-group-rule-sources-tf" && pwd)"
|
||||
cd "$SG_TF_DIR"
|
||||
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
SG_TF_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/security-group-rule-sources-tf" && pwd)"
|
||||
}
|
||||
|
||||
@test "SG rule sources: applying a config with group-referencing rules succeeds" {
|
||||
# setup_file returns non-zero if the apply failed, which fails every test in the
|
||||
# file. Asserting the resulting state here makes the cause obvious in the output.
|
||||
run terraform -chdir="$SG_TF_DIR" state list
|
||||
assert_success
|
||||
assert_output --partial "aws_security_group_rule.backend_from_alb"
|
||||
assert_output --partial "aws_security_group_rule.backend_self"
|
||||
assert_output --partial "aws_vpc_security_group_ingress_rule.backend_from_alb_v2"
|
||||
}
|
||||
|
||||
@test "SG rule sources: DescribeSecurityGroups reports the referencing group" {
|
||||
ALB=$(terraform -chdir="$SG_TF_DIR" output -raw alb_sg_id)
|
||||
BACKEND=$(terraform -chdir="$SG_TF_DIR" output -raw backend_sg_id)
|
||||
|
||||
run aws_cmd ec2 describe-security-groups --group-ids "$BACKEND" \
|
||||
--query "SecurityGroups[0].IpPermissions[?FromPort==\`8080\`].UserIdGroupPairs[0].GroupId | [0]" \
|
||||
--output text
|
||||
assert_success
|
||||
assert_output "$ALB"
|
||||
}
|
||||
|
||||
@test "SG rule sources: a self-referencing rule keeps its own group as the source" {
|
||||
BACKEND=$(terraform -chdir="$SG_TF_DIR" output -raw backend_sg_id)
|
||||
|
||||
run aws_cmd ec2 describe-security-groups --group-ids "$BACKEND" \
|
||||
--query "SecurityGroups[0].IpPermissions[?IpProtocol=='-1'].UserIdGroupPairs[0].GroupId | [0]" \
|
||||
--output text
|
||||
assert_success
|
||||
assert_output "$BACKEND"
|
||||
}
|
||||
|
||||
@test "SG rule sources: DescribeSecurityGroupRules reports referencedGroupInfo" {
|
||||
ALB=$(terraform -chdir="$SG_TF_DIR" output -raw alb_sg_id)
|
||||
RULE_ID=$(terraform -chdir="$SG_TF_DIR" output -raw backend_from_alb_v2_rule_id)
|
||||
|
||||
run aws_cmd ec2 describe-security-group-rules --security-group-rule-ids "$RULE_ID" \
|
||||
--query "SecurityGroupRules[0].ReferencedGroupInfo.GroupId" --output text
|
||||
assert_success
|
||||
assert_output "$ALB"
|
||||
}
|
||||
|
||||
@test "SG rule sources: a CIDR rule keeps its CIDR and gains no phantom reference" {
|
||||
RULE_ID=$(terraform -chdir="$SG_TF_DIR" output -raw backend_from_cidr_rule_id)
|
||||
|
||||
run aws_cmd ec2 describe-security-group-rules --security-group-rule-ids "$RULE_ID" \
|
||||
--query "SecurityGroupRules[0].CidrIpv4" --output text
|
||||
assert_success
|
||||
assert_output "10.71.0.0/16"
|
||||
|
||||
run aws_cmd ec2 describe-security-group-rules --security-group-rule-ids "$RULE_ID" \
|
||||
--query "SecurityGroupRules[0].ReferencedGroupInfo" --output text
|
||||
assert_success
|
||||
assert_output "None"
|
||||
}
|
||||
|
||||
# The assertion a successful apply cannot make. A response that reports the rule but
|
||||
# loses its source still applies cleanly; it only surfaces later, as a plan that never
|
||||
# converges. Scoped to this configuration, which is self-contained.
|
||||
@test "SG rule sources: re-planning reports no changes" {
|
||||
cd "$SG_TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode
|
||||
if [ "$status" -eq 2 ]; then
|
||||
echo "# drift detected on re-plan:" >&3
|
||||
echo "$output" >&3
|
||||
fi
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
#!/usr/bin/env bats
|
||||
# Terraform Compatibility Tests for floci
|
||||
|
||||
setup_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
cd "$TF_DIR"
|
||||
|
||||
echo "# === Terraform Compatibility Test ===" >&3
|
||||
echo "# Endpoint: $FLOCI_ENDPOINT" >&3
|
||||
|
||||
# Clean any previous state
|
||||
rm -rf .terraform .terraform.lock.hcl terraform.tfstate* 2>/dev/null || true
|
||||
|
||||
echo "# --- Setup: state bucket & lock table ---" >&3
|
||||
create_state_backend
|
||||
generate_backend_config
|
||||
|
||||
# 12 MiB source for aws_s3_object.large: above the provider's 5 MiB part size, so it goes multipart
|
||||
head -c $((12 * 1024 * 1024)) /dev/urandom > "$TF_DIR/large-object.bin"
|
||||
|
||||
echo "# --- terraform init ---" >&3
|
||||
run terraform init -backend-config=/tmp/floci-backend.hcl \
|
||||
-var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform init failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform validate ---" >&3
|
||||
run terraform validate -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform validate failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform plan ---" >&3
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform plan failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "# --- terraform apply ---" >&3
|
||||
run terraform apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "# terraform apply failed: $output" >&3
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
cd "$TF_DIR"
|
||||
|
||||
echo "# --- terraform destroy ---" >&3
|
||||
terraform destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
|
||||
rm -f "$TF_DIR/large-object.bin"
|
||||
}
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
}
|
||||
|
||||
# --- Spot Checks ---
|
||||
|
||||
@test "Terraform: S3 bucket created" {
|
||||
run aws_cmd s3api head-bucket --bucket floci-compat-app
|
||||
assert_success
|
||||
}
|
||||
|
||||
# Base64 of a hex SHA256 digest, the way S3 encodes checksums.
|
||||
hex_to_b64() {
|
||||
printf "$(echo "$1" | sed 's/../\\x&/g')" | base64 | tr -d '\n'
|
||||
}
|
||||
|
||||
# The composite SHA256 S3 stores for a multipart object: cut the local file at the part sizes S3
|
||||
# reports, hash each part, hash the concatenated binary digests and append the part count.
|
||||
sha256_composite_by_sizes() {
|
||||
local file="$1"
|
||||
shift
|
||||
local joined offset=0 count=0 size hex
|
||||
joined=$(mktemp)
|
||||
for size in "$@"; do
|
||||
hex=$(tail -c +$((offset + 1)) "$file" | head -c "$size" | sha256sum | cut -c1-64)
|
||||
printf "$(echo "$hex" | sed 's/../\\x&/g')" >> "$joined"
|
||||
offset=$((offset + size))
|
||||
count=$((count + 1))
|
||||
done
|
||||
echo "$(hex_to_b64 "$(sha256sum "$joined" | cut -c1-64)")-$count"
|
||||
rm -f "$joined"
|
||||
}
|
||||
|
||||
@test "Terraform: large aws_s3_object is multipart-uploaded with the composite SHA256 checksum" {
|
||||
run aws_cmd s3api head-object --bucket floci-compat-app --key large/multipart.bin \
|
||||
--checksum-mode ENABLED --output json
|
||||
assert_success
|
||||
local checksum type etag
|
||||
checksum=$(echo "$output" | jq -r '.ChecksumSHA256')
|
||||
type=$(echo "$output" | jq -r '.ChecksumType')
|
||||
etag=$(echo "$output" | jq -r '.ETag')
|
||||
[ "$type" = "COMPOSITE" ]
|
||||
[[ "$etag" =~ -[0-9]+\"$ ]]
|
||||
|
||||
run aws_cmd s3api get-object-attributes --bucket floci-compat-app --key large/multipart.bin \
|
||||
--object-attributes Checksum ObjectParts --output json
|
||||
assert_success
|
||||
[ "$(echo "$output" | jq -r '.ObjectParts.TotalPartsCount')" -gt 1 ]
|
||||
local sizes expected
|
||||
sizes=$(echo "$output" | jq -r '.ObjectParts.Parts[].Size')
|
||||
expected=$(sha256_composite_by_sizes "$TF_DIR/large-object.bin" $sizes)
|
||||
[ "$checksum" = "$expected" ]
|
||||
# GetObjectAttributes reports the composite without the part-count suffix
|
||||
[ "$(echo "$output" | jq -r '.Checksum.ChecksumSHA256')" = "${expected%-*}" ]
|
||||
}
|
||||
|
||||
@test "Terraform: state holds the composite checksum S3 reports for the large object" {
|
||||
cd "$TF_DIR"
|
||||
run terraform output -raw large_object_checksum_sha256
|
||||
assert_success
|
||||
local head_checksum
|
||||
head_checksum=$(aws_cmd s3api head-object --bucket floci-compat-app --key large/multipart.bin \
|
||||
--checksum-mode ENABLED --output json | jq -r '.ChecksumSHA256')
|
||||
[ "$output" = "$head_checksum" ]
|
||||
}
|
||||
|
||||
@test "Terraform: re-planning the large object reports no changes" {
|
||||
cd "$TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode \
|
||||
-target=aws_s3_object.large
|
||||
if [ "$status" -eq 2 ]; then
|
||||
echo "# drift detected on re-plan:" >&3
|
||||
echo "$output" >&3
|
||||
fi
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Terraform: SQS queue created" {
|
||||
run aws_cmd sqs get-queue-url --queue-name floci-compat-jobs
|
||||
assert_success
|
||||
assert_output --partial "QueueUrl"
|
||||
}
|
||||
|
||||
@test "Terraform: SNS topic created" {
|
||||
run aws_cmd sns list-topics
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-events"
|
||||
}
|
||||
|
||||
@test "Terraform: SES receipt filter created" {
|
||||
run aws_cmd ses list-receipt-filters
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-filter"
|
||||
assert_output --partial "10.10.10.0/24"
|
||||
}
|
||||
|
||||
@test "Terraform: SES receipt rule set created and active" {
|
||||
run aws_cmd ses describe-receipt-rule-set --rule-set-name floci-compat-rule-set
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-rule-set"
|
||||
|
||||
run aws_cmd ses describe-active-receipt-rule-set
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-rule-set"
|
||||
assert_output --partial "X-Floci-Compat"
|
||||
}
|
||||
|
||||
@test "Terraform: SES receipt rule round-trips its actions" {
|
||||
run aws_cmd ses describe-receipt-rule --rule-set-name floci-compat-rule-set --rule-name floci-compat-rule
|
||||
assert_success
|
||||
assert_output --partial "X-Floci-Compat"
|
||||
assert_output --partial "floci-compat-events"
|
||||
assert_output --partial "RuleSet"
|
||||
}
|
||||
|
||||
@test "Terraform: DynamoDB table created" {
|
||||
run aws_cmd dynamodb describe-table --table-name floci-compat-items
|
||||
assert_success
|
||||
assert_output --partial "ACTIVE"
|
||||
}
|
||||
|
||||
@test "Terraform: SSM parameter created" {
|
||||
run aws_cmd ssm get-parameter --name /floci-compat/db-url
|
||||
assert_success
|
||||
assert_output --partial "jdbc:"
|
||||
}
|
||||
|
||||
@test "Terraform: Secrets Manager secret created" {
|
||||
run aws_cmd secretsmanager describe-secret --secret-id "floci-compat/db-creds"
|
||||
assert_success
|
||||
assert_output --partial "floci-compat"
|
||||
}
|
||||
|
||||
@test "Terraform: ECR repository created" {
|
||||
run aws_cmd ecr describe-repositories --repository-names floci-compat-app
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-app"
|
||||
}
|
||||
|
||||
@test "Terraform: ECS cluster created" {
|
||||
run aws_cmd ecs describe-clusters --clusters floci-compat-cluster
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-cluster"
|
||||
assert_output --partial "ACTIVE"
|
||||
}
|
||||
|
||||
@test "Terraform: KMS key created" {
|
||||
run aws_cmd kms list-keys
|
||||
assert_success
|
||||
[ "$(aws_cmd kms list-keys --query 'length(Keys)' --output text)" -gt 0 ]
|
||||
}
|
||||
|
||||
@test "Terraform: Kinesis stream created" {
|
||||
run aws_cmd kinesis describe-stream-summary --stream-name floci-compat-events
|
||||
assert_success
|
||||
assert_output --partial "ACTIVE"
|
||||
}
|
||||
|
||||
@test "Terraform: CloudWatch log group created" {
|
||||
run aws_cmd logs describe-log-groups --log-group-name-prefix /floci/compat/app
|
||||
assert_success
|
||||
assert_output --partial "/floci/compat/app"
|
||||
}
|
||||
|
||||
@test "Terraform: EventBridge event bus created" {
|
||||
run aws_cmd events describe-event-bus --name floci-compat-bus
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-bus"
|
||||
}
|
||||
|
||||
@test "Terraform: Step Functions state machine created" {
|
||||
run aws_cmd stepfunctions list-state-machines
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-state-machine"
|
||||
}
|
||||
|
||||
@test "Terraform: CloudFormation stack created" {
|
||||
run aws_cmd cloudformation describe-stacks --stack-name floci-compat-stack
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-stack"
|
||||
}
|
||||
|
||||
@test "Terraform: EKS cluster created" {
|
||||
run aws_cmd eks describe-cluster --name floci-compat-eks
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-eks"
|
||||
assert_output --partial "ACTIVE"
|
||||
}
|
||||
|
||||
@test "Terraform: API Gateway v2 HTTP API created" {
|
||||
run aws_cmd apigatewayv2 get-apis
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-http-api"
|
||||
}
|
||||
|
||||
@test "Terraform: AppConfig application created" {
|
||||
run aws_cmd appconfig list-applications
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-appconfig"
|
||||
}
|
||||
|
||||
@test "Terraform: Backup vault created" {
|
||||
run aws_cmd backup describe-backup-vault --backup-vault-name floci-compat-vault
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-vault"
|
||||
}
|
||||
|
||||
@test "Terraform: Cloud Map namespace created" {
|
||||
run aws_cmd servicediscovery list-namespaces
|
||||
assert_success
|
||||
assert_output --partial "floci-compat.internal"
|
||||
}
|
||||
|
||||
@test "Terraform: CodeDeploy application created" {
|
||||
run aws_cmd deploy get-application --application-name floci-compat-codedeploy
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-codedeploy"
|
||||
}
|
||||
|
||||
@test "Terraform: ACM certificate created" {
|
||||
run aws_cmd acm list-certificates
|
||||
assert_success
|
||||
assert_output --partial "floci-compat.internal"
|
||||
}
|
||||
|
||||
@test "Terraform: SES identity created" {
|
||||
run aws_cmd ses get-identity-verification-attributes \
|
||||
--identities terraform@floci-compat.internal
|
||||
assert_success
|
||||
assert_output --partial "terraform@floci-compat.internal"
|
||||
}
|
||||
|
||||
@test "Terraform: EventBridge schedule created" {
|
||||
run aws_cmd scheduler get-schedule --name floci-compat-schedule
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-schedule"
|
||||
}
|
||||
|
||||
@test "Terraform: WAFv2 web ACL created" {
|
||||
run aws_cmd wafv2 list-web-acls --scope REGIONAL
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-waf"
|
||||
}
|
||||
|
||||
@test "Terraform: Cost and Usage Report created" {
|
||||
run aws_cmd cur describe-report-definitions
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-report"
|
||||
}
|
||||
|
||||
@test "Terraform: AppSync GraphQL API created" {
|
||||
run aws_cmd appsync list-graphql-apis
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-graphql"
|
||||
}
|
||||
|
||||
@test "Terraform: ElastiCache replication group created" {
|
||||
run aws_cmd elasticache describe-replication-groups --replication-group-id floci-compat-cache
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-cache"
|
||||
}
|
||||
|
||||
@test "Terraform: Firehose delivery stream created" {
|
||||
run aws_cmd firehose list-delivery-streams
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-firehose-basic"
|
||||
}
|
||||
|
||||
@test "Terraform: EventBridge pipe created" {
|
||||
run aws_cmd pipes list-pipes
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-pipe"
|
||||
}
|
||||
|
||||
@test "Terraform: RDS DB instance created and available" {
|
||||
run aws_cmd rds describe-db-instances --db-instance-identifier floci-compat-db
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-db"
|
||||
assert_output --partial "available"
|
||||
}
|
||||
|
||||
@test "Terraform: CloudWatch alarm created with tags" {
|
||||
run aws_cmd cloudwatch describe-alarms --alarm-names floci-compat-cpu-alarm
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-cpu-alarm"
|
||||
|
||||
ALARM_ARN=$(aws_cmd cloudwatch describe-alarms --alarm-names floci-compat-cpu-alarm \
|
||||
--query 'MetricAlarms[0].AlarmArn' --output text)
|
||||
run aws_cmd cloudwatch list-tags-for-resource --resource-arn "$ALARM_ARN"
|
||||
assert_success
|
||||
assert_output --partial "compat-test"
|
||||
}
|
||||
|
||||
@test "Terraform: VPC created with custom DNS settings" {
|
||||
run aws_cmd ec2 describe-vpcs \
|
||||
--filters "Name=tag:Name,Values=floci-compat-vpc"
|
||||
assert_success
|
||||
assert_output --partial "floci-compat-vpc"
|
||||
assert_output --partial "10.0.0.0/16"
|
||||
}
|
||||
|
||||
@test "Terraform: VPC enableDnsSupport persisted as false" {
|
||||
VPC_ID=$(aws_cmd ec2 describe-vpcs \
|
||||
--filters "Name=tag:Name,Values=floci-compat-vpc" \
|
||||
--query 'Vpcs[0].VpcId' --output text)
|
||||
run aws_cmd ec2 describe-vpc-attribute \
|
||||
--vpc-id "$VPC_ID" --attribute enableDnsSupport
|
||||
assert_success
|
||||
assert_output --partial '"Value": false'
|
||||
}
|
||||
|
||||
@test "Terraform: VPC enableDnsHostnames persisted as false" {
|
||||
VPC_ID=$(aws_cmd ec2 describe-vpcs \
|
||||
--filters "Name=tag:Name,Values=floci-compat-vpc" \
|
||||
--query 'Vpcs[0].VpcId' --output text)
|
||||
run aws_cmd ec2 describe-vpc-attribute \
|
||||
--vpc-id "$VPC_ID" --attribute enableDnsHostnames
|
||||
assert_success
|
||||
assert_output --partial '"Value": false'
|
||||
}
|
||||
|
||||
@test "Terraform: Route53 hosted zone created" {
|
||||
ZONE_ID=$(aws_cmd route53 list-hosted-zones \
|
||||
--query "HostedZones[?Name=='floci-compat.internal.'].Id | [0]" \
|
||||
--output text | sed 's|/hostedzone/||')
|
||||
[ -n "$ZONE_ID" ]
|
||||
run aws_cmd route53 get-hosted-zone --id "$ZONE_ID"
|
||||
assert_success
|
||||
assert_output --partial "floci-compat.internal"
|
||||
}
|
||||
|
||||
@test "Terraform: Route53 A record created" {
|
||||
ZONE_ID=$(aws_cmd route53 list-hosted-zones \
|
||||
--query "HostedZones[?Name=='floci-compat.internal.'].Id | [0]" \
|
||||
--output text | sed 's|/hostedzone/||')
|
||||
run aws_cmd route53 list-resource-record-sets --hosted-zone-id "$ZONE_ID"
|
||||
assert_success
|
||||
assert_output --partial "app.floci-compat.internal"
|
||||
assert_output --partial "10.0.1.10"
|
||||
}
|
||||
|
||||
@test "Terraform: Route53 zone has auto-created SOA and NS records" {
|
||||
ZONE_ID=$(aws_cmd route53 list-hosted-zones \
|
||||
--query "HostedZones[?Name=='floci-compat.internal.'].Id | [0]" \
|
||||
--output text | sed 's|/hostedzone/||')
|
||||
run aws_cmd route53 list-resource-record-sets --hosted-zone-id "$ZONE_ID"
|
||||
assert_success
|
||||
assert_output --partial '"SOA"'
|
||||
assert_output --partial '"NS"'
|
||||
}
|
||||
|
||||
@test "Terraform: Route53 health check created" {
|
||||
HEALTH_CHECK_ID=$(aws_cmd route53 list-health-checks \
|
||||
--query "HealthChecks[?HealthCheckConfig.FullyQualifiedDomainName=='app.floci-compat.internal'].Id | [0]" \
|
||||
--output text)
|
||||
[ -n "$HEALTH_CHECK_ID" ]
|
||||
run aws_cmd route53 get-health-check --health-check-id "$HEALTH_CHECK_ID"
|
||||
assert_success
|
||||
assert_output --partial "app.floci-compat.internal"
|
||||
assert_output --partial "HTTP"
|
||||
}
|
||||
|
||||
@test "Terraform: Route53 zone tags persisted" {
|
||||
ZONE_ID=$(aws_cmd route53 list-hosted-zones \
|
||||
--query "HostedZones[?Name=='floci-compat.internal.'].Id | [0]" \
|
||||
--output text | sed 's|/hostedzone/||')
|
||||
run aws_cmd route53 list-tags-for-resource \
|
||||
--resource-type hostedzone --resource-id "$ZONE_ID"
|
||||
assert_success
|
||||
assert_output --partial "compat-test"
|
||||
}
|
||||
|
||||
@test "Terraform: Cognito user pool client created without optional blocks" {
|
||||
POOL_ID=$(aws_cmd cognito-idp list-user-pools --max-results 10 \
|
||||
--query "UserPools[?Name=='floci-compat-pool'].Id | [0]" --output text)
|
||||
[ -n "$POOL_ID" ]
|
||||
run aws_cmd cognito-idp list-user-pool-clients --user-pool-id "$POOL_ID" \
|
||||
--query "UserPoolClients[?ClientName=='floci-compat-pool-client'].ClientId | [0]" --output text
|
||||
assert_success
|
||||
[ -n "$output" ]
|
||||
[ "$output" != "None" ]
|
||||
}
|
||||
|
||||
@test "Terraform: Firehose extended_s3 delivery stream created with correct config" {
|
||||
run aws_cmd firehose describe-delivery-stream --delivery-stream-name floci-compat-firehose \
|
||||
--query "DeliveryStreamDescription.Destinations[0].ExtendedS3DestinationDescription.CompressionFormat" --output text
|
||||
assert_success
|
||||
assert_output "GZIP"
|
||||
}
|
||||
|
||||
@test "Terraform: Application Auto Scaling scalable target created with tags" {
|
||||
run aws_cmd application-autoscaling describe-scalable-targets --service-namespace ecs \
|
||||
--resource-ids service/floci-compat-cluster/floci-compat-service \
|
||||
--query "ScalableTargets[0].MaxCapacity" --output text
|
||||
assert_success
|
||||
assert_output "20"
|
||||
|
||||
run aws_cmd application-autoscaling describe-scalable-targets --service-namespace ecs \
|
||||
--resource-ids service/floci-compat-cluster/floci-compat-service \
|
||||
--query "ScalableTargets[0].ScalableTargetARN" --output text
|
||||
assert_success
|
||||
[[ "$output" == arn:aws:application-autoscaling:* ]]
|
||||
|
||||
run aws_cmd application-autoscaling list-tags-for-resource --resource-arn "$output" \
|
||||
--query "Tags.Environment" --output text
|
||||
assert_success
|
||||
assert_output "compat-test"
|
||||
}
|
||||
|
||||
@test "Terraform: Application Auto Scaling target-tracking policy round-trips resource_label" {
|
||||
run aws_cmd application-autoscaling describe-scaling-policies --service-namespace ecs \
|
||||
--resource-id service/floci-compat-cluster/floci-compat-service \
|
||||
--query "ScalingPolicies[?PolicyName=='floci-compat-alb-request-count'].TargetTrackingScalingPolicyConfiguration.PredefinedMetricSpecification.ResourceLabel | [0]" \
|
||||
--output text
|
||||
assert_success
|
||||
assert_output "app/floci-compat-alb/abc123/targetgroup/floci-compat-tg/def456"
|
||||
|
||||
run aws_cmd application-autoscaling describe-scaling-policies --service-namespace ecs \
|
||||
--resource-id service/floci-compat-cluster/floci-compat-service \
|
||||
--query "ScalingPolicies[?PolicyName=='floci-compat-alb-request-count'].TargetTrackingScalingPolicyConfiguration.ScaleInCooldown | [0]" \
|
||||
--output text
|
||||
assert_success
|
||||
assert_output "240"
|
||||
}
|
||||
|
||||
# A successful apply proves very little on its own: a response that silently drops a
|
||||
# field still applies cleanly, and only shows up later as perpetual drift. Asserting an
|
||||
# empty second plan is what actually catches that class of bug.
|
||||
#
|
||||
# Whole-config: this used to be scoped to just the Application Auto Scaling resources
|
||||
# because a full re-plan reported drift on aws_cognito_user_pool (device_configuration,
|
||||
# email_configuration, user_pool_add_ons), aws_db_instance (auto_minor_version_upgrade)
|
||||
# and aws_kinesis_firehose_delivery_stream (s3_backup_mode) - see #2200. Now that those
|
||||
# are fixed, this guards every resource in the fixture instead of a hand-picked subset.
|
||||
@test "Terraform: re-planning the full configuration reports no changes" {
|
||||
cd "$TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode
|
||||
if [ "$status" -eq 2 ]; then
|
||||
echo "# drift detected on re-plan:" >&3
|
||||
echo "$output" >&3
|
||||
fi
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Terraform: IAM role attaches managed policies outside the curated set" {
|
||||
run aws_cmd iam list-attached-role-policies --role-name floci-compat-managed-policy-role \
|
||||
--query "sort_by(AttachedPolicies, &PolicyArn)[].PolicyArn" --output text
|
||||
assert_success
|
||||
assert_output --partial "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"
|
||||
assert_output --partial "arn:aws:iam::aws:policy/service-role/AmazonElasticMapReduceRole"
|
||||
}
|
||||
|
||||
# A policy AWS does not publish must still be rejected, otherwise a typo silently
|
||||
# "succeeds" and Terraform/CloudFormation never surface the mistake.
|
||||
@test "Terraform: attaching a nonexistent managed policy still fails" {
|
||||
run aws_cmd iam attach-role-policy --role-name floci-compat-managed-policy-role \
|
||||
--policy-arn "arn:aws:iam::aws:policy/AmazonS3FullAcess"
|
||||
assert_failure
|
||||
assert_output --partial "NoSuchEntity"
|
||||
}
|
||||
|
||||
# Tags are stored and readable via ListRoleTags, but the provider reads them off the
|
||||
# GetRole response. Omitting them there made every tagged role read back untagged.
|
||||
@test "Terraform: GetRole returns the role's tags" {
|
||||
run aws_cmd iam get-role --role-name floci-compat-tagged-role \
|
||||
--query "Role.Tags[?Key=='Environment'].Value" --output text
|
||||
assert_success
|
||||
assert_output "compat"
|
||||
}
|
||||
|
||||
@test "Terraform: GetPolicy returns the policy's tags" {
|
||||
policy_arn=$(aws_cmd iam list-policies --scope Local \
|
||||
--query "Policies[?PolicyName=='floci-compat-tagged-policy'].Arn" --output text)
|
||||
run aws_cmd iam get-policy --policy-arn "$policy_arn" \
|
||||
--query "Policy.Tags[?Key=='Environment'].Value" --output text
|
||||
assert_success
|
||||
assert_output "compat"
|
||||
}
|
||||
|
||||
# The other half of the contract: IAM's listing operations deliberately return a subset
|
||||
# of attributes. ListRoles "does not return the following attributes, even though they
|
||||
# are an attribute of the returned object: PermissionsBoundary, RoleLastUsed, Tags".
|
||||
# Emitting them here would deviate from AWS in the opposite direction.
|
||||
@test "Terraform: ListRoles omits tags even for a tagged role" {
|
||||
run aws_cmd iam list-roles \
|
||||
--query "Roles[?RoleName=='floci-compat-tagged-role'].Tags" --output text
|
||||
assert_success
|
||||
refute_output --partial "compat"
|
||||
}
|
||||
|
||||
@test "Terraform: ListPolicies omits tags and description even for a tagged policy" {
|
||||
run aws_cmd iam list-policies --scope Local \
|
||||
--query "Policies[?PolicyName=='floci-compat-tagged-policy'].[Tags,Description]" \
|
||||
--output text
|
||||
assert_success
|
||||
refute_output --partial "compat"
|
||||
refute_output --partial "round trip"
|
||||
}
|
||||
|
||||
# The assertion that actually matters for the drift class: tags that do not survive the
|
||||
# round trip apply cleanly and then re-plan dirty forever.
|
||||
@test "Terraform: re-planning the tagged IAM resources reports no changes" {
|
||||
cd "$TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode \
|
||||
-target=aws_iam_role.tagged \
|
||||
-target=aws_iam_policy.tagged
|
||||
if [ "$status" -eq 2 ]; then
|
||||
echo "# drift detected on re-plan:" >&3
|
||||
echo "$output" >&3
|
||||
fi
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Terraform: GuardDuty detector is created with features" {
|
||||
run aws_cmd guardduty list-detectors --query "DetectorIds[0]" --output text
|
||||
assert_success
|
||||
DETECTOR_ID="$output"
|
||||
run aws_cmd guardduty get-detector --detector-id "$DETECTOR_ID" \
|
||||
--query "[Status, FindingPublishingFrequency]" --output text
|
||||
assert_success
|
||||
assert_output --partial "ENABLED"
|
||||
assert_output --partial "SIX_HOURS"
|
||||
}
|
||||
|
||||
@test "Terraform: GuardDuty organization configuration round-trips" {
|
||||
run aws_cmd guardduty list-detectors --query "DetectorIds[0]" --output text
|
||||
assert_success
|
||||
DETECTOR_ID="$output"
|
||||
run aws_cmd guardduty describe-organization-configuration --detector-id "$DETECTOR_ID" \
|
||||
--query "AutoEnableOrganizationMembers" --output text
|
||||
assert_success
|
||||
assert_output "ALL"
|
||||
}
|
||||
|
||||
# additional_configuration is an ordered list block; drift here means the
|
||||
# emulator reordered the list on read-back.
|
||||
@test "Terraform: re-planning GuardDuty reports no changes" {
|
||||
cd "$TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode \
|
||||
-target=aws_guardduty_detector.compat \
|
||||
-target=aws_guardduty_detector_feature.runtime_monitoring \
|
||||
-target=aws_guardduty_organization_configuration.compat \
|
||||
-target=aws_guardduty_organization_configuration_feature.runtime_monitoring
|
||||
if [ "$status" -eq 2 ]; then
|
||||
echo "# drift detected on re-plan:" >&3
|
||||
echo "$output" >&3
|
||||
fi
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "Terraform: Transfer Family server created and ONLINE" {
|
||||
run aws_cmd transfer list-servers --query "Servers[0].State" --output text
|
||||
assert_success
|
||||
assert_output "ONLINE"
|
||||
}
|
||||
|
||||
@test "Terraform: CloudTrail trail created, logging and tagged" {
|
||||
run aws_cmd cloudtrail describe-trails --trail-name-list floci-compat-trail \
|
||||
--query "trailList[0].S3BucketName" --output text
|
||||
assert_success
|
||||
assert_output "floci-compat-trail-logs"
|
||||
run aws_cmd cloudtrail get-trail-status --name floci-compat-trail \
|
||||
--query "IsLogging" --output text
|
||||
assert_success
|
||||
assert_output "True"
|
||||
run aws_cmd cloudtrail describe-trails --trail-name-list floci-compat-trail \
|
||||
--query "trailList[0].TrailARN" --output text
|
||||
assert_success
|
||||
TRAIL_ARN="$output"
|
||||
run aws_cmd cloudtrail list-tags --resource-id-list "$TRAIL_ARN" \
|
||||
--query "ResourceTagList[0].TagsList[?Key=='Environment'].Value" --output text
|
||||
assert_success
|
||||
assert_output "compat-test"
|
||||
}
|
||||
|
||||
# The tag refresh path (issue #2800): a re-plan reads the trail back, including its
|
||||
# tags via ListTags, and must not report drift.
|
||||
@test "Terraform: re-planning CloudTrail reports no changes" {
|
||||
cd "$TF_DIR"
|
||||
run terraform plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color -detailed-exitcode \
|
||||
-target=aws_cloudtrail.compat
|
||||
if [ "$status" -eq 2 ]; then
|
||||
echo "# drift detected on re-plan:" >&3
|
||||
echo "$output" >&3
|
||||
fi
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# Common setup for Terraform bats tests
|
||||
|
||||
TF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
# Load bats helpers - support both local and Docker environments
|
||||
if [[ -d "${TF_DIR}/../lib/bats-support" ]]; then
|
||||
load "${TF_DIR}/../lib/bats-support/load"
|
||||
load "${TF_DIR}/../lib/bats-assert/load"
|
||||
elif [[ -d "${TF_DIR}/lib/bats-support" ]]; then
|
||||
load "${TF_DIR}/lib/bats-support/load"
|
||||
load "${TF_DIR}/lib/bats-assert/load"
|
||||
elif [[ -n "${BATS_LIB_PATH:-}" ]]; then
|
||||
load "${BATS_LIB_PATH}/bats-support/load"
|
||||
load "${BATS_LIB_PATH}/bats-assert/load"
|
||||
else
|
||||
echo "Error: Cannot find bats-support/bats-assert libraries" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Shared test helpers kept local to this module so Docker and local runs behave the same.
|
||||
export FLOCI_ENDPOINT="${FLOCI_ENDPOINT:-http://localhost:4566}"
|
||||
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-us-east-1}"
|
||||
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-test}"
|
||||
export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-test}"
|
||||
export AWS_ENDPOINT_URL="$FLOCI_ENDPOINT"
|
||||
|
||||
aws_cmd() {
|
||||
aws --endpoint-url "$FLOCI_ENDPOINT" --region "$AWS_DEFAULT_REGION" --output json "$@"
|
||||
}
|
||||
|
||||
json_get() {
|
||||
local json="$1"
|
||||
local path="$2"
|
||||
echo "$json" | jq -r "$path" 2>/dev/null || echo ""
|
||||
}
|
||||
|
||||
# Terraform-specific helpers
|
||||
create_state_backend() {
|
||||
# Create S3 state bucket if not exists
|
||||
if ! aws_cmd s3api head-bucket --bucket tfstate 2>/dev/null; then
|
||||
aws_cmd s3api create-bucket --bucket tfstate
|
||||
fi
|
||||
|
||||
# Create DynamoDB lock table if not exists
|
||||
if ! aws_cmd dynamodb describe-table --table-name tflock 2>/dev/null | grep -q ACTIVE; then
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name tflock \
|
||||
--attribute-definitions AttributeName=LockID,AttributeType=S \
|
||||
--key-schema AttributeName=LockID,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST
|
||||
fi
|
||||
}
|
||||
|
||||
generate_backend_config() {
|
||||
cat > /tmp/floci-backend.hcl <<EOF
|
||||
bucket = "tfstate"
|
||||
key = "floci-compat.tfstate"
|
||||
region = "us-east-1"
|
||||
|
||||
endpoint = "${FLOCI_ENDPOINT}"
|
||||
access_key = "test"
|
||||
secret_key = "test"
|
||||
skip_credentials_validation = true
|
||||
skip_region_validation = true
|
||||
use_path_style = true
|
||||
|
||||
dynamodb_endpoint = "${FLOCI_ENDPOINT}"
|
||||
dynamodb_table = "tflock"
|
||||
EOF
|
||||
}
|
||||
Reference in New Issue
Block a user