fix: answer ECR validation refusals with InvalidParameterException (#1184)

This commit is contained in:
Julian Sommer
2026-10-07 12:10:08 +11:00
committed by GitHub
parent d7f095fd39
commit 98a3f2b721
22 changed files with 83 additions and 61 deletions
+3
View File
@@ -388,6 +388,9 @@ var (
// real ECS both use the restJson1 wire code "InvalidParameterException",
// so this gives EKS call sites their own name for the same registered code.
ErrorEKSInvalidParameter = ErrorECSInvalidParameter
// ErrorECRInvalidParameter aliases ErrorECSInvalidParameter: every ECR
// operation declares the AWS JSON 1.1 wire code "InvalidParameterException".
ErrorECRInvalidParameter = ErrorECSInvalidParameter
// ErrorACMResourceInUse aliases ErrorEKSResourceInUse: real ACM's
// DeleteCertificate and real EKS's CreateCluster both legitimately use the
// wire code "ResourceInUseException", so this is a distinct, self-documenting
+7
View File
@@ -460,6 +460,13 @@ func TestDispatch_InvalidRepoName(t *testing.T) {
assertCode(t, w, "NAME_INVALID")
}
func TestDispatch_RepeatedSeparatorRepoName(t *testing.T) {
reg := newTestRegistry()
dg := pushBlob(t, reg, "team/a__b--c", []byte("layer"))
w := do(reg, http.MethodHead, "/v2/team/a__b--c/blobs/"+dg, nil, nil)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestDispatch_UnknownPath(t *testing.T) {
reg := newTestRegistry()
w := do(reg, http.MethodGet, "/v2/team/app/frobnicate", nil, nil)
+1 -1
View File
@@ -66,7 +66,7 @@ func TestECRRequest_FieldSpelledTwoWaysIsRejected(t *testing.T) {
err := dispatchECR(t, gw, "BatchCheckLayerAvailability",
`{"repositoryName":"dev","RepositoryName":"prod"}`)
require.Error(t, err)
assert.Equal(t, awserrors.ErrorInvalidParameterValue, awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, awserrors.ErrorECRInvalidParameter, awserrors.ValidErrorCodeFromError(err))
}
}
@@ -110,7 +110,7 @@ func TestCreateRepository_Tags(t *testing.T) {
_, err = createRepo(t, gw, `{"repositoryName":"team/badtag","tags":[{"Key":"","Value":"x"}]}`)
require.Error(t, err)
assert.Equal(t, "InvalidParameterValue", awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, "InvalidParameterException", awserrors.ValidErrorCodeFromError(err))
}
func TestCreateRepository_EncryptionAndScanningConfiguration(t *testing.T) {
@@ -135,12 +135,12 @@ func TestCreateRepository_EncryptionAndScanningConfiguration(t *testing.T) {
require.Error(t, err)
code, message, ok := awserrors.ResolveErrorDetail(err)
require.True(t, ok)
assert.Equal(t, "InvalidParameterValue", code)
assert.Equal(t, "InvalidParameterException", code)
assert.NotEmpty(t, message)
_, err = createRepo(t, gw, `{"repositoryName":"team/bad","encryptionConfiguration":{"encryptionType":"bogus"}}`)
require.Error(t, err)
assert.Equal(t, "InvalidParameterValue", awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, "InvalidParameterException", awserrors.ValidErrorCodeFromError(err))
}
func TestCreateRepository_Errors(t *testing.T) {
@@ -152,8 +152,8 @@ func TestCreateRepository_Errors(t *testing.T) {
name, body, expect string
}{
{"already exists", `{"repositoryName":"team/app"}`, "RepositoryAlreadyExistsException"},
{"invalid name", `{"repositoryName":"Team/App"}`, "InvalidParameterValue"},
{"empty name", `{}`, "InvalidParameterValue"},
{"invalid name", `{"repositoryName":"Team/App"}`, "InvalidParameterException"},
{"empty name", `{}`, "InvalidParameterException"},
{"cross-account", `{"repositoryName":"team/x","registryId":"999999999999"}`, "AccessDenied"},
}
for _, tc := range cases {
@@ -172,7 +172,7 @@ func TestCreateRepository_BadNameCarriesAWSMessage(t *testing.T) {
require.Error(t, err)
code, message, ok := awserrors.ResolveErrorDetail(err)
require.True(t, ok)
assert.Equal(t, awserrors.ErrorInvalidParameterValue, code)
assert.Equal(t, awserrors.ErrorECRInvalidParameter, code)
assert.Equal(t, `Invalid parameter at 'repositoryName' failed to satisfy constraint: 'must satisfy regular expression '[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*''`, message)
}
@@ -191,7 +191,7 @@ func TestCreateRepository_NoAccountAndMalformed(t *testing.T) {
_, err = createRepo(t, gw, `{`)
require.Error(t, err)
assert.Equal(t, "InvalidParameterValue", awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, "InvalidParameterException", awserrors.ValidErrorCodeFromError(err))
}
func TestDeleteRepository_NoAccountAndMalformed(t *testing.T) {
@@ -203,7 +203,7 @@ func TestDeleteRepository_NoAccountAndMalformed(t *testing.T) {
_, err = deleteRepo(t, gw, `{`)
require.Error(t, err)
assert.Equal(t, "InvalidParameterValue", awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, "InvalidParameterException", awserrors.ValidErrorCodeFromError(err))
}
func TestDeleteRepository_Happy(t *testing.T) {
+2 -2
View File
@@ -136,7 +136,7 @@ func normalizeEncryptionType(cfg *encryptionConfigurationInput) (string, error)
case handlers_ecr.EncryptionTypeAES256:
return cfg.EncryptionType, nil
case handlers_ecr.EncryptionTypeKMS:
return "", awserrors.Errorf(awserrors.ErrorInvalidParameterValue,
return "", awserrors.Errorf(awserrors.ErrorECRInvalidParameter,
"encryptionType KMS is not supported: no customer-managed key is used, and repositories are already encrypted at rest under a server-managed AES-256 key")
default:
return "", gateway_ecrapi.EnumValueError("encryptionConfiguration.encryptionType", cfg.EncryptionType,
@@ -145,7 +145,7 @@ func normalizeEncryptionType(cfg *encryptionConfigurationInput) (string, error)
}
// normalizeTagMutability validates the requested mutability, defaulting an empty
// value to MUTABLE. An unknown value is rejected with InvalidParameterValue.
// value to MUTABLE. An unknown value is rejected with InvalidParameterException.
func normalizeTagMutability(v string) (string, error) {
switch v {
case "":
+3 -3
View File
@@ -147,7 +147,7 @@ func TestBatchGetImage_CapExceeded(t *testing.T) {
body := `{"repositoryName":"team/app","imageIds":[` + strings.Join(ids, ",") + `]}`
_, err := callImage(t, gw, (*GatewayConfig).handleBatchGetImage, body)
require.Error(t, err)
assert.Equal(t, "InvalidParameterValue", awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, "InvalidParameterException", awserrors.ValidErrorCodeFromError(err))
}
func TestPutImage_HappyAndMissingManifest(t *testing.T) {
@@ -175,10 +175,10 @@ func TestPutImage_HappyAndMissingManifest(t *testing.T) {
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &list))
assert.Len(t, list.ImageIds, 1)
// Missing manifest -> InvalidParameterValue.
// Missing manifest -> InvalidParameterException.
_, err = callImage(t, gw, (*GatewayConfig).handlePutImage, `{"repositoryName":"team/app","imageTag":"v2"}`)
require.Error(t, err)
assert.Equal(t, "InvalidParameterValue", awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, "InvalidParameterException", awserrors.ValidErrorCodeFromError(err))
}
// TestPutImage_RepoNotCreated asserts the JSON PutImage path rejects an uncreated
@@ -81,7 +81,7 @@ func TestLifecyclePreview_Errors(t *testing.T) {
// Malformed override policy.
_, err = callImage(t, gw, (*GatewayConfig).handleGetLifecyclePolicyPreview, previewBody("team/app", "not-json"))
require.Error(t, err)
assert.Equal(t, "InvalidParameterValue", awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, "InvalidParameterException", awserrors.ValidErrorCodeFromError(err))
// Cross-account.
_, err = callImage(t, gw, (*GatewayConfig).handleStartLifecyclePolicyPreview, `{"repositoryName":"team/app","registryId":"999999999999","lifecyclePolicyText":`+strconvQuotePreview(previewExpireOldest)+`}`)
+5 -5
View File
@@ -44,7 +44,7 @@ func TestCreateRepository_InvalidMutability(t *testing.T) {
gw, _ := newRepoLifecycleGateway(t)
_, err := createRepo(t, gw, `{"repositoryName":"team/app","imageTagMutability":"SOMETIMES"}`)
require.Error(t, err)
assert.Equal(t, "InvalidParameterValue", awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, "InvalidParameterException", awserrors.ValidErrorCodeFromError(err))
}
func TestPutImageTagMutability_Happy(t *testing.T) {
@@ -82,9 +82,9 @@ func TestPutImageTagMutability_Errors(t *testing.T) {
cases := []struct {
name, body, expect string
}{
{"missing value", `{"repositoryName":"team/app"}`, "InvalidParameterValue"},
{"invalid value", `{"repositoryName":"team/app","imageTagMutability":"NOPE"}`, "InvalidParameterValue"},
{"invalid name", `{"repositoryName":"Team/App","imageTagMutability":"IMMUTABLE"}`, "InvalidParameterValue"},
{"missing value", `{"repositoryName":"team/app"}`, "InvalidParameterException"},
{"invalid value", `{"repositoryName":"team/app","imageTagMutability":"NOPE"}`, "InvalidParameterException"},
{"invalid name", `{"repositoryName":"Team/App","imageTagMutability":"IMMUTABLE"}`, "InvalidParameterException"},
{"cross-account", `{"repositoryName":"team/app","registryId":"999999999999","imageTagMutability":"IMMUTABLE"}`, "AccessDenied"},
{"not found", `{"repositoryName":"team/ghost","imageTagMutability":"IMMUTABLE"}`, "RepositoryNotFoundException"},
}
@@ -106,7 +106,7 @@ func TestPutImageTagMutability_NoAccountAndMalformed(t *testing.T) {
_, err = putTagMutability(t, gw, `{`)
require.Error(t, err)
assert.Equal(t, "InvalidParameterValue", awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, "InvalidParameterException", awserrors.ValidErrorCodeFromError(err))
}
func TestECRRequest_PutImageTagMutabilityDispatched(t *testing.T) {
+1 -1
View File
@@ -124,7 +124,7 @@ func ResourceARNs(action, region, accountID string, body []byte) ([]string, erro
// Parse fails only on a field spelled two ways, which it cannot scope.
scope, err := bodyscope.Parse(action, body)
if err != nil {
return nil, awserrors.Errorf(awserrors.ErrorInvalidParameterValue,
return nil, awserrors.Errorf(awserrors.ErrorECRInvalidParameter,
"The request body names the same field more than once in different letter case")
}
+1 -1
View File
@@ -129,6 +129,6 @@ func TestResourceARNs_FieldSpelledTwoWaysIsRejected(t *testing.T) {
for range 50 {
_, err := gateway_ecrapi.ResourceARNs("DeleteRepository", testRegion, testAccountID,
[]byte(`{"repositoryName":"dev","RepositoryName":"prod"}`))
assert.Equal(t, awserrors.ErrorInvalidParameterValue, awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, awserrors.ErrorECRInvalidParameter, awserrors.ValidErrorCodeFromError(err))
}
}
@@ -63,8 +63,8 @@ func TestPutImageScanningConfiguration_Errors(t *testing.T) {
name, body, expect string
}{
{"missing repo", `{"repositoryName":"team/ghost"}`, awserrors.ErrorRepositoryNotFound},
{"empty name", `{}`, awserrors.ErrorInvalidParameterValue},
{"invalid name", `{"repositoryName":"Team/App"}`, awserrors.ErrorInvalidParameterValue},
{"empty name", `{}`, awserrors.ErrorECRInvalidParameter},
{"invalid name", `{"repositoryName":"Team/App"}`, awserrors.ErrorECRInvalidParameter},
{"cross-account", `{"repositoryName":"team/app","registryId":"999999999999"}`, awserrors.ErrorAccessDenied},
}
for _, tc := range cases {
+1 -1
View File
@@ -49,7 +49,7 @@ func resolveLifecycleRepo(ctx context.Context, nc *nats.Conn, accountID string,
// PutLifecyclePolicy validates and stores the lifecycle-policy document for a
// repository. The document is parsed by the evaluation engine; a malformed or
// unsupported rule is rejected with InvalidParameterValue.
// unsupported rule is rejected with InvalidParameterException.
func PutLifecyclePolicy(ctx context.Context, nc *nats.Conn, accountID string, body []byte) (any, error) {
req, store, err := resolveLifecycleRepo(ctx, nc, accountID, body)
if err != nil {
@@ -72,9 +72,9 @@ func TestLifecyclePolicy_Errors(t *testing.T) {
expect string
}{
{"put missing repo", PutLifecyclePolicy, `{"repositoryName":"team/ghost","lifecyclePolicyText":` + strconvQuote(validLifecyclePolicy) + `}`, awserrors.ErrorRepositoryNotFound},
{"put invalid json", PutLifecyclePolicy, `{"repositoryName":"team/app","lifecyclePolicyText":"not-json"}`, awserrors.ErrorInvalidParameterValue},
{"put bad rule", PutLifecyclePolicy, `{"repositoryName":"team/app","lifecyclePolicyText":"{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"tagged\",\"countType\":\"imageCountMoreThan\",\"countNumber\":1},\"action\":{\"type\":\"expire\"}}]}"}`, awserrors.ErrorInvalidParameterValue},
{"put empty name", PutLifecyclePolicy, `{"lifecyclePolicyText":` + strconvQuote(validLifecyclePolicy) + `}`, awserrors.ErrorInvalidParameterValue},
{"put invalid json", PutLifecyclePolicy, `{"repositoryName":"team/app","lifecyclePolicyText":"not-json"}`, awserrors.ErrorECRInvalidParameter},
{"put bad rule", PutLifecyclePolicy, `{"repositoryName":"team/app","lifecyclePolicyText":"{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"tagged\",\"countType\":\"imageCountMoreThan\",\"countNumber\":1},\"action\":{\"type\":\"expire\"}}]}"}`, awserrors.ErrorECRInvalidParameter},
{"put empty name", PutLifecyclePolicy, `{"lifecyclePolicyText":` + strconvQuote(validLifecyclePolicy) + `}`, awserrors.ErrorECRInvalidParameter},
{"put cross-account", PutLifecyclePolicy, `{"repositoryName":"team/app","registryId":"999999999999","lifecyclePolicyText":` + strconvQuote(validLifecyclePolicy) + `}`, awserrors.ErrorAccessDenied},
{"get no policy", GetLifecyclePolicy, `{"repositoryName":"team/app"}`, awserrors.ErrorLifecyclePolicyNotFound},
{"delete no policy", DeleteLifecyclePolicy, `{"repositoryName":"team/app"}`, awserrors.ErrorLifecyclePolicyNotFound},
@@ -105,8 +105,8 @@ func TestRepositoryPolicy_Errors(t *testing.T) {
expect string
}{
{"set missing repo", SetRepositoryPolicy, `{"repositoryName":"team/ghost","policyText":"{}"}`, awserrors.ErrorRepositoryNotFound},
{"set invalid json policy", SetRepositoryPolicy, `{"repositoryName":"team/app","policyText":"not-json"}`, awserrors.ErrorInvalidParameterValue},
{"set empty name", SetRepositoryPolicy, `{"policyText":"{}"}`, awserrors.ErrorInvalidParameterValue},
{"set invalid json policy", SetRepositoryPolicy, `{"repositoryName":"team/app","policyText":"not-json"}`, awserrors.ErrorECRInvalidParameter},
{"set empty name", SetRepositoryPolicy, `{"policyText":"{}"}`, awserrors.ErrorECRInvalidParameter},
{"set cross-account", SetRepositoryPolicy, `{"repositoryName":"team/app","registryId":"999999999999","policyText":"{}"}`, awserrors.ErrorAccessDenied},
{"get no policy", GetRepositoryPolicy, `{"repositoryName":"team/app"}`, awserrors.ErrorRepositoryPolicyNotFound},
{"delete no policy", DeleteRepositoryPolicy, `{"repositoryName":"team/app"}`, awserrors.ErrorRepositoryPolicyNotFound},
+4 -4
View File
@@ -124,14 +124,14 @@ func TestTagResource_Errors(t *testing.T) {
_, err := TagResource(context.Background(), nc, policyTestAccount,
[]byte(`{"resourceArn":"`+tagResourceARN("team/app")+`","tags":[]}`))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorInvalidParameterValue, awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, awserrors.ErrorECRInvalidParameter, awserrors.ValidErrorCodeFromError(err))
})
t.Run("malformed arn", func(t *testing.T) {
_, err := TagResource(context.Background(), nc, policyTestAccount,
[]byte(`{"resourceArn":"not-an-arn","tags":[{"Key":"env","Value":"prod"}]}`))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorInvalidParameterValue, awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, awserrors.ErrorECRInvalidParameter, awserrors.ValidErrorCodeFromError(err))
})
t.Run("repo not found", func(t *testing.T) {
@@ -145,7 +145,7 @@ func TestTagResource_Errors(t *testing.T) {
_, err := TagResource(context.Background(), nc, policyTestAccount,
[]byte(`{"resourceArn":"`+tagResourceARN("team/app")+`","tags":[{"Key":"","Value":"prod"}]}`))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorInvalidParameterValue, awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, awserrors.ErrorECRInvalidParameter, awserrors.ValidErrorCodeFromError(err))
})
}
@@ -155,5 +155,5 @@ func TestUntagResource_Errors(t *testing.T) {
_, err := UntagResource(context.Background(), nc, policyTestAccount,
[]byte(`{"resourceArn":"`+tagResourceARN("team/app")+`","tagKeys":[]}`))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorInvalidParameterValue, awserrors.ValidErrorCodeFromError(err))
assert.Equal(t, awserrors.ErrorECRInvalidParameter, awserrors.ValidErrorCodeFromError(err))
}
+10 -16
View File
@@ -10,64 +10,58 @@ import (
handlers_ecr "github.com/mulgadc/spinifex/spinifex/handlers/ecr"
)
// repositoryNamePattern is the expression AWS names when it refuses a
// repositoryName.
const repositoryNamePattern = `[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*`
const (
minRepositoryNameLength = 2
maxRepositoryNameLength = 256
)
// ValidateRepositoryName returns InvalidParameterValue naming the first
// ValidateRepositoryName returns InvalidParameterException naming the first
// constraint name fails, or nil.
func ValidateRepositoryName(name string) error {
switch {
case name == "":
return RequiredParameterError("repositoryName")
case len(name) < minRepositoryNameLength:
return awserrors.Errorf(awserrors.ErrorInvalidParameterValue,
return awserrors.Errorf(awserrors.ErrorECRInvalidParameter,
"1 validation error detected: Value '%s' at 'repositoryName' failed to satisfy constraint: Member must have length greater than or equal to %d",
name, minRepositoryNameLength)
case len(name) > maxRepositoryNameLength:
return awserrors.Errorf(awserrors.ErrorInvalidParameterValue,
"1 validation error detected: Value '%s' at 'repositoryName' failed to satisfy constraint: Member must have length less than or equal to %d",
name, maxRepositoryNameLength)
return ConstraintError("repositoryName", fmt.Sprintf("must have length less than or equal to %d", maxRepositoryNameLength))
case handlers_ecr.ValidateRepoName(name) != nil:
return ConstraintError("repositoryName", "must satisfy regular expression '"+repositoryNamePattern+"'")
return ConstraintError("repositoryName", "must satisfy regular expression '"+handlers_ecr.RepoNamePattern+"'")
}
return nil
}
// RequiredParameterError refuses a request that omits field.
func RequiredParameterError(field string) error {
return awserrors.Errorf(awserrors.ErrorInvalidParameterValue,
return awserrors.Errorf(awserrors.ErrorECRInvalidParameter,
"1 validation error detected: Value null at '%s' failed to satisfy constraint: Member must not be null", field)
}
// EnumValueError refuses value for field, listing the values accepted.
func EnumValueError(field, value string, allowed ...string) error {
return awserrors.Errorf(awserrors.ErrorInvalidParameterValue,
return awserrors.Errorf(awserrors.ErrorECRInvalidParameter,
"1 validation error detected: Value '%s' at '%s' failed to satisfy constraint: Member must satisfy enum value set: [%s]",
value, field, strings.Join(allowed, ", "))
}
// MaxItemsError refuses a list field carrying more than limit members.
func MaxItemsError(field string, limit int) error {
return awserrors.Errorf(awserrors.ErrorInvalidParameterValue,
return awserrors.Errorf(awserrors.ErrorECRInvalidParameter,
"1 validation error detected: Value at '%s' failed to satisfy constraint: Member must have length less than or equal to %d", field, limit)
}
// ConstraintError refuses field for failing constraint.
func ConstraintError(field, constraint string) error {
return awserrors.Errorf(awserrors.ErrorInvalidParameterValue,
return awserrors.Errorf(awserrors.ErrorECRInvalidParameter,
"Invalid parameter at '%s' failed to satisfy constraint: '%s'", field, constraint)
}
// MalformedBodyError refuses a body that does not decode into the action's
// input shape.
func MalformedBodyError() error {
return awserrors.Errorf(awserrors.ErrorInvalidParameterValue,
return awserrors.Errorf(awserrors.ErrorECRInvalidParameter,
"The request body is not valid JSON or does not match the input shape of the action")
}
@@ -79,7 +73,7 @@ func ValidateTags(tags []*ecr.Tag) error {
return RequiredParameterError(field)
}
if aws.StringValue(t.Key) == "" {
return awserrors.Errorf(awserrors.ErrorInvalidParameterValue,
return awserrors.Errorf(awserrors.ErrorECRInvalidParameter,
"1 validation error detected: Value '' at '%s' failed to satisfy constraint: Member must have length greater than or equal to 1", field)
}
}
+9 -3
View File
@@ -21,7 +21,7 @@ func requireInvalidParameter(t *testing.T, err error, wantMessage string) {
require.Error(t, err)
code, message, ok := awserrors.ResolveErrorDetail(err)
require.True(t, ok)
assert.Equal(t, awserrors.ErrorInvalidParameterValue, code)
assert.Equal(t, awserrors.ErrorECRInvalidParameter, code)
assert.Equal(t, wantMessage, message)
}
@@ -33,14 +33,20 @@ func TestValidateRepositoryName(t *testing.T) {
{"uppercase", "Team/App", awsRepositoryNameMessage},
{"missing", "", "1 validation error detected: Value null at 'repositoryName' failed to satisfy constraint: Member must not be null"},
{"too short", "a", "1 validation error detected: Value 'a' at 'repositoryName' failed to satisfy constraint: Member must have length greater than or equal to 2"},
{"too long", strings.Repeat("a", 257), "1 validation error detected: Value '" + strings.Repeat("a", 257) + "' at 'repositoryName' failed to satisfy constraint: Member must have length less than or equal to 256"},
{"too long", strings.Repeat("a", 257), "Invalid parameter at 'repositoryName' failed to satisfy constraint: 'must have length less than or equal to 256'"},
{"triple underscore", "a___b", awsRepositoryNameMessage},
{"underscore then hyphen", "a_-b", awsRepositoryNameMessage},
{"leading separator", "_a", awsRepositoryNameMessage},
{"trailing separator", "a_", awsRepositoryNameMessage},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
requireInvalidParameter(t, gateway_ecrapi.ValidateRepositoryName(tc.input), tc.want)
})
}
assert.NoError(t, gateway_ecrapi.ValidateRepositoryName("team/app"))
for _, name := range []string{"team/app", "a__b", "a--b", "a---b", "a/b__c", strings.Repeat("a", 256)} {
assert.NoError(t, gateway_ecrapi.ValidateRepositoryName(name), "expected %q valid", name)
}
}
func TestValidateTags(t *testing.T) {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
// ErrInvalidLifecyclePolicy is returned when a lifecycle-policy document is
// malformed or violates the supported rule schema. The gateway maps it to
// InvalidParameterValue.
// InvalidParameterException.
var ErrInvalidLifecyclePolicy = errors.New("ecr: invalid lifecycle policy")
// Lifecycle tag-status selectors and count types (AWS lifecycle-policy schema).
+2 -2
View File
@@ -33,11 +33,11 @@ func TestRepoMeta_EncryptionTypeOrDefault(t *testing.T) {
}
func TestValidateRepoName(t *testing.T) {
valid := []string{"ab", "team/app", "a/b/c", "my-repo.name_1", "x0/y1/z2"}
valid := []string{"ab", "team/app", "a/b/c", "my-repo.name_1", "x0/y1/z2", "a__b", "a--b", "a---b", "a/b__c"}
for _, n := range valid {
assert.NoError(t, ValidateRepoName(n), "expected %q valid", n)
}
invalid := []string{"A", "Team/App", "a", "/leading", "trailing/", "a//b", strings.Repeat("a", 257)}
invalid := []string{"A", "Team/App", "a", "/leading", "trailing/", "a//b", strings.Repeat("a", 257), "a___b", "a_-b", "_a", "a_", "a..b", "Bad_Name!"}
for _, n := range invalid {
assert.Error(t, ValidateRepoName(n), "expected %q invalid", n)
}
+7 -4
View File
@@ -139,11 +139,14 @@ func DigestToken(digest string) string {
return strings.ReplaceAll(digest, ":", "-")
}
// repoNameRe is the OCI Distribution repository-name grammar. Compiled once.
var repoNameRe = regexp.MustCompile(`^(?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)*[a-z0-9]+(?:[._-][a-z0-9]+)*$`)
// RepoNamePattern is the repository-name expression AWS ECR enforces, which is
// also the OCI Distribution grammar: components split by '.', '_', '__' or '-'
// runs, joined by '/'.
const RepoNamePattern = `[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*`
// ValidateRepoName enforces the OCI repository-name grammar and 2-256 length
// bound. It returns nil for a valid name and a descriptive error otherwise.
var repoNameRe = regexp.MustCompile(`^(?:` + RepoNamePattern + `)$`)
// ValidateRepoName enforces RepoNamePattern and the 2-256 length bound. It returns nil for a valid name and a descriptive error otherwise.
func ValidateRepoName(name string) error {
if len(name) < 2 || len(name) > 256 {
return errors.New("repository name must be 2-256 characters")
@@ -41,7 +41,7 @@ The 2026-09-29 baseline sent 4047 requests across 371 operations: 980 pass, 506
|---|---:|---|
| A request breaking a model constraint is accepted | 420 | ECS 116, IAM 112, ACM 56, ELBv2 48, ECR 47, EC2 22, RDS 7, STS 6, EKS 6. Examples: ACM and ELBv2 accept a tag without `Key`, as IAM did; EC2 network `Describe*` accepted out-of-range `MaxResults`; ECS `CreateCapacityProvider` accepts every enum and range break inside `autoScalingGroupProvider` and `managedInstancesProvider`; ACM `RequestCertificate` accepts every break inside `DomainValidationOptions`; STS `GetSessionToken` accepted out-of-range `DurationSeconds` and a `SerialNumber` and `TokenCode` below their minimum length, and still accepts an out-of-range `MinimumSessionTokenSize`. |
| A model-valid request is refused as invalid | 74 | RDS 42, ECS 10, STS 10, EC2 8, EKS 2, IAM 2. Mostly deliberate limitations that say so: RDS `CreateDBInstance`, `ModifyDBInstance` and `RestoreDBInstanceFromDBSnapshot` members, STS session policies, tags and MFA, ECS `availabilityZoneRebalancing`. Others return a generic message whose cause is unresolved (ECS task and task-definition operations, EC2 `Modify*Attribute`), or add a member next to a seeded one that AWS may refuse in combination, such as EC2 `CreateCapacityReservation` `AvailabilityZoneId` beside `AvailabilityZone`. |
| A refusal uses an undeclared error code | 96 | ECR (90) and EKS (2) answer with `InvalidParameterValueException`; both models declare `InvalidParameterException`. IAM (4) answered `CreateGroup` and `Put*Policy` name-length breaks with `InvalidInput`, which those operations do not declare; they now answer `ValidationError`, as AWS does. |
| A refusal uses an undeclared error code | 96 | EKS (2) answers with `InvalidParameterValueException`; its model declares `InvalidParameterException`. ECR (90) answered with `InvalidParameterValueException` too; it now answers `InvalidParameterException`, as AWS's `CreateRepository` does for a name that breaks the pattern. IAM (4) answered `CreateGroup` and `Put*Policy` name-length breaks with `InvalidInput`, which those operations do not declare; they now answer `ValidationError`, as AWS does. |
How the sweep reaches its targets:
+9
View File
@@ -9,6 +9,7 @@ import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -33,6 +34,14 @@ func TestECRControlPlane(t *testing.T) {
_, err := c.CreateRepository(&ecr.CreateRepositoryInput{RepositoryName: aws.String(repo)})
require.NoError(t, err, "create-repository")
t.Run("an invalid name is refused with InvalidParameterException", func(t *testing.T) {
_, err := c.CreateRepository(&ecr.CreateRepositoryInput{RepositoryName: aws.String("Bad_Name!")})
var aerr awserr.Error
require.ErrorAs(t, err, &aerr)
assert.Equal(t, ecr.ErrCodeInvalidParameterException, aerr.Code())
assert.Equal(t, `Invalid parameter at 'repositoryName' failed to satisfy constraint: 'must satisfy regular expression '[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*''`, aerr.Message())
})
t.Run("DescribeRepositories lists the new repo", func(t *testing.T) {
out, err := c.DescribeRepositories(&ecr.DescribeRepositoriesInput{
RepositoryNames: []*string{aws.String(repo)},