Repository navigation
feat: Phase 1 AWS depth & stabilization for v1.0 #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f875ed6
feat: Phase 1 AWS depth & stabilization for v1.0
skyoo2003 004fb24
docs: regenerate changelog fragments via changie new (#96)
skyoo2003 28d06ee
test: address review feedback on unimplemented-op regression guards
skyoo2003 a57b938
test: close review gaps — unimplemented-op guards, dotted-target test…
skyoo2003 2adc980
fix: unify unimplemented-op errors to NotImplemented/501 and address …
skyoo2003 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| kind: Added | ||
| body: Document the stable `ServicePlugin` plugin API in docs/plugin-api.md (method contracts, config keys, error convention, and the v1.x stability policy), enforced by a conformance test over every registered service | ||
| time: 2026-07-18T22:26:51.495862+09:00 | ||
| custom: | ||
| Issue: "96" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| kind: Added | ||
| body: Enable and add boto3 compatibility tests for CodeConnections, DMS, and Verified Permissions (previously scaffolded but not enabled) | ||
| time: 2026-07-18T22:26:52.000000+09:00 | ||
| custom: | ||
| Issue: "96" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| kind: Fixed | ||
| body: Correct `X-Amz-Target` service routing for AWS services whose target prefix is dotted (e.g. CodeConnections, DMS); they previously misrouted to CloudTrail via an overbroad `com` prefix match | ||
| time: 2026-07-18T22:26:51.484673+09:00 | ||
| custom: | ||
| Issue: "96" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| kind: Fixed | ||
| body: Unimplemented operations now return a `NotImplemented` error (HTTP 501) instead of a misleading empty `200` success, so SDKs no longer see a false success and can tell a not-yet-emulated real operation apart from a genuinely invalid action; applied uniformly across all service providers (the ChangeResourceRecordSets record-action validation path keeps `InvalidChangeBatch`) | ||
| time: 2026-07-18T22:26:52.000000+09:00 | ||
| custom: | ||
| Issue: "96" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| kind: Removed | ||
| body: Remove the dead, unregistered STS provider stub (`internal/services/sts`); STS is served by the IAM package | ||
| time: 2026-07-18T22:26:51.501604+09:00 | ||
| custom: | ||
| Issue: "96" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/skyoo2003/devcloud/internal/plugin" | ||
| ) | ||
|
|
||
| // validProtocols is the closed set of wire protocols a ServicePlugin may declare. | ||
| var validProtocols = map[plugin.ProtocolType]bool{ | ||
| plugin.ProtocolRESTXML: true, | ||
| plugin.ProtocolRESTJSON: true, | ||
| plugin.ProtocolJSON10: true, | ||
| plugin.ProtocolJSON11: true, | ||
| plugin.ProtocolQuery: true, | ||
| } | ||
|
|
||
| // TestServicePluginConformance locks the stable ServicePlugin contract against | ||
| // every service registered via the blank imports in imports.go. It runs after | ||
| // package init, so DefaultRegistry holds the full plugin surface. See | ||
| // docs/plugin-api.md for the contract this enforces. | ||
| func TestServicePluginConformance(t *testing.T) { | ||
| ids := plugin.DefaultRegistry.RegisteredServices() | ||
|
|
||
| // Conservative floor. Broken registration wiring (a mangled imports.go, | ||
| // init-order regression, a dropped Register) collapses the live surface far | ||
| // below this, so a constant catches it without the fragility of parsing | ||
| // imports.go at runtime. Raise it if the real count ever nears it. | ||
| const minServices = 50 | ||
| if len(ids) < minServices { | ||
| t.Fatalf("registered %d services, want >= %d", len(ids), minServices) | ||
| } | ||
|
|
||
| var prev string | ||
| for i, id := range ids { | ||
| if id == "" { | ||
| t.Error("RegisteredServices() contains an empty service ID") | ||
| continue | ||
| } | ||
| // RegisteredServices() is documented to return a sorted slice. | ||
| if i > 0 && id < prev { | ||
| t.Errorf("RegisteredServices() not sorted: %q came before %q", prev, id) | ||
| } | ||
| prev = id | ||
|
|
||
| p, ok := plugin.DefaultRegistry.Construct(id) | ||
| if !ok { | ||
| t.Errorf("%s: registered but Construct returned not-found", id) | ||
| continue | ||
| } | ||
| if got := p.ServiceID(); got != id { | ||
| t.Errorf("%s: ServiceID() = %q, want the registry key %q", id, got, id) | ||
| } | ||
| if p.ServiceName() == "" { | ||
| t.Errorf("%s: ServiceName() is empty", id) | ||
| } | ||
| if proto := p.Protocol(); !validProtocols[proto] { | ||
| t.Errorf("%s: Protocol() = %q is not a valid ProtocolType", id, proto) | ||
| } | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| # 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 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.) | ||
|
|
||
| ## The interface | ||
|
|
||
| ```go | ||
| type ServicePlugin interface { | ||
| ServiceID() string | ||
| ServiceName() string | ||
| Protocol() ProtocolType | ||
| Init(config PluginConfig) error | ||
| Shutdown(ctx context.Context) error | ||
| HandleRequest(ctx context.Context, op string, req *http.Request) (*Response, error) | ||
| ListResources(ctx context.Context) ([]Resource, error) | ||
| GetMetrics(ctx context.Context) (*ServiceMetrics, error) | ||
| } | ||
| ``` | ||
|
|
||
| ### 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/dashboard. Must be 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 (dashboard). May be empty. | | ||
| | `GetMetrics(ctx)` | Return request/error/resource counters (dashboard). May be zero-valued. | | ||
|
|
||
| These invariants are enforced for every registered service by | ||
| [`TestServicePluginConformance`](../cmd/devcloud/conformance_test.go). | ||
|
|
||
| ## Protocols | ||
|
|
||
| `Protocol()` returns the wire protocol the gateway uses to detect and serialize | ||
| 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 | | ||
|
|
||
| ## 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`). | ||
| - **Query protocol** — the `Action` parameter. | ||
| - **REST protocols** — `""`; the operation is implicit in the URL + method, so | ||
| the plugin resolves it itself. | ||
|
|
||
| Service routing uses the target prefix / 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 | ||
| segment is treated as the `ServiceName_Date` token. | ||
|
|
||
| ## Configuration | ||
|
|
||
| ```go | ||
| type PluginConfig struct { | ||
| DataDir string | ||
| Options map[string]any | ||
| } | ||
| ``` | ||
|
|
||
| - `DataDir` — per-service data 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. | ||
|
|
||
| ## Error convention | ||
|
|
||
| Return AWS-shaped errors as a `*Response`, using the shared 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. | ||
|
|
||
| **Unknown / unimplemented operations must return an error, never a false | ||
| success.** The convention is `InvalidAction` with HTTP 400: | ||
|
|
||
| ```go | ||
| 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. | ||
|
|
||
| ## Registering a service | ||
|
|
||
| Each service registers a factory in its `init()`: | ||
|
|
||
| ```go | ||
| func init() { | ||
| plugin.DefaultRegistry.Register("myservice", func() plugin.ServicePlugin { | ||
| return &MyProvider{} | ||
| }) | ||
| } | ||
| ``` | ||
|
|
||
| The service package is blank-imported 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. | ||
|
|
||
| The registry also exposes `RegisteredServices()` (all registered IDs) and | ||
| `Construct(id)` (build an instance without `Init`, for introspection/tests). | ||
|
|
||
| ## API stability | ||
|
|
||
| Starting at **v1.0**, `ServicePlugin`, `PluginConfig`, `Response`, `Resource`, | ||
| `ServiceMetrics`, 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 requires a **major** version bump. | ||
|
|
||
| New optional capability is introduced additively (new `Options` keys, new | ||
| `ProtocolType` values) so existing plugins keep compiling and behaving. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.