diff --git a/README.md b/README.md index 7f17f77a..53d7d5ff 100644 --- a/README.md +++ b/README.md @@ -6,42 +6,36 @@ A **local development companion for cloud-native apps**. Iterate fast without cl ![Go](https://img.shields.io/badge/Go-1.26-00ADD8) ![License](https://img.shields.io/badge/License-Apache_2.0-blue) +## Quick Start + +```bash +docker run -p 4747:4747 ghcr.io/skyoo2003/devcloud:latest +``` + +Point any AWS SDK at `http://localhost:4747`. See [Getting Started](docs/getting-started.md) for boto3 / AWS CLI / Terraform examples and other install options. + ## Why DevCloud? -Modern cloud development is expensive and slow to iterate on: every test hits a billed service, every feature branch needs its own sandbox, and every new joiner waits for cloud credentials. DevCloud runs a **local, API-compatible cloud environment** so you can: +Every test against real AWS is billed, every feature branch wants its own sandbox, and every new joiner waits for credentials. DevCloud runs a local, API-compatible cloud environment so you can: - **Develop offline** — no VPN, no credentials, no internet -- **Iterate without a bill** — run integration tests at CI speed, not cloud speed +- **Iterate without a bill** — integration tests at CI speed, not cloud speed - **Onboard in minutes** — `docker run` and your team is productive -- **Ship with confidence** — compatibility tests against real SDKs mean your code that works locally works in production - -DevCloud is an **on-ramp to the cloud**, not a replacement for it. The goal is to make the local → CSP transition boring. +- **Ship with confidence** — compatibility tests run against real SDKs -## Vision: one local environment for every CSP - -Today DevCloud targets **AWS**. Our long-term goal is to support the full range of Cloud Service Providers (Azure, GCP, and beyond) behind the same local runtime and plugin architecture. We are rolling this out in phases — see [docs/roadmap.md](docs/roadmap.md) for the phased plan. +DevCloud is an **on-ramp to the cloud**, not a replacement for it. The goal is to make the local → CSP transition boring. It targets AWS today; Azure, GCP and beyond are on the [roadmap](docs/roadmap.md). ## Features -- **205 AWS services registered, 201 serving at least one operation** — the other 4 are routed and decline with a clean AWS error rather than letting the call bill a real AWS account. The split is the honest form of the number; see [coverage.md](docs/coverage.md) for what it does and does not promise, and [services-matrix.md](docs/services-matrix.md) for the list -- **boto3-compatible** — a 992-test compatibility suite runs in CI (`make test-compat`) and exercises every registered service; works with most boto3 apps, and unsupported operations return a clean AWS error, never a false success -- **Cross-service integration** — CFN provisioning, DDB Streams → Lambda, EventBridge targets, S3 → Lambda -- **Smithy-driven codegen** — auto-generate Go types, routers, and error catalogues from Smithy models -- **Weekly auto-sync** — GitHub Actions keeps generated code up to date with upstream AWS API changes -- **Single binary, zero-config** — one Docker image, one port (4747), no config file required (embedded defaults) -- **Environment variable overrides** — `DEVCLOUD_SERVICES`, `DEVCLOUD_DATA_DIR`, `DEVCLOUD_PORT` for quick configuration without YAML -- **SDK/CLI compatible** — works with the AWS SDK, CLI, Terraform, CDK out of the box -- **Admin API** — opt-in REST at `/devcloud/api/*` for service status, resource listing, and recent request logs (`admin.enabled: true`). The web dashboard UI lives in a separate repository. +- **205 AWS services registered, 201 serving at least one operation** — the remaining 4 are routed and decline with a clean AWS error rather than letting the call bill a real account. See [coverage.md](docs/coverage.md) for what the numbers do and do not promise. +- **boto3-compatible** — a 1,144-test suite runs in CI (`make test-compat`) across every registered service. Unsupported operations return a clean AWS error, never a false success. +- **Cross-service integration** — CloudFormation provisioning, DynamoDB Streams → Lambda, EventBridge targets, S3 → Lambda +- **Smithy-driven codegen** — Go types, routers and error catalogues generated from AWS models, with a weekly sync workflow that keeps them current +- **Single binary, zero-config** — one Docker image, one port (4747), no config file required; override with `DEVCLOUD_SERVICES`, `DEVCLOUD_DATA_DIR`, `DEVCLOUD_PORT` +- **SDK/CLI compatible** — AWS SDK, CLI, Terraform and CDK work out of the box +- **Admin API** — opt-in REST at `/devcloud/api/*` for service status, resources and request logs (`admin.enabled: true`) -## Quick Start - -```bash -docker run -p 4747:4747 ghcr.io/skyoo2003/devcloud:latest -``` - -Then point any AWS SDK at `http://localhost:4747`. See [Getting Started](docs/getting-started.md) for boto3 / AWS CLI / Terraform examples and installation options. - -## Supported Services (AWS) +## Core Services (AWS) | Service | Protocol | Storage | Docs | |---------|----------|---------|------| @@ -51,35 +45,31 @@ Then point any AWS SDK at `http://localhost:4747`. See [Getting Started](docs/ge | Lambda | REST-JSON | SQLite + Filesystem | [docs/services/lambda.md](docs/services/lambda.md) | | IAM/STS | Query | SQLite | [docs/services/iam-sts.md](docs/services/iam-sts.md) | -Azure and GCP support is on the [roadmap](docs/roadmap.md). +The rest of the registered surface is described in the [services matrix](docs/services-matrix.md). ## Documentation -Start at the [docs index](docs/) for the full map. Quick links: - -- [Getting Started](docs/getting-started.md) — Installation, first use, boto3 / AWS CLI / Terraform examples -- [Configuration](docs/configuration.md) — Config options, env-var overrides, tier shortcuts -- [Architecture](docs/architecture.md) — System design, codegen pipeline, plugin model, multi-CSP vision -- [Services Matrix](docs/services-matrix.md) — services, coverage status, boto3 compatibility -- [Compatibility Policy](docs/compatibility-policy.md) — what v1.0 guarantees across 1.x, and what it explicitly does not -- [Fidelity Manifest](docs/fidelity-manifest.md) — per-operation tiers: how much to trust any given call -- [Plugin API](docs/plugin-api.md) — the in-tree `ServicePlugin` contract for contributors -- [Roadmap](docs/roadmap.md) — Phased plan toward multi-CSP support -- [FAQ](docs/faq.md) / [Troubleshooting](docs/troubleshooting.md) — Common questions and errors -- [Contributing](docs/contributing.md) — Development setup, adding new services -- [Support](SUPPORT.md) / [Governance](GOVERNANCE.md) — Where to ask, how decisions are made -- [Changelog](CHANGELOG.md) — Release history +Start at the [docs index](docs/). Most-read pages: + +| | | +|---|---| +| [Getting Started](docs/getting-started.md) | Install, first run, SDK examples | +| [Configuration](docs/configuration.md) | Config file, env-var overrides, tier shortcuts | +| [Coverage](docs/coverage.md) | What the service counts mean, and the target | +| [Compatibility Policy](docs/compatibility-policy.md) | What v1.0 guarantees across 1.x — and what it does not | +| [Architecture](docs/architecture.md) | System design, codegen pipeline, plugin model | +| [Contributing](docs/contributing.md) | Dev setup, adding a service | + +Project files: [Support](SUPPORT.md) · [Governance](GOVERNANCE.md) · [Releasing](RELEASE.md) · [Changelog](CHANGELOG.md) ## Contributing -We welcome contributions — especially service implementations, compatibility fixes, and documentation. See the [Contributing Guide](docs/contributing.md) for development setup and the [Code of Conduct](CODE_OF_CONDUCT.md) for community standards. +Contributions are welcome — especially service implementations, compatibility fixes, and documentation. See the [Contributing Guide](docs/contributing.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). -For security issues, please follow the [Security Policy](SECURITY.md) and do not file a public issue. +For security issues, follow the [Security Policy](SECURITY.md) and do not file a public issue. ## License -Licensed under the [Apache License, Version 2.0](LICENSE). See [NOTICE](NOTICE) for attribution requirements. - -## Trademark Notice +Apache License, Version 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE). -DevCloud is an independent open-source project. References to cloud service providers describe **API compatibility** only. All trademarks (AWS, Azure, Google Cloud, etc.) are the property of their respective owners. See [TRADEMARKS.md](TRADEMARKS.md) for details. +DevCloud is an independent open-source project. References to cloud service providers describe **API compatibility** only; all trademarks are the property of their respective owners. See [TRADEMARKS.md](TRADEMARKS.md). diff --git a/RELEASE.md b/RELEASE.md index 74257170..49551780 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,88 +1,81 @@ # Releasing DevCloud -DevCloud ships versioned releases through **[Changie](https://changie.dev)** (changelog -management) and **[GoReleaser](https://goreleaser.com)** (build + publish). Release notes -are authored as Changie fragments during development, batched into a version file, and -handed to GoReleaser at tag time — commits and PR labels are **not** used to generate -release notes. +Releases go out through **[Changie](https://changie.dev)** (changelog) and +**[GoReleaser](https://goreleaser.com)** (build + publish). Release notes are +authored as Changie fragments during development and batched at release time — +commits and PR labels are **not** used to generate them. Versions follow [Semantic Versioning](https://semver.org): `vMAJOR.MINOR.PATCH`. ## Two pipelines -| Pipeline | Trigger | What it produces | Workflow | -|----------|---------|------------------|----------| -| **Release** | pushing a `v*` tag (or manual dispatch) | GitHub Release with binaries and checksums, versioned `*-alpine` container images, a Homebrew formula | [`.github/workflows/release.yml`](.github/workflows/release.yml) | -| **CD** | every successful CI run on a push | rolling multi-arch `latest` / branch container images to GHCR | [`.github/workflows/cd.yml`](.github/workflows/cd.yml) | +| Pipeline | Trigger | Produces | +|----------|---------|----------| +| **Release** ([`release.yml`](.github/workflows/release.yml)) | pushing a `v*` tag, or manual dispatch | GitHub Release with binaries and checksums, versioned `*-alpine` images, a Homebrew formula | +| **CD** ([`cd.yml`](.github/workflows/cd.yml)) | every successful CI run on a push | rolling multi-arch `latest` / branch images to GHCR | -CD keeps `ghcr.io/skyoo2003/devcloud:latest` current with `main`. The Release pipeline is -what produces an actual tagged, downloadable release. This document covers the Release pipeline. +CD keeps `ghcr.io/skyoo2003/devcloud:latest` current with `main`. This document +covers the Release pipeline. -## During development: add a changelog fragment +## During development: add a fragment -Any user-facing change should carry a Changie fragment. From the repo root: +Any user-facing change should carry one: ```sh changie new ``` -You'll be prompted for a **kind** (`Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, -`Security`, `Documentation`), a one-line **body**, and the **issue number**. This writes a -small YAML file under `changes/unreleased/`. Commit it alongside your code change. +You are prompted for a **kind** (`Added`, `Changed`, `Deprecated`, `Removed`, +`Fixed`, `Security`, `Documentation`), a one-line **body**, and the **issue +number**. It writes a small YAML file under `changes/unreleased/` — commit it +alongside your code. Prefer `changie new` over hand-writing the YAML: it enforces +the issue number, and a fragment without one renders as a dead link. -Config lives in [`.changie.yaml`](.changie.yaml). - -Prefer `changie new` over writing the YAML by hand: it enforces the issue number, and a -fragment without one renders as a dead link in the release notes. +Config: [`.changie.yaml`](.changie.yaml). ## Pre-flight checklist -Run through this before batching. - -The first three are re-run by the Release workflow against the tagged commit, which refuses to -publish if any fails — tick them to find out on your machine rather than from a failed tag. -The rest are only caught here. +The first three are re-run by the Release workflow against the tagged commit, +which refuses to publish if any fails — tick them to find out on your machine +rather than from a failed tag. The rest are only caught here. - [ ] **Generated code is current** — `rm -rf internal/generated && make codegen && git status --porcelain internal/generated` - prints nothing. `internal/generated` is committed but derived; a stale fidelity manifest - misreports what the release can be trusted to do. Clear the tree first, as CI does: the - generator overwrites the outputs it still emits but never removes one it has stopped - emitting, so regenerating in place leaves a retired file looking current. + prints nothing. Clear the tree first, as CI does: the generator overwrites + the outputs it still emits but never removes one it has stopped emitting, so + regenerating in place leaves a retired file looking current. A stale fidelity + manifest misreports what the release can be trusted to do. - [ ] **boto3 compatibility passes** — `make test-compat`. - [ ] **Go tests pass** — `CGO_ENABLED=0 go test ./...`. - [ ] **`main` is green**, including lint and CodeQL. - [ ] **Every unreleased fragment carries an issue number** — `grep -L 'Issue: "[0-9]' changes/unreleased/*.yaml` prints nothing. -- [ ] **`changes/unreleased/` is not empty.** No fragments means either nothing shipped or - someone forgot one. -- [ ] **Deprecation review.** If this release *removes* anything previously deprecated — a - config key, an env var, an admin route — confirm it shipped for at least one release - with a warning first. The precedent is the `dashboard` → `admin` rename in - [`internal/config/config.go`](internal/config/config.go): the old key kept working, - emitted a warning, and only then became removable. Removing without that overlap is a - major-version change. The full procedure, and the surfaces it applies to, is - [docs/compatibility-policy.md](docs/compatibility-policy.md). -- [ ] **Compatibility review.** If this release changes anything on the guaranteed list in - [docs/compatibility-policy.md](docs/compatibility-policy.md), it is a major bump — or it - is a bug. Additive change (a new config key, a new response field, a new service) is a - minor bump. +- [ ] **`changes/unreleased/` is not empty.** No fragments means either nothing + shipped or someone forgot one. +- [ ] **Deprecation review.** If this release *removes* anything previously + deprecated — a config key, an env var, an admin route — confirm it shipped + for at least one release with a warning first. Removing without that overlap + is a major-version change. Full procedure: + [docs/compatibility-policy.md](docs/compatibility-policy.md#deprecation-procedure). +- [ ] **Compatibility review.** Changing anything on the guaranteed list in + [docs/compatibility-policy.md](docs/compatibility-policy.md) is a major bump + — or it is a bug. Additive change (a new config key, response field, or + service) is a minor bump. ## Cutting a release -1. **Make sure `main` is green** and holds all changes you want in the release. +1. **Confirm `main` is green** and holds everything you want in the release. -2. **Batch the unreleased fragments** into a version file, then merge them into the changelog. - Pick the next version per SemVer: +2. **Batch the fragments**, picking the next version per SemVer: ```sh make changelog VERSION=v1.1.0 ``` - That is `changie batch v1.1.0 && changie merge`, which consumes everything in + That is `changie batch v1.1.0 && changie merge`: it consumes `changes/unreleased/`, writes `changes/v1.1.0.md`, and regenerates - [`CHANGELOG.md`](CHANGELOG.md) from all version files. Run the two commands directly if you - want to inspect the batched file before it reaches the changelog. + [`CHANGELOG.md`](CHANGELOG.md). Run the two commands separately if you want to + inspect the batched file first. 3. **Commit** the generated files: @@ -92,7 +85,7 @@ The rest are only caught here. git push origin main ``` -4. **Tag and push.** The tag name **must** match the batched version — the Release workflow +4. **Tag and push.** The tag **must** match the batched version — the workflow fails if `changes/.md` does not exist. ```sh @@ -100,61 +93,59 @@ The rest are only caught here. git push origin v1.1.0 ``` -Pushing the tag triggers the Release workflow. It will: - -- resolve the tag to a commit **once**, up front, and check that same SHA out in every job that - follows. Moving the tag mid-run therefore cannot make the guardrails vouch for one commit - while GoReleaser publishes another, -- re-run the guardrails against that commit and stop before publishing anything if any of them - fails: the Go test suite on amd64 and arm64, the boto3 compatibility suite (run against a - binary GoReleaser built, not `go build`), and the codegen drift check. CI is not relied on - here — it races the tag, and `compat.yml` does not trigger on tags at all, -- verify `changes/v1.1.0.md` exists (guard against tagging without release notes), carries - exactly one version heading and that it names *this* tag (a file copied from an earlier - release is rejected), contains only what `changie batch` renders, and that every entry ends - in a valid issue link, -- run GoReleaser, which builds binaries for **darwin/linux/windows × amd64/arm64**, packages - them as `tar.gz` (`zip` on Windows) with the `docs/` tree and the top-level files it and - `README.md` link to, and generates a SHA-256 `CHECKSUMS` file, -- build and push container images to `ghcr.io/skyoo2003/devcloud`, tagged - `v1.1.0-alpine`, `v1.1-alpine`, `v1-alpine`, and `latest-alpine`, -- publish `Formula/devcloud.rb` to the Homebrew tap (see below), -- publish a **GitHub Release** whose notes come from `changes/v1.1.0.md` - (`--release-notes`, `mode: replace`). - -GoReleaser config: [`.goreleaser.yaml`](.goreleaser.yaml). +## What the tag triggers + +- **Pins the commit once.** The tag is resolved to a SHA up front and that same + SHA is checked out in every following job, so moving the tag mid-run cannot make + the guardrails vouch for one commit while GoReleaser publishes another. +- **Re-runs the guardrails** against that commit and stops before publishing + anything if any fails: the Go suite on amd64 and arm64, the boto3 suite (against + a GoReleaser-built binary, not `go build`), and the codegen drift check. CI is + not relied on — it races the tag, and `compat.yml` does not trigger on tags at + all. +- **Validates the notes.** `changes/v1.1.0.md` must exist, carry exactly one + version heading naming *this* tag (a file copied from an earlier release is + rejected), contain only what `changie batch` renders, and end every entry in a + valid issue link. +- **Builds and publishes** — binaries for darwin/linux/windows × amd64/arm64 as + `tar.gz` (`zip` on Windows) with a SHA-256 `CHECKSUMS` file; container images + tagged `v1.1.0-alpine`, `v1.1-alpine`, `v1-alpine`, `latest-alpine`; the + Homebrew formula; and a GitHub Release whose notes come from + `changes/v1.1.0.md`. + +Config: [`.goreleaser.yaml`](.goreleaser.yaml). + +Archives carry the `docs/` tree and the top-level files it and `README.md` link +to, so docs are versioned by tag — the `docs/` inside +`devcloud_v1.1.0_linux_amd64.tar.gz` describes exactly the binary beside it. There +is no separate docs site to version. ## Homebrew tap -GoReleaser's `brews` section publishes `Formula/devcloud.rb` to a separate tap repository — -`homebrew-tap` under the same owner, named by `HOMEBREW_TAP_OWNER` / `HOMEBREW_TAP_REPO` in -[`.github/workflows/release.yml`](.github/workflows/release.yml). +GoReleaser's `brews` section publishes `Formula/devcloud.rb` to a separate tap +repository — `homebrew-tap` under the same owner, named by `HOMEBREW_TAP_OWNER` / +`HOMEBREW_TAP_REPO` in [`release.yml`](.github/workflows/release.yml). -The job's own `GITHUB_TOKEN` cannot write to another repository, so the workflow mints a -short-lived token from a GitHub App installed **only** on the tap repo, using the -`TAP_APP_ID` and `TAP_APP_PRIVATE_KEY` secrets. If a release fails at the formula step, check -that the App is still installed on the tap and that neither secret has expired. +The job's own `GITHUB_TOKEN` cannot write to another repository, so the workflow +mints a short-lived token from a GitHub App installed **only** on the tap repo, +using the `TAP_APP_ID` and `TAP_APP_PRIVATE_KEY` secrets. If a release fails at +the formula step, check that the App is still installed and neither secret has +expired. Token minting is skipped on a dry run. -The formula's `test` block only asserts the `-h` usage text: `devcloud` is a long-running -server with no subcommands, so actually starting it would hang the test. - -Token minting is skipped on a dry run, where GoReleaser publishes nothing. +The formula's `test` block only asserts the `-h` usage text: `devcloud` is a +long-running server with no subcommands, so actually starting it would hang. ## Dry run -To validate the build without publishing, run the workflow manually from the Actions tab -(**Release → Run workflow**) with a tag and `dry_run: true` (the default for manual dispatch). -This runs GoReleaser in `--snapshot` mode: it builds artifacts and uploads them to the run, but -publishes nothing to GHCR, the Homebrew tap, or GitHub Releases. +Run the workflow manually from the Actions tab (**Release → Run workflow**) with a +tag and `dry_run: true` (the default for manual dispatch). GoReleaser runs in +`--snapshot` mode: artifacts are built and uploaded to the run, but nothing is +published to GHCR, the tap, or GitHub Releases. ## Requirements recap - The tag (`v1.1.0`) and the fragment file (`changes/v1.1.0.md`) must match exactly. - `changie batch` + `changie merge` must be committed **before** the tag is pushed. -- The tagged commit must pass the Go test suite; the workflow will not publish otherwise. +- The tagged commit must pass the Go suite; the workflow will not publish otherwise. - Every entry in the batched notes needs an issue number. -- No manual GitHub Release editing — release notes are owned by Changie fragments. - -Docs ship inside the release archive, so they are versioned by tag: the `docs/` tree in -`devcloud_v1.1.0_linux_amd64.tar.gz` describes exactly the binary beside it. There is no -separate docs site to version. +- No manual GitHub Release editing — the notes are owned by Changie fragments. diff --git a/SUPPORT.md b/SUPPORT.md index 5dea9c00..aa6fffa7 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,6 +1,6 @@ # Getting Help -Thanks for using DevCloud. Choose the right channel below to get a fast, accurate response. +Pick the right channel below to get a fast, accurate response. ## Where to ask @@ -14,29 +14,26 @@ Thanks for using DevCloud. Choose the right channel below to get a fast, accurat ## Before you ask -Please do a quick check first — most questions already have answers: +Most questions already have answers: 1. **Search existing issues** — [open](https://github.com/skyoo2003/devcloud/issues) + [closed](https://github.com/skyoo2003/devcloud/issues?q=is%3Aissue+is%3Aclosed) -2. **Skim the docs** — [Getting Started](docs/getting-started.md), [Configuration](docs/configuration.md), [Architecture](docs/architecture.md) -3. **Check the [services matrix](docs/services-matrix.md)** — confirm your service and operation are supported -4. **Check the [roadmap](docs/roadmap.md)** — some requests may already be planned for a future phase +2. **Check [Troubleshooting](docs/troubleshooting.md)** and the [FAQ](docs/faq.md) +3. **Confirm the operation is served** — `curl -s 'localhost:4747/devcloud/api/fidelity?service='` with `admin.enabled: true`; see [fidelity-manifest.md](docs/fidelity-manifest.md) +4. **Check the [roadmap](docs/roadmap.md)** — the request may already be planned ## How to write a good question -A well-formed question gets answered faster: - -- **What you tried** — the exact command, code snippet, or SDK call -- **What you expected** — what should have happened -- **What actually happened** — full error message or unexpected behavior -- **Environment** — OS, Go/Python/Node version, DevCloud version (git SHA if built from source, or Docker image tag) -- **Minimal reproduction** — smallest example that shows the issue +- **What you tried** — the exact command, snippet, or SDK call +- **What you expected**, and **what actually happened** — full error message +- **Environment** — OS, language version, DevCloud version (git SHA or image tag) +- **Minimal reproduction** — the smallest example that shows the issue ## Response expectations -DevCloud is maintained by volunteers on best-effort basis. We do not offer a commercial SLA. +DevCloud is maintained by volunteers on a best-effort basis. There is no commercial SLA. -- **Bugs and security reports**: triaged within a few days -- **Feature requests**: reviewed against the [roadmap](docs/roadmap.md) -- **Questions**: answered when a maintainer or community member has time +- **Bugs and security reports** — triaged within a few days +- **Feature requests** — reviewed against the [roadmap](docs/roadmap.md) +- **Questions** — answered when a maintainer or community member has time -If you need guaranteed response times, consider sponsoring the project or contributing a fix yourself — see [CONTRIBUTING.md](CONTRIBUTING.md). +If you need guaranteed response times, consider sponsoring the project or contributing the fix yourself — see [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/changes/unreleased/Documentation-20260906-234500.yaml b/changes/unreleased/Documentation-20260906-234500.yaml new file mode 100644 index 00000000..d1fe140b --- /dev/null +++ b/changes/unreleased/Documentation-20260906-234500.yaml @@ -0,0 +1,5 @@ +kind: Documentation +body: 'The documentation no longer publishes the same figure at five different values. Six numbers were restated across pages and no two agreed: 205/148/101 registered services, 12,407/9,030/7,475 known operations, 992/1,144/775 compatibility tests, 11/12 services without a Smithy model, and 46 engine-wired services against a measured 155. Only docs/coverage.md, README.md and docs/README.md are gated against the binary by cmd/devcloud/coverage_test.go, so those were right and every restatement had rotted; coverage.md now owns the numbers and the other pages link to it. Three statements did not match the code and are corrected: docs/troubleshooting.md pointed at a GET /devcloud/api/health route that internal/admin/api.go does not register, and blamed a failed Lambda invoke on Docker when lambda/runtime.go is a stub that always returns a placeholder; docs/contributing.md listed interface.go, serializer.go and deserializer.go as codegen output when no template emits them. docs/crud-engine.md contradicted itself, serving five protocols in one table and claiming JSON-only across 46 services two screens later. docs/compatibility-policy.md said 193 model-backed services and 12 without, where manifest_gen.go holds 194 and 11. Eighteen files are shorter overall — 3,564 lines to 2,501 — with the 285-row demand.md evidence table and the per-service pages left intact' +time: 2026-09-06T23:45:00.000000+09:00 +custom: + Issue: "151" diff --git a/docs/README.md b/docs/README.md index 0e100f13..c67099f6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,52 +1,46 @@ # DevCloud Documentation -This directory holds DevCloud's technical documentation. Use the map below to jump to what you need. - ## Start here -- **[Getting Started](getting-started.md)** — install, first run, boto3 / AWS CLI / Terraform examples -- **[Configuration](configuration.md)** — YAML options, env-var overrides, tier shortcuts -- **[Architecture](architecture.md)** — system design, plugin model, codegen pipeline, multi-CSP vision -- **[Roadmap](roadmap.md)** — phased plan toward multi-CSP support -- **[Coverage](coverage.md)** — 205 registered / 201 serving, what the count promises, and the target -- **[Demand](demand.md)** — which unregistered services have demonstrated demand, and how that was measured -- **[Services Matrix](services-matrix.md)** — per-service coverage status and boto3 pass rate +| Page | What it covers | +|---|---| +| [Getting Started](getting-started.md) | Install, first run, boto3 / AWS CLI / Terraform examples | +| [Configuration](configuration.md) | YAML options, env-var overrides, tier shortcuts | +| [Architecture](architecture.md) | System design, plugin model, codegen pipeline, multi-CSP vision | +| [Roadmap](roadmap.md) | Phased plan toward multi-CSP support | ## What you can rely on -- **[Compatibility Policy](compatibility-policy.md)** — what v1.0 guarantees across 1.x, what it explicitly does not, and how deprecation works -- **[Fidelity Manifest](fidelity-manifest.md)** — per-operation tiers: how much to trust any given call -- **[CRUD Engine](crud-engine.md)** — how engine-served operations behave, and where they stop -- **[Plugin API](plugin-api.md)** — the in-tree `ServicePlugin` contract for contributors +| Page | What it covers | +|---|---| +| [Coverage](coverage.md) | 205 registered / 201 serving — what the counts promise, and the target | +| [Compatibility Policy](compatibility-policy.md) | What v1.0 guarantees across 1.x, what it does not, and how deprecation works | +| [Fidelity Manifest](fidelity-manifest.md) | Per-operation tiers: how much to trust any given call | +| [CRUD Engine](crud-engine.md) | How engine-served operations behave, and where they stop | +| [Services Matrix](services-matrix.md) | Depth by group, cross-service integrations, protocols | +| [Demand](demand.md) | Which unregistered services have demonstrated demand, and how it was measured | ## Per-service references -Located under [`services/`](services/): - -- [S3](services/s3.md) — object storage (REST-XML) -- [SQS](services/sqs.md) — message queue (Query + JSON) -- [DynamoDB](services/dynamodb.md) — NoSQL KV + document (JSON 1.0) -- [Lambda](services/lambda.md) — function runtime (REST-JSON) -- [IAM / STS](services/iam-sts.md) — identity & tokens (Query) +[S3](services/s3.md) · [SQS](services/sqs.md) · [DynamoDB](services/dynamodb.md) · [Lambda](services/lambda.md) · [IAM / STS](services/iam-sts.md) ## Problem solving -- **[FAQ](faq.md)** — common questions about scope, compatibility, CI use -- **[Troubleshooting](troubleshooting.md)** — common errors and fixes +- [FAQ](faq.md) — scope, compatibility, CI use +- [Troubleshooting](troubleshooting.md) — common errors and fixes +- [Support](../SUPPORT.md) — where to ask ## Contributing -- **[Contributing Guide](contributing.md)** — dev setup, testing, codegen, adding new services -- **[Releasing](../RELEASE.md)** — Changie + GoReleaser release process, versioning, Homebrew tap, dry runs -- Root-level pointers: [CONTRIBUTING.md](../CONTRIBUTING.md), [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md), [SECURITY.md](../SECURITY.md), [SUPPORT.md](../SUPPORT.md) +- [Contributing Guide](contributing.md) — dev setup, testing, codegen, adding a service +- [Plugin API](plugin-api.md) — the in-tree `ServicePlugin` contract +- [Releasing](../RELEASE.md) — Changie + GoReleaser, versioning, Homebrew tap, dry runs +- Root pointers: [CONTRIBUTING.md](../CONTRIBUTING.md) · [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) · [SECURITY.md](../SECURITY.md) · [GOVERNANCE.md](../GOVERNANCE.md) ## Meta -- [Changelog](../CHANGELOG.md) -- [License (Apache 2.0)](../LICENSE) -- [Trademarks](../TRADEMARKS.md) -- [NOTICE](../NOTICE) +[Changelog](../CHANGELOG.md) · [License (Apache 2.0)](../LICENSE) · [Trademarks](../TRADEMARKS.md) · [NOTICE](../NOTICE) --- -If something is missing from this index, that's a bug. Please file an [issue](https://github.com/skyoo2003/devcloud/issues) or open a PR to fix the link. +If something is missing from this index, that's a bug — please file an [issue](https://github.com/skyoo2003/devcloud/issues) or open a PR. diff --git a/docs/architecture.md b/docs/architecture.md index 9e0bc0db..9446841d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,29 +1,7 @@ # Architecture -## Multi-CSP Vision - -DevCloud's long-term direction is to support multiple Cloud Service Providers (AWS, Azure, GCP, and others) behind a single local runtime. Today's implementation targets **AWS only** — the sections below describe the current AWS-specific architecture. - -A phased refactor is planned (see [roadmap.md](roadmap.md)): - -- **Phase 1 (complete, shipped as v1.0)** — AWS services via Smithy codegen, single-port gateway. -- **Phase 2 (complete)** — Intermediate Representation (IR) between API models and codegen; `ModelSource` so OpenAPI (Azure) and Protocol Buffers / Discovery Documents (GCP) can feed the same pipeline; provider namespacing in config; per-provider auth adapters. -- **Phase 3 (pilot)** — First non-AWS service (candidate: Azure Blob Storage) validates the multi-CSP architecture. -- **Phase 4 (breadth)** — Additional services across CSPs; community-owned providers. - -After Phase 2, the four places a second CSP would have touched are each an -addition rather than an edit: - -| Seam | Package | How a provider joins | -|---|---|---| -| Model → code | [`internal/codegen/ir`](../internal/codegen/ir/ir.go), [`source.go`](../internal/codegen/source.go) | Implement `ModelSource`, append it to `DefaultSources` | -| Configuration | [`internal/config`](../internal/config/config.go) | `providers..services.*`; add the name to `knownProviders` | -| Service contract | [`internal/plugin`](../internal/plugin/plugin.go) | Implement the optional `ProviderScoped` on the plugin | -| Credentials | [`internal/auth`](../internal/auth/auth.go) | Implement `Adapter`, append it to `Adapters` | - -The plugin system, protocol detector, and storage abstractions stay CSP-agnostic -by convention rather than by enforcement — `ProtocolType` is an open string type, -not an enum, for exactly this reason. +DevCloud is a single Go binary that serves cloud provider APIs locally. It targets +**AWS today**; the seams that let a second CSP join are described at the bottom. ## Request Flow @@ -47,16 +25,15 @@ API Gateway (port 4747) ▼ Protocol Detector │ - ├─ X-Amz-Target header present → JSON protocol (DynamoDB, SQS JSON) - ├─ Content-Type: x-www-form-urlencoded + Action= → Query protocol (IAM, STS, SQS Query) - ├─ SigV4 credential scope → REST-JSON (Lambda, and the signing name decides the service) - └─ Default → REST-XML (S3) + ├─ X-Amz-Target header present → JSON protocol + ├─ x-www-form-urlencoded + Action= → Query protocol + ├─ SigV4 credential scope → REST-JSON (signing name picks the service) + └─ Default → REST-XML (S3) │ ▼ Service Router → Plugin Registry → ServicePlugin.HandleRequest() │ - ▼ -Service Implementation (S3, SQS, DynamoDB, Lambda, IAM, STS) + ├─ returns plugin.ErrUnhandledOp → generic CRUD engine (docs/crud-engine.md) │ ▼ Storage Backend → Response Serializer → HTTP Response @@ -64,7 +41,7 @@ Storage Backend → Response Serializer → HTTP Response ## Plugin System -All services implement the `ServicePlugin` interface: +Every service implements `ServicePlugin`: ```go type ServicePlugin interface { @@ -78,30 +55,32 @@ type ServicePlugin interface { } ``` -Plugins are registered in the `Registry` at startup. The gateway routes requests to the correct plugin based on protocol detection and service identification. +Plugins register themselves in the `Registry` at startup; the gateway routes to +one by protocol detection and service identification. The per-method contract, +config keys, error convention and v1.x stability guarantee are in +[plugin-api.md](plugin-api.md). -The full contract for each method, the configuration keys, the error convention, and the v1.x stability guarantee are documented in [plugin-api.md](plugin-api.md). +### Protocols -### Supported Protocols +| Protocol | Request format | Response | Example services | +|----------|----------------|----------|------------------| +| REST-XML | HTTP path/headers | XML | S3, Route53, CloudFront | +| REST-JSON | JSON body, REST path | JSON | Lambda, ACM, API Gateway | +| JSON 1.0 | JSON body, `X-Amz-Target` | JSON | DynamoDB, SQS, Kinesis | +| JSON 1.1 | JSON body, `X-Amz-Target` | JSON | ECS, Batch, CW Logs, SFN | +| Query | Form-encoded body with `Action=` | XML | IAM, STS, SNS, RDS, EC2 | -| Protocol | Services | Request Format | Response Format | -|----------|----------|----------------|-----------------| -| REST-XML | S3 | HTTP path/headers | XML | -| JSON 1.0 | DynamoDB, SQS | JSON body, `X-Amz-Target` header | JSON | -| JSON 1.1 | Lambda | JSON body, REST path | JSON | -| Query | IAM, STS, SQS | Form-encoded body with `Action=` | XML | - -SQS supports both Query and JSON protocols. The protocol is auto-detected per request based on Content-Type and headers. +SQS speaks both Query and JSON; the protocol is detected per request. ## Authentication DevCloud reads credentials and never verifies them. `internal/auth` holds one -`Adapter` per provider — `SigV4` for AWS today — which parses the credential -scope off the `Authorization` header or a presigned URL's query string and -reports the access key, region, signing name and session token the caller -claimed. `IdentityMiddleware` puts the result on the request context, where any -plugin can read it with `auth.FromContext`, and the protocol detector uses the -same parse to route by signing name so routing and identity cannot disagree. +`Adapter` per provider — `SigV4` for AWS — which parses the credential scope off +the `Authorization` header or a presigned URL and reports the access key, region, +signing name and session token the caller *claimed*. `IdentityMiddleware` puts the +result on the request context for `auth.FromContext`, and the protocol detector +uses the same parse to route by signing name, so routing and identity cannot +disagree. Signature validation is deliberately absent: a local server that rejected calls whose signature did not match would break every SDK pointed at it with @@ -110,20 +89,18 @@ placeholder credentials. See ## Code Generation -DevCloud auto-generates Go code from API models. This enables rapid tracking of -API changes with minimal manual work. AWS Smithy is the only format read today; -the pipeline is built so a second one is an added file rather than a rewrite. - -### Pipeline +Go code is generated from API models, so tracking upstream API changes costs +little manual work. AWS Smithy is the only format read today; a second one is an +added file, not a rewrite. ``` smithy-models/*.json (AWS Smithy model files) │ ▼ ModelSource (internal/codegen/source.go) - Each source detects its own format and parses it. SmithySource is - the first; OpenAPI and Protobuf join by implementing the interface - and appending to DefaultSources. cmd/codegen names no format. + Each source detects and parses its own format. SmithySource is the + first; OpenAPI and Protobuf join by implementing the interface and + appending to DefaultSources. cmd/codegen names no format. │ ▼ IR — *ir.Model (internal/codegen/ir) @@ -131,94 +108,86 @@ smithy-models/*.json (AWS Smithy model files) Everything downstream reads only this. │ ▼ - Generator (internal/codegen/generator.go) - Uses Go templates to produce: - │ - ├─ types.go — Request/response structs - ├─ router.go — Method+URI → operation routing (REST services) - ├─ errors.go — Service-specific error types - └─ base_provider.go — Stub implementation (NotImplementedError) - - There is no generated serializer: providers receive the raw - *http.Request and parse it themselves (map[string]any for JSON - protocols), so types.go/base_provider.go have no wire glue and only - router.go is consumed today — by the REST services that need - MatchOperation. See docs/crud-engine.md for how the long tail of - unimplemented operations is actually served. + Generator (internal/codegen/generator.go) — Go templates produce: + ├─ types.go — request/response structs + ├─ router.go — method+URI → operation routing (REST services) + ├─ errors.go — service-specific error types + └─ base_provider.go — stub implementation (NotImplementedError) │ ▼ internal/generated/{service}/ (DO NOT EDIT) ``` -### Running Codegen +There is no generated serializer: providers receive the raw `*http.Request` and +parse it themselves, so only `router.go` is consumed today — by the REST services +that need `MatchOperation`. ```bash -# Generate code for all services -make codegen - -# Generate for a specific service -make codegen-s3 +make codegen # all services +make codegen-s3 # one service — fast loop while editing templates ``` -### Weekly Auto-Sync - -A GitHub Actions workflow runs weekly to: -1. Re-download the Smithy models (`scripts/download-smithy-models.sh --refresh`) -2. Run codegen -3. Open a PR if the models or the generated code changed - -The models under `smithy-models/` are committed on purpose: the download URL -tracks `aws-sdk-go-v2` *main*, so they are the pin that makes `make codegen` -reproducible and offline. Only the weekly job passes `--refresh`, which is what -makes an upstream API change show up as a reviewable model diff next to the -regenerated code. +The models under `smithy-models/` are committed on purpose. The download URL +tracks `aws-sdk-go-v2` *main*, so the vendored copies are the pin that makes +`make codegen` reproducible and offline. A [weekly workflow](../.github/workflows/smithy-sync.yml) +passes `--refresh`, regenerates, and opens a PR — which is what makes an upstream +API change show up as a reviewable model diff next to the regenerated code. How to +review one: [contributing.md](contributing.md#reviewing-the-weekly-model-sync). ## Admin API -The admin API (`internal/admin/`) provides a **REST API** at `/devcloud/api/` — -service status, resource listing, and recent request logs. +`internal/admin/` serves a REST API at `/devcloud/api/` — service status, resource +listing, fidelity tiers, unrouted calls, and recent request logs (a circular +buffer of the last 1000). Disabled by default (`admin.enabled: false`). The web +dashboard UI that consumes it is a separate repository; this server serves no UI. -It is disabled by default (`admin.enabled: false`). The web dashboard UI is a -separate project (its own repository) that consumes this API; the Go server -serves no UI itself. +## Startup Flow -Log collector maintains a circular buffer of the last 1000 API requests for the admin log endpoint. +1. Load config from `devcloud.yaml` (or `--config`), applying `DEVCLOUD_PORT`, `DEVCLOUD_SERVICES`, `DEVCLOUD_DATA_DIR` +2. Initialize the structured logger (slog) and the plugin registry +3. Register service factories, then initialize services in dependency order (IAM before STS — STS receives the IAM store via plugin config options) +4. Set up the event bus, log collector and admin API +5. Build the gateway with its middleware chain and service router, and listen +6. On SIGINT/SIGTERM, shut down gracefully with a 15s budget ## Directory Structure ``` devcloud/ ├── cmd/ -│ ├── devcloud/ # Server entry point (main.go) -│ └── codegen/ # Smithy code generator CLI (main.go) +│ ├── devcloud/ # Server entry point +│ └── codegen/ # Smithy code generator CLI ├── internal/ │ ├── gateway/ # HTTP server, middleware, protocol detection, routing │ ├── plugin/ # ServicePlugin interface, Registry, ProviderScoped │ ├── auth/ # Per-provider credential adapters (SigV4 today) │ ├── codegen/ # ModelSource implementations, generators, templates │ │ └── ir/ # Provider-neutral intermediate representation -│ ├── config/ # YAML config loading, provider namespacing, env overrides -│ ├── generated/ # Auto-generated code (DO NOT EDIT; run `make stats` for count) -│ ├── services/ # Service implementations (run `make stats` for count) +│ ├── config/ # YAML loading, provider namespacing, env overrides +│ ├── generated/ # Auto-generated code (DO NOT EDIT) +│ ├── services/ # Service implementations +│ ├── shared/ # CRUD engine, HTTP routing, response helpers │ ├── admin/ # Admin REST API │ └── storage/ # Shared storage abstractions ├── docker/ # Dockerfile, docker-compose.yml ├── smithy-models/ # AWS Smithy JSON model files ├── tests/compatibility/ # Python/boto3 compatibility tests -├── docs/ # Documentation -├── Makefile -└── go.mod +└── docs/ ``` -## Startup Flow +## Multi-CSP seams -1. Load config from `devcloud.yaml` (or specified path), applying environment variable overrides (`DEVCLOUD_PORT`, `DEVCLOUD_SERVICES`, `DEVCLOUD_DATA_DIR`) -2. Initialize structured logger (slog) -3. Create plugin registry -4. Register service factories (run `make stats` for count) -5. Initialize services in dependency order (IAM before STS, etc.) -6. IAM store is shared with STS via plugin config options -7. Set up event bus, log collector, admin API -8. Create gateway with middleware chain and service router -9. Start HTTP server on configured port -10. Wait for shutdown signal (SIGINT/SIGTERM), graceful shutdown with 15s timeout +The long-term direction is to serve multiple CSPs behind one local runtime. Phase +2 of the [roadmap](roadmap.md) made each place a second provider would have +touched an *addition* rather than an edit: + +| Seam | Package | How a provider joins | +|---|---|---| +| Model → code | [`internal/codegen/ir`](../internal/codegen/ir/ir.go), [`source.go`](../internal/codegen/source.go) | Implement `ModelSource`, append it to `DefaultSources` | +| Configuration | [`internal/config`](../internal/config/config.go) | `providers..services.*`; add the name to `knownProviders` | +| Service contract | [`internal/plugin`](../internal/plugin/plugin.go) | Implement the optional `ProviderScoped` on the plugin | +| Credentials | [`internal/auth`](../internal/auth/auth.go) | Implement `Adapter`, append it to `Adapters` | + +The plugin system, protocol detector and storage abstractions stay CSP-agnostic +by convention rather than enforcement — `ProtocolType` is an open string type, +not an enum, for exactly this reason. diff --git a/docs/compatibility-policy.md b/docs/compatibility-policy.md index 1734b342..4fbc03f1 100644 --- a/docs/compatibility-policy.md +++ b/docs/compatibility-policy.md @@ -2,14 +2,12 @@ What DevCloud **v1.0** promises, and what it deliberately does not. -This document covers the surfaces you touch as a *user* of DevCloud — the config file, the -environment, the CLI, the admin API, and the AWS wire protocol. For the in-tree Go contract -that service implementations are written against, see -[plugin-api.md](plugin-api.md#api-stability); that surface lives under `internal/` and is not -importable from another module. +This covers the surfaces you touch as a *user*: the config file, the environment, the CLI, the +admin API, and the AWS wire protocol. For the in-tree Go contract that service implementations +are written against, see [plugin-api.md](plugin-api.md#api-stability). -Versions follow [Semantic Versioning](https://semver.org). "Across 1.x" below means every -release from v1.0.0 up to but not including v2.0.0. +Versions follow [Semantic Versioning](https://semver.org). "Across 1.x" means every release +from v1.0.0 up to but not including v2.0.0. ## Guaranteed across 1.x @@ -74,11 +72,11 @@ appears, and every operation the CRUD engine serves is present and not filed as `unimplemented` — all three fail the build, in [`cmd/devcloud/fidelity_test.go`](../cmd/devcloud/fidelity_test.go). -What no test can catch is an operation that never reaches the manifest at all, and that is -bounded rather than eliminated: for the 193 services with an in-tree Smithy model the operation -universe comes from the model, so an operation losing its implementation reclassifies to -`unimplemented` instead of disappearing. For the 12 without one, the universe *is* what the -providers serve, so the manifest lists no unimplemented tail for them — `modelBacked` on +What no test can catch is an operation that never reaches the manifest at all. That is bounded +rather than eliminated: for the 194 services with an in-tree Smithy model the operation universe +comes from the model, so an operation losing its implementation reclassifies to `unimplemented` +instead of disappearing. For the 11 without one, the universe *is* what the providers serve, so +the manifest lists no unimplemented tail for them. `modelBacked` on `GET /devcloud/api/fidelity` reports which is which. ### Wire behaviour — scoped to the compatibility suite @@ -87,22 +85,19 @@ providers serve, so the manifest lists no unimplemented tail for them — `model keeps holding across 1.x — that property, and nothing wider.** The promise is as wide as each individual assertion: not as wide as the field, and not as wide as -the operation. `CreateFunction` is covered by `test_lambda.py`, and its two assertions are worth -reading closely: - -- `FunctionName` is asserted **equal** to the name that was sent, so its key and its value are - both promised. -- `FunctionArn` is asserted only to be **present**, so its presence is promised and its type, - format and meaning are not. If it stopped being ARN-shaped the suite would stay green — so this - policy does not promise it stays ARN-shaped. -- `Runtime`, `Handler` and `MemorySize` are not asserted at all, so they carry no promise even - though today's response includes them. - -That narrowness is the point: it is the promise the repo can actually keep. The suite — 1,144 tests -driving real boto3 clients — runs in CI on every push and again against the tagged commit before -a release publishes, so breaking an assertion fails the build rather than depending on review -discipline. Anything the suite does not assert rests on nothing but intent. Widening the promise -means adding or strengthening assertions, and such contributions are welcome. +the operation. `CreateFunction` in `test_lambda.py` shows all three cases at once: + +| Field | Asserted as | Promised | +|---|---|---| +| `FunctionName` | equal to the name sent | key *and* value | +| `FunctionArn` | present | presence only — not that it stays ARN-shaped | +| `Runtime`, `Handler`, `MemorySize` | not asserted | nothing, though today's response includes them | + +That narrowness is the point: it is the promise the repo can actually keep. The suite — 1,144 +tests driving real boto3 clients — runs in CI on every push and again against the tagged commit +before a release publishes, so breaking an assertion fails the build rather than depending on +review discipline. Anything the suite does not assert rests on nothing but intent. Widening the +promise means adding or strengthening assertions, and such contributions are welcome. ## Not guaranteed @@ -124,22 +119,17 @@ Depending on any of the following will break, and breaking it is **not** a major today are a floor, not a ceiling — and not a promise of depth either: 4 of them serve no operation and only decline cleanly. See [coverage.md](coverage.md). - **Error codes, HTTP status and message wording.** What *is* guaranteed for an `unimplemented` - operation is that it **fails** — an AWS-shaped error, never a fabricated success. Which error - is not: it comes from whichever provider handles the request, so it is `InvalidAction` (400) - for services that fall through to the CRUD engine, `NotImplemented` (501) for the 33 providers - with their own dispatch default, and each path-routed provider's own vocabulary otherwise - (`s3` `MethodNotAllowed` 405, `bedrock` `UnsupportedOperation` 400). `sqs` even differs by - protocol. [fidelity-manifest.md](fidelity-manifest.md) records the current behaviour; - normalizing it is a minor release, not a major one. + operation is that it **fails** — an AWS-shaped error, never a fabricated success. *Which* error + is not guaranteed: it comes from whichever provider handles the request, and `sqs` even differs + by protocol. [fidelity-manifest.md](fidelity-manifest.md#what-an-unimplemented-call-returns) + records the current behaviour; normalizing it is a minor release, not a major one. - **Log output.** Format, levels and wording of server logs are operational, not an API. -- **Everything under `internal/`.** Go forbids importing it from another module, and DevCloud - reserves the right to restructure it freely across 1.x. The Phase 2 refactor on the - [roadmap](roadmap.md) is the precedent: the intermediate representation, `ModelSource`, - `ProviderScoped` and the `auth` adapters all landed inside a 1.x minor without a - compatibility event, because none of them is reachable from outside this module. Internal - churn is not a compatibility event. The in-tree `ServicePlugin` contract in - [plugin-api.md](plugin-api.md#api-stability) is not an exception to this: it is a convention - that keeps in-tree plugins compiling, and it does not gate release versioning. +- **Everything under `internal/`.** Go forbids importing it from another module, so DevCloud + restructures it freely across 1.x. The Phase 2 refactor is the precedent: the IR, + `ModelSource`, `ProviderScoped` and the `auth` adapters all landed inside a 1.x minor without + a compatibility event, because nothing outside this module can reach them. The in-tree + `ServicePlugin` contract in [plugin-api.md](plugin-api.md#api-stability) is no exception — it + is a convention that keeps in-tree plugins compiling, and it does not gate release versioning. - **Behavioural parity with AWS.** No release of DevCloud promises AWS's validation, business logic, eventual-consistency timing, rate limits, or IAM enforcement. Credentials are accepted without signature verification. diff --git a/docs/configuration.md b/docs/configuration.md index c06f57fd..30549722 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,103 +1,72 @@ # Configuration -**Configuration is optional.** DevCloud ships with built-in defaults: running `devcloud` with no flags enables every registered service on port 4747, storing data under `./data//` (run `make stats` for the current service count). The embedded defaults are compiled into the binary from [`internal/config/default.yaml`](https://github.com/skyoo2003/devcloud/blob/main/internal/config/default.yaml). +**Configuration is optional.** With no flags, `devcloud` enables every registered +service on port 4747 and stores data under `./data//`. The defaults are +compiled into the binary from +[`internal/config/default.yaml`](https://github.com/skyoo2003/devcloud/blob/main/internal/config/default.yaml). -To override defaults, provide a YAML file. DevCloud looks for config in this order: +Config is resolved in this order, and **environment beats YAML beats embedded +default**: -1. `--config ` flag (explicit; the file must exist) -2. `./devcloud.yaml` in the current working directory (auto-detected) -3. Embedded defaults (used when neither of the above is present) +1. `--config ` — explicit; the file must exist +2. `./devcloud.yaml` in the working directory — auto-detected +3. Embedded defaults -Environment variables override YAML values for selected keys (see [Environment Variable Overrides](#environment-variable-overrides)). All three env vars follow the same precedence: **env var wins over YAML, YAML wins over embedded default**. +## Environment variables | Variable | Overrides | Description | |----------|-----------|-------------| | `DEVCLOUD_PORT` | `server.port` | HTTP server port | -| `DEVCLOUD_SERVICES` | `services.*.enabled` | Comma-separated list of services to enable | +| `DEVCLOUD_SERVICES` | `services.*.enabled` | Comma-separated service list, or a tier shortcut | | `DEVCLOUD_DATA_DIR` | `services.*.data_dir` | Base data directory for all services | -## Configuration File - -### Server - -| Key | Default | Description | -|-----|---------|-------------| -| `server.port` | `4747` | HTTP server port | - -### Services - -The `services` block is **optional and authoritative**: omit it (as the embedded -default does) and every registered service starts with `data_dir -./data/`; list any service and *only* the services you list start. -Writing `services: {}` lists nothing, so nothing starts — a block is a block -even when it is empty. - -| Key | Default | Description | -|-----|---------|-------------| -| `services..enabled` | `false` | Enable the service. **Required per entry** — listing a service is not enough, `enabled: true` still has to be set. (With no `services` block at all, every service is enabled.) | -| `services..data_dir` | `./data/` | Data directory for persistent storage | - -Service-specific options: +### `DEVCLOUD_SERVICES` -| Key | Default | Description | -|-----|---------|-------------| -| `services.lambda.runtime` | `""` | Lambda runtime configuration | -| `services.lambda.warm_containers` | `0` | Number of warm containers to keep | -| `services.iam.enforce_policies` | `false` | Enforce IAM policies (experimental) | +When set, it names the running set outright: **only** the listed services start, +regardless of their `enabled` setting, and a service the `services` block omits +entirely still starts if you name it here (the block still supplies its +`data_dir`). When unset, each service uses its YAML `enabled` value. An unknown +`tierN` token is treated as a literal service name and logged as a warning. -### Provider namespacing +| Token | Expands to | +|-------|------------| +| `tier1` | Big 6 + core integration: s3, sqs, dynamodb, iam, sts, lambda, sns, kms, secretsmanager, ssm, cloudwatchlogs, cloudwatch, eventbridge, ec2, ecs, ecr, route53, acm | +| `tier2` | cognito, elasticloadbalancingv2, ebs, efs, states, apigateway, apigatewayv2, kinesis, firehose, ses, sesv2, rds, cloudformation | +| `tier3` | elasticache, cloudfront, wafv2, glue, athena, organizations, cloudtrail, eks, autoscaling, appsync, emr, batch | +| `all` | Disables the env-var filter — the `services` block decides | -DevCloud serves AWS today and is being prepared to serve more (see the -[roadmap](roadmap.md)). Service configuration can therefore be written under an -explicit provider: +The exact lists live in [`internal/config/config.go`](https://github.com/skyoo2003/devcloud/blob/main/internal/config/config.go). -```yaml -providers: - aws: - services: - s3: - enabled: true - data_dir: ./data/s3 +```bash +DEVCLOUD_SERVICES=s3,sqs ./dist/devcloud # only S3 and SQS +DEVCLOUD_SERVICES=tier1 ./dist/devcloud # all Tier 1 +DEVCLOUD_SERVICES=tier1,kinesis,firehose ./dist/devcloud +docker run -p 4747:4747 -e DEVCLOUD_SERVICES=tier1 ghcr.io/skyoo2003/devcloud:latest ``` -`providers.aws.services` and the top-level `services` block are **the same -block** — one under its forward-compatible name, one under its historical one. -Rules: - -- Write either. The top-level `services` block keeps working exactly as before - and is not deprecated. -- If both are present, `providers.aws.services` wins and a warning names the - ignored block. Do not write both. -- A block for a provider this build does not serve (`providers.azure`) parses - without error — a config written for a later DevCloud still loads — and logs - a warning saying it is ignored. That warning is also your typo check: - `providers.awss` produces it. -- `DEVCLOUD_SERVICES` and `DEVCLOUD_DATA_DIR` are AWS-scoped. There is no - syntax for naming another provider's services, so they cannot silently - disable one. -- AWS data directories stay flat (`/`). Any future provider nests - under its own name (`//`) so two CSPs offering a - same-named service cannot collide. - -### Admin API +### `DEVCLOUD_DATA_DIR` -| Key | Default | Description | -|-----|---------|-------------| -| `admin.enabled` | `false` | Enable the admin REST API at `/devcloud/api/*` | +Every service uses `/`, and per-service `data_dir` +values in YAML are ignored. Useful for CI jobs needing ephemeral per-run +directories, or for relocating state without editing `devcloud.yaml`. -> The `dashboard` key was renamed to `admin`. The old `dashboard.enabled` key is still honoured for one release (with a deprecation warning); migrate to `admin.enabled`. +```bash +DEVCLOUD_DATA_DIR=/tmp/devcloud-local ./dist/devcloud -> SigV4 signature validation is not implemented, so there is no `auth` key. Any -> credentials are accepted. +docker run -p 4747:4747 \ + -e DEVCLOUD_DATA_DIR=/app/data \ + -v $(pwd)/devcloud-data:/app/data \ + ghcr.io/skyoo2003/devcloud:latest +``` -### Logging +### `DEVCLOUD_PORT` -| Key | Default | Description | -|-----|---------|-------------| -| `logging.level` | `info` | Log level: `debug`, `info`, `warn`, `error` | -| `logging.format` | `text` | Log format: `text` or `json` | +```bash +DEVCLOUD_PORT=8080 ./dist/devcloud +docker run -p 8080:8080 -e DEVCLOUD_PORT=8080 ghcr.io/skyoo2003/devcloud:latest +``` -## Full Example +## Configuration file ```yaml server: @@ -113,16 +82,6 @@ services: enabled: true dynamodb: enabled: true - data_dir: ./data/dynamodb - iam: - enabled: true - data_dir: ./data/iam - sts: - enabled: true - data_dir: ./data/sts - lambda: - enabled: true - data_dir: ./data/lambda admin: enabled: false @@ -132,77 +91,65 @@ logging: format: text ``` -## Environment Variable Overrides - -### `DEVCLOUD_PORT` - -Overrides the HTTP server port. - -```bash -# Run on port 8080 instead of 4747 -DEVCLOUD_PORT=8080 ./dist/devcloud - -# With Docker (map the host port accordingly) -docker run -p 8080:8080 -e DEVCLOUD_PORT=8080 ghcr.io/skyoo2003/devcloud:latest -``` - -### `DEVCLOUD_SERVICES` - -Comma-separated list of services to enable. When set, it names the running set outright: **only** the listed services are enabled, all others are disabled regardless of their `enabled` setting in YAML, and a service the `services` block omits entirely still starts if you name it here. The block still supplies that service's `data_dir`. When not set, each service uses its YAML `enabled` value (or the embedded default of `true`). An unknown `tierN` token is treated as a literal service name and logged as a warning. - -**Tier shortcuts** (expand to predefined service groups — see [`internal/config/config.go`](https://github.com/skyoo2003/devcloud/blob/main/internal/config/config.go) for the exact list): - -| Token | Expands to | -|-------|------------| -| `tier1` | Big 6 + core integration: s3, sqs, dynamodb, iam, sts, lambda, sns, kms, secretsmanager, ssm, cloudwatchlogs, cloudwatch, eventbridge, ec2, ecs, ecr, route53, acm | -| `tier2` | Extended services: cognito, elasticloadbalancingv2, ebs, efs, states, apigateway, apigatewayv2, kinesis, firehose, ses, sesv2, rds, cloudformation | -| `tier3` | Analytics & platform: elasticache, cloudfront, wafv2, glue, athena, organizations, cloudtrail, eks, autoscaling, appsync, emr, batch | -| `all` | Disables the env-var filter (all enabled services in the config stay enabled) | - -Examples: - -```bash -# Enable only S3 and SQS -DEVCLOUD_SERVICES=s3,sqs ./dist/devcloud - -# Enable all Tier 1 services -DEVCLOUD_SERVICES=tier1 ./dist/devcloud +| Key | Default | Description | +|-----|---------|-------------| +| `server.port` | `4747` | HTTP server port | +| `services..enabled` | `false` | **Required per entry.** Listing a service is not enough — `enabled: true` still has to be set. | +| `services..data_dir` | `./data/` | Data directory for persistent storage | +| `services.lambda.runtime` | `""` | Lambda runtime configuration | +| `services.lambda.warm_containers` | `0` | Warm containers to keep | +| `services.iam.enforce_policies` | `false` | Enforce IAM policies (experimental) | +| `admin.enabled` | `false` | Serve the admin REST API at `/devcloud/api/*` | +| `logging.level` | `info` | `debug`, `info`, `warn`, `error` | +| `logging.format` | `text` | `text` or `json` | -# Enable Tier 1 + a few extras -DEVCLOUD_SERVICES=tier1,kinesis,firehose ./dist/devcloud +The `services` block is **optional and authoritative**: omit it and every +registered service starts; list any service and *only* the services you list +start. `services: {}` lists nothing, so nothing starts — a block is a block even +when empty. -# With Docker -docker run -p 4747:4747 -e DEVCLOUD_SERVICES=tier1 ghcr.io/skyoo2003/devcloud:latest -``` +> The `dashboard` key was renamed to `admin`. `dashboard.enabled` is still +> honoured for one release with a deprecation warning; migrate to `admin.enabled`. -### `DEVCLOUD_DATA_DIR` +> There is no `auth` key: SigV4 signature validation is not implemented and any +> credentials are accepted. -Overrides the base data directory for **all** services. When set, every service uses `/` — per-service `data_dir` values in YAML are ignored. When not set, each service falls back to its YAML `data_dir` value, or `./data/`. +## Provider namespacing -```bash -# Put all service data under /tmp/devcloud-local -DEVCLOUD_DATA_DIR=/tmp/devcloud-local ./dist/devcloud +DevCloud serves AWS today and is prepared to serve more ([roadmap](roadmap.md)), +so service configuration can be written under an explicit provider: -# With Docker (mount the host directory) -docker run -p 4747:4747 \ - -e DEVCLOUD_DATA_DIR=/app/data \ - -v $(pwd)/devcloud-data:/app/data \ - ghcr.io/skyoo2003/devcloud:latest +```yaml +providers: + aws: + services: + s3: + enabled: true + data_dir: ./data/s3 ``` -Useful for: -- CI jobs that need ephemeral per-run data dirs -- Quickly relocating state without editing `devcloud.yaml` - -## Data Directories - -Services with persistent storage create data under their configured `data_dir`: +`providers.aws.services` and the top-level `services` block are **the same +block** — one under its forward-compatible name, one under its historical one. -| Service | Default `data_dir` | Storage Backend | Contents | -|---------|-------------------|-----------------|----------| +- Write either. The top-level block is not deprecated. +- If both are present, `providers.aws.services` wins and a warning names the + ignored block. Do not write both. +- A block for a provider this build does not serve (`providers.azure`) parses + without error and logs a warning — so a config written for a later DevCloud + still loads, and a typo like `providers.awss` announces itself. +- `DEVCLOUD_SERVICES` and `DEVCLOUD_DATA_DIR` are AWS-scoped. There is no syntax + for naming another provider's services, so they cannot silently disable one. +- AWS data directories stay flat (`/`). A future provider nests under + its own name (`//`) so two CSPs offering a same-named + service cannot collide. + +## Data directories + +| Service | Default `data_dir` | Backend | Contents | +|---------|-------------------|---------|----------| | S3 | `./data/s3` | Filesystem + SQLite | Object files, `metadata.db` | | DynamoDB | `./data/dynamodb` | BadgerDB | BadgerDB data files | | IAM | `./data/iam` | SQLite | `iam.db` (users, roles, keys) | | STS | `./data/sts` | Shared with IAM | Uses IAM's database | -| Lambda | `./data/lambda` | SQLite + Filesystem | `lambda.db`, `code/` directory | +| Lambda | `./data/lambda` | SQLite + Filesystem | `lambda.db`, `code/` | | SQS | — | In-memory | No persistence | diff --git a/docs/contributing.md b/docs/contributing.md index 5090cbfd..217c2c42 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -3,24 +3,20 @@ ## Prerequisites - Go 1.26+ -- Docker (optional, for Lambda runtime and integration tests) -- Python 3.10+ with pip (for compatibility tests) -- `pre-commit` (optional but recommended — see [Code Style](#code-style)) +- Python 3.10+ with pip (compatibility tests) +- Docker (optional — Lambda runtime and integration tests) +- `pre-commit` (optional but recommended) -No C toolchain and no SQLite headers are needed. The SQLite driver is pure Go, -which is why everything here builds with `CGO_ENABLED=0`. +No C toolchain and no SQLite headers: the SQLite driver is pure Go, which is why +everything here builds with `CGO_ENABLED=0`. -## Development Setup +## Setup ```bash git clone https://github.com/skyoo2003/devcloud.git cd devcloud - -# Build Go binaries -make build - -# Run the server -make run +make build # builds dist/devcloud and dist/codegen +make run # starts the server on port 4747 ``` ## Make Targets @@ -45,32 +41,20 @@ make run ## Testing -### Go unit tests - -```bash -make test -``` - -Runs all Go tests with `CGO_ENABLED=0` — the mode `.goreleaser.yaml` publishes -in, so the tests exercise the same build the releases ship. - -### Compatibility tests - ```bash -make test-compat +make test # Go tests, CGO_ENABLED=0 — the same mode releases ship +make test-compat # Python/boto3 suite in tests/compatibility/ ``` -Runs the Python/boto3 tests in `tests/compatibility/` that verify DevCloud behaves like real AWS services. - -You do **not** need a server running first. The `devcloud_server` session -fixture in [`conftest.py`](../tests/compatibility/conftest.py) starts one via -`go run`, on a free port, against a temporary data directory it removes -afterwards. Three environment variables change that: +You do **not** need a server running first. The `devcloud_server` session fixture +in [`conftest.py`](../tests/compatibility/conftest.py) starts one via `go run`, on +a free port, against a temporary data directory it removes afterwards. Three +environment variables change that: | Variable | Effect | |----------|--------| -| `DEVCLOUD_EXTERNAL=1` | Do not start anything — connect to a server already running (e.g. in Docker) | -| `DEVCLOUD_BIN=` | Run a pre-built binary instead of `go run` (much faster) | +| `DEVCLOUD_EXTERNAL=1` | Connect to a server already running (e.g. in Docker) instead of starting one | +| `DEVCLOUD_BIN=` | Run a pre-built binary instead of `go run` — much faster | | `DEVCLOUD_PORT=` | Use a fixed port instead of an arbitrary free one | To run the suite directly: @@ -83,32 +67,30 @@ pytest -v ## Code Generation -DevCloud uses Smithy models to auto-generate Go code. **Never edit files in `internal/generated/`** — they are overwritten by codegen. - -### Run codegen +**Never edit files in `internal/generated/`** — codegen overwrites them. ```bash -# All services -make codegen - -# Single service -make codegen-s3 +make codegen # all services +make codegen-s3 # one service ``` -### Codegen internals +| Piece | Where | +|---|---| +| Models | `smithy-models/*.json` | +| Sources | `internal/codegen/source.go` — `ModelSource` per format; `SmithySource` today | +| Generator | `internal/codegen/generator.go` + `internal/codegen/templates/` | +| Output | `internal/generated/{service}/` — `types.go`, `router.go`, `errors.go`, `base_provider.go` | -- **Models:** `smithy-models/*.json` — AWS Smithy JSON definitions -- **Parser:** `internal/codegen/parser.go` — reads Smithy models -- **Generator:** `internal/codegen/generator.go` — produces Go files using templates in `internal/codegen/templates/` -- **Output:** `internal/generated/{service}/` — types, interface, serializer, deserializer, router, errors, base_provider +Providers parse the raw `*http.Request` themselves, so there is no generated +serializer or deserializer. See [architecture.md](architecture.md#code-generation). ### Reviewing the weekly model sync [`smithy-sync.yml`](../.github/workflows/smithy-sync.yml) refreshes all 194 -vendored models every Monday and opens a pull request. The diff is whole-tree — -a measured refresh moved 93 models and 134 generated files — so do not try to -read it. Read the PR body instead: it is generated by `scripts/model_churn.py` -and lists which services gained or lost operations. +vendored models every Monday and opens a pull request. The diff is whole-tree — a +measured refresh moved 93 models and 134 generated files — so do not try to read +it. Read the PR body instead: it is generated by `scripts/model_churn.py` and +lists which services gained or lost operations. Three things to check, in order: @@ -127,105 +109,70 @@ with a clean `codegen-drift` almost always means item 2. ## Adding a New AWS Service -1. **Add the Smithy model** — place the JSON model file in `smithy-models/` - -2. **Run codegen** — this generates the interface, types, and stubs: - ```bash - make codegen - ``` - -3. **Create the service directory** — `internal/services//` - -4. **Implement the provider** — create `provider.go` implementing methods from the generated interface. Start with the most commonly used operations. Unimplemented operations automatically return `NotImplementedError` via the generated base provider. - -5. **Implement the store** — create `store.go` for the storage backend. Choose based on the service's needs: - - SQLite for relational metadata - - BadgerDB for key-value data - - In-memory for ephemeral data - - Filesystem for binary blobs - -6. **Register the plugin** — register the service factory in the plugin registry from an `init()` function. Follow existing services as examples. The `ServicePlugin` interface contract, error convention, and config keys are documented in [plugin-api.md](plugin-api.md). - -7. **Update main.go** — add the service initialization in `cmd/devcloud/main.go` - -8. **Update config** — add default configuration in `devcloud.yaml` - -9. **Write tests** — unit tests in Go, compatibility tests in Python/boto3 - -10. **Update documentation** — add a service doc in `docs/services/` and update the README services table - -## Project Structure +1. **Add the Smithy model** to `smithy-models/`. +2. **Run `make codegen`** — generates types, router, errors and stubs. +3. **Implement the provider** in `internal/services//provider.go`. Start + with the most commonly used operations; the generated base provider makes + everything else return `NotImplementedError`. +4. **Implement the store** in `store.go` — SQLite for relational metadata, + BadgerDB for key-value, in-memory for ephemeral, filesystem for blobs. +5. **Register the plugin** from an `init()` in `register.go`, and blank-import the + package in [`cmd/devcloud/imports.go`](../cmd/devcloud/imports.go). The + interface contract, error convention and config keys are in + [plugin-api.md](plugin-api.md). +6. **Wire startup and config** — `cmd/devcloud/main.go` and + `internal/config/default.yaml`. +7. **Write tests** — Go unit tests plus boto3 tests in `tests/compatibility/`. +8. **Document it** — a page under `docs/services/` for a core service. ``` internal/ ├── generated/{service}/ # Auto-generated (DO NOT EDIT) │ ├── types.go # Request/response structs -│ ├── interface.go # Service interface -│ ├── serializer.go # Request marshaling -│ ├── deserializer.go # Response unmarshaling │ ├── router.go # Operation routing │ ├── errors.go # Error types │ └── base_provider.go # Stub (NotImplementedError) │ -├── services/{service}/ # Your implementation goes here -│ ├── provider.go # Service logic -│ ├── store.go # Storage backend -│ └── register.go # Plugin registration -``` - -## Docker - -```bash -# Build Docker image -make docker-build - -# Run Docker image -make docker-run +└── services/{service}/ # Your implementation + ├── provider.go # Service logic + ├── store.go # Storage backend + └── register.go # Plugin registration ``` ## Code Style -- Follow standard Go conventions (`gofmt`, `go vet`) -- Keep service implementations focused — one responsibility per file -- Use existing services as patterns for new ones -- Error messages should follow AWS error format (Code, Message, StatusCode) +- Standard Go conventions (`gofmt`, `go vet`) +- One responsibility per file; use existing services as patterns +- Errors follow the AWS shape (Code, Message, StatusCode) — see + [plugin-api.md](plugin-api.md#error-convention) +- New Go files start with `// SPDX-License-Identifier: Apache-2.0`. Generated + files keep the `DO NOT EDIT` marker on line 1 and the SPDX header on line 2. + The `go-spdx-header` pre-commit hook adds it if missing. ### Pre-commit hooks -Install once, and formatting and the fast checks run on every commit: - ```bash pre-commit install pre-commit run --all-files # optional: check the whole tree now ``` -[`.pre-commit-config.yaml`](../.pre-commit-config.yaml) runs, on Go files: -`go-spdx-header` (adds the licence header), `gofmt -l -w`, `go vet`, and -`go build` — the last two with `CGO_ENABLED=0`, so a contributor without a C -toolchain still gets them. Python files under `tests/` and `scripts/` are -linted and formatted by `ruff`. `internal/generated/` and `smithy-models/` are -excluded throughout. +[`.pre-commit-config.yaml`](../.pre-commit-config.yaml) runs `go-spdx-header`, +`gofmt -l -w`, `go vet` and `go build` on Go files — the last two with +`CGO_ENABLED=0`, so a contributor without a C toolchain still gets them. Python +under `tests/` and `scripts/` is handled by `ruff`. `internal/generated/` and +`smithy-models/` are excluded throughout. ### Linting -`gofmt` and `go vet` run in pre-commit, but the full linter does not — CI runs -`golangci-lint` separately ([`lint.yml`](../.github/workflows/lint.yml)). Run -it yourself before opening a PR: +The full linter is not in pre-commit; CI runs it separately +([`lint.yml`](../.github/workflows/lint.yml)). Run it before opening a PR: ```bash golangci-lint run --timeout=5m ``` -## License Header - -All new Go files must include the SPDX license identifier on the first line: - -```go -// SPDX-License-Identifier: Apache-2.0 -``` - -Generated files keep the `// Code generated by devcloud codegen. DO NOT EDIT.` marker on line 1 and the SPDX header on line 2. The `go-spdx-header` pre-commit hook auto-adds the header if missing, so in practice you don't need to manage this manually. - ## License of Contributions -DevCloud is licensed under the Apache License, Version 2.0. By submitting a contribution, you agree that it will be licensed under the same terms. See [LICENSE](../LICENSE) and [NOTICE](../NOTICE) for details. +DevCloud is licensed under the Apache License, Version 2.0. By submitting a +contribution you agree that it will be licensed under the same terms. See +[LICENSE](../LICENSE) and [NOTICE](../NOTICE). diff --git a/docs/coverage.md b/docs/coverage.md index 7869c583..d5f2ad9b 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -1,22 +1,15 @@ # Coverage -DevCloud's service count is not a capability claim on its own, so this page never -states it alone. Three numbers describe the surface, and they mean different -things: - -> **The coverage target is not 100% of AWS.** It was, and the evidence did not -> support it — see [The target, and the rule that set it](#the-target-and-the-rule-that-set-it). +Three numbers describe DevCloud's AWS surface, and they mean different things. +The service count alone is not a capability claim, so this page never states it +alone. | Number | What it means | Today | |---|---|---| -| **Registered** | The gateway routes the service. A call reaches DevCloud instead of falling through to real AWS. | **205** | -| **Serving ≥1 operation** | At least one operation returns a real, store-backed answer — hand-written or served by the generic CRUD engine. | **201** | -| **Registered-only** | Routed, but every operation declines with a clean AWS error. Nothing is served. | **4** | -| **Compatibility-tested** | A boto3 test exercises the service in CI and passes — either serving an operation or declining cleanly. | **203** | - -Every number on this page is asserted against the binary by -`go test ./cmd/devcloud/`. Editing one here without the code moving fails CI, and -so does the reverse. See [Reproducing these numbers](#reproducing-these-numbers). +| **Registered** | The gateway routes the service, so the call reaches DevCloud instead of real AWS. | **205** | +| **Serving ≥1 operation** | At least one operation returns a real, store-backed answer. | **201** | +| **Registered-only** | Routed, but every operation declines with a clean AWS error. | **4** | +| **Compatibility-tested** | A boto3 test exercises the service in CI and passes. | **203** | Per operation, from the [fidelity manifest](fidelity-manifest.md): @@ -27,192 +20,99 @@ Per operation, from the [fidelity manifest](fidelity-manifest.md): | `unimplemented` | 2,717 | | **total known** | **12,407** | -## Why a registered service can serve nothing +> **The coverage target is 205 services, not 431.** It was 431, and the evidence +> did not support it — see [The target](#the-target). -The generic CRUD engine has to know which operation a request is for, and it has -to recognise that operation as CRUD-shaped. Only the second one still stops it. +Every figure on this page is asserted against the binary by +`go test ./cmd/devcloud/`. Editing one here without the code moving fails CI, and +so does the reverse. See [Reproducing these numbers](#reproducing-these-numbers). -**The protocol always says which operation.** Each one says it somewhere -different, and the engine reads all of them: the `X-Amz-Target` JSON protocols -put it in a header; `rest-json` and `rest-xml` bind every operation to an HTTP -method and a URI template, which `internal/shared/httproute` matches a request -back to; `query` puts it in the `Action` field of the form body. This used to be -the main reason a service served nothing. It no longer is. +## Why a registered service can serve nothing -**The operation is not CRUD-shaped.** `GetThing`, `ListThings`, `CreateThing` -and their siblings map onto a generic store. `ExecuteStatement`, `InvokeEndpoint` -and `QueryForecast` do not, and the engine refuses them rather than inventing an -answer. A service whose entire API is that shape serves nothing whatever its -protocol. This is now the *only* reason. +The generic [CRUD engine](crud-engine.md) needs two things: to know which +operation a request is for, and to recognise that operation as CRUD-shaped. Only +the second still stops it. -Registered services by protocol: +**The protocol always says which operation**, and the engine reads every form: -| Protocol | Services | Engine-servable | +| Protocol | Services | Operation name comes from | |---|---|---| -| `json-1.1` | 64 | yes — operation from `X-Amz-Target` | -| `json-1.0` | 17 | yes — operation from `X-Amz-Target` | -| `rest-json` | 93 | yes — operation from method + path | -| `query` | 15 | yes — operation from the `Action` form field | -| `rest-xml` | 4 | yes — operation from method + path | +| `rest-json` | 93 | HTTP method + path (`internal/shared/httproute`) | +| `json-1.1` | 64 | the `X-Amz-Target` header | +| `json-1.0` | 17 | the `X-Amz-Target` header | +| `query` | 15 | the `Action` form field | +| `rest-xml` | 4 | HTTP method + path | | no in-tree model | 12 | n/a — hand-written providers | -Four services are registered and serve nothing, and all four are the same case — -no CRUD-shaped operation anywhere in their API: `forecastquery` (`QueryForecast`, -`QueryWhatIfForecast`), the two SageMaker Runtime variants `sagemaker-runtime` -and `sagemakerruntimehttp2` (`InvokeEndpoint*`), and `rds-data` -(`ExecuteStatement`, `BeginTransaction`, `CommitTransaction`, -`RollbackTransaction`). No protocol change reaches them. - -## Why the compatibility-tested number is 203, not 205 - -Every registered service is exercised by -`tests/compatibility/test_service_smoke.py`, which parametrises over the -generated service list rather than a hand-written one — so a service cannot be -registered and quietly go untested, which is what 31 of them were until this was -measured. Two are excluded, and they are not a backlog. - -**Two have no boto3 client at all.** botocore publishes 431 clients and neither -`sagemakerruntimehttp2` nor `transcribestreaming` is among them — -`sagemaker-runtime` and `transcribe` are different clients with different APIs. -No boto3 test can exist for a client that does not exist. This is a property of -the AWS SDK, not of DevCloud, and it is the ceiling on this number. - -**The four Lex services used to be a third exclusion, and are not any more.** -All four Lex clients — `lex-models`, `lex-runtime`, `lexv2-models`, -`lexv2-runtime` — sign as `lex`, and no service is named `lex`, so the alias is -contested and still routes nowhere on its own. What this page got wrong was the -escape it offered: "reached by its own unambiguous name" does not exist for a -boto3 caller, because boto3 signs with the contested name and offers nothing -else. Measuring it found four services registered, counted as serving, and -reachable by nobody. - -The split that resolved `opensearch` / `elasticsearchservice` and API Gateway -v1 / v2 does work here, and it needed no new routing. Services that share a -signing name are already separated by asking which one's route table models the -request; the group was simply never reached, because the lookup only recognised -a *member* service ID and `lex` is the group's *key*. Each Lex request now -reaches the sibling that models its method and path — `GET /bots` is -`lex-models`, `POST /bots` is `lexv2-models`, `/bot/…/session` is `lex-runtime`, -`/bots/…/botAliases/…/sessions/…` is `lexv2-runtime`. One operation is claimed by -two of them, `DeleteBot` at `DELETE /bots/{id}`, and it is still refused rather -than guessed at: deleting the wrong bot is worse than an honest error. - -Being engine-servable is not the same as being served. Thirteen of the fifteen -`query` services and three of the four `rest-xml` ones have hand-written -providers that answer their own unknown operations, so they never enter the -engine and their manifest did not move when the protocols were admitted — the -`EngineWired` flag records that per service. - -A registered operation is not automatically a reachable one. The engine is -entered only when a provider returns `plugin.ErrUnhandledOp`, so a hand-written -provider that refuses unknown operations itself — `apigatewayv2`, `xray` — never -reaches it, and the manifest records that per service as `EngineWired`. - -Registering a service the engine cannot serve is deliberate, not an oversight. -The alternative is worse: an unregistered service is not routed, so the SDK call -leaves the machine and bills a real AWS account. A registered-only service -answers locally, in AWS's own error vocabulary, and the developer finds out -immediately. What it must never do is fabricate a success — +**The operation is not CRUD-shaped.** `GetThing`, `ListThings` and `CreateThing` +map onto a generic store. `ExecuteStatement`, `InvokeEndpoint` and +`QueryForecast` do not, and the engine refuses them rather than inventing an +answer. This is now the *only* reason a service serves nothing, and it applies to +exactly four: `forecastquery`, the two SageMaker Runtime variants +`sagemaker-runtime` and `sagemakerruntimehttp2`, and `rds-data`. No protocol +change reaches them. + +Registering a service the engine cannot serve is deliberate. The alternative is +worse: an *unregistered* service is not routed, so the SDK call leaves the +machine and bills a real AWS account. A registered-only service answers locally, +in AWS's own error vocabulary. What it must never do is fabricate a success — `tests/compatibility/test_service_smoke.py::test_registered_only_service_declines_cleanly` is the check that keeps that true. -## AI / Machine Learning category +Engine-*servable* is not the same as engine-*served*. The engine is entered only +when a provider returns `plugin.ErrUnhandledOp`, so a hand-written provider that +refuses unknown operations itself (`apigatewayv2`, `xray`) never reaches it. The +manifest records this per service as `EngineWired`. -The first category taken to completion. All 48 upstream models are registered. +## Why compatibility-tested is 203, not 205 -| | Count | -|---|---| -| Registered | 48 | -| Engine-served or hand-written | 45 | -| Registered-only | 3 | - -When this category was completed, only 17 of its 48 members served anything: 30 -of the other 31 were `rest-json`, which the engine could not read at all. Teaching -it that protocol moved 28 of those 30 into coverage without touching a single one -of their providers. +`tests/compatibility/test_service_smoke.py` parametrises over the generated +service list rather than a hand-written one, so a service cannot be registered +and quietly go untested — which is what 31 of them were until this was measured. -The three that remain are the ones no protocol change can help — `forecastquery` -and the two SageMaker Runtime variants, none of which has a CRUD-shaped -operation. See [Why a registered service can serve nothing](#why-a-registered-service-can-serve-nothing). +Two are excluded, and they are not a backlog: botocore publishes no client for +`sagemakerruntimehttp2` or `transcribestreaming` (`sagemaker-runtime` and +`transcribe` are different clients with different APIs). No boto3 test can exist +for a client that does not exist. That is a property of the AWS SDK, and it is +the ceiling on this number. -## The demand set +## Contested signing names -All 57 are registered, and the target of 205 is met. They were worked in the -support-rank order of [demand.md](demand.md), so a stop at any point would have -stopped on the best surface available. +A runtime or control-plane split-out signs with its parent's name, so `sagemaker` +is claimed by eight services at once. Where exactly one claimant carries the name +as its own service ID, that service wins and the rest are borrowers — see +`codegen.BuildAliases`. Where none does, the request is handed to the group and +answered by the sibling whose route table models its method and path. -| | Count | -|---|---| -| Registered | 57 | -| Serving ≥1 operation | 56 | -| Registered-only | 1 | - -The 57 split by protocol as 34 `rest-json`, 15 `json-1.1`, 6 `json-1.0`, 1 -`query`, 1 `rest-xml`. Two thirds of the set is `rest-json`, which is why the -engine gaining that protocol is what made this milestone possible rather than a -matter of arithmetic: without it, 34 of the 57 would have registered and served -nothing. - -The one that does not meet the floor is `rds-data`, and it is the one worth -calling out: it is supported by all three projects in the demand survey — the -strongest signal in the whole set — and it still cannot be served generically, -because not one of its six operations is CRUD-shaped. Breadth does not reach -every service, and saying so is cheaper than a fabricated success. - -`elastic-load-balancing` and `s3-control` were the other two. They were the last -of the demand set because of their protocols, not their APIs, and both are -served now: 18 of ELB's 29 operations and 94 of S3 Control's 97. The eleven and -the three that remain are the not-CRUD-shaped case again — `ConfigureHealthCheck` -and `ApplySecurityGroupsToLoadBalancer` among them, which a Terraform `aws_elb` -resource does call. Breadth is what this milestone bought; depth is still not -claimed. +All four Lex clients sign as `lex` and none is named `lex`, so: `GET /bots` is +`lex-models`, `POST /bots` is `lexv2-models`, `/bot/…/session` is `lex-runtime`, +`/bots/…/botAliases/…/sessions/…` is `lexv2-runtime`. One route is claimed by two +siblings — `DeleteBot` at `DELETE /bots/{id}` — and it is refused rather than +guessed: deleting the wrong bot is worse than an honest error. ## What counts as a service -Upstream `aws-sdk-go-v2/aws-models` publishes 431 model files. That is not 431 -services from a caller's point of view: 12 of the AI/ML category's 48 files are -runtime or control-plane split-outs of another service (`sagemaker-runtime`, -`bedrock-runtime`, `forecastquery`, `personalize-runtime`, the four Lex -services, `bedrock-agentcore-control`, and the SageMaker `*-runtime` variants). - -DevCloud counts **model files**, one registered service each, because that is -what an SDK client selects: `boto3.client("sagemaker-runtime")` is a different -client with a different API from `boto3.client("sagemaker")`. The split-outs are -real choices a caller makes, not packaging artefacts. - -The consequence is visible in routing. A split-out signs with its parent's name, -so `sagemaker` is claimed by eight services at once. Where exactly one claimant -carries the name as its own service ID, that service wins and the rest are -treated as borrowers — see `codegen.BuildAliases`. Where none does, the alias is -left unrouted rather than guessed: all four Lex services sign as `lex` and none -is called `lex`, so `lex` resolves to nothing on its own. - -An unrouted alias is not the same as an unreachable service, and this page once -conflated them — it claimed each Lex service was "reached by its own unambiguous -name", which no boto3 caller has, since boto3 sends the contested name and -nothing else. The name is still not guessed; the request is instead handed to -the group of services that share it, and answered by the one whose route table -models its method and path. Two candidates, or none, and it is refused. - -## The target, and the rule that set it - -**Decided 2026-09-05. The target is 205 services, not 431.** - -DevCloud's roadmap previously aimed at every AWS service AWS publishes — 431 -model files. That target rested on an assumption nobody had tested: that the -services DevCloud does not register are services anyone wants. Before committing -to building ~283 of them, the assumption was tested, and it did not hold. - -**The rule was fixed before the numbers were seen.** Four outcomes were written -down in advance, including one for "the method itself failed", specifically so -the result could not be argued into whichever answer was most convenient once it -arrived. The full evidence is in [demand.md](demand.md), re-derivable with -`python3 scripts/demand_rank.py`. +Upstream publishes 431 model files, and DevCloud counts **model files**, one +registered service each — because that is what an SDK client selects. +`boto3.client("sagemaker-runtime")` is a different client with a different API +from `boto3.client("sagemaker")`. The split-outs are real choices a caller makes, +not packaging artefacts. + +## The target -**What was measured.** For each of the 283 unregistered services, how many of -three independent projects have already built it: `moto` (163 services), -LocalStack (119), `terraform-provider-aws` (273). Each serves a population -DevCloud names as its user, and each only adds a service when someone asks. +**Decided 2026-09-05. The target is 205 services, not 431 — and it is met.** + +The old target was every service AWS publishes. It rested on an assumption nobody +had tested: that the services DevCloud does not register are services anyone +wants. Before committing to building ~283 of them, the assumption was tested +against three independent projects that each only add a service when someone +asks. It did not hold. + +**The rule was fixed before the numbers were seen** — four outcomes written down +in advance, including one for "the method itself failed", specifically so the +result could not be argued into whichever answer was most convenient. Full +evidence in [demand.md](demand.md); re-derive with +`python3 scripts/demand_rank.py`. | Reading | Value | |---|---| @@ -223,15 +123,13 @@ DevCloud names as its user, and each only adds a service when someone asks. | `M` built by none | 115 | | DevCloud's own service requests, all time | **0** | -**The outcome that fired.** The rule kept the 100% target only if ≥60% of `M` -had support ≥ 2, and narrowed to a demand set if ≥100 did. 57 cleared neither -bar, so the pre-registered consequence applies: **the 100% claim is dropped from -the docs before any bulk work, and the published target becomes the demand set.** - -Four fifths of the AWS surface DevCloud does not cover is surface that three -projects — with far more history and staffing than DevCloud — have collectively -declined to build. That is not an accident of their roadmaps. It is what a long -tail looks like. +The rule kept the 100% target only if ≥60% of `M` had support ≥2, and narrowed to +a demand set if ≥100 did. 57 cleared neither bar, so the pre-registered +consequence applied: **the 100% claim is dropped and the published target becomes +the demand set.** All 57 are registered — 56 serve at least one operation, and +`rds-data` is the exception named above. It is supported by all three projects, +the strongest signal in the set, and still cannot be served generically. Breadth +does not reach every service, and saying so is cheaper than a fabricated success. | | Services | |---|---| @@ -239,16 +137,16 @@ tail looks like. | Target: registered + demonstrated demand | **205 — met** | | Explicitly not targeted | 226 | -The 226 are not refused. Any of them can be onboarded when someone asks — the -["Service not supported"](https://github.com/skyoo2003/devcloud/issues/new?template=service_request.yml) -form is that channel, and one report outranks all three proxies, because it is -demand rather than a stand-in for it. What changed is that they are no longer -work DevCloud has promised. +Four fifths of the AWS surface DevCloud does not cover is surface that three +projects with far more history and staffing have collectively declined to build. +That is what a long tail looks like. The 226 are not refused — any of them can be +onboarded when someone asks. What changed is that they are no longer work +DevCloud has promised. **What this verdict is not.** The three sources measure *emulator and provider effort*, not user demand. They are a proxy, chosen because DevCloud had no measurement of its own and the alternative was an unbounded wait. The instrument -below now accrues the real thing. +below accrues the real thing. ## Service not supported? @@ -271,30 +169,29 @@ curl -s localhost:4747/devcloud/api/unrouted | jq . } ``` -Then open a [service request](https://github.com/skyoo2003/devcloud/issues/new?template=service_request.yml) -and paste it. That output is what moves a service from the 226 into the target. +Paste that into a [service request](https://github.com/skyoo2003/devcloud/issues/new?template=service_request.yml). +One report outranks all three proxies, because it is demand rather than a +stand-in for it — and it is what moves a service from the 226 into the target. -**Two ceilings, so the number is not read as more than it is.** +Two ceilings, so the number is not read as more than it is: 1. **It is a floor, not a census.** `gateway.DetectProtocol` classifies a request it cannot identify as `("rest-xml", "s3")`, and S3 is registered — so an - unrecognisable request is routed to S3 rather than counted as a miss. What - this does catch is the case that matters: a real SDK or CLI call to an - unregistered service signs with that service's own name and misses the - registry. Verified with boto3 — `boto3.client("appflow")` is recorded as - `appflow`. + unrecognisable request is routed to S3 rather than counted as a miss. The case + that matters is caught: a real SDK or CLI call to an unregistered service + signs with that service's own name and misses the registry. Verified with + boto3 — `boto3.client("appflow")` is recorded as `appflow`. 2. **A non-zero `droppedServiceIds` means the list is incomplete.** Service IDs come from caller-controlled headers, so the collector caps how many distinct ones it holds and reports both the cap and what it dropped, rather than growing without bound or truncating in silence. -The counts live in memory and reset when the process does. This is a local -development tool; nothing is sent anywhere. +The counts live in memory and reset when the process does. Nothing is sent +anywhere. ## Runtime cost -Measured on an Apple Silicon machine, `CGO_ENABLED=0`, at each step of the -roadmap: +Apple Silicon, `CGO_ENABLED=0`, measured at each step of the roadmap: | | 105 services | 147 services | 205 services | |---|---|---|---| @@ -303,16 +200,14 @@ roadmap: | Service registration | — | — | **49 ms for all 205** | The single-binary, zero-config property holds at the target with room to spare. -Startup does not scale meaningfully with service count because registration is a -map insert per service in `init()`, and the generated type definitions are mostly -dead-code-eliminated by the linker — which is why 58 more services cost 1.8 MiB -of binary. +Startup does not scale meaningfully with service count: registration is a map +insert per service in `init()`, and the generated type definitions are mostly +dead-code-eliminated by the linker, which is why 58 more services cost 1.8 MiB. -Peak RSS did not rise with the service count; it fell. The earlier figures were -taken on a different day and are not a controlled comparison, so read this as -"memory is not the constraint at 205" rather than as a saving. What the two -readings agree on is the shape: memory is dominated by the runtime and the -store, not by how many services are registered. +The RSS readings were taken on different days and are not a controlled +comparison. Read them as "memory is not the constraint at 205" rather than as a +saving — what they agree on is the shape: memory is dominated by the runtime and +the store, not by how many services are registered. ## Keeping up with upstream @@ -332,44 +227,35 @@ them and opens a pull request. What that review costs was measured once, on | Wall-clock to download 194 models | 1 min 53 s | **This is one sample, and it is not one week of churn.** The 93 models that -changed were all vendored on 2026-04-18 — 141 days earlier. The other 101 were -vendored on 2026-09-05, and not one of them changed. So the reading above is an -accumulated backlog, and the only measurement at weekly scale is the second -cohort's: 101 models, one day, zero changes. The weekly rate is still unknown, -and a figure derived from a single 141-day sample should not be quoted as one. +changed were all vendored 141 days earlier; the other 101 were vendored the day +before, and not one of them changed. So the reading is an accumulated backlog, +and the weekly rate is still unknown. What the sample does settle is the *shape* of the work. None of the 93 was documentation-only, so no sync can be waved through on the assumption that AWS only reworded things. Thirty-two services gained operations — `ec2` alone gained -46 — which moves the manifest and makes the published-figure gate below fail on -purpose. That failure *is* the review: the numbers on this page have to be -re-derived, by a person, before the sync can merge. - -The sync PR states which operations moved, so that review reads a change rather -than a regeneration. Re-derive it with -`python3 scripts/model_churn.py --upstream`. +46 — which moves the manifest and makes the published-figure gate fail on +purpose. That failure *is* the review: the numbers here have to be re-derived, by +a person, before the sync can merge. The PR body states which operations moved; +re-derive it with `python3 scripts/model_churn.py --upstream`. ## Reproducing these numbers ```bash -make codegen # regenerate the manifest from the models -make stats # registered services and hand-written operations -go test ./cmd/devcloud/ # asserts every number on this page against the binary -make test-compat # the compatibility-tested number, over all 205 services +make codegen # regenerate the manifest from the models +make stats # registered services and hand-written operations +go test ./cmd/devcloud/ # asserts every number on this page against the binary +make test-compat # the compatibility-tested number, over all 205 services ``` -The service and operation numbers come from -`internal/generated/fidelity/manifest_gen.go` and nothing else, so they cannot -drift from what the binary actually serves — `TestFidelityManifestCoverage` and -`TestFidelityManifestCoversCRUDRegistry` fail if they do. - -They cannot drift from *this page* either. -`TestPublishedCoverageMatchesTheBinary` and -`TestPublishedOperationTiersMatchTheManifest` read the tables above and compare -each figure to the live registry and manifest, in both directions: a service -removed without editing the page fails, and a figure edited here without the -code moving fails identically. `TestRegisteredOnlyServicesAreNamedInTheDocs` -does the same for the depth claim, so a service that starts serving nothing -cannot join that set without being named. `TestDemandSetIsRegistered` checks the -target itself — all 57 services in [demand.md](demand.md) are still registered, -so the count cannot be held steady by swapping one out for something else. +Every figure comes from `internal/generated/fidelity/manifest_gen.go` and nothing +else, so it cannot drift from what the binary serves. It cannot drift from *this +page* either — five tests compare the two, in both directions: + +| Test | Gates | +|---|---| +| `TestPublishedCoverageMatchesTheBinary` | the three service counts | +| `TestPublishedOperationTiersMatchTheManifest` | the four operation tiers | +| `TestRegisteredOnlyServicesAreNamedInTheDocs` | that a service serving nothing is named here | +| `TestOtherDocsQuoteTheSameFigure` | that `README.md` and `docs/README.md` agree | +| `TestDemandSetIsRegistered` | that all 57 demand-set services are still registered | diff --git a/docs/crud-engine.md b/docs/crud-engine.md index 18a445ab..60199540 100644 --- a/docs/crud-engine.md +++ b/docs/crud-engine.md @@ -2,138 +2,99 @@ DevCloud auto-serves standard CRUD-shaped operations that a service's hand-written provider has not implemented, using a generic engine driven by the Smithy models. -This lets the long tail of ~4,900 CRUD-shaped operations respond to SDK calls -without hand-coding each one. +This is what lets the long tail respond to SDK calls without hand-coding each one. **Fidelity is deliberately "plausible, not faithful."** Responses are store-backed -and echo the caller's input plus synthesized ids/ARNs, so SDKs round-trip -(create → get → list → delete). There is **no** validation, cross-resource +and echo the caller's input plus synthesized ids and ARNs, so SDKs round-trip +create → get → list → delete. There is **no** validation, cross-resource integrity, pagination correctness, or business logic. Treat engine-served operations as scaffolding for local wiring, not as behavioural parity. +Operation counts are published in [coverage.md](coverage.md); which tier any given +operation carries is in the [fidelity manifest](fidelity-manifest.md). + ## How it works -- **Engine** — [`internal/shared/crud`](../internal/shared/crud/crud.go): an - in-memory resource store plus verb dispatch (Create/Get/List/Delete/Update/…). -- **Classification** — codegen inspects each operation's name and output shape - (`internal/codegen/gen_crud_meta.go`) and emits one aggregate registry, - `internal/generated/crudregistry/registry_gen.go`, whose `init()` registers - every classifiable operation. Regenerated by `make codegen`. -- **Integration** — a provider opts in by returning `plugin.ErrUnhandledOp` from - its dispatch `default:` case. The gateway - ([`internal/gateway/router.go`](../internal/gateway/router.go)) then calls the - engine; if the engine cannot classify the operation, the standard - `InvalidAction` error is returned instead. **Never a fabricated success for an - unclassifiable op.** +| Piece | Where | What it does | +|---|---|---| +| Engine | [`internal/shared/crud`](../internal/shared/crud/crud.go) | In-memory resource store plus verb dispatch (Create/Get/List/Delete/Update/…) | +| Classification | `internal/codegen/gen_crud_meta.go` | Inspects each operation's name and output shape, emits `internal/generated/crudregistry/registry_gen.go` whose `init()` registers every classifiable operation | +| Integration | [`internal/gateway/router.go`](../internal/gateway/router.go) | Calls the engine when a provider returns `plugin.ErrUnhandledOp`; returns `InvalidAction` if the engine cannot classify the operation | + +A provider opts in by returning `plugin.ErrUnhandledOp` from its dispatch +`default:` case. Hand-written operations always win — the engine is reached only +on fall-through, so it never shadows a real implementation. **Never a fabricated +success for an unclassifiable op.** ## Which protocols it can serve -The engine has to know which operation a request is for before it can classify -it. Where that name lives depends on the protocol: +The engine has to know which operation a request is for before it can classify it. -| Protocol | Operation comes from | Served | +| Protocol | Operation name comes from | Served | |---|---|---| | `json-1.0`, `json-1.1` | the `X-Amz-Target` header | yes | -| `rest-json` | the request method and path, matched against the model's URI templates (`internal/shared/httproute`) | yes | -| `rest-xml` | the same — every `restXml` operation is bound to a method and URI too | yes | +| `rest-json` | method + path, matched against the model's URI templates (`internal/shared/httproute`) | yes | +| `rest-xml` | the same — every `restXml` operation binds to a method and URI | yes | | `query` | the `Action` field of the form body | yes | | `ec2-query` | — | no | -Every protocol DevCloud registers is now readable, so a service that serves -nothing does so because none of its operations is CRUD-shaped, not because of -how it talks. `ec2-query` is the one exception and it is not a gap in practice: -the only service that speaks it is EC2, whose provider is hand-written and never -reaches the engine. +Every protocol DevCloud registers is readable, so a service that serves nothing +does so because none of its operations is CRUD-shaped, not because of how it +talks. `ec2-query` is the one exception and not a gap in practice: only EC2 speaks +it, and EC2's provider is hand-written and never reaches the engine. ### Where parameters come from -For the two REST protocols, three places, least authoritative first: values the -model binds with `httpQuery`, then the request body, then the path labels. The -URI is what addresses the resource, so a path label wins. A REST model never -binds one member to two of these, so real SDK traffic never exercises the -precedence; it is defined so a hand-rolled request cannot redirect a lookup. +For the two REST protocols, three places — least authoritative first: values the +model binds with `httpQuery`, then the request body, then the path labels. The URI +addresses the resource, so a path label wins. A REST model never binds one member +to two of these, so real SDK traffic never exercises the precedence; it is defined +so a hand-rolled request cannot redirect a lookup. -For `query`, the form body, flat keys only. `Action` and `Version` are dropped: -they describe the request rather than the resource, and storing them would echo +For `query`, the form body, flat keys only. `Action` and `Version` are dropped — +they describe the request, not the resource, and storing them would echo `CreateLoadBalancer` back inside a result element. A structured member arrives flattened as `Listeners.member.1.Protocol`; the engine has no nested shape to put it in, so it is not emitted in the response. -**`rest-xml` request bodies are never read at all.** The gateway does not buffer -them — S3 speaks `rest-xml` and its bodies are multi-gigabyte uploads that must -keep streaming — so a `rest-xml` operation is served from its path and query -alone. Every CRUD-shaped S3 Control operation addresses its resource that way, -so nothing is lost; an operation that carried its identifier only in a body -would get a generated id. +**`rest-xml` request bodies are never read.** The gateway does not buffer them — +S3 speaks `rest-xml` and its bodies are multi-gigabyte uploads that must keep +streaming — so a `rest-xml` operation is served from its path and query alone. +Every CRUD-shaped S3 Control operation addresses its resource that way, so nothing +is lost; an operation carrying its identifier only in a body would get a generated +id. The engine does **not** read `httpHeader` members, `httpPayload` blobs, or -streaming bodies. An operation whose identifier arrives only in a header is -therefore served with a generated id rather than the caller's — plausible, not -faithful, which is the engine's stated contract. +streaming bodies. An operation whose identifier arrives only in a header is served +with a generated id rather than the caller's — plausible, not faithful, which is +the stated contract. ### Responses -The JSON protocols get a JSON body. `query` and `rest-xml` get XML, and they do -not share an envelope: botocore's query parser looks for `` -nested inside `` and, given anything else, returns a result -with nothing in it rather than an error, while its `rest-xml` parser maps the -root element's children straight onto the output shape. List entries are wrapped -in ``, which is AWS's default for both dialects; a model that flattens a -list gets the unflattened form, because the engine has no flattening -information. No `xmlns` is emitted — botocore strips namespaces before matching -element names. +JSON protocols get a JSON body. `query` and `rest-xml` get XML, and they do not +share an envelope: botocore's query parser looks for `` nested +inside `` and, given anything else, returns an empty result +rather than an error, while its `rest-xml` parser maps the root element's children +straight onto the output shape. List entries are wrapped in ``, AWS's +default for both dialects; a model that flattens a list gets the unflattened form, +because the engine has no flattening information. No `xmlns` is emitted — botocore +strips namespaces before matching element names. ### Declining A request whose method and path match no route in the service's table, or whose `Action` names an operation the service did not register, is **declined** with `InvalidAction` and never served from the store. That is what keeps a registered -service from answering for operations it does not model — and it matters most -for `rest-xml`, because `DetectProtocol` routes anything it cannot classify to -S3. - -## Fidelity tiers - -| Tier | Meaning | -|------|---------| -| **hand-verified** | Implemented by the service's provider (explicit dispatch `case`). Highest fidelity. | -| **auto-crud** | Served by the engine with plausible, store-backed responses. Reaches the engine only for engine-wired services (below). | -| **unimplemented** | Not implemented and not CRUD-classifiable — the call is refused, never faked. JSON and Query services return `InvalidAction`; the path-routed providers (`s3`, `lambda`, `bedrock`) use their own error vocabulary. | - -Hand-written operations always win: the engine is only reached when a request -falls through to the provider's `default:` case, so it never shadows a real -implementation. - -## Scope - -- **JSON protocols only** (`json-1.0` / `json-1.1`). These carry the operation - name in `X-Amz-Target`, which the engine needs. Query/REST-XML services (EC2, - S3, Route53, IAM, RDS, CloudFormation) are hand-written and out of scope. -- **Engine-registered services**: 46 (all JSON-protocol services with - classifiable operations), covering ~2,200 operations. -- **Engine-wired services**: all 46 registered JSON services are wired — their - dispatch `default:` returns `plugin.ErrUnhandledOp`, so unimplemented CRUD ops - are engine-served. This **includes core services** (DynamoDB, SQS, KMS, ECS, - …): they hand-implement their common operations, and only their few remaining - unimplemented CRUD ops fall through to the engine. Non-CRUD / unclassifiable - ops still return an honest `InvalidAction` error. +service from answering for operations it does not model — and it matters most for +`rest-xml`, because `DetectProtocol` routes anything it cannot classify to S3. ## Known limits - `List*` responses return stored objects; when the real AWS output member is a - list of *names* (strings) rather than structures, an SDK may not populate it. + list of *names* rather than structures, an SDK may not populate it. - No required-parameter validation, so calls succeed with minimal input. -- The store is in-memory and per-process (not persisted across restarts). +- The store is in-memory and per-process — not persisted across restarts. To promote an operation from `auto-crud` to `hand-verified`, implement it as an -explicit `case` in the service provider following existing patterns. - -## Which tier is *this* operation? - -The tiers above are declared per operation by the generated -[fidelity manifest](fidelity-manifest.md). Ask it directly rather than inferring -from this page (requires `admin.enabled: true`): - -```bash -curl -s 'localhost:4747/devcloud/api/fidelity?service=dynamodb' -``` +explicit `case` in the service provider, following existing patterns. See +[fidelity-manifest.md](fidelity-manifest.md#getting-an-operation-promoted). diff --git a/docs/demand.md b/docs/demand.md index 344e7ebb..ee43f43b 100644 --- a/docs/demand.md +++ b/docs/demand.md @@ -1,35 +1,37 @@ # Demand for unregistered services -Sampled 2026-09-05. Re-derive with `python3 scripts/demand_rank.py`. +The evidence behind DevCloud's [coverage target](coverage.md#the-target). +Sampled 2026-09-05; re-derive with `python3 scripts/demand_rank.py`. -## What this measures, and what it does not +## What this measures -DevCloud has no usage telemetry, so this page does **not** measure what -DevCloud's users ask for. It measures *revealed* demand: which of the AWS -services DevCloud does not register have already been built by three -independent projects serving the same populations DevCloud names as its -users. +DevCloud has no usage telemetry, so this page does **not** measure what its users +ask for. It measures *revealed* demand: how many of three independent projects +have already built each AWS service DevCloud does not register. Each serves a +population DevCloud names as its own, and each only adds a service when somebody +asks. -| Source | What it is | Services | -|---|---|---| -| [moto](https://github.com/getmoto/moto) | Python AWS mocking library; the same population as DevCloud's primary user, boto3 developers running tests | 163 | -| [LocalStack](https://docs.localstack.cloud/references/coverage/) | Local AWS emulator; community and pro tiers combined | 119 | -| [terraform-provider-aws](https://github.com/hashicorp/terraform-provider-aws) | One Go package per AWS service; the IaC population | 273 | +| Source | What it is | Services | Unmatched names | +|---|---|---|---| +| [moto](https://github.com/getmoto/moto) | Python AWS mocking library — boto3 developers running tests | 163 | 0 | +| [LocalStack](https://docs.localstack.cloud/references/coverage/) | Local AWS emulator; community + pro | 119 | 4 | +| [terraform-provider-aws](https://github.com/hashicorp/terraform-provider-aws) | One Go package per service — the IaC population | 273 | 4 | -Each of those projects adds a service because somebody asked for it. None -of them measured DevCloud's users. Read a high support count as *this -service is worth emulating to someone*, not as *our users want this*. +Read a high support count as *this service is worth emulating to someone*, not as +*our users want this*. The live measurement of DevCloud's own traffic is +`GET /devcloud/api/unrouted`, which accrues behind this page; its ceiling is in +[coverage.md](coverage.md#service-not-supported). -The live measurement of DevCloud's own traffic is -`GET /devcloud/api/unrouted`, which accrues behind this page. Its ceiling -is documented in [coverage.md](coverage.md). +"Unmatched names" is the join diagnostic: a name a source publishes that matches +neither the missing set nor a registered service. A large number there would mean +name normalisation is dropping matches and the support counts are too low. ## Readings | Reading | Value | |---|---| | Upstream model files | 431 | -| Registered by DevCloud | 148 | +| Registered when sampled | 148 | | **R1 — missing set `M`** | **283** | | R2 — `M` with support 3 | 8 | | R2 — `M` with support 2 | 49 | @@ -37,28 +39,16 @@ is documented in [coverage.md](coverage.md). | R2 — `M` with support 0 | 115 | | **R2 — `M` with support ≥ 2** | **57 (20.1% of `M`)** | -Join diagnostics — a name a source publishes that matches neither `M` nor a -registered DevCloud service. A large number here means the name -normalisation is dropping matches and the support counts are too low: +## Full ranking -| Source | Unmatched names | -|---|---| -| moto | 0 | -| LocalStack | 4 | -| terraform-provider-aws | 4 | - -## Ranking - -Ordered by support count, then name. This is the order Milestone 4 worked -in, so stopping at any point would have stopped on the best surface available. +Ordered by support count, then name — the order the work was done in, so stopping +at any point would have stopped on the best surface available. -**All 57 services with support ≥ 2 are now registered** — the rows down to and -including `workspaces-web`, and `TestDemandSetIsRegistered` fails if any of them -stops being. 56 of them serve at least one operation; the one that does not -(`rds-data`) is named with its reason in [coverage.md](coverage.md). -`elastic-load-balancing` and `s3-control` were the other two until Milestone 5 -served both. The rows below them, support 1 and support 0, are the 226 services -that remain explicitly not targeted. +**All 57 services with support ≥ 2 are registered**, down to and including +`workspaces-web`; `TestDemandSetIsRegistered` fails if any of them stops being. +56 serve at least one operation, and the one that does not (`rds-data`) is named +with its reason in [coverage.md](coverage.md). Everything below them — support 1 +and support 0 — is the 226 services that remain explicitly not targeted. | Service | moto | LocalStack | terraform-provider-aws | Support | |---|---|---|---|---| diff --git a/docs/faq.md b/docs/faq.md index eb52f537..92397928 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,74 +1,58 @@ -# Frequently Asked Questions +# FAQ ## General -### What is DevCloud? +**What is DevCloud?** +A local, API-compatible cloud environment for inner-loop development. Run it on your laptop or in CI and point your AWS SDKs, CLI or Terraform at it instead of real AWS. -A local, API-compatible cloud environment for inner-loop development. You run it on your laptop or in CI, and your AWS SDKs / CLI / Terraform talk to it instead of real AWS. See [README.md](../README.md). +**Is it a production service?** +No. It is a local development tool, not designed or tested for production, and should not be exposed to untrusted networks. See [SECURITY.md](../SECURITY.md). -### Is DevCloud a production service? +**How is it different from other local cloud emulators?** +DevCloud positions itself as an *on-ramp* rather than a replacement: -No. DevCloud is a local development tool. It is **not** designed or tested for production use, and you should not expose it to untrusted networks. See [SECURITY.md](../SECURITY.md). +- Go single binary — starts in under a second, no JVM or Python runtime +- Smithy-driven codegen from official AWS models, with a weekly sync +- Multi-CSP vision — AWS today, Azure and GCP planned ([roadmap](roadmap.md)) +- Apache 2.0 with an explicit patent grant -### How is this different from other local cloud emulators? +It does not aim to displace any existing tool. Use whatever works for your workflow. -DevCloud positions itself as an **on-ramp** rather than a replacement. Key differentiators: - -- **Go-based single binary** — starts in under a second, no JVM or Python runtime. -- **Smithy-driven codegen** — 101 AWS services scaffolded from official models, weekly auto-sync. -- **Multi-CSP vision** — AWS today, Azure and GCP planned; see [roadmap.md](roadmap.md). -- **Apache 2.0 licensed** with explicit patent grant. - -DevCloud does not aim to displace any existing tool. Use whatever works for your workflow. - -### Why "on-ramp, not replacement"? - -Developers should be able to iterate locally without cloud bills and deploy to their target CSP with confidence. DevCloud's compatibility targets the SDK surface, not the full behavioral model of real cloud providers. Code that works against DevCloud should be "close enough" that the remaining gap is caught by a staging environment. +**Why "on-ramp, not replacement"?** +Compatibility targets the SDK surface, not the full behavioural model of a real cloud. Code that works against DevCloud should be close enough that a staging environment catches the rest. ## Running DevCloud -### Which services are supported? - -148 AWS services are registered and routed, 117 of them serving at least one operation; coverage depth varies. See [services-matrix.md](services-matrix.md) for the canonical list and per-service operation counts. Core services (S3, SQS, DynamoDB, Lambda, IAM, STS, SNS, CloudWatch, KMS, Secrets Manager, EventBridge, CloudFormation) pass 100% of their boto3 compatibility tests. - -### Does DevCloud work on Windows? - -DevCloud is developed and tested on Linux and macOS. Windows support via WSL2 is expected to work but is not part of the CI matrix. Native Windows is currently unsupported. Contributions welcome. - -### Can I run DevCloud in CI? +**Which services are supported?** +See [coverage.md](coverage.md) for the counts and what they promise, and [services-matrix.md](services-matrix.md) for depth by group. Per-operation depth is queryable at runtime via the [fidelity manifest](fidelity-manifest.md). -Yes. A typical pattern is to start DevCloud as a service container in your CI job and point your tests at `http://localhost:4747`. The Docker image starts in well under a second and needs no external dependencies. +**Does it work on Windows?** +Developed and tested on Linux and macOS. WSL2 is expected to work but is not in the CI matrix; native Windows is unsupported. Contributions welcome. -### Does DevCloud persist data across restarts? +**Can I run it in CI?** +Yes — start DevCloud as a service container and point your tests at `http://localhost:4747`. The image starts in well under a second and needs no external dependencies. -Yes, for services with persistent backends (S3, DynamoDB, Lambda, IAM/STS). Data is written under each service's `data_dir`. SQS is in-memory only and loses state on restart. - -See [configuration.md](configuration.md) for data directory options. +**Does data persist across restarts?** +For services with a persistent backend (S3, DynamoDB, Lambda, IAM/STS), yes — under each service's `data_dir`. SQS is in-memory and loses state. See [configuration.md](configuration.md#data-directories). ## Compatibility -### Will my existing boto3 code work? - -~96% of boto3's official SDK test suite passes against DevCloud (run `make test-compat` for the current rate). Most apps that use the Big 6 (S3, SQS, DynamoDB, Lambda, IAM, STS) and common integration services (SNS, CloudWatch, KMS, Secrets Manager, EventBridge, CloudFormation) will work with only an `endpoint_url` change. - -### What about Terraform / CDK? - -Point the AWS provider / CDK at `http://localhost:4747` and set dummy credentials. Most `resource "aws_s3_bucket"`, `aws_dynamodb_table`, `aws_lambda_function` resources work out of the box. Complex IAM policies and deeply CSP-coupled resources (e.g., ACM certificates for real domains) are out of scope. +**Will my existing boto3 code work?** +Most apps using the core services (S3, SQS, DynamoDB, Lambda, IAM, STS) and common integration services (SNS, CloudWatch, KMS, Secrets Manager, EventBridge, CloudFormation) work with only an `endpoint_url` change. The 1,144-test boto3 suite runs green in CI on every push; what that does and does not promise is spelled out in [compatibility-policy.md](compatibility-policy.md#wire-behaviour--scoped-to-the-compatibility-suite). -### Does DevCloud enforce IAM policies? +**What about Terraform / CDK?** +Point the AWS provider or CDK at `http://localhost:4747` with dummy credentials. Common resources (`aws_s3_bucket`, `aws_dynamodb_table`, `aws_lambda_function`) work out of the box. Complex IAM policies and deeply CSP-coupled resources are out of scope. -Not by default. IAM accepts policy documents for roundtrip compatibility but does not evaluate them when handling requests. You can enable experimental enforcement with `services.iam.enforce_policies: true`. See [configuration.md](configuration.md). +**Does it enforce IAM policies?** +Not by default. IAM accepts policy documents for round-trip compatibility but does not evaluate them. Experimental enforcement: `services.iam.enforce_policies: true`. ## Contributing -### I want to add a service. Where do I start? - -See the "Adding a New AWS Service" section in [contributing.md](contributing.md). Short version: add the Smithy model, run `make codegen`, implement the provider and store, register the plugin. - -### Will you accept my PR for service X? - -We prioritize services that (a) appear in the Big 6 / Tier 1-3 lists, (b) are requested by multiple users, or (c) come with both implementation and boto3 tests. Early-stage services that fail compatibility tests will be accepted as scaffolds with `NotImplementedError` stubs. +**I want to add a service. Where do I start?** +[contributing.md](contributing.md#adding-a-new-aws-service). Short version: add the Smithy model, run `make codegen`, implement the provider and store, register the plugin. -### What's the licensing policy for contributions? +**Will you accept my PR for service X?** +Priority goes to services that are in the core/integration set, have [demonstrated demand](demand.md), or arrive with both an implementation and boto3 tests. Early-stage services are accepted as scaffolds that decline cleanly. -All contributions are accepted under Apache 2.0. See [CONTRIBUTING.md](../CONTRIBUTING.md). +**What is the licensing policy for contributions?** +Apache 2.0, same as the project. See [CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/docs/fidelity-manifest.md b/docs/fidelity-manifest.md index 63fb530d..461f1f4b 100644 --- a/docs/fidelity-manifest.md +++ b/docs/fidelity-manifest.md @@ -1,69 +1,46 @@ # Fidelity Manifest -DevCloud registers 148 AWS services, but not every operation is served the same -way. The **fidelity manifest** answers, per operation, *how much you can trust -this call* — so you never have to guess whether a green response means DevCloud -implemented the operation or merely made something plausible up. +Not every operation is served the same way. The **fidelity manifest** answers, per +operation, *how much you can trust this call* — so a green response never leaves +you guessing whether DevCloud implemented the operation or made something +plausible up. It is generated, never hand-written: `make codegen` derives it from the in-tree -Smithy models, the CRUD registry, and each provider's dispatch code. +Smithy models, the CRUD registry, and each provider's dispatch code. Current +totals are published in [coverage.md](coverage.md). ## Tiers | Tier | Meaning | Trust it for | |------|---------|--------------| -| `hand-verified` | The service's provider implements the operation explicitly. | Behaviour, not just shape. Covered by the boto3 compatibility suite where tests exist. | -| `auto-crud` | Served by the [generic CRUD engine](crud-engine.md) with plausible, store-backed responses. No validation, no business logic, no cross-resource integrity. | Wiring your SDK calls and round-tripping create → get → list → delete. Nothing else. | -| `unimplemented` | Not served — the call fails instead of inventing a success. The error is whichever the handling provider emits, and that varies (see below). | Knowing early that DevCloud will not serve this call. | +| `hand-verified` | The service's provider implements the operation explicitly. | Behaviour, not just shape. Covered by the boto3 suite where tests exist. | +| `auto-crud` | Served by the [generic CRUD engine](crud-engine.md): store-backed, plausible responses with no validation, business logic, or cross-resource integrity. | Wiring your SDK up and round-tripping create → get → list → delete. Nothing else. | +| `unimplemented` | Not served — the call fails instead of inventing a success. | Knowing early that DevCloud will not serve this call. | `hand-verified` always wins: the CRUD engine is reached only when a provider's dispatch falls through, so a hand-written implementation is never shadowed. -### What an `unimplemented` call actually returns +## What an `unimplemented` call returns -There is no single error. Which one you get depends on how the owning provider -declines the operation: +There is no single error. It depends on how the owning provider declines: | How the provider declines | Error | Status | Providers | |---|---|---|---| -| Returns `ErrUnhandledOp`, and the CRUD engine cannot classify the operation either — [`gateway/router.go`](../internal/gateway/router.go) emits the fallback | `InvalidAction` | 400 | 46 | -| Its dispatch `default:` answers directly | `NotImplemented` | 501 | 33 | +| Returns `ErrUnhandledOp`, and the CRUD engine cannot classify the operation either — [`gateway/router.go`](../internal/gateway/router.go) emits the fallback | `InvalidAction` | 400 | 155 | +| Its dispatch `default:` answers directly | `NotImplemented` | 501 | 32 | | Its dispatch `default:` answers in its own vocabulary | `UnsupportedOperation` / `MethodNotAllowed` | 400 / 405 | `iot`, `iotwireless`, `apigatewayv2`, `backup`, `bedrock`, `s3` | -A service can even answer differently per protocol: `sqs` returns -`NotImplemented` (501) on the Query protocol and `InvalidAction` (400) on JSON. +A service can even differ by protocol: `sqs` returns `NotImplemented` (501) on +Query and `InvalidAction` (400) on JSON. Only the *failure* is stable, and that is all [compatibility-policy.md](compatibility-policy.md) promises — an `unimplemented` operation never fabricates a success. The specific code and status are not -guaranteed across 1.x; normalizing them is a minor release. - -## Current coverage - -| Tier | Operations | -|------|-----------:| -| `hand-verified` | 4,496 | -| `auto-crud` | 1,415 | -| `unimplemented` | 3,119 | -| **Total** | **9,030** across 148 services | - -Examples: - -| Service | hand-verified | auto-crud | unimplemented | -|---------|--------------:|----------:|--------------:| -| sqs | 23 | 0 | 0 | -| ecs | 57 | 22 | 3 | -| s3 | 37 | 0 | 70 | -| lambda | 25 | 0 | 60 | -| dynamodb | 20 | 28 | 9 | -| cloudwatch | 23 | 17 | 6 | -| bedrock | 19 | 0 | 84 | - -_Regenerate with `make codegen`; these numbers change with the surface._ +guaranteed across 1.x; normalizing them would be a minor release. ## Reading it -**At runtime** — the admin API, which is off by default. Enable it in your +**At runtime** — via the admin API, which is off by default. Enable it in `devcloud.yaml` ([configuration](configuration.md)): ```yaml @@ -72,11 +49,8 @@ admin: ``` ```bash -# Tier counts for every service -curl -s localhost:4747/devcloud/api/fidelity - -# Per-operation tiers for one service -curl -s 'localhost:4747/devcloud/api/fidelity?service=s3' +curl -s localhost:4747/devcloud/api/fidelity # tier counts, every service +curl -s 'localhost:4747/devcloud/api/fidelity?service=s3' # per-operation, one service ``` ```json @@ -89,7 +63,8 @@ curl -s 'localhost:4747/devcloud/api/fidelity?service=s3' } ``` -The unfiltered response omits `operations` — the full manifest is ~7,000 entries. +The unfiltered response omits `operations` — the full manifest runs to five +figures. **In Go** — [`internal/generated/fidelity`](../internal/generated/fidelity/manifest_gen.go): @@ -98,63 +73,60 @@ tier, ok := fidelity.Lookup("s3", "PutObject") // TierHandVerified, true ``` `ok` is false for a service or operation DevCloud does not know at all, which is -not the same as `unimplemented`: an unknown service is not routed at all, -whereas an unimplemented operation reaches its provider and is refused. - -## Limits - -- **11 services have no in-tree Smithy model** (`account`, `cloudcontrol`, - `codeconnections`, `dms`, `identitystore`, `mediaconvert`, `pipes`, `s3tables`, - `scheduler`, `serverlessrepo`, `verifiedpermissions`). Their `modelBacked` flag - is `false` and the manifest lists only what DevCloud serves — the - unimplemented tail is not enumerable without a model. -- **`hand-verified` means "the provider dispatches this operation"**, not "this - operation matches AWS byte for byte". Depth varies; the boto3 compatibility - suite (`make test-compat`) is the stronger signal for the services it covers. -- The manifest describes the **whole registered surface**, not the services - enabled in your config. +not the same as `unimplemented`: an unknown service is not routed, whereas an +unimplemented operation reaches its provider and is refused. ## How it is derived | Input | Source | Contributes | |-------|--------|-------------| -| Operation universe | `smithy-models/*.json`, parsed including resource-attached operations | every known operation | +| Operation universe | `smithy-models/*.json`, including resource-attached operations | every known operation | | `auto-crud` | the generated [CRUD registry](crud-engine.md) | engine-servable operations | -| `hand-verified` | the case literals of the dispatch switches inside each provider's `HandleRequest` | implemented operations | +| `hand-verified` | the case literals of each provider's `HandleRequest` dispatch | implemented operations | -Three providers (`s3`, `lambda`, `bedrock`) route on HTTP method and path rather -than an operation name, so they declare their operations explicitly in -`pathRoutedOps` (`internal/codegen/scan_handverified.go`). +Three providers (`s3`, `lambda`, `bedrock`) route on method and path rather than +an operation name, so they declare their operations explicitly in `pathRoutedOps` +(`internal/codegen/scan_handverified.go`). -The universe is *model ∪ hand-verified*: a provider may serve an operation the +The universe is *model ∪ hand-verified*. A provider may serve an operation its model does not declare — `bedrock` serves `InvokeModel`, which AWS models under bedrock-runtime, and `dynamodbstreams` dispatches 22 operations against a -4-operation model — and hiding them would understate what DevCloud does. So a +4-operation model — and hiding those would understate what DevCloud does. So a `hand-verified` entry is a statement about **DevCloud**, not about AWS: it can name an operation your SDK has never heard of. -The scan is scoped to `HandleRequest` (and whatever it delegates dispatch to) -for a reason. That is the only place an operation name means "operation": -elsewhere in the same package, `identitystore` switches on `"DisplayName"` to -apply an attribute patch and `pipes` switches on `"POST"` to resolve a path. -Reading the whole package would file both as operations. +The scan is scoped to `HandleRequest` and whatever it delegates dispatch to, +because that is the only place an operation name means "operation". Elsewhere in +the same package `identitystore` switches on `"DisplayName"` to apply an +attribute patch and `pipes` switches on `"POST"` to resolve a path; reading the +whole package would file both as operations. -## Getting an operation promoted +## Limits + +- **11 services have no in-tree Smithy model** — `account`, `cloudcontrol`, + `codeconnections`, `dms`, `identitystore`, `mediaconvert`, `pipes`, `s3tables`, + `scheduler`, `serverlessrepo`, `verifiedpermissions`. Their `modelBacked` flag + is `false` and the manifest lists only what DevCloud serves; the unimplemented + tail is not enumerable without a model. +- **`hand-verified` means "the provider dispatches this operation"**, not "this + matches AWS byte for byte". Depth varies; `make test-compat` is the stronger + signal for the services it covers. +- The manifest describes the **whole registered surface**, not the services + enabled in your config. -Promotion from `auto-crud` (or `unimplemented`) to `hand-verified` happens **on request, -with a use case** — [open a feature request](https://github.com/skyoo2003/devcloud/issues/new?template=feature_request.yml) -naming the service, the operation, and what you are trying to run locally. Check the tier -first with the endpoint above so the request is concrete. +## Getting an operation promoted -Requests beat guesswork here. Reading what the unpromoted operations *are* shows why: they -are overwhelmingly operational surface — DynamoDB backups, global tables and Contributor -Insights; KMS custom key stores and key rotation; CloudWatch Insight Rules and Metric -Streams; ECR pull-through cache and registry policy. A local inner loop does not call them, -and the operations it does call — S3, SQS, Lambda, IAM and STS in full — are already -`hand-verified` with **zero** `auto-crud` operations between them. +Promotion to `hand-verified` happens **on request, with a use case** — +[open a feature request](https://github.com/skyoo2003/devcloud/issues/new?template=feature_request.yml) +naming the service, the operation, and what you are trying to run locally. Check +the tier first with the endpoint above so the request is concrete. -So v1.0 ships the long tail **declared rather than implemented**, and lets real use decide -what gets promoted in 1.x. +Requests beat guesswork. The unpromoted operations are overwhelmingly operational +surface — DynamoDB backups and global tables, KMS custom key stores and rotation, +CloudWatch Insight Rules and Metric Streams, ECR pull-through cache. A local +inner loop does not call them, while the ones it does call (S3, SQS, Lambda, IAM, +STS in full) are already `hand-verified`. So v1.0 ships the long tail *declared +rather than implemented*, and lets real use decide what gets promoted in 1.x. ## Guarantees @@ -169,7 +141,7 @@ when: - any `auto-crud` operation is one the engine will not actually serve (`TestAutoCRUDIsServedOverJSON`). -The engine reads the protocol from the **request**, not from the provider, so +The engine reads the protocol from the **request**, not the provider, so `auto-crud` survives a Query-speaking provider: `cloudwatch` answers boto3 over Query and falls through to the engine for `X-Amz-Target` callers. Filtering the registry by the provider's declared protocol would delete that coverage. diff --git a/docs/getting-started.md b/docs/getting-started.md index 2819124d..c7b8f806 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -12,47 +12,33 @@ To persist data across restarts, mount a volume: docker run -p 4747:4747 -v $(pwd)/data:/app/data ghcr.io/skyoo2003/devcloud:latest ``` -### GHCR Authentication - -If pulling from a private registry: +## Docker Compose (development) ```bash -docker login ghcr.io -# Username: -# Password: +docker compose -f docker/docker-compose.yml up ``` -## Build from Source +Builds the image from source and starts the Go server on port 4747 with `./data` +mounted. It runs a subset, not everything — +[`docker-compose.yml`](../docker/docker-compose.yml) sets +`DEVCLOUD_SERVICES=s3,sqs,dynamodb,iam,sts,lambda`. Edit that line, or see +[Configuration](configuration.md#devcloud_services), to run more. -Building from source is recommended only for contributors. For full prerequisites, development setup, and workflow, see [contributing.md](contributing.md). +## Build from source -Quick version: +Recommended only for contributors — see [contributing.md](contributing.md) for +full prerequisites and workflow. ```bash git clone https://github.com/skyoo2003/devcloud.git cd devcloud make build # builds the Go binaries (devcloud + codegen) -make run # starts server on port 4747 +make run # starts the server on port 4747 ``` -## Docker Compose (Development) +## Verify it works -```bash -docker compose -f docker/docker-compose.yml up -``` - -This builds the image from source and starts one service: the Go server on port -4747, with `./data` mounted for persistence and the host Docker socket mounted -so the Lambda runtime can start containers. - -It runs a subset, not everything — -[`docker-compose.yml`](../docker/docker-compose.yml) sets -`DEVCLOUD_SERVICES=s3,sqs,dynamodb,iam,sts,lambda`. Edit that line, or see -[Configuration](configuration.md#devcloud_services), to run more. - -## Verify Installation - -### Using boto3 +### boto3 ```python import boto3 @@ -70,7 +56,10 @@ print(s3.list_buckets()["Buckets"]) # [{'Name': 'test-bucket', 'CreationDate': ...}] ``` -### Using AWS CLI +Any dummy credentials work — DevCloud reads them but never verifies them. They +must be non-empty. + +### AWS CLI ```bash # Configure a profile (one-time) @@ -78,33 +67,33 @@ aws configure set aws_access_key_id test --profile devcloud aws configure set aws_secret_access_key test --profile devcloud aws configure set region us-east-1 --profile devcloud -# Use it aws --endpoint-url http://localhost:4747 --profile devcloud s3 mb s3://test-bucket aws --endpoint-url http://localhost:4747 --profile devcloud s3 ls ``` -You can also set an alias for convenience: +Or alias it: `alias awslocal='aws --endpoint-url http://localhost:4747'`. -```bash -alias awslocal='aws --endpoint-url http://localhost:4747' -awslocal s3 ls -``` +### Terraform / CDK + +Point the AWS provider at `http://localhost:4747` with dummy credentials. See the +[FAQ](faq.md#compatibility) for what works and what does not. ## Admin API -When the server runs with `admin.enabled: true` in config, an opt-in admin API -is exposed under `/devcloud/api/*`: +Opt-in — set `admin.enabled: true` in config, then: -- `GET /devcloud/api/services` — service status overview -- `GET /devcloud/api/services/{id}/resources` — resource browser (buckets, queues, tables, functions) -- `GET /devcloud/api/logs` — recent API call logs (`?limit=`) -- `GET /devcloud/api/fidelity` — per-operation fidelity tiers (`?service=` to filter); see [fidelity-manifest.md](fidelity-manifest.md) -- `GET /devcloud/api/unrouted` — calls made to services this build does not register; see [coverage.md](coverage.md) +| Route | Returns | +|---|---| +| `GET /devcloud/api/services` | Service status overview | +| `GET /devcloud/api/services/{id}/resources` | Buckets, queues, tables, functions | +| `GET /devcloud/api/logs` | Recent API call logs (`?limit=`) | +| `GET /devcloud/api/fidelity` | Per-operation tiers (`?service=`) — [fidelity-manifest.md](fidelity-manifest.md) | +| `GET /devcloud/api/unrouted` | Calls to services this build does not register — [coverage.md](coverage.md) | The web dashboard UI that consumes this API lives in a separate repository. -## Next Steps +## Next steps -- [Configuration](configuration.md) — All configuration options -- [Services](services/) — Per-service API reference and examples -- [Architecture](architecture.md) — System design overview +- [Configuration](configuration.md) — all options and env-var overrides +- [Services](services/) — per-service API reference and examples +- [Troubleshooting](troubleshooting.md) — when something does not work diff --git a/docs/plugin-api.md b/docs/plugin-api.md index fad50526..65170f71 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -1,13 +1,12 @@ # ServicePlugin API -This document specifies the **`ServicePlugin`** interface — the stable contract -every DevCloud service implements. As of **v1.0** this interface is a public API: -see [API stability](#api-stability) for the compatibility guarantee. +The stable contract every DevCloud service implements. As of **v1.0** this is a +public API — see [API stability](#api-stability). The interface and registry live in [`internal/plugin/`](../internal/plugin/). Plugins are compiled in-tree and register themselves at startup; there is no -dynamic/external plugin loading. (The package lives under `internal/` for that -reason — it is imported by DevCloud's own binaries, not by third-party modules.) +dynamic loading. (That is why the package is under `internal/` — it is imported +by DevCloud's own binaries, not by third-party modules.) ## The interface @@ -23,75 +22,19 @@ type ServicePlugin interface { } ``` -### Method contracts - | Method | Contract | |--------|----------| -| `ServiceID()` | Stable lowercase identifier, e.g. `"s3"`. **Must equal the key the plugin is registered under** (the gateway routes by this key). Constant; safe to call before `Init`. | -| `ServiceName()` | Human-readable name for logs/admin API. Must be non-empty. Constant; safe before `Init`. | +| `ServiceID()` | Stable lowercase identifier, e.g. `"s3"`. **Must equal the key the plugin is registered under** — the gateway routes by it. Constant; safe before `Init`. | +| `ServiceName()` | Human-readable name for logs and the admin API. Non-empty, constant, safe before `Init`. | | `Protocol()` | One of the [`ProtocolType`](#protocols) constants. Constant; safe before `Init`. | -| `Init(config)` | Called once at startup before any request. Open stores, read `config.Options`. Return a non-nil error to abort startup (services in the fixed init order) or skip the service (others). | -| `Shutdown(ctx)` | Called once at graceful shutdown (15s budget). Flush/close resources. Idempotent-friendly. | -| `HandleRequest(ctx, op, req)` | Handle one API call. `op` is the operation name pre-extracted by the gateway (see [operation names](#operation-names)); it may be `""` for REST protocols, in which case derive it from method+path or `X-Amz-Target`. Return a `*Response`; return an `error` only for unexpected internal failures (the gateway wraps those as a `500 InternalError`). **Model AWS errors as a normal `*Response`** with the right status and error body, not as a Go `error`. | -| `ListResources(ctx)` | Return the resources this service currently holds (admin API). May be empty. | +| `Init(config)` | Called once at startup before any request. Open stores, read `config.Options`. A non-nil error aborts startup (services in the fixed init order) or skips the service (others). | +| `Shutdown(ctx)` | Called once at graceful shutdown, 15s budget. Flush and close. Idempotent-friendly. | +| `HandleRequest(ctx, op, req)` | Handle one API call. See [operation names](#operation-names) — `op` may be `""` for REST protocols, in which case derive it yourself. Return a `*Response`; return a Go `error` only for unexpected internal failures (the gateway wraps those as `500 InternalError`). **Model AWS errors as a normal `*Response`.** | +| `ListResources(ctx)` | The resources this service currently holds, for the admin API. May be empty. | These invariants are enforced for every registered service by [`TestServicePluginConformance`](../cmd/devcloud/conformance_test.go). -## Providers and CSP neutrality - -Phase 2 of the [roadmap](roadmap.md) reviewed this interface for CSP neutrality. -The conclusion was that **`ServicePlugin` needs no change**: every method is -about serving an HTTP API, and none of them assumes AWS. - -What is AWS-specific is the vocabulary around it, and each piece is open rather -than closed: - -| Surface | Why it is already neutral | -|---|---| -| `ProtocolType` | An open `string` type, not an enum. The constants are AWS's wire protocols; another CSP declares its own values. | -| `ServiceID()` | An opaque registry key. A non-AWS service registers under whatever id it wants. | -| `DefaultAccountID` | An AWS concept used only by AWS services. Nothing in the interface refers to it. | - -The one thing missing was a way to ask *which CSP a plugin serves*. That is the -optional `ProviderScoped` interface: - -```go -type ProviderScoped interface { - Provider() string -} - -func ProviderOf(p ServicePlugin) string // defaults to plugin.DefaultProvider ("aws") -``` - -It is **not** a `ServicePlugin` method on purpose. Adding one would force an -edit to every service in the tree so it could state the value it already -defaults to, and would break the [v1.x contract](#api-stability) below for no -gain. Always read the provider through `ProviderOf`, never by type-asserting. - -Startup uses it to resolve configuration: `cmd/devcloud` looks up each service -with `cfg.ProviderService(plugin.ProviderOf(p), id)`, so a plugin lands in its -own provider's config namespace without the config package needing to know which -services exist. See [configuration.md](configuration.md#provider-namespacing). - -## Caller identity - -The gateway parses whatever credentials a request carried and attaches the -result to the request context. Inside `HandleRequest`: - -```go -if id, ok := auth.FromContext(ctx); ok { - region := id.Region // what the SDK signed for; "" if unsigned -} -``` - -`auth.Identity` carries `Provider`, `AccessKeyID`, `Region`, `Service` (the -signing name as presented) and `SessionToken`. **None of it is verified** — -DevCloud accepts any credentials, so treat these as a claim, never as an -authorization decision. Adapters live in [`internal/auth`](../internal/auth/); -`SigV4` is AWS's, and Azure (AAD/SAS) and GCP (OAuth2) join by implementing -`auth.Adapter`. - ## Protocols `Protocol()` returns the wire protocol the gateway uses to detect and serialize @@ -100,26 +43,26 @@ for the service: | Constant | Value | Example services | |----------|-------|------------------| | `ProtocolRESTXML` | `rest-xml` | S3, Route53, CloudFront | -| `ProtocolRESTJSON` | `rest-json` | ACM, API Gateway, Lambda REST | -| `ProtocolJSON10` | `json-1.0` | DynamoDB, Kinesis, CodeConnections | -| `ProtocolJSON11` | `json-1.1` | ECS, Lambda, DMS, many others | -| `ProtocolQuery` | `query` | IAM, STS, SQS, SNS, RDS, EC2 | +| `ProtocolRESTJSON` | `rest-json` | Lambda, ACM, API Gateway | +| `ProtocolJSON10` | `json-1.0` | DynamoDB, SQS, Kinesis | +| `ProtocolJSON11` | `json-1.1` | ECS, DMS, and many others | +| `ProtocolQuery` | `query` | IAM, STS, SNS, RDS, EC2 | ## Operation names The gateway derives `op` in [`extractOperationName`](../internal/gateway/router.go): -- **JSON protocols** — the suffix of the `X-Amz-Target` header after the last - `.` (e.g. `DynamoDB_20120810.GetItem` → `GetItem`). +- **JSON protocols** — the suffix of `X-Amz-Target` after the last `.` + (`DynamoDB_20120810.GetItem` → `GetItem`). - **Query protocol** — the `Action` parameter. -- **REST protocols** — `""`; the operation is implicit in the URL + method, so - the plugin resolves it itself. +- **REST protocols** — `""`; the operation is implicit in method + URL, so the + plugin resolves it itself. -Service routing uses the target prefix / signing name; see +Service routing uses the target prefix or signing name — see [`serviceFromTarget` and `normalizeServiceID`](../internal/gateway/protocol.go). -Target prefixes may be dotted (e.g. -`com.amazonaws.codeconnections.CodeConnections_20231201`); the last dotted +Target prefixes may be dotted +(`com.amazonaws.codeconnections.CodeConnections_20231201`); the last dotted segment is treated as the `ServiceName_Date` token. ## Configuration @@ -131,23 +74,41 @@ type PluginConfig struct { } ``` -- `DataDir` — per-service data directory from config (`data_dir:`). +- `DataDir` — the per-service directory from config (`data_dir:`). - `Options` — cross-cutting values injected by [`buildOptions`](../cmd/devcloud/main.go). Keys a plugin may rely on: - - `server_port` (`int`) — the HTTP port, for building resource URLs (e.g. SQS - queue URLs). Passed to every service. - - `iam_store` — set for `sts` so it shares the IAM store; services needing a - peer's store receive it here. + - `server_port` (`int`) — the HTTP port, for building resource URLs such as SQS + queue URLs. Passed to every service. + - `iam_store` — set for `sts` so it shares the IAM store. A service needing a + peer's store receives it here. + +## Caller identity + +The gateway parses whatever credentials a request carried and attaches the result +to the request context: + +```go +if id, ok := auth.FromContext(ctx); ok { + region := id.Region // what the SDK signed for; "" if unsigned +} +``` + +`auth.Identity` carries `Provider`, `AccessKeyID`, `Region`, `Service` (the +signing name as presented) and `SessionToken`. **None of it is verified** — +DevCloud accepts any credentials, so treat these as a claim, never as an +authorization decision. Adapters live in [`internal/auth`](../internal/auth/); +`SigV4` is AWS's, and Azure (AAD/SAS) and GCP (OAuth2) join by implementing +`auth.Adapter`. ## Error convention -Return AWS-shaped errors as a `*Response`, using the shared helpers in +Return AWS-shaped errors as a `*Response`, using the helpers in [`internal/shared/response.go`](../internal/shared/response.go): -- JSON protocols → `shared.JSONError(code, message, status)` (`{"__type", "message"}`). -- Query/XML protocols → `shared.QueryXMLError(...)` or the service's XML error helper. +- JSON protocols → `shared.JSONError(code, message, status)` (`{"__type", "message"}`) +- Query/XML protocols → `shared.QueryXMLError(...)` or the service's XML helper -**Unknown / unimplemented operations must return an error, never a false +**Unknown or unimplemented operations must return an error, never a false success.** The convention is `InvalidAction` with HTTP 400: ```go @@ -155,13 +116,14 @@ default: return shared.JSONError("InvalidAction", "unknown action: "+op, http.StatusBadRequest), nil ``` -A silent `200 OK {}` for an unimplemented op is a bug: it makes an SDK believe -the call succeeded. +A silent `200 OK {}` is a bug: it makes an SDK believe the call succeeded. To opt +into the [generic CRUD engine](crud-engine.md) instead, return +`plugin.ErrUnhandledOp` from the `default:` case — the gateway falls back to the +engine and emits `InvalidAction` itself if the engine cannot classify the +operation. ## Registering a service -Each service registers a factory in its `init()`: - ```go func init() { plugin.DefaultRegistry.Register("myservice", func() plugin.ServicePlugin { @@ -170,35 +132,68 @@ func init() { } ``` -The service package is blank-imported in +Blank-import the package in [`cmd/devcloud/imports.go`](../cmd/devcloud/imports.go) so its `init()` runs, and -enabled in [`internal/config/default.yaml`](../internal/config/default.yaml) so -it is initialized at startup. See [contributing.md](contributing.md) for the -full "add a service" checklist. +enable it in [`internal/config/default.yaml`](../internal/config/default.yaml) so +it is initialized at startup. Full checklist: +[contributing.md](contributing.md#adding-a-new-aws-service). The registry also exposes `RegisteredServices()` (all registered IDs) and -`Construct(id)` (build an instance without `Init`, for introspection/tests). +`Construct(id)` (build an instance without `Init`, for introspection and tests). + +## Providers and CSP neutrality + +Phase 2 of the [roadmap](roadmap.md) reviewed this interface for CSP neutrality +and concluded that **`ServicePlugin` needs no change**: every method is about +serving an HTTP API, and none assumes AWS. What is AWS-specific is the vocabulary +around it, and each piece is open rather than closed: + +| Surface | Why it is already neutral | +|---|---| +| `ProtocolType` | An open `string` type, not an enum. Another CSP declares its own values. | +| `ServiceID()` | An opaque registry key. A non-AWS service registers under whatever id it wants. | +| `DefaultAccountID` | An AWS concept used only by AWS services. Nothing in the interface refers to it. | + +The one thing missing was a way to ask *which CSP a plugin serves*. That is the +optional `ProviderScoped` interface: + +```go +type ProviderScoped interface { + Provider() string +} + +func ProviderOf(p ServicePlugin) string // defaults to plugin.DefaultProvider ("aws") +``` + +It is **not** a `ServicePlugin` method on purpose: adding one would force an edit +to every service in the tree just to state the value it already defaults to, and +would break the [v1.x contract](#api-stability) for no gain. Always read the +provider through `ProviderOf`, never by type-asserting. + +Startup uses it to resolve configuration — +`cfg.ProviderService(plugin.ProviderOf(p), id)` — so a plugin lands in its own +provider's config namespace without the config package needing to know which +services exist. See [configuration.md](configuration.md#provider-namespacing). ## API stability -This section is the **in-tree** contract — it constrains contributors writing -service plugins inside this repository. `internal/plugin` cannot be imported -from another Go module, so it is not the promise a *user* of DevCloud depends -on. That is [compatibility-policy.md](compatibility-policy.md), which covers the -config file, environment variables, CLI, admin API and wire behaviour. - -Starting at **v1.0**, `ServicePlugin`, `PluginConfig`, `Response`, `Resource`, -and the `ProtocolType` constants are stable within the `v1.x` series: - -- No method will be **removed** from `ServicePlugin` and no existing method - **signature** will change in a `v1.x` release. -- Struct fields will only be **added**, never removed or repurposed. -- Any breaking change to this contract must be called out in the release notes, - and every in-tree plugin updated in the same change. It is not on its own a - major-version event: nothing outside this module can import - `internal/plugin`, so no user can be depending on it. Release versioning is - governed by [compatibility-policy.md](compatibility-policy.md). - -New optional capability is introduced additively (new `Options` keys, new -`ProtocolType` values, optional side interfaces such as `ProviderScoped`) so +This section is the **in-tree** contract: it constrains contributors writing +service plugins inside this repository. `internal/plugin` cannot be imported from +another Go module, so it is not the promise a *user* of DevCloud depends on — +that is [compatibility-policy.md](compatibility-policy.md). + +Starting at **v1.0**, `ServicePlugin`, `PluginConfig`, `Response`, `Resource` and +the `ProtocolType` constants are stable within `v1.x`: + +- No method is **removed** from `ServicePlugin`, and no existing **signature** + changes, in a `v1.x` release. +- Struct fields are only **added**, never removed or repurposed. +- Any breaking change must be called out in the release notes with every in-tree + plugin updated in the same change. It is not on its own a major-version event: + nothing outside this module can import `internal/plugin`, so no user depends on + it. Release versioning is governed by + [compatibility-policy.md](compatibility-policy.md). + +New optional capability is introduced additively — new `Options` keys, new +`ProtocolType` values, optional side interfaces such as `ProviderScoped` — so existing plugins keep compiling and behaving. diff --git a/docs/roadmap.md b/docs/roadmap.md index 005af9ec..647e7562 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,83 +1,84 @@ # Roadmap -DevCloud's long-term vision is to be a **local development companion for cloud-native apps across every major Cloud Service Provider (CSP)** — not a production replacement, but an on-ramp that lets developers iterate fast without cloud bills and deploy to their target CSP with confidence. +DevCloud aims to be a **local development companion for cloud-native apps across +every major CSP** — not a production replacement, but an on-ramp that lets you +iterate without cloud bills and deploy to your target CSP with confidence. It gets +there in phases, to keep scope, architectural complexity and community +expectations manageable. -We pursue this vision through a **phased rollout** to manage scope, architectural complexity, and community expectations. +## Guiding principles -## Guiding Principles - -1. **Local-first, cost-free** — developers should not incur cloud charges for inner-loop development. +1. **Local-first, cost-free** — no cloud charges for inner-loop development. 2. **On-ramp, not replacement** — DevCloud helps you *land on* a CSP, not avoid it. -3. **API-compatible, not behavior-perfect** — prioritize SDK compatibility (boto3, azure-sdk, google-cloud-python) over edge-case parity. -4. **Community-owned** — scope is too large for a single maintainer; plugin architecture and contributor experience are first-class concerns. +3. **API-compatible, not behaviour-perfect** — SDK compatibility over edge-case parity. +4. **Community-owned** — the scope is too large for one maintainer, so the plugin architecture and contributor experience are first-class. 5. **Trademark-respectful** — see [TRADEMARKS.md](../TRADEMARKS.md). -## Phases - -### Phase 1 — AWS Depth & Stabilization (Complete, shipped as v1.0) +## Phase 1 — AWS depth and stabilization (complete, shipped as v1.0) -**Goal**: mature the already-broad AWS surface into a stable, well-tested v1.0. +Mature the already-broad AWS surface into a stable, well-tested v1.0. -- [x] 100+ AWS services scaffolded from official Smithy models via in-tree codegen (run `make stats` for current counts) -- [x] Deep hand-written coverage on core, integration, and major extended services (see [services-matrix.md](services-matrix.md) for per-tier depth) -- [x] Cross-service integration (CloudFormation, DynamoDB Streams → Lambda, SQS → Lambda, S3 → Lambda, EventBridge, SNS → SQS) -- [x] boto3 compatibility suite (`make test-compat`); core services — S3, SQS, DynamoDB, Lambda, IAM, STS, SNS, CloudWatch, KMS, Secrets Manager, EventBridge, CloudFormation — pass 100% -- [x] boto3 compatibility suite green in CI (`make test-compat`) — a failing test fails the build -- [x] Unimplemented operations return a consistent AWS error (`InvalidAction`, HTTP 400) instead of a false `200` success; dead scaffold code removed -- [x] boto3 compatibility coverage added for previously-untested services (CodeConnections, DMS, Verified Permissions) -- [x] Stable `ServicePlugin` API finalized and documented ([plugin-api.md](plugin-api.md)), enforced by a conformance test over every registered service -- [x] Generic CRUD fallback engine ([crud-engine.md](crud-engine.md)) auto-serves ~2,200 CRUD-shaped operations across all 46 JSON-protocol services with plausible, store-backed responses; every registered JSON service is wired. **Follow-up**: promote high-value auto-crud ops to hand-verified fidelity. -- [x] v1.0 release — see [compatibility-policy.md](compatibility-policy.md) for what 1.x guarantees +- [x] AWS services scaffolded from official Smithy models via in-tree codegen — counts in [coverage.md](coverage.md) +- [x] Deep hand-written coverage on core and integration services +- [x] Cross-service integration — CloudFormation, DynamoDB Streams → Lambda, SQS → Lambda, S3 → Lambda, EventBridge, SNS → SQS +- [x] boto3 compatibility suite green in CI; a failing test fails the build +- [x] Unimplemented operations return an AWS-shaped error instead of a false `200` +- [x] Stable `ServicePlugin` API ([plugin-api.md](plugin-api.md)), enforced by a conformance test over every registered service +- [x] Generic [CRUD fallback engine](crud-engine.md) serving the long tail with plausible, store-backed responses. **Follow-up:** promote high-value `auto-crud` operations to hand-verified fidelity. +- [x] v1.0 release — see [compatibility-policy.md](compatibility-policy.md) -### Phase 2 — Architectural Preparation (Complete, v1.x) +## Phase 2 — Architectural preparation (complete, v1.x) -**Goal**: internal refactor so adding a new CSP doesn't require forking the project. +Refactor internally so adding a CSP does not require forking the project. Each +item made a future provider an *addition* rather than an edit — the seams are +tabulated in [architecture.md](architecture.md#multi-csp-seams). -- [x] Intermediate Representation (IR) between API models and codegen — [`internal/codegen/ir`](../internal/codegen/ir/ir.go). The generators read `*ir.Model` and nothing in the IR names Smithy; `ir.Model.Provider` carries the owning CSP. -- [x] `internal/codegen/parser.go` refactored behind a [`ModelSource`](../internal/codegen/source.go) interface. `SmithySource` is the first implementation and owns its own format detection, so `cmd/codegen` hands every file to `SourceFor` and never names a format. OpenAPI/Protobuf are an added file. -- [x] Provider namespacing in config — `providers.aws.services.*`, forward-compatible with `providers.azure.*`. The top-level `services` block is the same AWS block under its historical name and keeps working ([configuration.md](configuration.md#provider-namespacing)). -- [x] Plugin interface review — `ServicePlugin` needs no change; the CSP is carried by the optional [`ProviderScoped`](plugin-api.md#providers-and-csp-neutrality) interface, read through `plugin.ProviderOf`. Adding a method would have broken every in-tree plugin to make it state a value it already defaults to. -- [x] Per-provider auth adapter interface — [`internal/auth`](../internal/auth/auth.go). `Adapter` reads one provider's credential form; `SigV4` is the AWS implementation, and AAD/SAS and OAuth2 slot in beside it without touching the gateway. The caller's claimed identity reaches plugins via `auth.FromContext`. +- [x] Intermediate Representation between models and codegen ([`internal/codegen/ir`](../internal/codegen/ir/ir.go)). Generators read `*ir.Model`; nothing in the IR names Smithy. +- [x] Parser refactored behind [`ModelSource`](../internal/codegen/source.go). `SmithySource` owns its own format detection, so `cmd/codegen` never names a format. +- [x] Provider namespacing in config — `providers.aws.services.*`, forward-compatible with `providers.azure.*` ([configuration.md](configuration.md#provider-namespacing)). +- [x] Plugin interface review — `ServicePlugin` needed no change; the CSP is carried by the optional [`ProviderScoped`](plugin-api.md#providers-and-csp-neutrality). +- [x] Per-provider auth adapters ([`internal/auth`](../internal/auth/auth.go)). `SigV4` is the AWS implementation; AAD/SAS and OAuth2 slot in beside it without touching the gateway. -What Phase 2 deliberately did **not** do: no non-AWS service, no second `ModelSource`, and no signature verification. Those are Phase 3 and beyond — Phase 2's job was to make each of them an addition rather than a fork. +Phase 2 deliberately shipped **no** non-AWS service, no second `ModelSource`, and +no signature verification. Those are Phase 3 and beyond; Phase 2's job was to make +each of them an addition rather than a fork. -### Phase 3 — First Non-AWS Service (Next, v2.0, exploratory) +## Phase 3 — First non-AWS service (next, v2.0, exploratory) -**Goal**: validate the multi-CSP architecture with a single, well-scoped pilot. +Validate the multi-CSP architecture with one well-scoped pilot. -- [ ] Pick one Azure service as pilot (candidate: **Azure Blob Storage** — closest to S3 semantically) +- [ ] Pick an Azure pilot service — candidate: **Azure Blob Storage**, closest to S3 semantically - [ ] OpenAPI → IR → codegen proof of concept -- [ ] Azure authentication adapter (Shared Key for starters) +- [ ] Azure authentication adapter (Shared Key to start) - [ ] Compatibility tests against `azure-sdk-for-python` - [ ] Documentation pattern for multi-CSP service docs -### Phase 4 — Breadth Expansion (v2.x+) - -**Goal**: community-driven growth across CSPs. +## Phase 4 — Breadth expansion (v2.x+) -- [ ] Additional Azure services (Queue Storage, Table Storage, Cosmos DB) -- [ ] Google Cloud pilot (candidate: **Google Cloud Storage**) -- [ ] Other providers as community interest justifies (OCI, Alibaba, Tencent) -- [ ] Federated identity playground (simulate cross-CSP IAM) +- [ ] More Azure services — Queue Storage, Table Storage, Cosmos DB +- [ ] Google Cloud pilot — candidate: **Google Cloud Storage** +- [ ] Other providers as community interest justifies +- [ ] Federated identity playground (cross-CSP IAM simulation) -## Out of Scope +## Out of scope - Production hosting or high-availability guarantees -- Billing/quota simulation matching real CSP pricing -- Exact replication of CSP-internal behavior (eventual consistency timing, rate limits, etc.) -- Redistribution of CSP-owned branding assets, logos, or documentation +- Billing and quota simulation matching real CSP pricing +- Exact replication of CSP-internal behaviour (consistency timing, rate limits) +- Redistribution of CSP-owned branding assets, logos or documentation -## How to Influence the Roadmap +## Influencing the roadmap -- Open a [Feature Request](https://github.com/skyoo2003/devcloud/issues/new?template=feature_request.yml) describing the service or capability you need -- Upvote existing requests with reactions — we look at vote counts when prioritizing -- Contribute a service implementation following [docs/contributing.md](contributing.md) +- **Missing service?** File a [service request](https://github.com/skyoo2003/devcloud/issues/new?template=service_request.yml) with `GET /devcloud/api/unrouted` output — that is what moves a service into the target ([coverage.md](coverage.md#service-not-supported)). +- **Missing operation or capability?** File a [feature request](https://github.com/skyoo2003/devcloud/issues/new?template=feature_request.yml). +- Upvote existing requests with reactions; vote counts inform prioritization. +- Or contribute it — see [contributing.md](contributing.md). -## Version Mapping +## Version mapping | Version | Focus | |---------|-------| | 0.x | AWS services, unstable API | -| 1.x ← current | AWS depth, stable plugin API, multi-CSP groundwork (IR, `ModelSource`, provider namespacing, auth adapters) | +| 1.x ← current | AWS depth, stable plugin API, multi-CSP groundwork | | 2.x | Multi-CSP architecture, Azure pilot | | 3.x+ | Broad CSP coverage, community-owned providers | diff --git a/docs/services-matrix.md b/docs/services-matrix.md index 366f40c7..57d4e9f4 100644 --- a/docs/services-matrix.md +++ b/docs/services-matrix.md @@ -1,121 +1,73 @@ -# DevCloud Services Matrix - -**Total**: 148 services registered, 117 serving at least one operation (run `make stats`). The boto3 compatibility suite in `tests/compatibility/` passes in CI. A registered service that serves nothing still declines with a clean AWS error rather than letting the call reach real AWS — see [coverage.md](coverage.md). - -> To refresh these numbers, run `make stats` (service/handler counts) and `make test-compat` (compatibility suite), then update the line above. Note: `make stats`' "Operations" counts dispatch cases (a service carrying both Query and JSON protocols counts each), not distinct AWS operations. - -_Last updated with each release. For unreleased changes, see [CHANGELOG.md](../CHANGELOG.md)._ - -DevCloud is a Go-based local cloud environment with AWS API compatibility. This matrix tracks implemented operations per service. - -## Summary by tier - -| Tier | Services | Ops | Description | -|------|----------|-----|-------------| -| Tier 1 (Big 6) | S3, SQS, DynamoDB, Lambda, IAM, STS | 128+ | Core services (SQS at full 23/23 op coverage) | -| Tier 2 (Integration) | EventBridge, SNS, CW Logs, CloudWatch, KMS, Secrets Manager, SSM, ECR | 157+ | Integration services | -| Tier 3 (Extended) | EFS, EBS, EC2, Route53, ACM, ECS, Bedrock, Account, Pipes, CloudControl, RGTAPI, AppAutoScaling, Firehose, S3Tables, MWAA, Scheduler, Support, IdentityStore, MediaConvert, Textract, ServerlessRepo, DDB Streams, SFN, Kinesis, CloudFormation | 900+ | Extended platform services, networking, and services requiring custom integration logic | -| Category Expansion | remaining services | varies | Smithy-scaffolded services with working dispatch — common operations implemented; less-common ones return a clean `InvalidAction` error rather than a false success | - -## Top 25 services (by ops count) - -| # | Service | Ops | Category | -|---|---------|-----|----------| -| 1 | sesv2 | 155 | Business Apps | -| 2 | appconfig | 97 | Management | -| 3 | pinpoint | 93 | Business Apps | -| 4 | opensearch | 87 | Analytics | -| 5 | iot | 82 | IoT | -| 6 | backup | 82 | Storage | -| 7 | apigatewayv2 | 79 | Networking | -| 8 | waf | 77 | Security | -| 9 | neptune | 71 | Databases | -| 10 | elasticsearchservice | 67 | Analytics | -| 11 | sagemaker | 65 | ML | -| 12 | glue | 65 | Analytics | -| 13 | route53resolver | 64 | Networking | -| 14 | ssoadmin | 62 | Security | -| 15 | athena | 62 | Analytics | -| 16 | rds | 61 | Databases | -| 17 | lakeformation | 61 | Analytics | -| 18 | cloudformation | 61 | Management | -| 19 | emr | 60 | Analytics | -| 20 | kafka | 59 | Analytics | -| 21 | cognitoidentityprovider | 59 | Security | -| 22 | ecs | 57 | Containers | -| 23 | eks | 56 | Containers | -| 24 | docdb | 55 | Databases | -| 25 | codecommit | 53 | DevTools | +# Services Matrix -## Cross-service integrations +Which AWS services DevCloud serves, and how deeply. + +**Counts live in [coverage.md](coverage.md)** — it is the only page that publishes +them, and CI fails if its figures and the binary disagree. This page describes the +*shape* of the surface instead. + +## How to look up one service -| Integration | Status | Implementation | -|-------------|--------|----------------| -| CloudFormation → 6 resource types | ✅ | `cloudformation/engine.go` with topological sort, intrinsic functions | -| DynamoDB Streams → Lambda | ✅ | `lambda/eventsource.go` polls DDB stream shards | -| SQS → Lambda | ✅ | Event source poller (pre-existing) | -| S3 → Lambda | ✅ | `s3/notifications.go` on PUT events | -| EventBridge → SQS/SNS/Lambda | ✅ | Rule matching + `dispatchToTarget` | -| SNS → SQS subscription | ✅ | Topic publish triggers queue delivery | -| DynamoDB → DynamoDB Streams | ✅ | Write-path publishes records | - -## boto3 compatibility - -- Tests: `tests/compatibility/` (775 tests) -- Status: the full suite passes in CI (`.github/workflows/compat.yml`); any failing test fails the build -- Run: `make test-compat` - -Every service the suite covers passes — including S3Tables (ARN path parsing), -ServerlessRepo (restJson1 `jsonName`), Textract, and Support, which were once -rough edges. Core services (S3, SQS, DynamoDB, Lambda, IAM, STS, SNS, CloudWatch, -KMS, Secrets Manager, EventBridge, CloudFormation) have the deepest coverage. -Services implement their common operations; less-common operations return a clean -AWS error rather than a false success, so an SDK always gets a truthful response. - -## Operation coverage & the CRUD fallback engine - -Hand-written providers implement each service's common operations. For the long -tail of standard CRUD-shaped operations across 46 JSON-protocol services, a -generic engine can serve plausible, store-backed responses so SDK calls -round-trip. This coverage is **plausible, not faithful** — no validation or -business logic. See [crud-engine.md](crud-engine.md) for the wired services and -limits. Operations that are neither hand-written nor CRUD-classifiable return an -honest `InvalidAction` error, never a fabricated success. - -Every operation's tier is declared in the generated -[fidelity manifest](fidelity-manifest.md) — 4,496 `hand-verified`, 948 -`auto-crud`, 2,031 `unimplemented` across 7,475 operations. Query it per service -(requires `admin.enabled: true`): +Per-operation depth is declared by the generated +[fidelity manifest](fidelity-manifest.md), not by a hand-maintained table. Ask it +directly (requires `admin.enabled: true`): ```bash curl -s 'localhost:4747/devcloud/api/fidelity?service=s3' ``` -## Supported protocols +| Tier | What it means | +|------|---------------| +| `hand-verified` | The service's provider implements the operation explicitly. | +| `auto-crud` | Served by the [CRUD engine](crud-engine.md) — store-backed and plausible, not faithful. | +| `unimplemented` | Refused with an AWS-shaped error. Never a fabricated success. | -- **JSON 1.0** (`application/x-amz-json-1.0`): DynamoDB, DynamoDB Streams, Kinesis -- **JSON 1.1** (`application/x-amz-json-1.1`): ECS, Lambda, Batch, CloudWatch Logs, SFN, many others -- **REST-JSON** (`application/json`): ACM, APIGW, Lambda REST, S3Tables, ServerlessRepo, MWAA, IdentityStore -- **REST-XML** (`application/xml`): S3, Route53, CloudFront -- **Query** (`application/x-www-form-urlencoded`): IAM, STS, SQS, SNS, RDS, CloudFormation, EC2, AutoScaling +## Depth by group -## Architecture +| Group | Services | Depth | +|-------|----------|-------| +| Core | S3, SQS, DynamoDB, Lambda, IAM, STS | Hand-written throughout; the deepest boto3 coverage | +| Integration | SNS, EventBridge, CloudWatch, CW Logs, KMS, Secrets Manager, SSM, ECR, CloudFormation | Hand-written common operations, plus the cross-service wiring below | +| Extended | EC2, ECS, EKS, Route53, ACM, RDS, Kinesis, Firehose, SFN, Bedrock, and the rest of the registered set | Common operations hand-written; the long tail is engine-served or declines cleanly | -For system design and plugin architecture, see [architecture.md](architecture.md). -For the phased multi-CSP vision, see [roadmap.md](roadmap.md). +The `tier1` / `tier2` / `tier3` tokens accepted by `DEVCLOUD_SERVICES` are a +*startup* grouping, not a depth claim — see +[configuration.md](configuration.md#devcloud_services) for their exact contents. -## How to verify +## Cross-service integrations -```bash -# Build and run unit tests -make build -make test +These are wired end to end, not stubbed: -# Run boto3 compatibility tests -make test-compat +| Integration | Implementation | +|-------------|----------------| +| CloudFormation → 6 resource types | `cloudformation/engine.go` — topological sort, intrinsic functions | +| DynamoDB → DynamoDB Streams | Write path publishes records | +| DynamoDB Streams → Lambda | `lambda/eventsource.go` polls stream shards | +| SQS → Lambda | Event source poller | +| S3 → Lambda | `s3/notifications.go` on PUT events | +| EventBridge → SQS / SNS / Lambda | Rule matching + `dispatchToTarget` | +| SNS → SQS | Topic publish triggers queue delivery | -# Print service and operation counts -make stats +## Protocols + +| Protocol | Content type | Example services | +|----------|--------------|------------------| +| JSON 1.0 | `application/x-amz-json-1.0` | DynamoDB, DynamoDB Streams, Kinesis | +| JSON 1.1 | `application/x-amz-json-1.1` | ECS, Lambda, Batch, CW Logs, SFN | +| REST-JSON | `application/json` | ACM, API Gateway, S3Tables, MWAA, IdentityStore | +| REST-XML | `application/xml` | S3, Route53, CloudFront | +| Query | `application/x-www-form-urlencoded` | IAM, STS, SQS, SNS, RDS, EC2, AutoScaling | + +SQS speaks both Query and JSON; the protocol is detected per request. + +## Verifying + +```bash +make test # Go unit tests +make test-compat # boto3 compatibility suite +make stats # registered services and hand-written operations ``` -See [Getting Started](getting-started.md) for installation and [contributing.md](contributing.md) for development setup. +The compatibility suite runs in CI on every push — a failing test fails the +build. What it does and does not promise is +[compatibility-policy.md](compatibility-policy.md#wire-behaviour--scoped-to-the-compatibility-suite). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index eb9752f3..54cd8d35 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,18 +1,14 @@ # Troubleshooting -Common issues and how to resolve them. If your problem isn't listed here, check [SUPPORT.md](../SUPPORT.md) for where to ask. +If your problem isn't here, see [SUPPORT.md](../SUPPORT.md) for where to ask. -## Installation & Build +## Build and startup -### `go: module github.com/skyoo2003/devcloud: Go 1.26 required` +**`go: module github.com/skyoo2003/devcloud: Go 1.26 required`** +Upgrade Go to 1.26+ (`go version`, then [go.dev/dl](https://go.dev/dl/)). -Upgrade Go to 1.26 or later. Check with `go version`. See [go.dev/dl](https://go.dev/dl/). - -## Running the Server - -### `bind: address already in use` on port 4747 - -Another process is already on port 4747. Either stop it or change DevCloud's port: +**`bind: address already in use` on port 4747** +Something else holds the port. Change DevCloud's: ```yaml # devcloud.yaml @@ -20,74 +16,75 @@ server: port: 4100 ``` -Or for Docker, map to a different host port: +Or map a different host port: `docker run -p 4100:4747 …`. -```bash -docker run -p 4100:4747 ghcr.io/skyoo2003/devcloud:latest -``` - -### Server starts but clients get `connection refused` - -Check that the server is actually listening: +**Server starts but clients get `connection refused`** +Check it is actually listening — any request works, DevCloud has no health +endpoint: ```bash -curl http://localhost:4747/devcloud/api/health +curl -i http://localhost:4747/ ``` If that fails: -- On Docker for Mac/Windows, make sure you used `-p 4747:4747` (not just `-p 4747`) -- If you're inside another container, `localhost` refers to *that* container. Use the host bridge (e.g., `host.docker.internal` on Docker Desktop) or a shared Docker network. +- On Docker Desktop, make sure you used `-p 4747:4747`, not just `-p 4747`. +- From inside another container, `localhost` means *that* container. Use + `host.docker.internal` or a shared Docker network. -### Permission denied on `./data/` - -The Docker container may write as root while your host user owns the mount. Either run the container with your UID: +**Permission denied on `./data/`** +The container may write as root while your host user owns the mount. Run it as +your user: ```bash docker run --user $(id -u):$(id -g) -p 4747:4747 -v $(pwd)/data:/app/data ghcr.io/skyoo2003/devcloud:latest ``` -Or pre-create the data directory with permissive mode (`chmod 777 data`) if you're OK with that for local development. - -## SDK / Client Errors +## SDK and client errors -### `SignatureDoesNotMatch` or signature-related errors +**`SignatureDoesNotMatch` or other signature errors** +DevCloud checks the SigV4 *format* but never verifies the secret. Make sure your +client has dummy but **non-empty** credentials (`aws_access_key_id="test"`, +`aws_secret_access_key="test"`) and that `endpoint_url` points at DevCloud. -By default, DevCloud accepts any signature — it checks the SigV4 *format* but does not verify the secret. If you see signature errors, make sure: +**`NoSuchBucket` / `ResourceNotFoundException` after restart** +SQS is in-memory and loses state. S3, DynamoDB, Lambda and IAM persist only if +`data_dir` points somewhere durable — under Docker, a mounted volume. See +[configuration.md](configuration.md#data-directories). -- Your client is configured with dummy but **non-empty** credentials (`aws_access_key_id="test"`, `aws_secret_access_key="test"`). -- The `endpoint_url` points to DevCloud, not real AWS. +**`InvalidAction`, `NotImplemented` or `UnsupportedOperation`** +That operation is not served. Check its tier — enable `admin.enabled: true`, then: -### `NoSuchBucket` / `ResourceNotFoundException` after restart - -Check whether the service has a persistent backend (see [configuration.md](configuration.md#data-directories)). SQS is in-memory and loses state; S3 / DynamoDB / Lambda / IAM persist only if `data_dir` points to a mounted volume (when using Docker). - -### `Operation X is not implemented` errors - -Not every operation of every service is implemented. Check [services-matrix.md](services-matrix.md) for the current coverage, and file a [Feature Request](https://github.com/skyoo2003/devcloud/issues/new?template=feature_request.yml) if the operation you need is missing. +```bash +curl -s 'localhost:4747/devcloud/api/fidelity?service=s3' +``` -### Terraform apply succeeds but `terraform plan` shows drift next time +An `unimplemented` operation is a deliberate honest failure, never a fabricated +success ([fidelity-manifest.md](fidelity-manifest.md)). To ask for it, open a +[feature request](https://github.com/skyoo2003/devcloud/issues/new?template=feature_request.yml). -Some services return default values that real AWS does not echo back, and vice versa. This is a known compatibility gap. Workarounds: +**The call reached real AWS instead of DevCloud** +The service is not registered, so nothing routed it. Confirm with +`GET /devcloud/api/unrouted` and file a +[service request](https://github.com/skyoo2003/devcloud/issues/new?template=service_request.yml) — +see [coverage.md](coverage.md#service-not-supported). -- Use `ignore_changes` on the specific attributes -- Pin DevCloud and Terraform AWS-provider versions together -- File a bug with the exact resource and attributes involved +**Terraform apply succeeds but the next plan shows drift** +Some services return defaults real AWS does not echo back, and vice versa. This is +a known gap. Use `ignore_changes` on the affected attributes, pin DevCloud and the +AWS provider together, or file a bug with the exact resource and attributes. ## Lambda -### Lambda function returns `runtime not available` - -DevCloud's Lambda implementation is a stub — it accepts function registration but does not execute your handler code locally. Real function execution requires configuring `services.lambda.runtime`. This is an area under active development; see [roadmap.md](roadmap.md). - -### Lambda invocations hang - -Ensure Docker is running if you have configured Lambda to use a Docker-based runtime. Check DevCloud's logs for container startup errors. +**Invoking a function returns `Lambda invoke requires Docker runtime`** +Expected. `internal/services/lambda/runtime.go` is a stub: DevCloud registers +functions, stores their code, and drives event source mappings (SQS, DynamoDB +Streams, S3 notifications), but it does not execute your handler. Real local +execution is not implemented — see [roadmap.md](roadmap.md). ## Admin API -### `GET /devcloud/api/*` returns 404 - -The admin API is disabled by default. Enable it: +**`GET /devcloud/api/*` returns 404** +It is disabled by default: ```yaml # devcloud.yaml @@ -95,9 +92,11 @@ admin: enabled: true ``` -Then restart the server. The web dashboard UI is a separate project (its own -repository); this server only exposes the admin API, not a bundled UI. +Restart afterwards. The routes are `services`, `services/{id}/resources`, `logs`, +`fidelity` and `unrouted` — there is no `health` route, and no bundled UI (the web +dashboard is a separate repository). -## When to open an issue +## Still stuck? -If your problem isn't listed here **and** you have checked [SUPPORT.md](../SUPPORT.md), open a [bug report](https://github.com/skyoo2003/devcloud/issues/new?template=bug_report.yml) with the reproduction template filled in. +Open a [bug report](https://github.com/skyoo2003/devcloud/issues/new?template=bug_report.yml) +with the reproduction template filled in.