feat: answer ECR DescribeRegistry with the caller's registry and an empty replication configuration (#1182)

This commit is contained in:
Julian Sommer
2026-10-07 12:06:17 +11:00
committed by GitHub
parent ff87f65a43
commit d7f095fd39
8 changed files with 101 additions and 10 deletions
+3 -3
View File
@@ -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** |
+1 -1
View File
@@ -1983,6 +1983,7 @@
"DeleteRepository",
"DeleteRepositoryPolicy",
"DescribeImages",
"DescribeRegistry",
"DescribeRepositories",
"GetAuthorizationToken",
"GetLifecyclePolicy",
@@ -2002,7 +2003,6 @@
"stubbed": [
"BatchCheckLayerAvailability",
"CompleteLayerUpload",
"DescribeRegistry",
"GetDownloadUrlForLayer",
"GetRegistryPolicy",
"InitiateLayerUpload",
+3 -2
View File
@@ -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` |
+1 -1
View File
@@ -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.
+1 -2
View File
@@ -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`
+1 -1
View File
@@ -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
+28
View File
@@ -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
}
+63
View File
@@ -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")
}