# Contributing to MiniStack Thanks for wanting to contribute. The codebase is intentionally simple — each AWS service is a single self-contained Python file inside `ministack/services/`. Adding a new service or fixing a bug should take minutes, not hours. ## Project Structure ``` ministack/ ├── ministack/ │ ├── app.py # ASGI entry point, service routing, reset endpoint │ ├── core/ │ │ ├── responses.py # json_response, error_response_json, new_uuid │ │ ├── router.py # detect_service(), SERVICE_PATTERNS │ │ ├── lambda_runtime.py │ │ └── persistence.py │ └── services/ │ ├── s3.py, sqs.py, sns.py, dynamodb.py, ... │ └── cognito.py # example of a two-client service file ├── tests/ │ ├── conftest.py # pytest fixtures (boto3 clients) │ └── test_.py # integration tests by service ├── Dockerfile ├── pyproject.toml └── CHANGELOG.md ``` ----- ## For infrastructure changes (Dockerfiles, CI/CD, pyproject, dependencies), open an issue first. PRs containing such changes without a prior issue will be rejected. ## FOR NEW SERVICES — Open an Issue First > **This section applies only when you are adding a brand-new AWS service** (a new file under `ministack/services/`). **Before writing any code for a new service, open a GitHub issue.** Use the `enhancement` label and describe: 1. **Which AWS service** (full name + the API namespace, e.g. `inspector2`, `appconfig`, `qbusiness`). 2. **Which operations** you actually need — not the full API surface. MiniStack favors the operations real users hit (CRUD + the handful of list/get calls SDKs and Terraform providers rely on) over wire-format completeness for every action. 3. **A real use case** — what tool, framework, or workflow drove the need (Terraform module, CDK construct, app code, integration test, etc.). "I want full parity" is not a use case. 4. **Scope boundaries** — what's explicitly out of scope for the first PR (e.g. "no async job lifecycle, no findings persistence, no scheduled scans" for Inspector v2). It's fine to ship a partial service; it's not fine to ship a 50-operation stub where 45 return empty lists. A maintainer will confirm the scope, flag overlap with existing work, and point you at the right protocol (JSON vs XML/Query vs REST) before you write code. This saves you from large PRs that get rejected for scope drift, AWS-parity gaps, or duplicating in-flight work. **PRs that add a new service without a corresponding scoped issue will be closed and the contributor asked to open one.** This rule is specific to new services — it's the only way we keep MiniStack's AWS parity bar high while staying a one-file-per-service codebase. --- ## Adding a New Service For a new service, follow these steps: ### 1. Create `ministack/services/myservice.py` ```python """ MyService Emulator. JSON-based API via X-Amz-Target. Supports: OperationOne, OperationTwo, ... """ import copy import json import logging from ministack.core.responses import AccountRegionScopedDict, json_response, error_response_json logger = logging.getLogger("myservice") _state = AccountRegionScopedDict() # for account- and region-scoped resources async def handle_request(method, path, headers, body, query_params): target = headers.get("x-amz-target", "") action = target.split(".")[-1] if "." in target else "" try: data = json.loads(body) if body else {} except json.JSONDecodeError: return error_response_json("SerializationException", "Invalid JSON", 400) handlers = { "OperationOne": _operation_one, "OperationTwo": _operation_two, } handler = handlers.get(action) if not handler: return error_response_json("InvalidAction", f"Unknown action: {action}", 400) return handler(data) def _operation_one(data): return json_response({"result": "ok"}) def _operation_two(data): return json_response({}) def get_state(): return {"state": copy.deepcopy(_state)} def load_persisted_state(data): if data: _state.update(data.get("state", {})) def reset(): _state.clear() ``` **Protocol guide:** Check the botocore service model and an existing handler before choosing how to dispatch an operation. - **JSON:** Commonly uses `X-Amz-Target` and the shared `json_response` / `error_response_json` helpers. - **Query:** Uses `Action` in the URL or a form-encoded POST body and returns XML; verify response shapes against botocore with `Loader().load_service_model()`. - **S3:** Uses a REST-style API with XML responses and dispatches by method, path, and subresource. Some services support more than one protocol, so follow the operation's actual wire format. ### 2. Register in `ministack/app.py` ```python from ministack.services import myservice SERVICE_REGISTRY = { # ... existing ... "myservice": {"module": "myservice"}, } ``` If the service needs aliases, add them in the registry entry. ### 3. Add detection to `ministack/core/router.py` ```python SERVICE_PATTERNS = { # ... existing ... "myservice": { "target_prefixes": ["AWSMyService"], # for X-Amz-Target routing "host_patterns": [r"myservice\."], # for host-based routing }, } ``` Add any credential scope or `Action`-based routing as needed. ### 4. Add a fixture to `tests/conftest.py` ```python @pytest.fixture(scope="session") def mysvc(): return make_client("myservice") ``` ### 5. Add tests to `tests/test_myservice.py` ```python def test_myservice_operation_one(mysvc): resp = mysvc.operation_one(Param="value") assert resp["result"] == "ok" ``` **Note:** If your service has operations that mutate global state or require isolation (like calling the reset endpoint), add them to the `_SERIAL_TESTS` set in `conftest.py`. This ensures they run sequentially after the parallel-safe tests. --- ## Running Tests Locally ```bash # Start the stack docker compose up -d # Install test dependencies pip install boto3 pytest pytest-xdist duckdb docker cbor2 # Full suite (the default local verification command) pytest tests/ -v # Control-plane parallel lane: excludes serial and live-container tests pytest tests/ -v -n 4 --dist=loadfile -m "not serial and not data_plane and not auth_contract" # Control-plane serial/global-state lane pytest tests/ -v -m "serial and not data_plane and not auth_contract" # Docker-backed data-plane lane (requires a Docker daemon and network) DOCKER_NETWORK=ministack-data-local LAMBDA_EXECUTOR=docker \ pytest tests/ -v -m "data_plane and not auth_contract" # Run a specific service pytest tests/ -v -k "cognito" ``` --- ## Testing Permission Behavior Test whether an operation is allowed or denied with an SDK/HTTP test in `tests/test_.py` marked `auth_contract`. CI runs these against servers with `AUTH=false` and `AUTH=true`; the other jobs exclude them. `iam_user` creates a user with no permissions (pass `policy={...}` for an inline policy) and cleans up after the test; `auth_enabled` is the server's mode, checked against the `AUTH` the tests run with. ```python @pytest.mark.auth_contract def test_list_buckets_without_permission(auth_enabled, iam_user): client = iam_user().client("s3") if auth_enabled: with pytest.raises(ClientError) as exc: client.list_buckets() assert exc.value.response["Error"]["Code"] == "AccessDenied" else: assert "Buckets" in client.list_buckets() ``` Run them locally with the server and pytest in the same mode, then repeat with `AUTH=true`: ```bash AUTH=false IOT_MTLS_ENABLED=0 python -m hypercorn ministack.app:app --bind 127.0.0.1:4566 AUTH=false pytest tests/ -m auth_contract -v ``` --- ## Code Conventions - **Imports** — always `from ministack.core.responses import ...`, never `from core.responses import ...` - **In-memory state** — scope resources by account and region where AWS does; follow the existing `AccountRegionScopedDict` pattern - **State contract** — every registered service module defines `get_state()`, `load_persisted_state(data)`, and `reset()`; `tests/test_persistence.py` checks this, and `/_ministack/reset` calls `reset()` - **No external AWS deps** — no `boto3`, `botocore`, or `aws-sdk` in service code - **Minimal dependencies** — `duckdb` and `docker` are optional; guard with `try/except ImportError` - **Error responses** — match real AWS error codes and HTTP status codes as closely as possible - **Logging** — `logger = logging.getLogger("servicename")`; DEBUG for request details, INFO for significant events --- ## Pull Request Rules - **Brand-new GitHub accounts can only open issues**, not PRs. - **At most 5 open PRs per contributor.** If the work was agreed beforehand in an issue, we may lift the limit temporarily. - **Real AWS evidence.** Every behavior change should be backed by a response captured from a real AWS account. PRs based on documentation, botocore model reading, LocalStack or other third-party recordings, or inference that turn out to differ from AWS will be closed. - **PRs that are not in line with AWS are closed**, and we implement the behavior ourselves. - **No AWS account?** Open an issue describing the behavior and your workflow instead of a PR, and we'll take it from there. --- ## Pull Request Checklist - [ ] New service file in `ministack/services/` - [ ] Registered in `ministack/app.py` SERVICE_REGISTRY - [ ] Detection patterns added to `ministack/core/router.py` - [ ] Fixture added to `tests/conftest.py` - [ ] Tests added and passing (`pytest tests/ -v`) - [ ] Registered modules expose `get_state()`, `load_persisted_state(data)`, and `reset()` - [ ] Linting passes (`ruff check ministack/`) - [ ] Service added to the table in `README.md` - [ ] Entry added to `CHANGELOG.md` --- ## What We're Looking For ONLY REAL AWS USERS WHO BUMP INTO REAL LIFE ISSUES. --- ## Questions? Open a GitHub Discussion or file an issue with the `question` label.