Skip to content

Fix: add preferred resource IDs - #174

Merged
DaviReisVieira merged 1 commit into
DaviReisVieira:mainfrom
amjadjibon:fix/preffered-resource-ids
Sep 23, 2026
Merged

DaviReisVieira merged 1 commit into
DaviReisVieira:mainfrom
amjadjibon:fix/preffered-resource-ids

Conversation

@amjadjibon

Copy link
Copy Markdown
Contributor

Summary

Fixes #170.

Generic resource lists selected the wrong identifier for five resource types, causing detail requests to return HTTP 500:

Resource Previously selected Correct identifier
IAM policies PolicyName Arn
EC2 subnets VpcId SubnetId
EC2 security groups VpcId GroupId
EFS filesystems Name FileSystemId
RDS clusters HostedZoneId DBClusterIdentifier

Adds the five _PREFERRED_ID_FIELD overrides, fixing the generic API and CLI list output.

Replaces the API Gateway-only test with parameterized coverage for all six resource types. Fixtures include every field from the botocore list-response model to catch identifier collisions. CLI table and CSV output are also checked.

Testing

Create script: scripts/issue_170.sh

#!/usr/bin/env bash

set -Eeuo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
readonly REPO_ROOT
cd "$REPO_ROOT"
readonly MINISTACK_PORT="${STACKPORT_ISSUE_170_MINISTACK_PORT:-14566}"
readonly APP_PORT="${STACKPORT_ISSUE_170_APP_PORT:-18080}"
readonly MINISTACK_URL="http://localhost:$MINISTACK_PORT"
readonly APP_URL="http://localhost:$APP_PORT"
readonly CONTAINER_NAME="stackport-issue-170-ministack-$$"

STACKPORT_PID=""
REPRO_TMP_DIR=""
STACKPORT_LOG=""

die() {
  printf 'error: %s\n' "$*" >&2
  exit 1
}

cleanup() {
  local exit_code=$?
  trap - EXIT INT TERM
  set +e

  if [[ -n "$STACKPORT_PID" ]] && kill -0 "$STACKPORT_PID" 2>/dev/null; then
    kill "$STACKPORT_PID" 2>/dev/null
    wait "$STACKPORT_PID" 2>/dev/null
  fi

  docker stop "$CONTAINER_NAME" >/dev/null 2>&1

  if ((exit_code != 0)) && [[ -f "$STACKPORT_LOG" ]]; then
    printf '\nStackPort log (last 40 lines):\n' >&2
    tail -40 "$STACKPORT_LOG" >&2
  fi

  if [[ -n "$REPRO_TMP_DIR" && -d "$REPRO_TMP_DIR" ]]; then
    case "$(basename "$REPRO_TMP_DIR")" in
      stackport-issue-170.*) rm -rf -- "$REPRO_TMP_DIR" ;;
    esac
  fi

  exit "$exit_code"
}

trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

require_command() {
  command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

wait_for_url() {
  local url=$1
  local label=$2

  for _ in {1..60}; do
    if curl --max-time 2 -fsS "$url" >/dev/null 2>&1; then
      return 0
    fi
    sleep 1
  done

  die "$label did not become ready at $url"
}

aws_local() {
  aws --endpoint-url "$MINISTACK_URL" --region us-east-1 "$@"
}

urlencode() {
  jq -rn --arg value "$1" '$value | @uri'
}

http_status() {
  local service=$1
  local resource_type=$2
  local resource_id=$3
  local encoded_id
  encoded_id=$(urlencode "$resource_id")
  curl -sS -o /dev/null -w '%{http_code}' \
    "$APP_URL/api/resources/$service/$resource_type/$encoded_id"
}

failures=0

check_detail() {
  local label=$1
  local service=$2
  local resource_type=$3
  local emitted_id=$4
  local correct_field=$5
  local correct_id=$6
  local emitted_status
  local correct_status

  emitted_status=$(http_status "$service" "$resource_type" "$emitted_id")
  correct_status=$(http_status "$service" "$resource_type" "$correct_id")

  printf '%s\n' "$label"
  printf '  emitted id=%s -> HTTP %s\n' \
    "$emitted_id" "$emitted_status"
  printf '  correct %s=%s -> HTTP %s\n' \
    "$correct_field" "$correct_id" "$correct_status"

  if [[ -z "$correct_id" || "$emitted_id" != "$correct_id" || "$emitted_status" != 200 || "$correct_status" != 200 ]]; then
    printf '  FAIL: expected the emitted ID to match %s and both requests to return HTTP 200\n' "$correct_field" >&2
    failures=$((failures + 1))
  fi
}

for command_name in docker aws curl jq; do
  require_command "$command_name"
done
docker info >/dev/null 2>&1 || die "Docker is not running"

if [[ -n "${PYTHON_BIN:-}" ]]; then
  python_bin=$PYTHON_BIN
elif [[ -x "$REPO_ROOT/.venv/bin/python" ]]; then
  python_bin="$REPO_ROOT/.venv/bin/python"
elif command -v python3 >/dev/null 2>&1; then
  python_bin=$(command -v python3)
else
  die "Python 3 was not found; set PYTHON_BIN to the project interpreter"
fi
readonly python_bin

"$python_bin" -c 'import boto3, fastapi, uvicorn' >/dev/null 2>&1 || \
  die "StackPort dependencies are missing; run: $python_bin -m pip install -e $REPO_ROOT"

REPRO_TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/stackport-issue-170.XXXXXX")
STACKPORT_LOG="$REPRO_TMP_DIR/stackport.log"

printf 'Starting isolated MiniStack on port %s...\n' "$MINISTACK_PORT"
docker run --rm -d \
  --name "$CONTAINER_NAME" \
  -p "$MINISTACK_PORT:4566" \
  ministackorg/ministack:latest >/dev/null
wait_for_url "$MINISTACK_URL/_ministack/health" "MiniStack"

export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
export AWS_PAGER=""
unset AWS_SESSION_TOKEN AWS_SECURITY_TOKEN

printf 'Creating IAM, EC2, EFS, and RDS fixtures with AWS CLI...\n'
aws_local iam create-policy \
  --policy-name stackport-issue-170-policy \
  --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:ListAllMyBuckets","Resource":"*"}]}' \
  >/dev/null

VPC_ID=$(aws_local ec2 create-vpc \
  --cidr-block 10.170.0.0/16 \
  --query 'Vpc.VpcId' \
  --output text)
SUBNET_ID=$(aws_local ec2 create-subnet \
  --vpc-id "$VPC_ID" \
  --cidr-block 10.170.1.0/24 \
  --query 'Subnet.SubnetId' \
  --output text)
GROUP_ID=$(aws_local ec2 create-security-group \
  --vpc-id "$VPC_ID" \
  --group-name stackport-issue-170-sg \
  --description 'StackPort issue 170 reproduction' \
  --query 'GroupId' \
  --output text)

aws_local efs create-file-system \
  --creation-token stackport-issue-170-efs \
  --tags Key=Name,Value=stackport-issue-170-efs \
  >/dev/null
aws_local rds create-db-cluster \
  --db-cluster-identifier stackport-issue-170-cluster \
  --engine aurora-mysql \
  --master-username admin \
  --master-user-password stackport170pass \
  >/dev/null

printf 'Starting StackPort on port %s...\n' "$APP_PORT"
AWS_ENDPOINT_URL="$MINISTACK_URL" \
AWS_REGION=us-east-1 \
AWS_ACCESS_KEY_ID=test \
AWS_SECRET_ACCESS_KEY=test \
STACKPORT_DATA_DIR="$REPRO_TMP_DIR/state" \
STACKPORT_PORT="$APP_PORT" \
"$python_bin" -m backend.main >"$STACKPORT_LOG" 2>&1 &
STACKPORT_PID=$!

for _ in {1..60}; do
  if curl -fsS "$APP_URL/api/health" >/dev/null 2>&1; then
    break
  fi
  kill -0 "$STACKPORT_PID" 2>/dev/null || die "StackPort exited before becoming ready"
  sleep 1
done
curl -fsS "$APP_URL/api/health" >/dev/null 2>&1 || \
  die "StackPort did not become ready at $APP_URL"

BASE_URL="$APP_URL/api/resources"
IAM_JSON=$(curl -fsS "$BASE_URL/iam")
EC2_JSON=$(curl -fsS "$BASE_URL/ec2")
EFS_JSON=$(curl -fsS "$BASE_URL/elasticfilesystem")
RDS_JSON=$(curl -fsS "$BASE_URL/rds")

read -r IAM_EMITTED IAM_CORRECT < <(
  jq -r '.resources.policies[]
    | select(.PolicyName == "stackport-issue-170-policy")
    | [.id, .Arn] | @tsv' <<<"$IAM_JSON"
)
read -r SUBNET_EMITTED SUBNET_CORRECT < <(
  jq -r --arg subnet_id "$SUBNET_ID" '.resources.subnets[]
    | select(.SubnetId == $subnet_id)
    | [.id, .SubnetId] | @tsv' <<<"$EC2_JSON"
)
read -r GROUP_EMITTED GROUP_CORRECT < <(
  jq -r --arg group_id "$GROUP_ID" '.resources.security_groups[]
    | select(.GroupId == $group_id)
    | [.id, .GroupId] | @tsv' <<<"$EC2_JSON"
)
read -r EFS_EMITTED EFS_CORRECT < <(
  jq -r '.resources.file_systems[]
    | select(.Name == "stackport-issue-170-efs")
    | [.id, .FileSystemId] | @tsv' <<<"$EFS_JSON"
)
read -r RDS_EMITTED RDS_CORRECT < <(
  jq -r '.resources.db_clusters[]
    | select(.DBClusterIdentifier == "stackport-issue-170-cluster")
    | [.id, .DBClusterIdentifier] | @tsv' <<<"$RDS_JSON"
)

printf '\nChecking generic detail routes...\n'
check_detail iam/policies iam policies "$IAM_EMITTED" Arn "$IAM_CORRECT"
check_detail ec2/subnets ec2 subnets "$SUBNET_EMITTED" SubnetId "$SUBNET_CORRECT"
check_detail ec2/security_groups ec2 security_groups "$GROUP_EMITTED" GroupId "$GROUP_CORRECT"
check_detail elasticfilesystem/file_systems elasticfilesystem file_systems "$EFS_EMITTED" FileSystemId "$EFS_CORRECT"
check_detail rds/db_clusters rds db_clusters "$RDS_EMITTED" DBClusterIdentifier "$RDS_CORRECT"

if ((failures != 0)); then
  die "issue #170 regression check failed for $failures of 5 resource types"
fi

printf '\nPASS: all five resource types emit the correct ID and return HTTP 200.\n'

Befor Fix (main)

./scripts/reproduce_issue_170.sh
Starting isolated MiniStack on port 14566...
Creating IAM, EC2, EFS, and RDS fixtures with AWS CLI...
Starting StackPort on port 18080...

Checking generic detail routes...
iam/policies
  emitted id=stackport-issue-170-policy -> HTTP 500
  correct Arn=arn:aws:iam::000000000000:policy/stackport-issue-170-policy -> HTTP 200
  FAIL: expected the emitted ID to match Arn and both requests to return HTTP 200
ec2/subnets
  emitted id=vpc-006fcefc0a92fe98e -> HTTP 500
  correct SubnetId=subnet-8c1172996373e9a7c -> HTTP 200
  FAIL: expected the emitted ID to match SubnetId and both requests to return HTTP 200
ec2/security_groups
  emitted id=vpc-006fcefc0a92fe98e -> HTTP 500
  correct GroupId=sg-dee4ec63273fa5886 -> HTTP 200
  FAIL: expected the emitted ID to match GroupId and both requests to return HTTP 200
elasticfilesystem/file_systems
  emitted id=stackport-issue-170-efs -> HTTP 500
  correct FileSystemId=fs-7633796da52d0828b -> HTTP 200
  FAIL: expected the emitted ID to match FileSystemId and both requests to return HTTP 200
rds/db_clusters
  emitted id=Z2R2ITUGPM61AM -> HTTP 500
  correct DBClusterIdentifier=stackport-issue-170-cluster -> HTTP 200
  FAIL: expected the emitted ID to match DBClusterIdentifier and both requests to return HTTP 200
error: issue #170 regression check failed for 5 of 5 resource types

StackPort log (last 40 lines):
           ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/amjadhossain/github.com/amjadjibon/stackport/.venv/lib/python3.14/site-packages/botocore/context.py", line 123, in wrapper
    return func(*args, **kwargs)
  File "/Users/amjadhossain/github.com/amjadjibon/stackport/.venv/lib/python3.14/site-packages/botocore/client.py", line 1094, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (InvalidGroupId.Malformed) when calling the DescribeSecurityGroups operation: Invalid id: "vpc-006fcefc0a92fe98e"
INFO:     127.0.0.1:61520 - "GET /api/resources/ec2/security_groups/vpc-006fcefc0a92fe98e HTTP/1.1" 500 Internal Server Error
INFO:     127.0.0.1:61522 - "GET /api/resources/ec2/security_groups/sg-dee4ec63273fa5886 HTTP/1.1" 200 OK
2026-09-22 18:28:44,456 backend.routes.resources WARNING Failed to get detail for elasticfilesystem/file_systems/stackport-issue-170-efs
Traceback (most recent call last):
  File "/Users/amjadhossain/github.com/amjadjibon/stackport/backend/routes/resources.py", line 276, in get_resource_detail
    resp = method(**{id_param: res_id})
  File "/Users/amjadhossain/github.com/amjadjibon/stackport/.venv/lib/python3.14/site-packages/botocore/client.py", line 606, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/amjadhossain/github.com/amjadjibon/stackport/.venv/lib/python3.14/site-packages/botocore/context.py", line 123, in wrapper
    return func(*args, **kwargs)
  File "/Users/amjadhossain/github.com/amjadjibon/stackport/.venv/lib/python3.14/site-packages/botocore/client.py", line 1094, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.FileSystemNotFound: An error occurred (FileSystemNotFound) when calling the DescribeFileSystems operation: File system 'stackport-issue-170-efs' does not exist.
INFO:     127.0.0.1:61524 - "GET /api/resources/elasticfilesystem/file_systems/stackport-issue-170-efs HTTP/1.1" 500 Internal Server Error
INFO:     127.0.0.1:61526 - "GET /api/resources/elasticfilesystem/file_systems/fs-7633796da52d0828b HTTP/1.1" 200 OK
2026-09-22 18:28:44,487 backend.routes.resources WARNING Failed to get detail for rds/db_clusters/Z2R2ITUGPM61AM
Traceback (most recent call last):
  File "/Users/amjadhossain/github.com/amjadjibon/stackport/backend/routes/resources.py", line 276, in get_resource_detail
    resp = method(**{id_param: res_id})
  File "/Users/amjadhossain/github.com/amjadjibon/stackport/.venv/lib/python3.14/site-packages/botocore/client.py", line 606, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/amjadhossain/github.com/amjadjibon/stackport/.venv/lib/python3.14/site-packages/botocore/context.py", line 123, in wrapper
    return func(*args, **kwargs)
  File "/Users/amjadhossain/github.com/amjadjibon/stackport/.venv/lib/python3.14/site-packages/botocore/client.py", line 1094, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.DBClusterNotFoundFault: An error occurred (DBClusterNotFoundFault) when calling the DescribeDBClusters operation: DBCluster Z2R2ITUGPM61AM not found.
INFO:     127.0.0.1:61528 - "GET /api/resources/rds/db_clusters/Z2R2ITUGPM61AM HTTP/1.1" 500 Internal Server Error
INFO:     127.0.0.1:61530 - "GET /api/resources/rds/db_clusters/stackport-issue-170-cluster HTTP/1.1" 200 OK
INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [29228]

After Fix

Starting isolated MiniStack on port 14566...
Creating IAM, EC2, EFS, and RDS fixtures with AWS CLI...
Starting StackPort on port 18080...

Checking generic detail routes...
iam/policies
  emitted id=arn:aws:iam::000000000000:policy/stackport-issue-170-policy -> HTTP 200
  correct Arn=arn:aws:iam::000000000000:policy/stackport-issue-170-policy -> HTTP 200
ec2/subnets
  emitted id=subnet-871cffa526f840c11 -> HTTP 200
  correct SubnetId=subnet-871cffa526f840c11 -> HTTP 200
ec2/security_groups
  emitted id=sg-698645efbe43494ba -> HTTP 200
  correct GroupId=sg-698645efbe43494ba -> HTTP 200
elasticfilesystem/file_systems
  emitted id=fs-13316df220206a11c -> HTTP 200
  correct FileSystemId=fs-13316df220206a11c -> HTTP 200
rds/db_clusters
  emitted id=stackport-issue-170-cluster -> HTTP 200
  correct DBClusterIdentifier=stackport-issue-170-cluster -> HTTP 200

PASS: all five resource types emit the correct ID and return HTTP 200.

…ty groups, EFS file systems, and RDS DB clusters
@DaviReisVieira
DaviReisVieira merged commit 76245ad into DaviReisVieira:main Sep 23, 2026
2 checks passed
@DaviReisVieira

Copy link
Copy Markdown
Owner

Merged!! Thanks again, two in a row. The before/after script made it easy to trust, and I ran the same check on MiniStack and Floci with your branch: every type now emits the right id and the detail returns 200 on both.

One idea for later, not needed now: the test covers the six known cases, but it could walk every entry in DESCRIBE_REGISTRY with the same botocore-shape trick. Then a new resource type with the same collision would fail CI on its own. If you feel like it, it's yours!

@amjadjibon
amjadjibon deleted the fix/preffered-resource-ids branch September 24, 2026 01:41
@amjadjibon

amjadjibon commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Merged!! Thanks again, two in a row. The before/after script made it easy to trust, and I ran the same check on MiniStack and Floci with your branch: every type now emits the right id and the detail returns 200 on both.

One idea for later, not needed now: the test covers the six known cases, but it could walk every entry in DESCRIBE_REGISTRY with the same botocore-shape trick. Then a new resource type with the same collision would fail CI on its own. If you feel like it, it's yours!

Thanks for the suggestion! I created #175 to track the registry-wide botocore invariant test.

I’d be happy to take this one on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generic detail 500s for five more resource types: list emits the wrong field as the id

2 participants