docs: publish AWS operation coverage inventory (#1075)

This commit is contained in:
Josh Siviour
2026-09-17 21:33:53 +10:00
committed by GitHub
parent b26478bd06
commit 451df1a8cd
9 changed files with 3419 additions and 13 deletions
+1 -1
View File
@@ -198,7 +198,7 @@ test-integration:
@echo "AWS operation coverage: $(AWS_MODEL_OPERATION_COVERAGE_DIR)"
generate-aws-model-coverage:
@go run ./cmd/aws-model-coverage -out $(AWS_MODEL_OPERATION_COVERAGE_DIR)
@go run ./cmd/aws-model-coverage -out $(AWS_MODEL_OPERATION_COVERAGE_DIR) -json $(AWS_MODEL_OPERATION_COVERAGE_DIR)/coverage.json
aws-model-coverage: generate-aws-model-coverage
@go run ./cmd/aws-model-coverage
+11 -3
View File
@@ -14,18 +14,26 @@ import (
func main() {
outputDir := flag.String("out", "", "write one publishable page per service into this directory")
jsonOutput := flag.String("json", "", "write a machine-readable coverage inventory to this file")
flag.Parse()
coverages, err := compareAll()
if err != nil {
fail(err)
}
if *outputDir == "" {
if *outputDir == "" && *jsonOutput == "" {
fmt.Print(awsmodel.RenderCoverageSummary(coverages))
return
}
if err := awsmodel.WritePages(*outputDir, coverages); err != nil {
fail(err)
if *outputDir != "" {
if err := awsmodel.WritePages(*outputDir, coverages); err != nil {
fail(err)
}
}
if *jsonOutput != "" {
if err := awsmodel.WriteCoverageJSON(*jsonOutput, coverages); err != nil {
fail(err)
}
}
}
+2
View File
@@ -4,6 +4,8 @@ This directory contains the Spinifex documentation. This is available in individ
The [AWS API coverage](coverage/README.md) pages are generated from Spinifex's dispatch tables and the pinned AWS API models, one page per service. Run `make aws-model-coverage` to regenerate them and print a summary. Only `pages.json` and the `intro.md` files under `coverage/` are hand-written; every `README.md` there is generated and will be overwritten.
The same run writes [`coverage/coverage.json`](coverage/coverage.json), a versioned machine-readable model-versus-dispatch inventory. It is generated and must not be edited. Its operation states are structural: `implemented` means a registered action has neither a stub nor an explicit refusal. It is not a behavioural compatibility claim; conformance evidence and capability profiles establish that separately.
A published page names the operations Spinifex serves, and nothing else. It carries no percentage, no unimplemented operation and no operation the platform will never offer: the same run writes those to [`coverage/internal-report.md`](coverage/internal-report.md), which no slug in the docs site's configuration points at, so it stays internal. A `notApplicable` declaration in `pages.json` therefore publishes nothing; it keeps a permanent absence out of the report's candidate work.
## Frontmatter
+3 -3
View File
@@ -17,7 +17,7 @@ tags:
## Overview
The platform serves **381 operations** across the AWS APIs below. Every page names the operations its service implements from the pinned model, generated from the dispatch tables on each build rather than written by hand.
The platform serves **385 operations** across the AWS APIs below. Every page names the operations its service implements from the pinned model, generated from the dispatch tables on each build rather than written by hand.
| Service | Operations |
|---|---:|
@@ -29,6 +29,6 @@ The platform serves **381 operations** across the AWS APIs below. Every page nam
| [ELBv2](/coverage/elbv2) | 34 |
| [IAM](/coverage/iam) | 76 |
| [RDS](/coverage/rds) | 26 |
| [S3](/coverage/s3) | 19 |
| [S3](/coverage/s3) | 23 |
| [STS](/coverage/sts) | 5 |
| **Total** | **381** |
| **Total** | **385** |
File diff suppressed because it is too large Load Diff
+1 -5
View File
@@ -973,7 +973,7 @@ option-groups: Option groups configure engine add-ons for engines this platform
## s3
`2006-03-01` — 19 of 99 modelled operations implemented (19.2%), 0 stubbed, 6 not applicable, 0 registered outside the pinned model.
`2006-03-01` — 23 of 99 modelled operations implemented (23.2%), 0 stubbed, 6 not applicable, 0 registered outside the pinned model.
### Not implemented
@@ -987,7 +987,6 @@ option-groups: Option groups configure engine add-ons for engines this platform
- `DeleteBucketOwnershipControls`
- `DeleteBucketPolicy`
- `DeleteBucketReplication`
- `DeleteBucketTagging`
- `DeleteBucketWebsite`
- `DeleteObjectTagging`
- `DeletePublicAccessBlock`
@@ -999,7 +998,6 @@ option-groups: Option groups configure engine add-ons for engines this platform
- `GetBucketInventoryConfiguration`
- `GetBucketLifecycle`
- `GetBucketLifecycleConfiguration`
- `GetBucketLocation`
- `GetBucketLogging`
- `GetBucketMetricsConfiguration`
- `GetBucketNotification`
@@ -1008,7 +1006,6 @@ option-groups: Option groups configure engine add-ons for engines this platform
- `GetBucketPolicy`
- `GetBucketPolicyStatus`
- `GetBucketReplication`
- `GetBucketTagging`
- `GetBucketVersioning`
- `GetBucketWebsite`
- `GetObjectAcl`
@@ -1040,7 +1037,6 @@ option-groups: Option groups configure engine add-ons for engines this platform
- `PutBucketOwnershipControls`
- `PutBucketPolicy`
- `PutBucketReplication`
- `PutBucketTagging`
- `PutBucketVersioning`
- `PutBucketWebsite`
- `PutObjectAcl`
+5 -1
View File
@@ -18,7 +18,7 @@ tags:
## Overview
Predastore implements **19 operations** in the S3 `2006-03-01` API model.
Predastore implements **23 operations** in the S3 `2006-03-01` API model.
### Predastore serves this endpoint
@@ -40,8 +40,11 @@ That behaviour is measured separately, against the `ceph/s3-tests` suite Ceph RG
| `CreateBucket` |
| `CreateMultipartUpload` |
| `DeleteBucket` |
| `DeleteBucketTagging` |
| `DeleteObject` |
| `DeleteObjects` |
| `GetBucketLocation` |
| `GetBucketTagging` |
| `GetObject` |
| `HeadBucket` |
| `HeadObject` |
@@ -50,6 +53,7 @@ That behaviour is measured separately, against the `ceph/s3-tests` suite Ceph RG
| `ListObjects` |
| `ListObjectsV2` |
| `ListParts` |
| `PutBucketTagging` |
| `PutObject` |
| `UploadPart` |
| `UploadPartCopy` |
+92
View File
@@ -1,7 +1,9 @@
package awsmodel
import (
"encoding/json"
"fmt"
"os"
"sort"
"strings"
)
@@ -33,6 +35,42 @@ type OperationCoverage struct {
Note string
}
// CoverageJSONSchemaVersion identifies the generated coverage.json schema.
// Increment it only for an incompatible change to the JSON document.
const CoverageJSONSchemaVersion = 1
// coverageJSON is the generated, machine-readable form of the operation
// inventory. It deliberately reports dispatch state only: an operation in
// implemented has a registered, non-stubbed and non-refused dispatcher, but
// that alone does not establish behavioural AWS compatibility.
type coverageJSON struct {
SchemaVersion int `json:"schema_version"`
ModelSource coverageJSONModelSource `json:"model_source"`
Services []coverageJSONService `json:"services"`
}
type coverageJSONModelSource struct {
AWSSDKGoVersion string `json:"aws_sdk_go_version"`
}
type coverageJSONService struct {
Service string `json:"service"`
APIVersion string `json:"api_version"`
Opaque bool `json:"opaque"`
Note string `json:"note"`
Operations coverageJSONOperations `json:"operations"`
}
type coverageJSONOperations struct {
Modelled []string `json:"modelled"`
Registered []string `json:"registered"`
Implemented []string `json:"implemented"`
Stubbed []string `json:"stubbed"`
Unsupported []string `json:"unsupported"`
Missing []string `json:"missing"`
Extra []string `json:"extra"`
}
// CompareOperations compares one embedded service model with an authoritative
// gateway dispatch inventory.
func CompareOperations(service Service, dispatch DispatchInventory) (OperationCoverage, error) {
@@ -140,6 +178,60 @@ func RenderCoverageSummary(coverages []OperationCoverage) string {
return report.String()
}
// RenderCoverageJSON renders a deterministic, versioned inventory suitable
// for tools. The operation lists describe only the generated model-versus-
// dispatch comparison; consumers must establish behavioural support from
// their own evidence rather than inferring it from dispatch registration.
func RenderCoverageJSON(coverages []OperationCoverage) (string, error) {
document := coverageJSON{
SchemaVersion: CoverageJSONSchemaVersion,
ModelSource: coverageJSONModelSource{
AWSSDKGoVersion: SourceSDKVersion,
},
Services: make([]coverageJSONService, 0, len(coverages)),
}
for _, coverage := range sortedCoverages(coverages) {
document.Services = append(document.Services, coverageJSONService{
Service: string(coverage.Service),
APIVersion: coverage.APIVersion,
Opaque: coverage.Opaque,
Note: coverage.Note,
Operations: coverageJSONOperations{
Modelled: copiedOperations(coverage.Modelled),
Registered: copiedOperations(coverage.Registered),
Implemented: copiedOperations(coverage.Implemented),
Stubbed: copiedOperations(coverage.Stubbed),
Unsupported: copiedOperations(coverage.Unsupported),
Missing: copiedOperations(coverage.Missing),
Extra: copiedOperations(coverage.Extra),
},
})
}
contents, err := json.MarshalIndent(document, "", " ")
if err != nil {
return "", fmt.Errorf("awsmodel: marshal coverage JSON: %w", err)
}
return string(contents) + "\n", nil
}
// WriteCoverageJSON writes the generated machine-readable inventory to path.
func WriteCoverageJSON(path string, coverages []OperationCoverage) error {
contents, err := RenderCoverageJSON(coverages)
if err != nil {
return err
}
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
return fmt.Errorf("awsmodel: write coverage JSON: %w", err)
}
return nil
}
// copiedOperations represents an empty operation set as [] rather than null,
// so consumers can use every array field without a special case.
func copiedOperations(operations []string) []string {
return append([]string{}, operations...)
}
// RenderInternalReport renders the full comparison, including the operations
// the published pages leave out. It is not a site page: the docs build renders
// only the slugs it is configured with, and this file is not one of them.
+94
View File
@@ -0,0 +1,94 @@
package awsmodel
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestRenderCoverageJSONIsDeterministicAndPreservesDispatchStates(t *testing.T) {
sts, err := CompareOperations(STS, DispatchInventory{
Registered: []string{"AssumeRole", "GetCallerIdentity", "GetSessionToken"},
Stubbed: []string{"GetSessionToken"},
Unsupported: []string{"GetCallerIdentity"},
})
if err != nil {
t.Fatal(err)
}
iam, err := CompareOperations(IAM, DispatchInventory{})
if err != nil {
t.Fatal(err)
}
contents, err := RenderCoverageJSON([]OperationCoverage{sts, iam})
if err != nil {
t.Fatal(err)
}
var document struct {
SchemaVersion int `json:"schema_version"`
ModelSource struct {
AWSSDKGoVersion string `json:"aws_sdk_go_version"`
} `json:"model_source"`
Services []struct {
Service string `json:"service"`
Operations struct {
Implemented []string `json:"implemented"`
Stubbed []string `json:"stubbed"`
Unsupported []string `json:"unsupported"`
Registered []string `json:"registered"`
} `json:"operations"`
} `json:"services"`
}
if err := json.Unmarshal([]byte(contents), &document); err != nil {
t.Fatalf("generated JSON is invalid: %v\n%s", err, contents)
}
if document.SchemaVersion != CoverageJSONSchemaVersion {
t.Errorf("schema version = %d, want %d", document.SchemaVersion, CoverageJSONSchemaVersion)
}
if document.ModelSource.AWSSDKGoVersion != SourceSDKVersion {
t.Errorf("source version = %q, want %q", document.ModelSource.AWSSDKGoVersion, SourceSDKVersion)
}
if !strings.Contains(contents, `"registered": []`) {
t.Errorf("an empty operation set must be an array, not null:\n%s", contents)
}
if len(document.Services) != 2 || document.Services[0].Service != "iam" || document.Services[1].Service != "sts" {
t.Fatalf("services are not deterministic: %#v", document.Services)
}
states := document.Services[1].Operations
if got, want := strings.Join(states.Implemented, ","), "AssumeRole"; got != want {
t.Errorf("implemented = %q, want %q", got, want)
}
if got, want := strings.Join(states.Stubbed, ","), "GetSessionToken"; got != want {
t.Errorf("stubbed = %q, want %q", got, want)
}
if got, want := strings.Join(states.Unsupported, ","), "GetCallerIdentity"; got != want {
t.Errorf("unsupported = %q, want %q", got, want)
}
if got, want := strings.Join(states.Registered, ","), "AssumeRole,GetCallerIdentity,GetSessionToken"; got != want {
t.Errorf("registered = %q, want %q", got, want)
}
}
func TestWriteCoverageJSONWritesTheSameDocument(t *testing.T) {
coverage, err := CompareOperations(STS, DispatchInventory{Registered: []string{"AssumeRole"}})
if err != nil {
t.Fatal(err)
}
want, err := RenderCoverageJSON([]OperationCoverage{coverage})
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "coverage.json")
if err := WriteCoverageJSON(path, []OperationCoverage{coverage}); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(got) != want {
t.Errorf("written JSON differs from renderer:\n got: %s\nwant: %s", got, want)
}
}