Skip to content

Latest commit

 

History

History
219 lines (154 loc) · 7.77 KB

File metadata and controls

219 lines (154 loc) · 7.77 KB

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_services.py    # all integration tests
├── 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

Every service follows the same 4-step pattern:

1. Create ministack/services/myservice.py

"""
MyService Emulator.
JSON-based API via X-Amz-Target.
Supports: OperationOne, OperationTwo, ...
"""

import json
import logging
from ministack.core.responses import json_response, error_response_json, new_uuid

logger = logging.getLogger("myservice")

ACCOUNT_ID = "000000000000"
REGION = "us-east-1"

_state: dict = {}  # in-memory storage


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 reset():
    _state.clear()

Protocol guide:

  • JSON services (DynamoDB, SecretsManager, Glue, Athena, Cognito, etc.) — use json_response / error_response_json, route via X-Amz-Target
  • XML/Query services (S3, SQS, SNS, IAM, STS, RDS, ElastiCache, EC2) — build XML responses, route via Action query param; use _xml(status, root_tag, inner) pattern; verify field names against botocore shapes via Loader().load_service_model()
  • REST services (Lambda, ECS, Route53) — route via URL path

2. Register in ministack/app.py

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

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

@pytest.fixture(scope="session")
def mysvc():
    return make_client("myservice")

5. Add tests to tests/test_services.py

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

# Start the stack
docker compose up -d

# Install test dependencies
pip install boto3 pytest pytest-xdist duckdb docker cbor2 

# Parallel-safe phase: run tests that are safe to run concurrently
pytest tests/ -v -n 4 --dist=loadfile -m "not serial"

# Serial/global-state phase: run tests that mutate runtime state or require isolation
pytest tests/ -v -m serial

# Run a specific service
pytest tests/ -v -k "cognito"

Code Conventions

  • One file per service — keep everything for a service in ministack/services/myservice.py
  • Imports — always from ministack.core.responses import ..., never from core.responses import ...
  • In-memory state — use module-level dicts (_things: dict = {})
  • reset() — every service must expose a reset() that clears all module-level state; it's called by /_ministack/reset
  • No external AWS deps — no boto3, botocore, or aws-sdk in service code
  • Minimal dependenciesduckdb and docker are optional; guard with try/except ImportError
  • Error responses — match real AWS error codes and HTTP status codes as closely as possible
  • Logginglogger = logging.getLogger("servicename"); DEBUG for request details, INFO for significant events

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)
  • Linting passes (ruff check ministack/)
  • Service added to the table in README.md
  • Entry added to CHANGELOG.md

What We're Looking For

High-value contributions right now:

  • CloudFront — distribution CRUD, invalidations, origin configuration
  • CodeBuild / CodePipeline — CI/CD pipeline stubs
  • AppSync — GraphQL API CRUD
  • SQS FIFO — message group / deduplication support
  • More Cognito flows — hosted UI, federated identity providers, custom auth triggers

Questions?

Open a GitHub Discussion or file an issue with the question label.