diff --git a/docs/coverage/README.md b/docs/coverage/README.md index 07c9a6d09..d6acf04ad 100644 --- a/docs/coverage/README.md +++ b/docs/coverage/README.md @@ -17,13 +17,13 @@ tags: ## Overview -The platform serves **391 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 **392 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 | |---|---:| | [ACM](/coverage/acm) | 9 | | [EC2](/coverage/ec2) | 127 | -| [ECR](/coverage/ecr) | 22 | +| [ECR](/coverage/ecr) | 23 | | [ECS](/coverage/ecs) | 31 | | [EKS](/coverage/eks) | 27 | | [ELBv2](/coverage/elbv2) | 36 | @@ -31,4 +31,4 @@ The platform serves **391 operations** across the AWS APIs below. Every page nam | [RDS](/coverage/rds) | 27 | | [S3](/coverage/s3) | 26 | | [STS](/coverage/sts) | 5 | -| **Total** | **391** | +| **Total** | **392** | diff --git a/docs/coverage/coverage.json b/docs/coverage/coverage.json index 25b004a40..7c1e76f83 100644 --- a/docs/coverage/coverage.json +++ b/docs/coverage/coverage.json @@ -1983,6 +1983,7 @@ "DeleteRepository", "DeleteRepositoryPolicy", "DescribeImages", + "DescribeRegistry", "DescribeRepositories", "GetAuthorizationToken", "GetLifecyclePolicy", @@ -2002,7 +2003,6 @@ "stubbed": [ "BatchCheckLayerAvailability", "CompleteLayerUpload", - "DescribeRegistry", "GetDownloadUrlForLayer", "GetRegistryPolicy", "InitiateLayerUpload", diff --git a/docs/coverage/ecr/README.md b/docs/coverage/ecr/README.md index 0e5642494..eadbf73f3 100644 --- a/docs/coverage/ecr/README.md +++ b/docs/coverage/ecr/README.md @@ -18,7 +18,7 @@ tags: ## Overview -Spinifex implements **22 operations** in the ECR `2015-09-21` API model. +Spinifex implements **23 operations** in the ECR `2015-09-21` API model. ### Two endpoints, one registry @@ -26,7 +26,7 @@ Repository metadata is served over the AWS API on the gateway endpoint. Image da The split explains the stubs below. The layer-transfer operations — `BatchCheckLayerAvailability`, `InitiateLayerUpload`, `UploadLayerPart`, `CompleteLayerUpload` and `GetDownloadUrlForLayer` — are registered stubs because the `/v2/` endpoint carries that traffic instead. A client using `docker` or any OCI-compatible tool never calls them. -Registry replication is a stub for the same kind of reason: a deployment is a single registry, with no cross-region peer to replicate to. +Registry replication is a stub for the same kind of reason: a deployment is a single registry, with no cross-region peer to replicate to. `DescribeRegistry` reports this as a replication configuration with no rules. ### Operations @@ -39,6 +39,7 @@ Registry replication is a stub for the same kind of reason: a deployment is a si | `DeleteRepository` | | `DeleteRepositoryPolicy` | | `DescribeImages` | +| `DescribeRegistry` | | `DescribeRepositories` | | `GetAuthorizationToken` | | `GetLifecyclePolicy` | diff --git a/docs/coverage/ecr/intro.md b/docs/coverage/ecr/intro.md index 420d0453e..55e361ac1 100644 --- a/docs/coverage/ecr/intro.md +++ b/docs/coverage/ecr/intro.md @@ -4,4 +4,4 @@ Repository metadata is served over the AWS API on the gateway endpoint. Image da The split explains the stubs below. The layer-transfer operations — `BatchCheckLayerAvailability`, `InitiateLayerUpload`, `UploadLayerPart`, `CompleteLayerUpload` and `GetDownloadUrlForLayer` — are registered stubs because the `/v2/` endpoint carries that traffic instead. A client using `docker` or any OCI-compatible tool never calls them. -Registry replication is a stub for the same kind of reason: a deployment is a single registry, with no cross-region peer to replicate to. +Registry replication is a stub for the same kind of reason: a deployment is a single registry, with no cross-region peer to replicate to. `DescribeRegistry` reports this as a replication configuration with no rules. diff --git a/docs/coverage/internal-report.md b/docs/coverage/internal-report.md index 16ebb7672..010219672 100644 --- a/docs/coverage/internal-report.md +++ b/docs/coverage/internal-report.md @@ -762,7 +762,7 @@ spot-price: On owned hardware there is no spot-to-on-demand price differential, ## ecr -`2015-09-21` — 22 of 58 modelled operations implemented (37.9%), 11 stubbed, 0 not applicable, 3 registered outside the pinned model. +`2015-09-21` — 23 of 58 modelled operations implemented (39.7%), 10 stubbed, 0 not applicable, 3 registered outside the pinned model. ### Not implemented @@ -793,7 +793,6 @@ spot-price: On owned hardware there is no spot-to-on-demand price differential, - `BatchCheckLayerAvailability` - `CompleteLayerUpload` -- `DescribeRegistry` - `GetDownloadUrlForLayer` - `GetRegistryPolicy` - `InitiateLayerUpload` diff --git a/spinifex/gateway/ecrapi/handler.go b/spinifex/gateway/ecrapi/handler.go index 17830fb68..6692cb5c7 100644 --- a/spinifex/gateway/ecrapi/handler.go +++ b/spinifex/gateway/ecrapi/handler.go @@ -79,7 +79,7 @@ var Actions = map[string]Handler{ "DeleteRepositoryPolicy": DeleteRepositoryPolicy, "GetRegistryPolicy": NotImplemented, "PutRegistryPolicy": NotImplemented, - "DescribeRegistry": NotImplemented, + "DescribeRegistry": DescribeRegistry, // Image scanning is unsupported (no scanner backend), except that // PutImageScanningConfiguration accepts the scanOnPush=false no-op rather diff --git a/spinifex/gateway/ecrapi/registry.go b/spinifex/gateway/ecrapi/registry.go new file mode 100644 index 000000000..c3ae18c5c --- /dev/null +++ b/spinifex/gateway/ecrapi/registry.go @@ -0,0 +1,28 @@ +package gateway_ecrapi + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecr" + "github.com/nats-io/nats.go" +) + +// DescribeRegistry reports the caller's registry. A deployment has no +// replication, so the configuration is always present with an empty rules +// list, which is what AWS returns for an account with none configured. +func DescribeRegistry(_ context.Context, _ *nats.Conn, accountID string, body []byte) (any, error) { + if len(body) > 0 { + var req struct{} + if err := json.Unmarshal(body, &req); err != nil { + return nil, MalformedBodyError() + } + } + return &ecr.DescribeRegistryOutput{ + RegistryId: aws.String(accountID), + ReplicationConfiguration: &ecr.ReplicationConfiguration{ + Rules: []*ecr.ReplicationRule{}, + }, + }, nil +} diff --git a/spinifex/gateway/ecrapi/registry_test.go b/spinifex/gateway/ecrapi/registry_test.go new file mode 100644 index 000000000..343122661 --- /dev/null +++ b/spinifex/gateway/ecrapi/registry_test.go @@ -0,0 +1,63 @@ +package gateway_ecrapi_test + +import ( + "context" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/mulgadc/spinifex/spinifex/awserrors" + gateway_ecrapi "github.com/mulgadc/spinifex/spinifex/gateway/ecrapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// describeRegistryWire runs the dispatched DescribeRegistry handler and +// serialises its output the way the gateway does, returning the decoded body. +func describeRegistryWire(t *testing.T, accountID string, body []byte) map[string]any { + t.Helper() + out, err := gateway_ecrapi.Actions["DescribeRegistry"](context.Background(), nil, accountID, body) + require.NoError(t, err) + w := httptest.NewRecorder() + gateway_ecrapi.WriteJSONResponse(w, out) + var decoded map[string]any + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &decoded)) + return decoded +} + +// AWS answers an account with no replication with the configuration present +// and an empty rules list, not with the configuration omitted. +func TestDescribeRegistry_CallerAccountAndEmptyReplication(t *testing.T) { + for _, body := range []string{"", "{}", "null"} { + t.Run("body="+body, func(t *testing.T) { + decoded := describeRegistryWire(t, "123456789012", []byte(body)) + assert.Equal(t, map[string]any{ + "registryId": "123456789012", + "replicationConfiguration": map[string]any{"rules": []any{}}, + }, decoded) + }) + } +} + +func TestDescribeRegistry_ScopedToCaller(t *testing.T) { + a := describeRegistryWire(t, "111111111111", nil) + b := describeRegistryWire(t, "222222222222", nil) + assert.Equal(t, "111111111111", a["registryId"]) + assert.Equal(t, "222222222222", b["registryId"]) +} + +func TestDescribeRegistry_MalformedBody(t *testing.T) { + for _, body := range []string{"{", "[]", `"x"`} { + t.Run(body, func(t *testing.T) { + out, err := gateway_ecrapi.DescribeRegistry(context.Background(), nil, "123456789012", []byte(body)) + assert.Nil(t, out) + require.Error(t, err) + assert.Equal(t, awserrors.ValidErrorCodeFromError(gateway_ecrapi.MalformedBodyError()), + awserrors.ValidErrorCodeFromError(err)) + }) + } +} + +func TestDescribeRegistry_NotStubbed(t *testing.T) { + assert.NotContains(t, gateway_ecrapi.StubbedActionNames(), "DescribeRegistry") +}