mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
docs: publish AWS operation coverage inventory (#1075)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user