second first commit

This commit is contained in:
Zakaria
2026-09-22 08:57:00 -04:00
parent e88d7e2ff3
commit 0aef7fb2d8
4766 changed files with 1236986 additions and 0 deletions
@@ -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,29 @@
FROM ghcr.io/opentofu/opentofu:1.8 AS opentofu
# Same CLI as compat-terraform: 2.15 predates the S3 checksum type (x-amz-checksum-type, December 2024)
# and cannot show ChecksumType in head-object / get-object-attributes output.
FROM amazon/aws-cli:2.34.32
# Copy tofu binary from official OpenTofu image
COPY --from=opentofu /usr/local/bin/tofu /usr/local/bin/tofu
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
force_path_style = true
dynamodb_endpoint = "http://localhost:4566"
dynamodb_table = "tflock"
+705
View File
@@ -0,0 +1,705 @@
# NOTE: Keep resource definitions in sync with ../compat-terraform/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 (or -otf-only.txt for the
# reverse direction) as a deliberate gap. Currently allowlisted: Application
# Auto Scaling, RDS, the CloudWatch alarm, the IAM managed-policy-attach
# path, IAM tag coverage, and the GuardDuty org feature -- see the allowlist
# files and https://github.com/floci-io/floci/issues/2263 for status.
# ── 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) ──────────────────────────────────────────────────
resource "aws_iam_role" "lambda_exec" {
name = "floci-compat-lambda-exec"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
}
# ── 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 OpenTofu 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 OpenTofu 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 OpenTofu 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 OpenTofu 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 OpenTofu 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"
})
}
# ── 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
}
# ── 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
}
# ── 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
}
# ── 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
}
# ── 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
}
# -- 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"
}
output "guardduty_detector_id" {
value = aws_guardduty_detector.compat.id
}
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,73 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.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
ec2 = var.endpoint
route53 = 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"
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
set -euo pipefail
# Runs the OpenTofu 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,479 @@
#!/usr/bin/env bats
# OpenTofu Compatibility Tests for floci
setup_file() {
load 'test_helper/common-setup'
cd "$TOFU_DIR"
echo "# === OpenTofu 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 > "$TOFU_DIR/large-object.bin"
echo "# --- tofu init ---" >&3
run tofu init -backend-config=/tmp/floci-backend.hcl \
-var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color
if [ "$status" -ne 0 ]; then
echo "# tofu init failed: $output" >&3
return 1
fi
echo "# --- tofu validate ---" >&3
run tofu validate -no-color
if [ "$status" -ne 0 ]; then
echo "# tofu validate failed: $output" >&3
return 1
fi
echo "# --- tofu plan ---" >&3
run tofu plan -var="endpoint=${FLOCI_ENDPOINT}" -input=false -no-color
if [ "$status" -ne 0 ]; then
echo "# tofu plan failed: $output" >&3
return 1
fi
echo "# --- tofu apply ---" >&3
run tofu apply -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color
if [ "$status" -ne 0 ]; then
echo "# tofu apply failed: $output" >&3
return 1
fi
}
teardown_file() {
load 'test_helper/common-setup'
cd "$TOFU_DIR"
echo "# --- tofu destroy ---" >&3
tofu destroy -var="endpoint=${FLOCI_ENDPOINT}" -input=false -auto-approve -no-color || true
rm -f "$TOFU_DIR/large-object.bin"
}
setup() {
load 'test_helper/common-setup'
}
# --- Spot Checks ---
@test "OpenTofu: 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 "OpenTofu: 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 "$TOFU_DIR/large-object.bin" $sizes)
[ "$checksum" = "$expected" ]
# GetObjectAttributes reports the composite without the part-count suffix
[ "$(echo "$output" | jq -r '.Checksum.ChecksumSHA256')" = "${expected%-*}" ]
}
@test "OpenTofu: state holds the composite checksum S3 reports for the large object" {
cd "$TOFU_DIR"
run tofu 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 "OpenTofu: re-planning the large object reports no changes" {
cd "$TOFU_DIR"
run tofu 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 "OpenTofu: SQS queue created" {
run aws_cmd sqs get-queue-url --queue-name floci-compat-jobs
assert_success
assert_output --partial "QueueUrl"
}
@test "OpenTofu: SNS topic created" {
run aws_cmd sns list-topics
assert_success
assert_output --partial "floci-compat-events"
}
@test "OpenTofu: 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 "OpenTofu: 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 "OpenTofu: 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 "OpenTofu: DynamoDB table created" {
run aws_cmd dynamodb describe-table --table-name floci-compat-items
assert_success
assert_output --partial "ACTIVE"
}
@test "OpenTofu: SSM parameter created" {
run aws_cmd ssm get-parameter --name /floci-compat/db-url
assert_success
assert_output --partial "jdbc:"
}
@test "OpenTofu: Secrets Manager secret created" {
run aws_cmd secretsmanager describe-secret --secret-id "floci-compat/db-creds"
assert_success
assert_output --partial "floci-compat"
}
@test "OpenTofu: ECR repository created" {
run aws_cmd ecr describe-repositories --repository-names floci-compat-app
assert_success
assert_output --partial "floci-compat-app"
}
@test "OpenTofu: 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 "OpenTofu: KMS key created" {
run aws_cmd kms list-keys
assert_success
[ "$(aws_cmd kms list-keys --query 'length(Keys)' --output text)" -gt 0 ]
}
@test "OpenTofu: Kinesis stream created" {
run aws_cmd kinesis describe-stream-summary --stream-name floci-compat-events
assert_success
assert_output --partial "ACTIVE"
}
@test "OpenTofu: 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 "OpenTofu: EventBridge event bus created" {
run aws_cmd events describe-event-bus --name floci-compat-bus
assert_success
assert_output --partial "floci-compat-bus"
}
@test "OpenTofu: Step Functions state machine created" {
run aws_cmd stepfunctions list-state-machines
assert_success
assert_output --partial "floci-compat-state-machine"
}
@test "OpenTofu: CloudFormation stack created" {
run aws_cmd cloudformation describe-stacks --stack-name floci-compat-stack
assert_success
assert_output --partial "floci-compat-stack"
}
@test "OpenTofu: 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 "OpenTofu: API Gateway v2 HTTP API created" {
run aws_cmd apigatewayv2 get-apis
assert_success
assert_output --partial "floci-compat-http-api"
}
@test "OpenTofu: AppConfig application created" {
run aws_cmd appconfig list-applications
assert_success
assert_output --partial "floci-compat-appconfig"
}
@test "OpenTofu: 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 "OpenTofu: Cloud Map namespace created" {
run aws_cmd servicediscovery list-namespaces
assert_success
assert_output --partial "floci-compat.internal"
}
@test "OpenTofu: CodeDeploy application created" {
run aws_cmd deploy get-application --application-name floci-compat-codedeploy
assert_success
assert_output --partial "floci-compat-codedeploy"
}
@test "OpenTofu: ACM certificate created" {
run aws_cmd acm list-certificates
assert_success
assert_output --partial "floci-compat.internal"
}
@test "OpenTofu: 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 "OpenTofu: EventBridge schedule created" {
run aws_cmd scheduler get-schedule --name floci-compat-schedule
assert_success
assert_output --partial "floci-compat-schedule"
}
@test "OpenTofu: WAFv2 web ACL created" {
run aws_cmd wafv2 list-web-acls --scope REGIONAL
assert_success
assert_output --partial "floci-compat-waf"
}
@test "OpenTofu: Cost and Usage Report created" {
run aws_cmd cur describe-report-definitions
assert_success
assert_output --partial "floci-compat-report"
}
@test "OpenTofu: AppSync GraphQL API created" {
run aws_cmd appsync list-graphql-apis
assert_success
assert_output --partial "floci-compat-graphql"
}
@test "OpenTofu: 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 "OpenTofu: Firehose delivery stream created" {
run aws_cmd firehose list-delivery-streams
assert_success
assert_output --partial "floci-compat-firehose-basic"
}
@test "OpenTofu: EventBridge pipe created" {
run aws_cmd pipes list-pipes
assert_success
assert_output --partial "floci-compat-pipe"
}
@test "OpenTofu: 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 "OpenTofu: VPC enableDnsSupport persisted as false" {
VPC_ID=$(tofu output -raw vpc_id)
run aws_cmd ec2 describe-vpc-attribute \
--vpc-id "$VPC_ID" --attribute enableDnsSupport
assert_success
assert_output --partial '"Value": false'
}
@test "OpenTofu: VPC enableDnsHostnames persisted as false" {
VPC_ID=$(tofu output -raw vpc_id)
run aws_cmd ec2 describe-vpc-attribute \
--vpc-id "$VPC_ID" --attribute enableDnsHostnames
assert_success
assert_output --partial '"Value": false'
}
@test "OpenTofu: 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 "OpenTofu: 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 "OpenTofu: 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 "OpenTofu: 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 "OpenTofu: 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 "OpenTofu: 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 "OpenTofu: 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 "OpenTofu: GuardDuty detector is created with organization configuration" {
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" --output text
assert_success
assert_output "ENABLED"
run aws_cmd guardduty describe-organization-configuration --detector-id "$DETECTOR_ID" \
--query "AutoEnableOrganizationMembers" --output text
assert_success
assert_output "ALL"
}
@test "OpenTofu: Transfer Family server created and ONLINE" {
run aws_cmd transfer list-servers --query "Servers[0].State" --output text
assert_success
assert_output "ONLINE"
}
@test "OpenTofu: 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 "OpenTofu: re-planning CloudTrail reports no changes" {
cd "$TOFU_DIR"
run tofu 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 OpenTofu bats tests
TOFU_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# Load bats helpers - support both local and Docker environments
if [[ -d "${TOFU_DIR}/../lib/bats-support" ]]; then
load "${TOFU_DIR}/../lib/bats-support/load"
load "${TOFU_DIR}/../lib/bats-assert/load"
elif [[ -d "${TOFU_DIR}/lib/bats-support" ]]; then
load "${TOFU_DIR}/lib/bats-support/load"
load "${TOFU_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 ""
}
# OpenTofu-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
}