diff --git a/tests/e2e/TEST_COVERAGE.md b/tests/e2e/TEST_COVERAGE.md index 144362480..9878be84a 100644 --- a/tests/e2e/TEST_COVERAGE.md +++ b/tests/e2e/TEST_COVERAGE.md @@ -635,106 +635,6 @@ Runs on a real 3-node libvirt cluster provisioned by OpenTofu (`scripts/tofu-clu --- -## VPC (`run-vpc-e2e.sh`) - -Standalone VPC networking test suite. Runs against a running Spinifex endpoint (configurable via `ENDPOINT` env var). OVN topology tests are skipped when OVN is unavailable. - -### Phase 1: VPC CRUD -- `create-vpc` (10.99.0.0/16, verify VpcId) -- `describe-vpcs` (by ID, verify exactly 1 returned) - -### Phase 2: Subnet CRUD -- `create-subnet` (10.99.1.0/24 in VPC, verify SubnetId) -- `describe-subnets` (by ID, verify exactly 1 returned) - -### Phase 3: Internet Gateway CRUD -- `create-internet-gateway` (verify InternetGatewayId) -- `describe-internet-gateways` (by ID, verify exactly 1 returned) - -### Phase 4: Internet Gateway Attach / Detach -- `attach-internet-gateway` (IGW to VPC) -- `describe-internet-gateways` (verify attachment VpcId) -- `delete-internet-gateway` on attached IGW (expect `DependencyViolation` or rejection) -- `detach-internet-gateway` (IGW from VPC) -- `describe-internet-gateways` (verify no attachments) - -### Phase 5: OVN Topology Verification (requires OVN) -- Re-attach IGW for topology inspection -- Verify OVN logical switch exists for subnet -- Verify OVN logical router exists for VPC -- Verify OVN external switch exists (IGW attached) -- Verify SNAT rule on VPC router -- Verify default route (0.0.0.0/0) on VPC router -- Dump full OVN NB DB topology for debugging -- Detach IGW -- Skipped when `ovn-nbctl` is not available - -### Phase 6: Cleanup -- `delete-internet-gateway` -- `delete-subnet` -- `delete-vpc` -- Verify OVN cleanup (no VPC routers remaining, when OVN available) - ---- - -## ELBv2 / ALB (`run-elbv2-e2e.sh`) - -Standalone ELBv2 (Application Load Balancer) E2E test suite. Requires VPC networking -for ALB ENI creation. Configurable endpoint via `ENDPOINT` env var. - -### Phase 0: VPC + Subnet Setup -- `create-vpc` (10.200.0.0/16, prerequisite for ALB) -- `create-subnet` (10.200.1.0/24) - -### Phase 1: Target Group CRUD -- `create-target-group` (HTTP, port 80, verify health check defaults) -- `describe-target-groups` (by ARN, verify single result) -- `create-target-group` (second TG, port 8080) -- `describe-target-groups` (all, verify >= 2) -- Duplicate name detection (same name → error) - -### Phase 2: Target Registration -- `register-targets` (2 fake instances) -- `describe-target-health` (verify 2 targets, initial state) -- `deregister-targets` (remove 1 target) -- `describe-target-health` (verify 1 target remains) - -### Phase 3: Load Balancer CRUD -- `create-load-balancer` (ALB with subnet) -- Verify fields: type=application, state=active, DNS name, scheme=internet-facing -- Verify ENIs created (describe-network-interfaces with ELB filter) -- `describe-load-balancers` (by ARN, verify single result) -- Duplicate name detection (same name → error) - -### Phase 4: Listener CRUD -- `create-listener` (port 80, forward to TG) -- Verify fields: port=80, protocol=HTTP -- `describe-listeners` (by LB ARN, verify single result) -- Duplicate port detection (same port → error) - -### Phase 5: In-Use Protection -- `delete-target-group` while referenced by listener → error (ResourceInUse) - -### Phase 6: Listener Deletion -- `delete-listener` -- Verify 0 listeners remain -- `delete-target-group` (now succeeds after listener removed) - -### Phase 7: Load Balancer Deletion -- `delete-load-balancer` -- Verify ALB gone from describe -- Verify ENIs cleaned up -- `delete-target-group` (second TG) - -### Phase 8: Error Path Tests -- Describe non-existent ALB → empty result -- Delete non-existent ALB → error -- Delete non-existent TG → error -- Create TG without name → error -- Create listener on non-existent ALB → error - ---- - ## Consolidated LB Data Plane (`run-lb-e2e.sh`) Consolidated LB data plane test covering all 4 variants: ALB internet-facing, diff --git a/tests/e2e/run-elbv2-e2e.sh b/tests/e2e/run-elbv2-e2e.sh deleted file mode 100755 index 9da9c1974..000000000 --- a/tests/e2e/run-elbv2-e2e.sh +++ /dev/null @@ -1,607 +0,0 @@ -#!/bin/bash -set -e - -# ELBv2 (ALB) E2E Test Suite -# Tests Application Load Balancer operations via the AWS CLI against a running Spinifex cluster. -# Requires VPC networking to be functional (ALBs create ENIs in subnets). -# -# Usage: -# ./tests/e2e/run-elbv2-e2e.sh # Run against default endpoint -# ENDPOINT=https://10.11.12.1:9999 ./tests/e2e/run-elbv2-e2e.sh # Custom endpoint - -cd "$(dirname "$0")/../.." - -ENDPOINT="${ENDPOINT:-https://127.0.0.1:9999}" -export AWS_PROFILE=spinifex -AWS_EC2="aws --endpoint-url ${ENDPOINT} ec2" -AWS_ELBV2="aws --endpoint-url ${ENDPOINT} elbv2" - -PASSED=0 -FAILED=0 - -pass() { - echo " ✅ $1" - PASSED=$((PASSED + 1)) -} - -fail() { - echo " ❌ $1" - FAILED=$((FAILED + 1)) -} - -# Track created resources for cleanup -VPC_ID="" -SUBNET_ID="" -TG_ARN="" -TG2_ARN="" -LB_ARN="" -LISTENER_ARN="" - -cleanup() { - local exit_code=$? - echo "" - echo "Cleanup..." - - # Delete listener - if [ -n "$LISTENER_ARN" ]; then - $AWS_ELBV2 delete-listener --listener-arn "$LISTENER_ARN" 2>/dev/null || true - fi - - # Delete load balancer (also cascade-deletes listeners) - if [ -n "$LB_ARN" ]; then - $AWS_ELBV2 delete-load-balancer --load-balancer-arn "$LB_ARN" 2>/dev/null || true - fi - - # Deregister any targets and delete target groups - if [ -n "$TG_ARN" ]; then - $AWS_ELBV2 delete-target-group --target-group-arn "$TG_ARN" 2>/dev/null || true - fi - if [ -n "$TG2_ARN" ]; then - $AWS_ELBV2 delete-target-group --target-group-arn "$TG2_ARN" 2>/dev/null || true - fi - - # Delete subnet - if [ -n "$SUBNET_ID" ]; then - $AWS_EC2 delete-subnet --subnet-id "$SUBNET_ID" 2>/dev/null || true - fi - - # Delete VPC - if [ -n "$VPC_ID" ]; then - $AWS_EC2 delete-vpc --vpc-id "$VPC_ID" 2>/dev/null || true - fi - - echo "Cleanup complete" - echo "" - echo "========================================" - echo "ELBv2 E2E Results: $PASSED passed, $FAILED failed" - echo "========================================" - - if [ $FAILED -gt 0 ]; then - exit 1 - fi - exit $exit_code -} -trap cleanup EXIT - -echo "========================================" -echo "ELBv2 (ALB) E2E Test Suite" -echo "========================================" -echo "Endpoint: $ENDPOINT" -echo "" - -# Phase 0: VPC + Subnet Setup (prerequisite for ALB) -echo "Phase 0: VPC + Subnet Setup" -echo "========================================" - -echo "Creating VPC..." -VPC_OUTPUT=$($AWS_EC2 create-vpc --cidr-block 10.200.0.0/16 --output json 2>&1) || { - fail "create-vpc (prerequisite)" - echo " Output: $VPC_OUTPUT" - exit 1 -} -VPC_ID=$(echo "$VPC_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['Vpc']['VpcId'])" 2>/dev/null) || { - fail "create-vpc (parse VpcId)" - exit 1 -} -pass "create-vpc: $VPC_ID" - -echo "Creating subnet..." -SUBNET_OUTPUT=$($AWS_EC2 create-subnet --vpc-id "$VPC_ID" --cidr-block 10.200.1.0/24 --output json 2>&1) || { - fail "create-subnet (prerequisite)" - echo " Output: $SUBNET_OUTPUT" - exit 1 -} -SUBNET_ID=$(echo "$SUBNET_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['Subnet']['SubnetId'])" 2>/dev/null) || { - fail "create-subnet (parse SubnetId)" - exit 1 -} -pass "create-subnet: $SUBNET_ID" - -# Phase 1: Target Group CRUD -echo "" -echo "Phase 1: Target Group CRUD" -echo "========================================" - -# Create target group -echo "Creating target group..." -TG_OUTPUT=$($AWS_ELBV2 create-target-group \ - --name e2e-tg-1 \ - --protocol HTTP \ - --port 80 \ - --vpc-id "$VPC_ID" \ - --output json 2>&1) || { - fail "create-target-group" - echo " Output: $TG_OUTPUT" - exit 1 -} -TG_ARN=$(echo "$TG_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['TargetGroups'][0]['TargetGroupArn'])" 2>/dev/null) || { - fail "create-target-group (parse ARN)" - echo " Output: $TG_OUTPUT" - exit 1 -} -pass "create-target-group: $TG_ARN" - -# Verify health check defaults -echo "Verifying health check defaults..." -HC_PATH=$(echo "$TG_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['TargetGroups'][0]['HealthCheckPath'])" 2>/dev/null) -HC_PROTOCOL=$(echo "$TG_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['TargetGroups'][0]['HealthCheckProtocol'])" 2>/dev/null) -if [ "$HC_PATH" = "/" ] && [ "$HC_PROTOCOL" = "HTTP" ]; then - pass "health check defaults: path=/ protocol=HTTP" -else - fail "health check defaults: path=$HC_PATH protocol=$HC_PROTOCOL (expected / HTTP)" -fi - -# Describe target groups -echo "Describing target groups..." -DESC_TG_OUTPUT=$($AWS_ELBV2 describe-target-groups --target-group-arns "$TG_ARN" --output json 2>&1) || { - fail "describe-target-groups" - echo " Output: $DESC_TG_OUTPUT" -} -TG_COUNT=$(echo "$DESC_TG_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['TargetGroups']))" 2>/dev/null) -if [ "$TG_COUNT" = "1" ]; then - pass "describe-target-groups: found 1 target group" -else - fail "describe-target-groups: expected 1, got $TG_COUNT" -fi - -# Create second target group (for later tests) -echo "Creating second target group..." -TG2_OUTPUT=$($AWS_ELBV2 create-target-group \ - --name e2e-tg-2 \ - --protocol HTTP \ - --port 8080 \ - --vpc-id "$VPC_ID" \ - --output json 2>&1) || { - fail "create-target-group-2" - echo " Output: $TG2_OUTPUT" -} -TG2_ARN=$(echo "$TG2_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['TargetGroups'][0]['TargetGroupArn'])" 2>/dev/null) || { - fail "create-target-group-2 (parse ARN)" -} -pass "create-target-group-2: $TG2_ARN" - -# Describe all target groups (no filter) — should find both -echo "Describing all target groups..." -DESC_ALL_TG=$($AWS_ELBV2 describe-target-groups --output json 2>&1) || { - fail "describe-target-groups (all)" -} -ALL_TG_COUNT=$(echo "$DESC_ALL_TG" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['TargetGroups']))" 2>/dev/null) -if [ "$ALL_TG_COUNT" -ge 2 ]; then - pass "describe-target-groups (all): found $ALL_TG_COUNT target groups" -else - fail "describe-target-groups (all): expected >= 2, got $ALL_TG_COUNT" -fi - -# Duplicate name detection -echo "Testing duplicate target group name..." -DUP_OUTPUT=$($AWS_ELBV2 create-target-group \ - --name e2e-tg-1 \ - --protocol HTTP \ - --port 80 \ - --vpc-id "$VPC_ID" \ - --output json 2>&1) && { - fail "duplicate target group should have been rejected" -} || { - if echo "$DUP_OUTPUT" | grep -qi "DuplicateTargetGroup\|already exists"; then - pass "duplicate target group correctly rejected" - else - pass "duplicate target group rejected (error returned)" - fi -} - -# Phase 2: Target Registration -echo "" -echo "Phase 2: Target Registration" -echo "========================================" - -# Register fake instance targets (they won't have real IPs but the API should accept them) -echo "Registering targets..." -REG_OUTPUT=$($AWS_ELBV2 register-targets \ - --target-group-arn "$TG_ARN" \ - --targets Id=i-e2etest00001 Id=i-e2etest00002 \ - --output json 2>&1) || { - fail "register-targets" - echo " Output: $REG_OUTPUT" -} -pass "register-targets: 2 instances registered" - -# Describe target health -echo "Describing target health..." -HEALTH_OUTPUT=$($AWS_ELBV2 describe-target-health \ - --target-group-arn "$TG_ARN" \ - --output json 2>&1) || { - fail "describe-target-health" - echo " Output: $HEALTH_OUTPUT" -} -HEALTH_COUNT=$(echo "$HEALTH_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['TargetHealthDescriptions']))" 2>/dev/null) -if [ "$HEALTH_COUNT" = "2" ]; then - pass "describe-target-health: 2 targets reported" -else - fail "describe-target-health: expected 2 targets, got $HEALTH_COUNT" -fi - -# Verify target state is "initial" (no real health checks running yet) -FIRST_STATE=$(echo "$HEALTH_OUTPUT" | python3 -c " -import sys,json -d = json.load(sys.stdin)['TargetHealthDescriptions'][0] -print(d.get('TargetHealth', {}).get('State', 'unknown')) -" 2>/dev/null) -if [ "$FIRST_STATE" = "initial" ]; then - pass "target health state: initial (expected)" -else - # Accept any state — might be "unhealthy" if health checks ran - pass "target health state: $FIRST_STATE" -fi - -# Deregister one target -echo "Deregistering one target..." -DEREG_OUTPUT=$($AWS_ELBV2 deregister-targets \ - --target-group-arn "$TG_ARN" \ - --targets Id=i-e2etest00002 \ - --output json 2>&1) || { - fail "deregister-targets" - echo " Output: $DEREG_OUTPUT" -} -pass "deregister-targets: removed i-e2etest00002" - -# Verify only 1 target remains -HEALTH_OUTPUT=$($AWS_ELBV2 describe-target-health \ - --target-group-arn "$TG_ARN" \ - --output json 2>&1) || { - fail "describe-target-health (after deregister)" -} -HEALTH_COUNT=$(echo "$HEALTH_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['TargetHealthDescriptions']))" 2>/dev/null) -if [ "$HEALTH_COUNT" = "1" ]; then - pass "describe-target-health after deregister: 1 target remains" -else - fail "describe-target-health after deregister: expected 1, got $HEALTH_COUNT" -fi - -# Phase 3: Load Balancer CRUD -echo "" -echo "Phase 3: Load Balancer CRUD" -echo "========================================" - -# Create ALB -echo "Creating ALB..." -LB_OUTPUT=$($AWS_ELBV2 create-load-balancer \ - --name e2e-alb \ - --subnets "$SUBNET_ID" \ - --output json 2>&1) || { - fail "create-load-balancer" - echo " Output: $LB_OUTPUT" - exit 1 -} -LB_ARN=$(echo "$LB_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['LoadBalancers'][0]['LoadBalancerArn'])" 2>/dev/null) || { - fail "create-load-balancer (parse ARN)" - echo " Output: $LB_OUTPUT" - exit 1 -} -pass "create-load-balancer: $LB_ARN" - -# Verify ALB fields -echo "Verifying ALB fields..." -LB_TYPE=$(echo "$LB_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['LoadBalancers'][0].get('Type', ''))" 2>/dev/null) -LB_DNS=$(echo "$LB_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['LoadBalancers'][0].get('DNSName', ''))" 2>/dev/null) -LB_SCHEME=$(echo "$LB_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['LoadBalancers'][0].get('Scheme', ''))" 2>/dev/null) - -if [ "$LB_TYPE" = "application" ]; then - pass "ALB type: application" -else - fail "ALB type: expected 'application', got '$LB_TYPE'" -fi - -# Wait for ALB to transition from provisioning to active. -# The ALB VM needs to boot and the HTTP agent must respond to a ping. -echo "Waiting for ALB to become active (up to 120s)..." -LB_STATE="" -for i in $(seq 1 40); do - DESCRIBE_LB=$($AWS_ELBV2 describe-load-balancers --load-balancer-arns "$LB_ARN" --output json 2>/dev/null) - LB_STATE=$(echo "$DESCRIBE_LB" | python3 -c "import sys,json; print(json.load(sys.stdin)['LoadBalancers'][0].get('State', {}).get('Code', ''))" 2>/dev/null) - if [ "$LB_STATE" = "active" ]; then - break - fi - sleep 3 -done - -if [ "$LB_STATE" = "active" ]; then - pass "ALB state: active (after ${i} polls)" -else - fail "ALB state: expected 'active', got '$LB_STATE' after 120s" -fi - -if [ -n "$LB_DNS" ]; then - pass "ALB DNS name: $LB_DNS" -else - fail "ALB DNS name is empty" -fi - -if [ "$LB_SCHEME" = "internet-facing" ]; then - pass "ALB scheme: internet-facing (default)" -else - fail "ALB scheme: expected 'internet-facing', got '$LB_SCHEME'" -fi - -# Verify ENIs were created -echo "Checking ENIs created for ALB..." -ENI_OUTPUT=$($AWS_EC2 describe-network-interfaces \ - --filters "Name=description,Values=ELB *" \ - --output json 2>&1) || { - fail "describe-network-interfaces (ENI check)" - echo " Output: $ENI_OUTPUT" -} -ENI_COUNT=$(echo "$ENI_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['NetworkInterfaces']))" 2>/dev/null) -if [ "$ENI_COUNT" -ge 1 ]; then - pass "ALB ENIs created: $ENI_COUNT ENI(s) found" -else - fail "ALB ENIs: expected >= 1, got $ENI_COUNT" -fi - -# Describe load balancers -echo "Describing load balancers..." -DESC_LB_OUTPUT=$($AWS_ELBV2 describe-load-balancers --load-balancer-arns "$LB_ARN" --output json 2>&1) || { - fail "describe-load-balancers" - echo " Output: $DESC_LB_OUTPUT" -} -LB_COUNT=$(echo "$DESC_LB_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['LoadBalancers']))" 2>/dev/null) -if [ "$LB_COUNT" = "1" ]; then - pass "describe-load-balancers: found 1 ALB" -else - fail "describe-load-balancers: expected 1, got $LB_COUNT" -fi - -# Duplicate name detection -echo "Testing duplicate ALB name..." -DUP_LB_OUTPUT=$($AWS_ELBV2 create-load-balancer \ - --name e2e-alb \ - --subnets "$SUBNET_ID" \ - --output json 2>&1) && { - fail "duplicate ALB should have been rejected" -} || { - pass "duplicate ALB correctly rejected" -} - -# Phase 4: Listener CRUD -echo "" -echo "Phase 4: Listener CRUD" -echo "========================================" - -# Create listener -echo "Creating listener (port 80 -> TG)..." -LISTENER_OUTPUT=$($AWS_ELBV2 create-listener \ - --load-balancer-arn "$LB_ARN" \ - --protocol HTTP \ - --port 80 \ - --default-actions "Type=forward,TargetGroupArn=$TG_ARN" \ - --output json 2>&1) || { - fail "create-listener" - echo " Output: $LISTENER_OUTPUT" - exit 1 -} -LISTENER_ARN=$(echo "$LISTENER_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['Listeners'][0]['ListenerArn'])" 2>/dev/null) || { - fail "create-listener (parse ARN)" - echo " Output: $LISTENER_OUTPUT" - exit 1 -} -pass "create-listener: $LISTENER_ARN" - -# Verify listener fields -LISTENER_PORT=$(echo "$LISTENER_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['Listeners'][0].get('Port', 0))" 2>/dev/null) -LISTENER_PROTO=$(echo "$LISTENER_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['Listeners'][0].get('Protocol', ''))" 2>/dev/null) -if [ "$LISTENER_PORT" = "80" ] && [ "$LISTENER_PROTO" = "HTTP" ]; then - pass "listener fields: port=80 protocol=HTTP" -else - fail "listener fields: port=$LISTENER_PORT protocol=$LISTENER_PROTO (expected 80 HTTP)" -fi - -# Describe listeners -echo "Describing listeners..." -DESC_LISTENER_OUTPUT=$($AWS_ELBV2 describe-listeners \ - --load-balancer-arn "$LB_ARN" \ - --output json 2>&1) || { - fail "describe-listeners" - echo " Output: $DESC_LISTENER_OUTPUT" -} -LISTENER_COUNT=$(echo "$DESC_LISTENER_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['Listeners']))" 2>/dev/null) -if [ "$LISTENER_COUNT" = "1" ]; then - pass "describe-listeners: found 1 listener" -else - fail "describe-listeners: expected 1, got $LISTENER_COUNT" -fi - -# Duplicate listener port detection -echo "Testing duplicate listener port..." -DUP_LISTENER_OUTPUT=$($AWS_ELBV2 create-listener \ - --load-balancer-arn "$LB_ARN" \ - --protocol HTTP \ - --port 80 \ - --default-actions "Type=forward,TargetGroupArn=$TG_ARN" \ - --output json 2>&1) && { - fail "duplicate listener port should have been rejected" -} || { - pass "duplicate listener port correctly rejected" -} - -# Phase 5: In-Use Protection -echo "" -echo "Phase 5: In-Use Protection" -echo "========================================" - -# Try to delete target group that's referenced by a listener — should fail -echo "Testing delete target group in use..." -INUSE_OUTPUT=$($AWS_ELBV2 delete-target-group --target-group-arn "$TG_ARN" 2>&1) && { - fail "delete in-use target group should have been rejected" -} || { - if echo "$INUSE_OUTPUT" | grep -qi "InUse\|in use\|ResourceInUse"; then - pass "delete in-use target group correctly rejected (ResourceInUse)" - else - pass "delete in-use target group rejected" - fi -} - -# Phase 6: Listener Deletion -echo "" -echo "Phase 6: Listener Deletion" -echo "========================================" - -echo "Deleting listener..." -$AWS_ELBV2 delete-listener --listener-arn "$LISTENER_ARN" 2>&1 && { - pass "delete-listener: $LISTENER_ARN" - LISTENER_ARN="" -} || { - fail "delete-listener" -} - -# Verify listener is gone -DESC_LISTENER_OUTPUT=$($AWS_ELBV2 describe-listeners \ - --load-balancer-arn "$LB_ARN" \ - --output json 2>&1) || true -LISTENER_COUNT=$(echo "$DESC_LISTENER_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('Listeners', [])))" 2>/dev/null) -if [ "$LISTENER_COUNT" = "0" ]; then - pass "describe-listeners after delete: 0 listeners" -else - fail "describe-listeners after delete: expected 0, got $LISTENER_COUNT" -fi - -# Now the target group should be deletable -echo "Deleting target group (no longer in use)..." -$AWS_ELBV2 delete-target-group --target-group-arn "$TG_ARN" 2>&1 && { - pass "delete-target-group: $TG_ARN (after listener removed)" - TG_ARN="" -} || { - fail "delete-target-group (after listener removed)" -} - -# Phase 7: Load Balancer Deletion + Cleanup Verification -echo "" -echo "Phase 7: Load Balancer Deletion" -echo "========================================" - -echo "Deleting ALB..." -$AWS_ELBV2 delete-load-balancer --load-balancer-arn "$LB_ARN" 2>&1 && { - pass "delete-load-balancer: $LB_ARN" -} || { - fail "delete-load-balancer" -} - -# Verify ALB is gone -DESC_LB_OUTPUT=$($AWS_ELBV2 describe-load-balancers --output json 2>&1) || true -LB_REMAINING=$(echo "$DESC_LB_OUTPUT" | python3 -c " -import sys,json -lbs = json.load(sys.stdin).get('LoadBalancers', []) -print(len([lb for lb in lbs if lb.get('LoadBalancerArn') == '$LB_ARN'])) -" 2>/dev/null) -if [ "$LB_REMAINING" = "0" ]; then - pass "ALB deleted: no longer in describe-load-balancers" -else - fail "ALB still exists after deletion" -fi -LB_ARN="" # Don't re-delete in cleanup - -# Verify ENIs were cleaned up (VM termination is async — poll until ENIs are gone) -echo "Verifying ALB ENIs cleaned up (up to 30s)..." -ENI_COUNT="" -for i in $(seq 1 10); do - ENI_OUTPUT=$($AWS_EC2 describe-network-interfaces \ - --filters "Name=description,Values=ELB *" \ - --output json 2>&1) || true - ENI_COUNT=$(echo "$ENI_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('NetworkInterfaces', [])))" 2>/dev/null) - if [ "$ENI_COUNT" = "0" ]; then - break - fi - sleep 3 -done -if [ "$ENI_COUNT" = "0" ]; then - pass "ALB ENIs cleaned up: 0 remaining (after ${i} polls)" -else - fail "ALB ENI cleanup: $ENI_COUNT ENIs still exist after 30s" -fi - -# Delete second target group -echo "Deleting second target group..." -$AWS_ELBV2 delete-target-group --target-group-arn "$TG2_ARN" 2>&1 && { - pass "delete-target-group-2: $TG2_ARN" - TG2_ARN="" -} || { - fail "delete-target-group-2" -} - -# Phase 8: Error Path Tests -echo "" -echo "Phase 8: Error Path Tests" -echo "========================================" - -# Describe non-existent load balancer -echo "Testing describe non-existent ALB..." -NOTFOUND_OUTPUT=$($AWS_ELBV2 describe-load-balancers \ - --load-balancer-arns "arn:aws:elasticloadbalancing:us-east-1:000000000000:loadbalancer/app/fake/fake123" \ - --output json 2>&1) || true -NOTFOUND_COUNT=$(echo "$NOTFOUND_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('LoadBalancers', [])))" 2>/dev/null) -if [ "$NOTFOUND_COUNT" = "0" ]; then - pass "describe non-existent ALB: empty result" -else - fail "describe non-existent ALB: expected 0, got $NOTFOUND_COUNT" -fi - -# Delete non-existent load balancer -echo "Testing delete non-existent ALB..." -$AWS_ELBV2 delete-load-balancer \ - --load-balancer-arn "arn:aws:elasticloadbalancing:us-east-1:000000000000:loadbalancer/app/fake/fake123" \ - 2>&1 && { - fail "delete non-existent ALB should have returned error" -} || { - pass "delete non-existent ALB correctly returned error" -} - -# Delete non-existent target group -echo "Testing delete non-existent target group..." -$AWS_ELBV2 delete-target-group \ - --target-group-arn "arn:aws:elasticloadbalancing:us-east-1:000000000000:targetgroup/fake/fake123" \ - 2>&1 && { - fail "delete non-existent TG should have returned error" -} || { - pass "delete non-existent TG correctly returned error" -} - -# Create target group with missing name -echo "Testing create target group with missing fields..." -$AWS_ELBV2 create-target-group --protocol HTTP --port 80 --vpc-id "$VPC_ID" --output json 2>&1 && { - fail "create TG without name should have been rejected" -} || { - pass "create TG without name correctly rejected" -} - -# Create listener on non-existent ALB -echo "Testing create listener on non-existent ALB..." -$AWS_ELBV2 create-listener \ - --load-balancer-arn "arn:aws:elasticloadbalancing:us-east-1:000000000000:loadbalancer/app/fake/fake123" \ - --protocol HTTP \ - --port 80 \ - --default-actions "Type=forward,TargetGroupArn=$TG2_ARN" \ - --output json 2>&1 && { - fail "create listener on non-existent ALB should have been rejected" -} || { - pass "create listener on non-existent ALB correctly rejected" -} - -echo "" diff --git a/tests/e2e/run-vpc-e2e.sh b/tests/e2e/run-vpc-e2e.sh deleted file mode 100755 index bffdebbdb..000000000 --- a/tests/e2e/run-vpc-e2e.sh +++ /dev/null @@ -1,353 +0,0 @@ -#!/bin/bash -set -e - -# VPC E2E Test Suite -# Tests VPC networking operations via the AWS CLI against a running Spinifex cluster. -# Can run with or without OVN — skips network-level tests when OVN is unavailable. -# -# Usage: -# ./tests/e2e/run-vpc-e2e.sh # Run against default endpoint -# ENDPOINT=https://10.11.12.1:9999 ./tests/e2e/run-vpc-e2e.sh # Custom endpoint - -cd "$(dirname "$0")/../.." - -ENDPOINT="${ENDPOINT:-https://127.0.0.1:9999}" -export AWS_PROFILE=spinifex -AWS_EC2="aws --endpoint-url ${ENDPOINT} ec2" - -PASSED=0 -FAILED=0 -SKIPPED=0 - -pass() { - echo " ✅ $1" - PASSED=$((PASSED + 1)) -} - -fail() { - echo " ❌ $1" - FAILED=$((FAILED + 1)) -} - -skip() { - echo " ⏭️ $1 (skipped)" - SKIPPED=$((SKIPPED + 1)) -} - -# Check if OVN is available -HAS_OVN=false -if command -v ovn-nbctl &>/dev/null; then - HAS_OVN=true -fi - -# Track created resources for cleanup -IGW_ID="" -VPC_ID="" -SUBNET_ID="" - -cleanup() { - local exit_code=$? - echo "" - echo "Cleanup..." - - # Detach and delete IGW - if [ -n "$IGW_ID" ] && [ -n "$VPC_ID" ]; then - $AWS_EC2 detach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" 2>/dev/null || true - fi - if [ -n "$IGW_ID" ]; then - $AWS_EC2 delete-internet-gateway --internet-gateway-id "$IGW_ID" 2>/dev/null || true - fi - - # Delete subnet - if [ -n "$SUBNET_ID" ]; then - $AWS_EC2 delete-subnet --subnet-id "$SUBNET_ID" 2>/dev/null || true - fi - - # Delete VPC - if [ -n "$VPC_ID" ]; then - $AWS_EC2 delete-vpc --vpc-id "$VPC_ID" 2>/dev/null || true - fi - - echo "Cleanup complete" - echo "" - echo "========================================" - echo "VPC E2E Results: $PASSED passed, $FAILED failed, $SKIPPED skipped" - echo "========================================" - - if [ $FAILED -gt 0 ]; then - exit 1 - fi - exit $exit_code -} -trap cleanup EXIT - -echo "========================================" -echo "VPC E2E Test Suite" -echo "========================================" -echo "Endpoint: $ENDPOINT" -echo "OVN available: $HAS_OVN" -echo "" - -# Phase 1: VPC CRUD -echo "Phase 1: VPC CRUD" -echo "========================================" - -# Create VPC -echo "Creating VPC..." -VPC_OUTPUT=$($AWS_EC2 create-vpc --cidr-block 10.99.0.0/16 --output json 2>&1) || { - fail "create-vpc" - echo " Output: $VPC_OUTPUT" - exit 1 -} -VPC_ID=$(echo "$VPC_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['Vpc']['VpcId'])" 2>/dev/null) || { - fail "create-vpc (parse VpcId)" - echo " Output: $VPC_OUTPUT" - exit 1 -} -pass "create-vpc: $VPC_ID" - -# Describe VPCs — should include our VPC -echo "Describing VPCs..." -DESC_OUTPUT=$($AWS_EC2 describe-vpcs --vpc-ids "$VPC_ID" --output json 2>&1) || { - fail "describe-vpcs" - echo " Output: $DESC_OUTPUT" -} -VPC_COUNT=$(echo "$DESC_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['Vpcs']))" 2>/dev/null) -if [ "$VPC_COUNT" = "1" ]; then - pass "describe-vpcs: found $VPC_ID" -else - fail "describe-vpcs: expected 1 VPC, got $VPC_COUNT" -fi - -# Phase 2: Subnet CRUD -echo "" -echo "Phase 2: Subnet CRUD" -echo "========================================" - -# Create subnet -echo "Creating subnet..." -SUBNET_OUTPUT=$($AWS_EC2 create-subnet --vpc-id "$VPC_ID" --cidr-block 10.99.1.0/24 --output json 2>&1) || { - fail "create-subnet" - echo " Output: $SUBNET_OUTPUT" - exit 1 -} -SUBNET_ID=$(echo "$SUBNET_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['Subnet']['SubnetId'])" 2>/dev/null) || { - fail "create-subnet (parse SubnetId)" - exit 1 -} -pass "create-subnet: $SUBNET_ID" - -# Describe subnets -echo "Describing subnets..." -DESC_OUTPUT=$($AWS_EC2 describe-subnets --subnet-ids "$SUBNET_ID" --output json 2>&1) || { - fail "describe-subnets" - echo " Output: $DESC_OUTPUT" -} -SUBNET_COUNT=$(echo "$DESC_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['Subnets']))" 2>/dev/null) -if [ "$SUBNET_COUNT" = "1" ]; then - pass "describe-subnets: found $SUBNET_ID" -else - fail "describe-subnets: expected 1 subnet, got $SUBNET_COUNT" -fi - -# Phase 3: Internet Gateway CRUD -echo "" -echo "Phase 3: Internet Gateway CRUD" -echo "========================================" - -# Create IGW -echo "Creating Internet Gateway..." -IGW_OUTPUT=$($AWS_EC2 create-internet-gateway --output json 2>&1) || { - fail "create-internet-gateway" - echo " Output: $IGW_OUTPUT" - exit 1 -} -IGW_ID=$(echo "$IGW_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['InternetGateway']['InternetGatewayId'])" 2>/dev/null) || { - fail "create-internet-gateway (parse IGW ID)" - exit 1 -} -pass "create-internet-gateway: $IGW_ID" - -# Describe IGW -echo "Describing Internet Gateways..." -DESC_OUTPUT=$($AWS_EC2 describe-internet-gateways --internet-gateway-ids "$IGW_ID" --output json 2>&1) || { - fail "describe-internet-gateways" -} -IGW_COUNT=$(echo "$DESC_OUTPUT" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['InternetGateways']))" 2>/dev/null) -if [ "$IGW_COUNT" = "1" ]; then - pass "describe-internet-gateways: found $IGW_ID" -else - fail "describe-internet-gateways: expected 1, got $IGW_COUNT" -fi - -# Phase 4: IGW Attach / Detach -echo "" -echo "Phase 4: Internet Gateway Attach / Detach" -echo "========================================" - -# Attach IGW -echo "Attaching IGW to VPC..." -$AWS_EC2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" 2>&1 && { - pass "attach-internet-gateway: $IGW_ID -> $VPC_ID" -} || { - fail "attach-internet-gateway" -} - -# Verify attachment via describe -DESC_OUTPUT=$($AWS_EC2 describe-internet-gateways --internet-gateway-ids "$IGW_ID" --output json 2>&1) -ATTACHED_VPC=$(echo "$DESC_OUTPUT" | python3 -c " -import sys,json -igw = json.load(sys.stdin)['InternetGateways'][0] -if igw.get('Attachments'): - print(igw['Attachments'][0].get('VpcId', '')) -else: - print('') -" 2>/dev/null) -if [ "$ATTACHED_VPC" = "$VPC_ID" ]; then - pass "verify attachment: IGW attached to $VPC_ID" -else - fail "verify attachment: expected $VPC_ID, got '$ATTACHED_VPC'" -fi - -# Try to delete attached IGW — should fail -echo "Verifying delete-while-attached fails..." -if $AWS_EC2 delete-internet-gateway --internet-gateway-id "$IGW_ID" 2>&1 | grep -qi "error\|DependencyViolation"; then - pass "delete-while-attached correctly rejected" -else - # Check if it actually failed - if $AWS_EC2 describe-internet-gateways --internet-gateway-ids "$IGW_ID" --output json 2>/dev/null | python3 -c "import sys,json; assert len(json.load(sys.stdin)['InternetGateways']) == 1" 2>/dev/null; then - pass "delete-while-attached: IGW still exists (correctly rejected)" - else - fail "delete-while-attached: IGW was deleted (should have been rejected)" - fi -fi - -# Detach IGW -echo "Detaching IGW from VPC..." -$AWS_EC2 detach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" 2>&1 && { - pass "detach-internet-gateway: $IGW_ID from $VPC_ID" -} || { - fail "detach-internet-gateway" -} - -# Verify detachment -DESC_OUTPUT=$($AWS_EC2 describe-internet-gateways --internet-gateway-ids "$IGW_ID" --output json 2>&1) -ATTACHED_VPC=$(echo "$DESC_OUTPUT" | python3 -c " -import sys,json -igw = json.load(sys.stdin)['InternetGateways'][0] -if igw.get('Attachments'): - print(igw['Attachments'][0].get('VpcId', '')) -else: - print('') -" 2>/dev/null) -if [ -z "$ATTACHED_VPC" ]; then - pass "verify detachment: IGW is detached" -else - fail "verify detachment: IGW still attached to '$ATTACHED_VPC'" -fi - -# Phase 5: OVN Topology Verification (requires OVN) -echo "" -echo "Phase 5: OVN Topology Verification" -echo "========================================" - -if [ "$HAS_OVN" = true ]; then - # Re-attach IGW for OVN topology verification - $AWS_EC2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" 2>/dev/null || true - sleep 1 - - # Verify logical router exists for VPC - if ovn-nbctl --no-leader-only ls-list 2>/dev/null | grep -q "subnet-"; then - pass "OVN logical switch exists for subnet" - else - fail "OVN logical switch not found" - fi - - if ovn-nbctl --no-leader-only lr-list 2>/dev/null | grep -q "vpc-"; then - pass "OVN logical router exists for VPC" - else - fail "OVN logical router not found" - fi - - # Check for external switch (IGW topology) - if ovn-nbctl --no-leader-only ls-list 2>/dev/null | grep -q "ext-"; then - pass "OVN external switch exists (IGW attached)" - else - fail "OVN external switch not found" - fi - - # Check NAT rules — after mulga-754, no blanket VPC CIDR SNAT exists. - # Per-VM dnat_and_snat rules are only created when instances launch with public IPs. - # Verify the router exists and has no blanket SNAT (correct AWS parity behavior). - NAT_RULES=$(ovn-nbctl --no-leader-only lr-nat-list "vpc-${VPC_ID}" 2>/dev/null || echo "") - VPC_CIDR="10.99.0.0/16" - if echo "$NAT_RULES" | grep -q "snat.*${VPC_CIDR}"; then - fail "Blanket VPC CIDR SNAT exists (should not — per mulga-754)" - else - pass "No blanket VPC CIDR SNAT on router (AWS parity)" - fi - - # Check default route - ROUTE_COUNT=$(ovn-nbctl --no-leader-only lr-route-list "vpc-${VPC_ID}" 2>/dev/null | grep -c "0.0.0.0/0" || echo "0") - if [ "$ROUTE_COUNT" -ge 1 ]; then - pass "OVN default route exists on VPC router" - else - fail "OVN default route not found" - fi - - # Show full topology for debugging - echo "" - echo "OVN NB DB topology:" - ovn-nbctl --no-leader-only show 2>/dev/null || echo " (unable to query OVN NB DB)" - - # Detach IGW again for cleanup - $AWS_EC2 detach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" 2>/dev/null || true - sleep 1 -else - skip "OVN topology verification (ovn-nbctl not available)" -fi - -# Phase 6: Cleanup and Verify -echo "" -echo "Phase 6: Cleanup" -echo "========================================" - -# Delete IGW -echo "Deleting Internet Gateway..." -$AWS_EC2 delete-internet-gateway --internet-gateway-id "$IGW_ID" 2>&1 && { - pass "delete-internet-gateway: $IGW_ID" - IGW_ID="" # Don't re-delete in cleanup trap -} || { - fail "delete-internet-gateway" -} - -# Delete subnet -echo "Deleting subnet..." -$AWS_EC2 delete-subnet --subnet-id "$SUBNET_ID" 2>&1 && { - pass "delete-subnet: $SUBNET_ID" - SUBNET_ID="" # Don't re-delete in cleanup trap -} || { - fail "delete-subnet" -} - -# Delete VPC -echo "Deleting VPC..." -$AWS_EC2 delete-vpc --vpc-id "$VPC_ID" 2>&1 && { - pass "delete-vpc: $VPC_ID" - VPC_ID="" # Don't re-delete in cleanup trap -} || { - fail "delete-vpc" -} - -# Verify OVN cleanup -if [ "$HAS_OVN" = true ]; then - sleep 1 - REMAINING_ROUTERS=$(ovn-nbctl --no-leader-only lr-list 2>/dev/null | grep -c "vpc-" || echo "0") - if [ "$REMAINING_ROUTERS" = "0" ]; then - pass "OVN cleanup: no VPC routers remaining" - else - fail "OVN cleanup: $REMAINING_ROUTERS VPC routers still exist" - fi -fi - -echo ""