Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Delete prior ECR images after a stack update #2637

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/COVERAGE
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22.63
22.19
2 changes: 1 addition & 1 deletion cli/cmd/COVERAGE
Original file line number Diff line number Diff line change
@@ -1 +1 @@
11.8
11.7
21 changes: 21 additions & 0 deletions cli/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ func validateStackExists(ctx context.Context, stackName string, happyClient *Hap
}

func updateStack(ctx context.Context, cmd *cobra.Command, stack *stackservice.Stack, forceFlag bool, happyClient *HappyClient) error {
stackInfo, err := stack.GetStackInfo(ctx)
if err != nil {
return errors.Wrap(err, "unable to get stack info")
}

// 1.) update the workspace's meta variables
stackMeta, err := updateStackMeta(ctx, stack.Name, happyClient)
if err != nil {
Expand Down Expand Up @@ -156,6 +161,22 @@ func updateStack(ctx context.Context, cmd *cobra.Command, stack *stackservice.St
// 4.) print to stdout
stack.PrintOutputs(ctx)

// Remove images with the previous tag from all ECRs, unless the previous tag is the same as the current tag
found := false
for _, tag := range happyClient.ArtifactBuilder.GetTags() {
if tag == stackInfo.StackMetadata.Tag {
found = true
break
}
}

if !found {
err = happyClient.ArtifactBuilder.DeleteImages(ctx, stackInfo.StackMetadata.Tag)
if err != nil {
return errors.Wrap(err, "failed to delete images")
}
}

return nil
}

Expand Down
14 changes: 14 additions & 0 deletions cli/mocks/mock_artifact_builder.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/pkg/artifact_builder/COVERAGE
Original file line number Diff line number Diff line change
@@ -1 +1 @@
56.2
53.5
80 changes: 63 additions & 17 deletions cli/pkg/artifact_builder/artifact_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,23 +199,9 @@ func (ab ArtifactBuilder) RetagImages(
}

func (ab ArtifactBuilder) getRegistryImages(ctx context.Context, registry *config.RegistryConfig, tag string) (*ecr.BatchGetImageOutput, *RegistryDescriptor, error) {
parts := strings.SplitN(registry.URL, "/", 2)
if len(parts) < 2 {
return nil, nil, errors.Errorf("invalid registry url format: %s", registry.URL)
}
registryId := parts[0]
repositoryName := parts[1]

if util.IsLocalstackMode() {
registryId = "000000000000"
} else {
parts = strings.Split(registryId, ".")
if len(parts) == 6 {
// Real AWS registry ID
registryId = parts[0]
} else {
return nil, nil, errors.Errorf("invalid registry format: %s", registryId)
}
registryId, repositoryName, err := parseRepositoryURL(registry.URL)
if err != nil {
return nil, nil, errors.Wrapf(err, "invalid registry url format: %s", registry.URL)
}

input := &ecr.BatchGetImageInput{
Expand Down Expand Up @@ -394,6 +380,47 @@ func (ab ArtifactBuilder) push(ctx context.Context, tags []string, servicesImage
return nil
}

func (ab ArtifactBuilder) DeleteImages(ctx context.Context, tag string) error {
if !ab.happyConfig.GetData().FeatureFlags.EnableUnusedImageDeletion {
return nil
}
defer diagnostics.AddProfilerRuntime(ctx, time.Now(), "DeleteImages")
err := ab.validate()
if err != nil {
return errors.Wrap(err, "artifact builder configuration is incomplete")
}

serviceRegistries := ab.backend.Conf().GetServiceRegistries()

stackECRS, err := ab.GetECRsForServices(ctx)
if err != nil {
log.Debugf("unable to get ECRs for services: %s", err)
}
if len(stackECRS) > 0 && err == nil {
serviceRegistries = stackECRS
}

ecrClient := ab.backend.GetECRClient()

for serviceName, registry := range serviceRegistries {
registryId, repositoryName, err := parseRepositoryURL(registry.URL)
if err != nil {
log.Debugf("unable to parse repository URL %s: %s", registry.URL, err.Error())
}
log.Debugf("Deleting image %s:%s from ECR\n", registry.URL, tag)
_, err = ecrClient.BatchDeleteImage(ctx, &ecr.BatchDeleteImageInput{
RegistryId: aws.String(registryId),
RepositoryName: aws.String(repositoryName),
ImageIds: []ecrtypes.ImageIdentifier{{ImageTag: aws.String(tag)}},
})
if err != nil {
log.Debugf("unable to delete service %s image from ECR - %s:%s : %s\n", serviceName, registry.URL, tag, err.Error())
}
}

return nil
}

type repositoryConfig struct {
scanOnPush bool
descriptor *RegistryDescriptor
Expand Down Expand Up @@ -601,6 +628,25 @@ func (ab ArtifactBuilder) GetServices(ctx context.Context) (map[string]ServiceCo
return config.Services, nil
}

func parseRepositoryURL(url string) (registryId string, repositoryName string, err error) {
parts := strings.SplitN(url, "/", 2)
if len(parts) < 2 {
return "", "", errors.Errorf("invalid repository url format: %s", url)
}
registryId = parts[0]
repositoryName = parts[1]
if util.IsLocalstackMode() {
registryId = "000000000000"
} else {
parts = strings.Split(registryId, ".")
if len(parts) != 6 {
return "", "", errors.Errorf("invalid registry format: %s", registryId)
}
registryId = parts[0]
}
return registryId, repositoryName, nil
}

func (ab ArtifactBuilder) GetAllServices(ctx context.Context) (map[string]ServiceConfig, error) {
bc := ab.config.Clone()
bc.configData = nil
Expand Down
1 change: 1 addition & 0 deletions cli/pkg/artifact_builder/artifact_builder_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type ArtifactBuilderIface interface {
PushFromWithTag(ctx context.Context, servicesImage map[string]string, tag string) error
Pull(ctx context.Context, stackName, tag string) (map[string]string, error)
BuildAndPush(ctx context.Context) error
DeleteImages(ctx context.Context, tag string) error
GetServices(ctx context.Context) (map[string]ServiceConfig, error)
GetAllServices(ctx context.Context) (map[string]ServiceConfig, error)
}
Expand Down
8 changes: 8 additions & 0 deletions cli/pkg/artifact_builder/artifact_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,11 @@ func TestBuildAndPush(t *testing.T) {
err = artifactBuilder.BuildAndPush(ctx)
r.NoError(err)
}

func TestParseRegistryURL(t *testing.T) {
r := require.New(t)
registryId, repositoryName, err := parseRepositoryURL("1234567890.dkr.ecr.us-west-2.amazonaws.com/stackname/envname/servicename")
r.NoError(err)
r.Equal("1234567890", registryId)
r.Equal("stackname/envname/servicename", repositoryName)
}
4 changes: 4 additions & 0 deletions cli/pkg/artifact_builder/dry_run_artifact_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ func (ab *DryRunArtifactBuilder) GetServices(ctx context.Context) (map[string]Se
return config.Services, nil
}

func (ab *DryRunArtifactBuilder) DeleteImages(ctx context.Context, tag string) error {
return nil
}

func (ab *DryRunArtifactBuilder) GetAllServices(ctx context.Context) (map[string]ServiceConfig, error) {
bc := ab.config.Clone()
bc.configData = nil
Expand Down
1 change: 1 addition & 0 deletions shared/aws/interfaces/ecr.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ type ECRAPI interface {
DescribeImageScanFindings(context.Context, *ecr.DescribeImageScanFindingsInput, ...func(*ecr.Options)) (*ecr.DescribeImageScanFindingsOutput, error)
BatchGetRepositoryScanningConfiguration(ctx context.Context, params *ecr.BatchGetRepositoryScanningConfigurationInput, optFns ...func(*ecr.Options)) (*ecr.BatchGetRepositoryScanningConfigurationOutput, error)
GetRegistryScanningConfiguration(ctx context.Context, params *ecr.GetRegistryScanningConfigurationInput, optFns ...func(*ecr.Options)) (*ecr.GetRegistryScanningConfigurationOutput, error)
BatchDeleteImage(ctx context.Context, params *ecr.BatchDeleteImageInput, optFns ...func(*ecr.Options)) (*ecr.BatchDeleteImageOutput, error)
}
20 changes: 20 additions & 0 deletions shared/aws/interfaces/mock_aws_ecr.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions shared/config/happy_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ type EnvironmentContext struct {
}

type Features struct {
EnableDynamoLocking bool `yaml:"enable_dynamo_locking" json:"enable_dynamo_locking,omitempty"`
EnableHappyApiUsage bool `yaml:"enable_happy_api_usage" json:"enable_happy_api_usage,omitempty"`
EnableECRAutoCreation bool `yaml:"enable_ecr_auto_creation" json:"enable_ecr_auto_creation,omitempty"`
EnableUnifiedConfig bool `yaml:"enable_unified_config" json:"enable_unified_config,omitempty"`
EnableDynamoLocking bool `yaml:"enable_dynamo_locking" json:"enable_dynamo_locking,omitempty"`
EnableHappyApiUsage bool `yaml:"enable_happy_api_usage" json:"enable_happy_api_usage,omitempty"`
EnableECRAutoCreation bool `yaml:"enable_ecr_auto_creation" json:"enable_ecr_auto_creation,omitempty"`
EnableUnifiedConfig bool `yaml:"enable_unified_config" json:"enable_unified_config,omitempty"`
EnableUnusedImageDeletion bool `yaml:"enable_unused_image_deletion" json:"enable_unused_image_deletion,omitempty"`
}

type HappyApiConfig struct {
Expand Down
Loading