CONTRIBUTING.md
Rama main · réplica de lectura del repositorio original.
# 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_<service>.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_<service>.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.