From c558872e8b9e6166c998bb5b20b15dead164d84a Mon Sep 17 00:00:00 2001 From: joshsiv-mulga Date: Thu, 1 Oct 2026 15:20:49 +1000 Subject: [PATCH] refactor: move ecr create action to domain --- .../domains/ecr/awsapi/create_repository.go | 141 ++++++++++++++++++ .../ecr/awsapi/create_repository_test.go | 54 +++++++ .../ecr/awsapi/describe_repositories_test.go | 72 ++------- .../ecr/awsapi/repository_action_test.go | 65 ++++++++ spinifex/gateway/ecr_createrepository.go | 134 +---------------- 5 files changed, 275 insertions(+), 191 deletions(-) create mode 100644 spinifex/domains/ecr/awsapi/create_repository.go create mode 100644 spinifex/domains/ecr/awsapi/create_repository_test.go create mode 100644 spinifex/domains/ecr/awsapi/repository_action_test.go diff --git a/spinifex/domains/ecr/awsapi/create_repository.go b/spinifex/domains/ecr/awsapi/create_repository.go new file mode 100644 index 000000000..919c92309 --- /dev/null +++ b/spinifex/domains/ecr/awsapi/create_repository.go @@ -0,0 +1,141 @@ +package awsapi + +import ( + "context" + "encoding/json" + "errors" + "log/slog" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecr" + handlers_ecr "github.com/mulgadc/spinifex/spinifex/domains/ecr" + "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" + "github.com/nats-io/nats.go" +) + +// createRepositoryRequest is the camelCase AWS JSON 1.1 input shape. ecr.Tag +// carries no locationName, so its wire keys are Key/Value (capitalised); using +// the SDK type preserves that spelling rather than reimplementing it. +type createRepositoryRequest struct { + RepositoryName string `json:"repositoryName"` + RegistryID string `json:"registryId"` + ImageTagMutability string `json:"imageTagMutability"` + Tags []*ecr.Tag `json:"tags"` + EncryptionConfiguration *encryptionConfigurationInput `json:"encryptionConfiguration"` + ImageScanningConfiguration *imageScanningConfigurationInput `json:"imageScanningConfiguration"` +} + +// encryptionConfigurationInput is the camelCase input shape for +// encryptionConfiguration. kmsKey is not decoded: KMS is rejected outright, +// so any key supplied alongside it is never used. +type encryptionConfigurationInput struct { + EncryptionType string `json:"encryptionType"` +} + +// CreateRepository creates only the account-scoped ECR metadata record. The +// Predastore bucket remains lazy until the first image push. An omitted +// mutability value defaults to MUTABLE and an omitted encryption type defaults +// to AES256. +// +// The advertised endpoint is deployment composition rather than ECR resource +// state, so the caller supplies it to project the AWS response. +func CreateRepository(ctx context.Context, nc *nats.Conn, endpoint RepositoryEndpoint, accountID string, body []byte) (*ecr.CreateRepositoryOutput, error) { + var req createRepositoryRequest + if err := json.Unmarshal(body, &req); err != nil { + return nil, MalformedBodyError() + } + if err := ValidateRepositoryName(req.RepositoryName); err != nil { + return nil, err + } + if req.RegistryID != "" && req.RegistryID != accountID { + return nil, errors.New(awserrors.ErrorAccessDenied) + } + mutability, err := normalizeTagMutability(req.ImageTagMutability) + if err != nil { + return nil, err + } + encryptionType, err := normalizeEncryptionType(req.EncryptionConfiguration) + if err != nil { + return nil, err + } + tags, err := tagMapFromInput(req.Tags) + if err != nil { + return nil, err + } + + store := handlers_ecr.NewNATSMetaStore(nc) + if _, err := store.GetRepo(ctx, accountID, req.RepositoryName); err == nil { + return nil, errors.New(awserrors.ErrorRepositoryAlreadyExists) + } else if !errors.Is(err, handlers_ecr.ErrNotFound) { + slog.ErrorContext(ctx, "ECR CreateRepository: get repository failed", "repository", req.RepositoryName, "err", err) + return nil, errors.New(awserrors.ErrorServerInternal) + } + + meta := handlers_ecr.RepoMeta{ + Name: req.RepositoryName, + CreatedAt: time.Now().UTC(), + ImageTagMutability: mutability, + EncryptionType: encryptionType, + ScanOnPush: req.ImageScanningConfiguration != nil && req.ImageScanningConfiguration.ScanOnPush, + Tags: tags, + } + if err := store.PutRepo(ctx, accountID, meta); err != nil { + slog.ErrorContext(ctx, "ECR CreateRepository: put repository failed", "repository", req.RepositoryName, "err", err) + return nil, errors.New(awserrors.ErrorServerInternal) + } + + return &ecr.CreateRepositoryOutput{ + Repository: endpoint.RepositoryFromMeta(accountID, req.RepositoryName, meta), + }, nil +} + +// tagMapFromInput folds the create-time tag list into RepoMeta's map, rejecting +// an empty key exactly as TagResource does. An absent list is nil so the record +// round-trips identically to one created before tags were supported. +func tagMapFromInput(in []*ecr.Tag) (map[string]string, error) { + if len(in) == 0 { + return nil, nil + } + if err := ValidateTags(in); err != nil { + return nil, err + } + out := make(map[string]string, len(in)) + for _, tag := range in { + out[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value) + } + return out, nil +} + +// normalizeEncryptionType validates the requested encryption configuration, +// defaulting an absent one to AES256. KMS is rejected because no +// customer-managed key material is implemented. +func normalizeEncryptionType(cfg *encryptionConfigurationInput) (string, error) { + if cfg == nil || cfg.EncryptionType == "" { + return handlers_ecr.EncryptionTypeAES256, nil + } + switch cfg.EncryptionType { + case handlers_ecr.EncryptionTypeAES256: + return cfg.EncryptionType, nil + case handlers_ecr.EncryptionTypeKMS: + return "", awserrors.Errorf(awserrors.ErrorInvalidParameterValue, + "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 "", EnumValueError("encryptionConfiguration.encryptionType", cfg.EncryptionType, + handlers_ecr.EncryptionTypeAES256, handlers_ecr.EncryptionTypeKMS) + } +} + +// normalizeTagMutability validates the create-time mutability, defaulting an +// empty value to MUTABLE. +func normalizeTagMutability(value string) (string, error) { + switch value { + case "": + return handlers_ecr.TagMutabilityMutable, nil + case handlers_ecr.TagMutabilityMutable, handlers_ecr.TagMutabilityImmutable: + return value, nil + default: + return "", EnumValueError("imageTagMutability", value, + handlers_ecr.TagMutabilityMutable, handlers_ecr.TagMutabilityImmutable) + } +} diff --git a/spinifex/domains/ecr/awsapi/create_repository_test.go b/spinifex/domains/ecr/awsapi/create_repository_test.go new file mode 100644 index 000000000..7a556750a --- /dev/null +++ b/spinifex/domains/ecr/awsapi/create_repository_test.go @@ -0,0 +1,54 @@ +package awsapi + +import ( + "context" + "testing" + + handlers_ecr "github.com/mulgadc/spinifex/spinifex/domains/ecr" + "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateRepository_PersistsConfiguredMetadata(t *testing.T) { + nc := newRepositoryActionTestConn(t) + body := []byte(`{"repositoryName":"team/app","imageTagMutability":"IMMUTABLE","tags":[{"Key":"environment","Value":"test"}],"imageScanningConfiguration":{"scanOnPush":true}}`) + + out, err := CreateRepository(context.Background(), nc, repositoryActionEndpoint(), repositoryActionTestAccount, body) + require.NoError(t, err) + require.NotNil(t, out.Repository) + assert.Equal(t, "team/app", *out.Repository.RepositoryName) + assert.Equal(t, "IMMUTABLE", *out.Repository.ImageTagMutability) + assert.True(t, *out.Repository.ImageScanningConfiguration.ScanOnPush) + + meta, err := handlers_ecr.NewNATSMetaStore(nc).GetRepo(context.Background(), repositoryActionTestAccount, "team/app") + require.NoError(t, err) + assert.Equal(t, handlers_ecr.TagMutabilityImmutable, meta.ImageTagMutability) + assert.Equal(t, map[string]string{"environment": "test"}, meta.Tags) + assert.True(t, meta.ScanOnPush) +} + +func TestCreateRepository_RejectsInvalidRequestsAndDuplicates(t *testing.T) { + nc := newRepositoryActionTestConn(t) + _, err := CreateRepository(context.Background(), nc, repositoryActionEndpoint(), repositoryActionTestAccount, []byte(`{"repositoryName":"team/app"}`)) + require.NoError(t, err) + + cases := []struct { + name string + body string + code string + }{ + {"duplicate", `{"repositoryName":"team/app"}`, awserrors.ErrorRepositoryAlreadyExists}, + {"malformed", `{`, awserrors.ErrorInvalidParameterValue}, + {"missing name", `{}`, awserrors.ErrorInvalidParameterValue}, + {"cross account", `{"repositoryName":"team/other","registryId":"999999999999"}`, awserrors.ErrorAccessDenied}, + {"unsupported KMS", `{"repositoryName":"team/other","encryptionConfiguration":{"encryptionType":"KMS"}}`, awserrors.ErrorInvalidParameterValue}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := CreateRepository(context.Background(), nc, repositoryActionEndpoint(), repositoryActionTestAccount, []byte(tc.body)) + require.Error(t, err) + assert.Equal(t, tc.code, awserrors.ValidErrorCodeFromError(err)) + }) + } +} diff --git a/spinifex/domains/ecr/awsapi/describe_repositories_test.go b/spinifex/domains/ecr/awsapi/describe_repositories_test.go index e7a9f8819..4fb426fab 100644 --- a/spinifex/domains/ecr/awsapi/describe_repositories_test.go +++ b/spinifex/domains/ecr/awsapi/describe_repositories_test.go @@ -2,71 +2,19 @@ package awsapi import ( "context" - "encoding/json" "testing" - "time" - "github.com/mulgadc/spinifex/internal/testkit" - handlers_ecr "github.com/mulgadc/spinifex/spinifex/domains/ecr" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - "github.com/mulgadc/spinifex/spinifex/utils" - "github.com/nats-io/nats.go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -const describeRepositoriesTestAccount = "000000000000" - -func newDescribeRepositoriesTestConn(t *testing.T) *nats.Conn { - t.Helper() - _, nc, _ := testutil.StartTestJetStream(t) - js := testutil.NewJetStream(t, nc) - svc := handlers_ecr.NewKVMetaService(js) - serveDescribeRepositoriesMeta(t, nc, handlers_ecr.SubjectRepoCreate, svc.RepoCreate) - serveDescribeRepositoriesMeta(t, nc, handlers_ecr.SubjectRepoDescribe, svc.RepoDescribe) - serveDescribeRepositoriesMeta(t, nc, handlers_ecr.SubjectRepoList, svc.RepoList) - return nc -} - -func serveDescribeRepositoriesMeta[I any, O any](t *testing.T, nc *nats.Conn, subject string, fn func(context.Context, *I, string) (*O, error)) { - t.Helper() - sub, err := nc.Subscribe(subject, func(msg *nats.Msg) { - accountID := utils.AccountIDFromMsg(msg) - in := new(I) - if errResp := utils.UnmarshalJsonPayload(in, msg.Data); errResp != nil { - _ = msg.Respond(errResp) - return - } - out, err := fn(context.Background(), in, accountID) - if err != nil { - _ = msg.Respond(utils.GenerateErrorPayload("ServerInternal")) - return - } - data, _ := json.Marshal(out) - _ = msg.Respond(data) - }) - require.NoError(t, err) - t.Cleanup(func() { _ = sub.Unsubscribe() }) -} - -func seedDescribeRepository(t *testing.T, nc *nats.Conn, name string) { - t.Helper() - store := handlers_ecr.NewNATSMetaStore(nc) - require.NoError(t, store.PutRepo(context.Background(), describeRepositoriesTestAccount, handlers_ecr.RepoMeta{ - Name: name, CreatedAt: time.Now(), - })) -} - -func describeRepositoriesEndpoint() RepositoryEndpoint { - return RepositoryEndpoint{Region: "ap-southeast-2", ServicesDomain: "spinifex.test"} -} - func TestDescribeRepositories_ListsAccountScoped(t *testing.T) { - nc := newDescribeRepositoriesTestConn(t) - seedDescribeRepository(t, nc, "team/app") - seedDescribeRepository(t, nc, "team/web") + nc := newRepositoryActionTestConn(t) + seedRepositoryForAction(t, nc, "team/app") + seedRepositoryForAction(t, nc, "team/web") - out, err := DescribeRepositories(context.Background(), nc, describeRepositoriesEndpoint(), describeRepositoriesTestAccount, []byte(`{}`)) + out, err := DescribeRepositories(context.Background(), nc, repositoryActionEndpoint(), repositoryActionTestAccount, []byte(`{}`)) require.NoError(t, err) require.Len(t, out.Repositories, 2) @@ -75,14 +23,14 @@ func TestDescribeRepositories_ListsAccountScoped(t *testing.T) { byName[*repository.RepositoryName] = i } app := out.Repositories[byName["team/app"]] - assert.Equal(t, describeRepositoriesTestAccount, *app.RegistryId) - assert.Equal(t, "arn:aws:ecr:ap-southeast-2:"+describeRepositoriesTestAccount+":repository/team/app", *app.RepositoryArn) - assert.Equal(t, describeRepositoriesTestAccount+".dkr.ecr.ap-southeast-2.spinifex.test/team/app", *app.RepositoryUri) + assert.Equal(t, repositoryActionTestAccount, *app.RegistryId) + assert.Equal(t, "arn:aws:ecr:ap-southeast-2:"+repositoryActionTestAccount+":repository/team/app", *app.RepositoryArn) + assert.Equal(t, repositoryActionTestAccount+".dkr.ecr.ap-southeast-2.spinifex.test/team/app", *app.RepositoryUri) } func TestDescribeRepositories_ValidatesRequestAndRepository(t *testing.T) { - nc := newDescribeRepositoriesTestConn(t) - seedDescribeRepository(t, nc, "team/app") + nc := newRepositoryActionTestConn(t) + seedRepositoryForAction(t, nc, "team/app") cases := []struct { name string @@ -95,7 +43,7 @@ func TestDescribeRepositories_ValidatesRequestAndRepository(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - _, err := DescribeRepositories(context.Background(), nc, describeRepositoriesEndpoint(), describeRepositoriesTestAccount, []byte(tc.body)) + _, err := DescribeRepositories(context.Background(), nc, repositoryActionEndpoint(), repositoryActionTestAccount, []byte(tc.body)) require.Error(t, err) assert.Equal(t, tc.code, awserrors.ValidErrorCodeFromError(err)) }) diff --git a/spinifex/domains/ecr/awsapi/repository_action_test.go b/spinifex/domains/ecr/awsapi/repository_action_test.go new file mode 100644 index 000000000..b706a9150 --- /dev/null +++ b/spinifex/domains/ecr/awsapi/repository_action_test.go @@ -0,0 +1,65 @@ +package awsapi + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/mulgadc/spinifex/internal/testkit" + handlers_ecr "github.com/mulgadc/spinifex/spinifex/domains/ecr" + "github.com/mulgadc/spinifex/spinifex/utils" + "github.com/nats-io/nats.go" + "github.com/stretchr/testify/require" +) + +// repositoryActionTestAccount is the account served by the direct repository +// AWS-action tests. It is intentionally separate from gateway auth tests. +const repositoryActionTestAccount = "000000000000" + +// newRepositoryActionTestConn provides only the metadata subjects needed by +// repository actions. It keeps those action tests independent of gateway HTTP +// construction while exercising the deployed NATS request contract. +func newRepositoryActionTestConn(t *testing.T) *nats.Conn { + t.Helper() + _, nc, _ := testutil.StartTestJetStream(t) + js := testutil.NewJetStream(t, nc) + svc := handlers_ecr.NewKVMetaService(js) + serveRepositoryActionMeta(t, nc, handlers_ecr.SubjectRepoCreate, svc.RepoCreate) + serveRepositoryActionMeta(t, nc, handlers_ecr.SubjectRepoDescribe, svc.RepoDescribe) + serveRepositoryActionMeta(t, nc, handlers_ecr.SubjectRepoList, svc.RepoList) + return nc +} + +func serveRepositoryActionMeta[I any, O any](t *testing.T, nc *nats.Conn, subject string, fn func(context.Context, *I, string) (*O, error)) { + t.Helper() + sub, err := nc.Subscribe(subject, func(msg *nats.Msg) { + accountID := utils.AccountIDFromMsg(msg) + in := new(I) + if errResp := utils.UnmarshalJsonPayload(in, msg.Data); errResp != nil { + _ = msg.Respond(errResp) + return + } + out, err := fn(context.Background(), in, accountID) + if err != nil { + _ = msg.Respond(utils.GenerateErrorPayload("ServerInternal")) + return + } + data, _ := json.Marshal(out) + _ = msg.Respond(data) + }) + require.NoError(t, err) + t.Cleanup(func() { _ = sub.Unsubscribe() }) +} + +func seedRepositoryForAction(t *testing.T, nc *nats.Conn, name string) { + t.Helper() + store := handlers_ecr.NewNATSMetaStore(nc) + require.NoError(t, store.PutRepo(context.Background(), repositoryActionTestAccount, handlers_ecr.RepoMeta{ + Name: name, CreatedAt: time.Now(), + })) +} + +func repositoryActionEndpoint() RepositoryEndpoint { + return RepositoryEndpoint{Region: "ap-southeast-2", ServicesDomain: "spinifex.test"} +} diff --git a/spinifex/gateway/ecr_createrepository.go b/spinifex/gateway/ecr_createrepository.go index 85d1cbf6b..9aba01936 100644 --- a/spinifex/gateway/ecr_createrepository.go +++ b/spinifex/gateway/ecr_createrepository.go @@ -1,49 +1,18 @@ package gateway import ( - "encoding/json" "errors" "io" "log/slog" "net/http" - "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ecr" - handlers_ecr "github.com/mulgadc/spinifex/spinifex/domains/ecr" awsapi "github.com/mulgadc/spinifex/spinifex/domains/ecr/awsapi" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" ) -// createRepositoryRequest is the camelCase AWS JSON 1.1 input shape. ecr.Tag -// carries no locationName, so its wire keys are Key/Value (capitalized); -// reusing the SDK type keeps that spelling rather than a lowercase hand-roll. -type createRepositoryRequest struct { - RepositoryName string `json:"repositoryName"` - RegistryID string `json:"registryId"` - ImageTagMutability string `json:"imageTagMutability"` - Tags []*ecr.Tag `json:"tags"` - EncryptionConfiguration *encryptionConfigurationInput `json:"encryptionConfiguration"` - ImageScanningConfiguration *imageScanningConfigurationInput `json:"imageScanningConfiguration"` -} - -// encryptionConfigurationInput is the camelCase input shape for -// encryptionConfiguration. kmsKey is not decoded: KMS is rejected outright, so -// any key material supplied alongside it is never read. -type encryptionConfigurationInput struct { - EncryptionType string `json:"encryptionType"` -} - -// imageScanningConfigurationInput is the camelCase input shape for -// imageScanningConfiguration. -type imageScanningConfigurationInput struct { - ScanOnPush bool `json:"scanOnPush"` -} - -// handleCreateRepository provisions an empty repository in the caller account. -// The predastore object bucket stays lazily created on first push; this writes -// only the per-account KV meta record. imageTagMutability (MUTABLE/IMMUTABLE) -// is validated, persisted, and echoed; an unset value defaults to MUTABLE. +// handleCreateRepository adapts the authenticated HTTP request to the ECR AWS +// action. ECR request semantics and resource state are domain-adapter +// responsibilities; gateway retains HTTP and auth-context handling. func (gw *GatewayConfig) handleCreateRepository(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() accountID, _ := ctx.Value(ctxAccountID).(string) @@ -57,103 +26,10 @@ func (gw *GatewayConfig) handleCreateRepository(w http.ResponseWriter, r *http.R slog.ErrorContext(ctx, "CreateRepository: failed to read body", "err", err) return awsapi.MalformedBodyError() } - var req createRepositoryRequest - if err := json.Unmarshal(body, &req); err != nil { - return awsapi.MalformedBodyError() - } - if err := awsapi.ValidateRepositoryName(req.RepositoryName); err != nil { - return err - } - if req.RegistryID != "" && req.RegistryID != accountID { - return errors.New(awserrors.ErrorAccessDenied) - } - mutability, err := normalizeTagMutability(req.ImageTagMutability) + output, err := awsapi.CreateRepository(ctx, gw.NATSConn, gw.ecrRepositoryEndpoint(), accountID, body) if err != nil { return err } - encryptionType, err := normalizeEncryptionType(req.EncryptionConfiguration) - if err != nil { - return err - } - scanOnPush := req.ImageScanningConfiguration != nil && req.ImageScanningConfiguration.ScanOnPush - tags, err := tagMapFromInput(req.Tags) - if err != nil { - return err - } - - store := handlers_ecr.NewNATSMetaStore(gw.NATSConn) - if _, err := store.GetRepo(ctx, accountID, req.RepositoryName); err == nil { - return errors.New(awserrors.ErrorRepositoryAlreadyExists) - } else if !errors.Is(err, handlers_ecr.ErrNotFound) { - slog.ErrorContext(ctx, "CreateRepository: get repo failed", "repo", req.RepositoryName, "err", err) - return errors.New(awserrors.ErrorServerInternal) - } - - meta := handlers_ecr.RepoMeta{ - Name: req.RepositoryName, - CreatedAt: time.Now().UTC(), - ImageTagMutability: mutability, - EncryptionType: encryptionType, - ScanOnPush: scanOnPush, - Tags: tags, - } - if err := store.PutRepo(ctx, accountID, meta); err != nil { - slog.ErrorContext(ctx, "CreateRepository: put repo failed", "repo", req.RepositoryName, "err", err) - return errors.New(awserrors.ErrorServerInternal) - } - - awsapi.WriteJSONResponse(w, &ecr.CreateRepositoryOutput{ - Repository: gw.ecrRepositoryEndpoint().RepositoryFromMeta(accountID, req.RepositoryName, meta), - }) + awsapi.WriteJSONResponse(w, output) return nil } - -// tagMapFromInput folds the create-time tag list into the map RepoMeta stores, -// rejecting an empty key exactly as TagResource does. An absent list yields a -// nil map so the record round-trips identically to one created without tags. -func tagMapFromInput(in []*ecr.Tag) (map[string]string, error) { - if len(in) == 0 { - return nil, nil - } - if err := awsapi.ValidateTags(in); err != nil { - return nil, err - } - out := make(map[string]string, len(in)) - for _, t := range in { - out[aws.StringValue(t.Key)] = aws.StringValue(t.Value) - } - return out, nil -} - -// normalizeEncryptionType validates the requested encryption configuration, -// defaulting an absent one to AES256. KMS is rejected: no customer key -// material is plumbed anywhere for it to honor. -func normalizeEncryptionType(cfg *encryptionConfigurationInput) (string, error) { - if cfg == nil || cfg.EncryptionType == "" { - return handlers_ecr.EncryptionTypeAES256, nil - } - switch cfg.EncryptionType { - case handlers_ecr.EncryptionTypeAES256: - return cfg.EncryptionType, nil - case handlers_ecr.EncryptionTypeKMS: - return "", awserrors.Errorf(awserrors.ErrorInvalidParameterValue, - "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 "", awsapi.EnumValueError("encryptionConfiguration.encryptionType", cfg.EncryptionType, - handlers_ecr.EncryptionTypeAES256, handlers_ecr.EncryptionTypeKMS) - } -} - -// normalizeTagMutability validates the requested mutability, defaulting an empty -// value to MUTABLE. An unknown value is rejected with InvalidParameterValue. -func normalizeTagMutability(v string) (string, error) { - switch v { - case "": - return handlers_ecr.TagMutabilityMutable, nil - case handlers_ecr.TagMutabilityMutable, handlers_ecr.TagMutabilityImmutable: - return v, nil - default: - return "", awsapi.EnumValueError("imageTagMutability", v, - handlers_ecr.TagMutabilityMutable, handlers_ecr.TagMutabilityImmutable) - } -}