diff --git a/spinifex/domains/ecr/awsapi/lifecycle_preview.go b/spinifex/domains/ecr/awsapi/lifecycle_preview.go index bbbb2de75..168879663 100644 --- a/spinifex/domains/ecr/awsapi/lifecycle_preview.go +++ b/spinifex/domains/ecr/awsapi/lifecycle_preview.go @@ -97,3 +97,32 @@ func StartLifecyclePolicyPreview(ctx context.Context, policies LifecyclePolicySt Status: aws.String(ecr.LifecyclePolicyPreviewStatusComplete), }, nil } + +// GetLifecyclePolicyPreview evaluates synchronously and returns every expiry +// result. v1 has no asynchronous preview-job state, so its status is always +// COMPLETE when evaluation succeeds. +func GetLifecyclePolicyPreview(ctx context.Context, policies LifecyclePolicyStore, catalog ImageCatalog, accountID string, body []byte) (*ecr.GetLifecyclePolicyPreviewOutput, error) { + preview, err := EvaluateLifecyclePreview(ctx, policies, catalog, accountID, body) + if err != nil { + return nil, err + } + + results := make([]*ecr.LifecyclePolicyPreviewResult, 0, len(preview.Expiries)) + for _, expiry := range preview.Expiries { + results = append(results, &ecr.LifecyclePolicyPreviewResult{ + Action: &ecr.LifecyclePolicyRuleAction{Type: aws.String(ecr.ImageActionTypeExpire)}, + AppliedRulePriority: aws.Int64(int64(expiry.RulePriority)), + ImageDigest: aws.String(expiry.Digest), + ImagePushedAt: aws.Time(expiry.PushedAt), + ImageTags: aws.StringSlice(expiry.Tags), + }) + } + return &ecr.GetLifecyclePolicyPreviewOutput{ + RegistryId: aws.String(accountID), + RepositoryName: aws.String(preview.RepositoryName), + LifecyclePolicyText: aws.String(preview.LifecyclePolicyText), + Status: aws.String(ecr.LifecyclePolicyPreviewStatusComplete), + PreviewResults: results, + Summary: &ecr.LifecyclePolicyPreviewSummary{ExpiringImageTotalCount: aws.Int64(int64(len(preview.Expiries)))}, + }, nil +} diff --git a/spinifex/domains/ecr/awsapi/lifecycle_preview_test.go b/spinifex/domains/ecr/awsapi/lifecycle_preview_test.go index 45af05fef..d6e535aca 100644 --- a/spinifex/domains/ecr/awsapi/lifecycle_preview_test.go +++ b/spinifex/domains/ecr/awsapi/lifecycle_preview_test.go @@ -41,6 +41,25 @@ func TestStartLifecyclePolicyPreview_EvaluatesOverride(t *testing.T) { assert.Equal(t, ecr.LifecyclePolicyPreviewStatusComplete, *out.Status) } +func TestGetLifecyclePolicyPreview_ProjectsExpiryResults(t *testing.T) { + catalog := fakeImageCatalog{records: []ecrregistry.ImageRecord{ + {Digest: "sha256:older", Tags: []string{"v1"}, PushedAt: time.Now().Add(-time.Hour)}, + {Digest: "sha256:newer", Tags: []string{"v2"}, PushedAt: time.Now()}, + }} + encodedPolicy, err := json.Marshal(lifecyclePreviewPolicy) + require.NoError(t, err) + body := []byte(`{"repositoryName":"team/app","lifecyclePolicyText":` + string(encodedPolicy) + `}`) + + out, err := GetLifecyclePolicyPreview(context.Background(), fakeLifecyclePolicyStore{}, catalog, "123456789012", body) + require.NoError(t, err) + assert.Equal(t, ecr.LifecyclePolicyPreviewStatusComplete, *out.Status) + require.Len(t, out.PreviewResults, 1) + assert.Equal(t, "sha256:older", *out.PreviewResults[0].ImageDigest) + assert.Equal(t, int64(7), *out.PreviewResults[0].AppliedRulePriority) + require.NotNil(t, out.Summary) + assert.Equal(t, int64(1), *out.Summary.ExpiringImageTotalCount) +} + func TestEvaluateLifecyclePreview_MapsPolicyAndRepositoryFailures(t *testing.T) { cases := []struct { name string diff --git a/spinifex/gateway/ecr_lifecyclepreview.go b/spinifex/gateway/ecr_lifecyclepreview.go index fae9cac12..5bf8585c3 100644 --- a/spinifex/gateway/ecr_lifecyclepreview.go +++ b/spinifex/gateway/ecr_lifecyclepreview.go @@ -4,8 +4,6 @@ import ( "io" "net/http" - "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" ) @@ -30,7 +28,9 @@ func (gw *GatewayConfig) handleStartLifecyclePolicyPreview(w http.ResponseWriter return nil } -// handleGetLifecyclePolicyPreview returns the evaluated expiry set. +// handleGetLifecyclePolicyPreview adapts the authenticated HTTP request to the +// ECR action. Lifecycle evaluation and AWS response projection live in the +// ECR adapter; gateway supplies the policy store and image catalog. func (gw *GatewayConfig) handleGetLifecyclePolicyPreview(w http.ResponseWriter, r *http.Request) error { accountID, err := gw.ecrImageAccount(r) if err != nil { @@ -40,28 +40,10 @@ func (gw *GatewayConfig) handleGetLifecyclePolicyPreview(w http.ResponseWriter, if err != nil { return awsapi.MalformedBodyError() } - preview, err := awsapi.EvaluateLifecyclePreview(r.Context(), handlers_ecr.NewNATSMetaStore(gw.NATSConn), gw.ECRRegistry, accountID, body) + output, err := awsapi.GetLifecyclePolicyPreview(r.Context(), handlers_ecr.NewNATSMetaStore(gw.NATSConn), gw.ECRRegistry, accountID, body) if err != nil { return err } - - results := make([]*ecr.LifecyclePolicyPreviewResult, 0, len(preview.Expiries)) - for _, e := range preview.Expiries { - results = append(results, &ecr.LifecyclePolicyPreviewResult{ - Action: &ecr.LifecyclePolicyRuleAction{Type: aws.String(ecr.ImageActionTypeExpire)}, - AppliedRulePriority: aws.Int64(int64(e.RulePriority)), - ImageDigest: aws.String(e.Digest), - ImagePushedAt: aws.Time(e.PushedAt), - ImageTags: aws.StringSlice(e.Tags), - }) - } - awsapi.WriteJSONResponse(w, &ecr.GetLifecyclePolicyPreviewOutput{ - RegistryId: aws.String(accountID), - RepositoryName: aws.String(preview.RepositoryName), - LifecyclePolicyText: aws.String(preview.LifecyclePolicyText), - Status: aws.String(ecr.LifecyclePolicyPreviewStatusComplete), - PreviewResults: results, - Summary: &ecr.LifecyclePolicyPreviewSummary{ExpiringImageTotalCount: aws.Int64(int64(len(preview.Expiries)))}, - }) + awsapi.WriteJSONResponse(w, output) return nil }