mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
refactor: move ecr create action to domain
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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"}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user