mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
refactor: localize ecr repository projection
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
awsecr "github.com/aws/aws-sdk-go/service/ecr"
|
||||
"github.com/mulgadc/spinifex/spinifex/domains/ecr"
|
||||
)
|
||||
|
||||
// RepositoryEndpoint is the ECR endpoint profile the composition root supplies
|
||||
// to AWS action adapters. It projects a configured registry listener into the
|
||||
// AWS-visible repository ARN and URI forms without depending on gateway state.
|
||||
type RepositoryEndpoint struct {
|
||||
Region string
|
||||
ServicesDomain string
|
||||
RegistryHost string
|
||||
RegistryPort string
|
||||
}
|
||||
|
||||
// RepositoryARN returns the ECR ARN for an account-scoped repository.
|
||||
func (e RepositoryEndpoint) RepositoryARN(accountID, name string) string {
|
||||
return "arn:aws:ecr:" + e.Region + ":" + accountID + ":repository/" + name
|
||||
}
|
||||
|
||||
// RegistryURIHost returns the configured registry listener for an account. An
|
||||
// explicit advertised host takes precedence; otherwise the AWS-parity ECR DNS
|
||||
// name is used. Standard HTTPS omits port 443 from the URI.
|
||||
func (e RepositoryEndpoint) RegistryURIHost(accountID string) string {
|
||||
host := e.RegistryHost
|
||||
if host == "" {
|
||||
host = accountID + ".dkr.ecr." + e.Region + "." + e.ServicesDomain
|
||||
}
|
||||
if e.RegistryPort != "" && e.RegistryPort != "443" {
|
||||
return host + ":" + e.RegistryPort
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// RepositoryURI returns the pull/push URI for an account-scoped repository.
|
||||
func (e RepositoryEndpoint) RepositoryURI(accountID, name string) string {
|
||||
return e.RegistryURIHost(accountID) + "/" + name
|
||||
}
|
||||
|
||||
// RepositoryFromMeta projects domain repository metadata into the AWS ECR
|
||||
// response shape shared by create, describe and delete actions.
|
||||
func (e RepositoryEndpoint) RepositoryFromMeta(accountID, name string, meta ecr.RepoMeta) *awsecr.Repository {
|
||||
return &awsecr.Repository{
|
||||
RegistryId: aws.String(accountID),
|
||||
RepositoryName: aws.String(name),
|
||||
RepositoryArn: aws.String(e.RepositoryARN(accountID, name)),
|
||||
RepositoryUri: aws.String(e.RepositoryURI(accountID, name)),
|
||||
CreatedAt: aws.Time(meta.CreatedAt),
|
||||
ImageTagMutability: aws.String(meta.TagMutability()),
|
||||
EncryptionConfiguration: &awsecr.EncryptionConfiguration{
|
||||
EncryptionType: aws.String(meta.EncryptionTypeOrDefault()),
|
||||
},
|
||||
ImageScanningConfiguration: &awsecr.ImageScanningConfiguration{
|
||||
ScanOnPush: aws.Bool(meta.ScanOnPush),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/mulgadc/spinifex/spinifex/domains/ecr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepositoryEndpointRegistryURIHost(t *testing.T) {
|
||||
accountID := "123456789012"
|
||||
cases := []struct {
|
||||
name string
|
||||
registryHost string
|
||||
port string
|
||||
want string
|
||||
}{
|
||||
{"parity, port-less", "", "", accountID + ".dkr.ecr.ap-southeast-2.services.internal"},
|
||||
{"parity, 443 port-less", "", "443", accountID + ".dkr.ecr.ap-southeast-2.services.internal"},
|
||||
{"parity, non-standard port", "", "9999", accountID + ".dkr.ecr.ap-southeast-2.services.internal:9999"},
|
||||
{"advertised host", "registry.example.com", "9999", "registry.example.com:9999"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
endpoint := RepositoryEndpoint{
|
||||
Region: "ap-southeast-2",
|
||||
ServicesDomain: "services.internal",
|
||||
RegistryHost: tc.registryHost,
|
||||
RegistryPort: tc.port,
|
||||
}
|
||||
assert.Equal(t, tc.want, endpoint.RegistryURIHost(accountID))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryEndpointRepositoryFromMeta(t *testing.T) {
|
||||
createdAt := time.Date(2026, time.October, 1, 12, 0, 0, 0, time.UTC)
|
||||
endpoint := RepositoryEndpoint{Region: "us-east-1", ServicesDomain: "services.internal"}
|
||||
|
||||
got := endpoint.RepositoryFromMeta("123456789012", "team/app", ecr.RepoMeta{
|
||||
CreatedAt: createdAt,
|
||||
ImageTagMutability: ecr.TagMutabilityImmutable,
|
||||
EncryptionType: ecr.EncryptionTypeKMS,
|
||||
ScanOnPush: true,
|
||||
})
|
||||
|
||||
assert.Equal(t, "123456789012", aws.StringValue(got.RegistryId))
|
||||
assert.Equal(t, "team/app", aws.StringValue(got.RepositoryName))
|
||||
assert.Equal(t, "arn:aws:ecr:us-east-1:123456789012:repository/team/app", aws.StringValue(got.RepositoryArn))
|
||||
assert.Equal(t, "123456789012.dkr.ecr.us-east-1.services.internal/team/app", aws.StringValue(got.RepositoryUri))
|
||||
assert.Equal(t, createdAt, aws.TimeValue(got.CreatedAt))
|
||||
assert.Equal(t, ecr.TagMutabilityImmutable, aws.StringValue(got.ImageTagMutability))
|
||||
assert.Equal(t, ecr.EncryptionTypeKMS, aws.StringValue(got.EncryptionConfiguration.EncryptionType))
|
||||
assert.True(t, aws.BoolValue(got.ImageScanningConfiguration.ScanOnPush))
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
|
||||
// TestAdvertisedEndpoint_ResolvesFromRegistryHost asserts DescribeRegions'
|
||||
// endpoint tracks this gateway's own reachable host:port (the same
|
||||
// RegistryHost/RegistryPort precedence ecrRegistryHost already applies),
|
||||
// RegistryHost/RegistryPort precedence the ECR repository endpoint also
|
||||
// applies),
|
||||
// rather than a literal that never changes with deployment.
|
||||
func TestAdvertisedEndpoint_ResolvesFromRegistryHost(t *testing.T) {
|
||||
cases := []struct {
|
||||
|
||||
@@ -103,7 +103,7 @@ func (gw *GatewayConfig) handleCreateRepository(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
awsapi.WriteJSONResponse(w, &ecr.CreateRepositoryOutput{
|
||||
Repository: gw.buildRepository(accountID, req.RepositoryName, meta),
|
||||
Repository: gw.ecrRepositoryEndpoint().RepositoryFromMeta(accountID, req.RepositoryName, meta),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func (gw *GatewayConfig) handleDeleteRepository(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
awsapi.WriteJSONResponse(w, &ecr.DeleteRepositoryOutput{
|
||||
Repository: gw.buildRepository(accountID, req.RepositoryName, meta),
|
||||
Repository: gw.ecrRepositoryEndpoint().RepositoryFromMeta(accountID, req.RepositoryName, meta),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -68,35 +68,9 @@ func (gw *GatewayConfig) handleDescribeRepositories(w http.ResponseWriter, r *ht
|
||||
slog.ErrorContext(ctx, "DescribeRepositories: get repo failed", "repo", name, "err", err)
|
||||
return errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
repos = append(repos, gw.buildRepository(accountID, name, meta))
|
||||
repos = append(repos, gw.ecrRepositoryEndpoint().RepositoryFromMeta(accountID, name, meta))
|
||||
}
|
||||
|
||||
awsapi.WriteJSONResponse(w, &ecr.DescribeRepositoriesOutput{Repositories: repos})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ecrRepositoryArn builds the ECR repository ARN for an account-scoped repo.
|
||||
func (gw *GatewayConfig) ecrRepositoryArn(accountID, name string) string {
|
||||
return "arn:aws:ecr:" + gw.Region + ":" + accountID + ":repository/" + name
|
||||
}
|
||||
|
||||
// ecrRegistryHost builds the registry host, appending the gateway's advertised
|
||||
// port so docker dials the right port (omitted for 443). When the gateway
|
||||
// advertises a reachable host it is used directly — the account comes from the
|
||||
// auth token, so docker needs no DNS; otherwise the per-account
|
||||
// <acct>.dkr.ecr.<region>.<suffix> parity name is used.
|
||||
func (gw *GatewayConfig) ecrRegistryHost(accountID string) string {
|
||||
host := gw.RegistryHost
|
||||
if host == "" {
|
||||
host = accountID + ".dkr.ecr." + gw.Region + "." + gw.InternalSuffix
|
||||
}
|
||||
if gw.RegistryPort != "" && gw.RegistryPort != "443" {
|
||||
return host + ":" + gw.RegistryPort
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// ecrRepositoryUri builds the registry pull/push URI for an account-scoped repo.
|
||||
func (gw *GatewayConfig) ecrRepositoryUri(accountID, name string) string {
|
||||
return gw.ecrRegistryHost(accountID) + "/" + name
|
||||
}
|
||||
|
||||
@@ -128,8 +128,9 @@ func TestECRRegistryHost_AppendsAdvertisedPort(t *testing.T) {
|
||||
Region: ecrTestRegion, InternalSuffix: ecrTestSuffix,
|
||||
RegistryHost: tc.registryHost, RegistryPort: tc.port,
|
||||
}
|
||||
assert.Equal(t, tc.want, gw.ecrRegistryHost(ecrTestAccount))
|
||||
assert.Equal(t, tc.want+"/team/app", gw.ecrRepositoryUri(ecrTestAccount, "team/app"))
|
||||
endpoint := gw.ecrRepositoryEndpoint()
|
||||
assert.Equal(t, tc.want, endpoint.RegistryURIHost(ecrTestAccount))
|
||||
assert.Equal(t, tc.want+"/team/app", endpoint.RepositoryURI(ecrTestAccount, "team/app"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (gw *GatewayConfig) handleGetAuthorizationToken(w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
authToken := base64.StdEncoding.EncodeToString([]byte("AWS:" + token))
|
||||
proxyEndpoint := "https://" + gw.ecrRegistryHost(accountID)
|
||||
proxyEndpoint := "https://" + gw.ecrRepositoryEndpoint().RegistryURIHost(accountID)
|
||||
|
||||
awsapi.WriteJSONResponse(w, &ecr.GetAuthorizationTokenOutput{
|
||||
AuthorizationData: []*ecr.AuthorizationData{{
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"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/domains/ecr/awsapi"
|
||||
)
|
||||
|
||||
// buildRepository projects a RepoMeta record into the AWS *ecr.Repository
|
||||
// shape shared by CreateRepository, DescribeRepositories, and
|
||||
// DeleteRepository, so a new field is added here once rather than thrice.
|
||||
func (gw *GatewayConfig) buildRepository(accountID, name string, meta handlers_ecr.RepoMeta) *ecr.Repository {
|
||||
return &ecr.Repository{
|
||||
RegistryId: aws.String(accountID),
|
||||
RepositoryName: aws.String(name),
|
||||
RepositoryArn: aws.String(gw.ecrRepositoryArn(accountID, name)),
|
||||
RepositoryUri: aws.String(gw.ecrRepositoryUri(accountID, name)),
|
||||
CreatedAt: aws.Time(meta.CreatedAt),
|
||||
ImageTagMutability: aws.String(meta.TagMutability()),
|
||||
EncryptionConfiguration: &ecr.EncryptionConfiguration{
|
||||
EncryptionType: aws.String(meta.EncryptionTypeOrDefault()),
|
||||
},
|
||||
ImageScanningConfiguration: &ecr.ImageScanningConfiguration{
|
||||
ScanOnPush: aws.Bool(meta.ScanOnPush),
|
||||
},
|
||||
// ecrRepositoryEndpoint adapts gateway composition settings to the ECR domain
|
||||
// action adapter. AWS-visible repository projection remains domain ownership.
|
||||
func (gw *GatewayConfig) ecrRepositoryEndpoint() awsapi.RepositoryEndpoint {
|
||||
return awsapi.RepositoryEndpoint{
|
||||
Region: gw.Region,
|
||||
ServicesDomain: gw.InternalSuffix,
|
||||
RegistryHost: gw.RegistryHost,
|
||||
RegistryPort: gw.RegistryPort,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user