Ctrl K
ComunidadGuías…

CONTRIBUTING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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.