Skip to content

Commit b91358e

Browse files
committed
feat: added exclude filter for purge command
1 parent 86c7448 commit b91358e

File tree

3 files changed

+123
-44
lines changed

3 files changed

+123
-44
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ Examples of filters
127127
| Untag all tags that are older than the duration in app repository | --filter `"app:.*"` |
128128
| Untag all tags that are older than the duration in all repositories | --filter `".*:.*"` |
129129

130+
##### Exclude filter tag
131+
132+
To exclude tags from the purging, use the --exclude-filter parameter. Exclude filters take precedence over filters.
133+
134+
```sh
135+
acr purge \
136+
--registry <Registry Name> \
137+
--filter <Repository Name>:<Regex filter> \
138+
--exclude-filter <Regex exclude>
139+
...
140+
```
141+
130142
#### Ago flag
131143

132144
The ago flag can be used to change the default expiration time of a tag, for example, the following command would purge all tags that are older than 30 days:

cmd/acr/purge.go

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const (
4949
5050
- Delete all tags that are older than 7 days in the example.azurecr.io registry inside all repositories, with a page size of 50 repositories
5151
acr purge -r example --filter ".*:.*" --ago 7d --repository-page-size 50
52+
53+
- Delete all tags that starts with hello but not if the tag is a production tag ending on -prod
54+
acr purge -r example --filter "hello-world:hello.*" --exclude-filter ".*-prod$"
5255
`
5356
maxPoolSize = 32 // The max number of parallel delete requests recommended by ACR server
5457
headerLink = "Link"
@@ -73,6 +76,7 @@ type purgeParameters struct {
7376
keep int
7477
filters []string
7578
filterTimeout int64
79+
excludeFilter string
7680
untagged bool
7781
dryRun bool
7882
concurrency int
@@ -123,7 +127,7 @@ func newPurgeCmd(rootParams *rootParameters) *cobra.Command {
123127
fmt.Printf("Specified concurrency value too large. Set to maximum value: %d \n", maxPoolSize)
124128
}
125129

126-
singleDeletedTagsCount, err := purgeTags(ctx, acrClient, poolSize, loginURL, repoName, purgeParams.ago, tagRegex, purgeParams.keep, purgeParams.filterTimeout)
130+
singleDeletedTagsCount, err := purgeTags(ctx, acrClient, poolSize, loginURL, repoName, purgeParams.ago, tagRegex, purgeParams.keep, purgeParams.filterTimeout, purgeParams.excludeFilter)
127131
if err != nil {
128132
return errors.Wrap(err, "failed to purge tags")
129133
}
@@ -140,7 +144,7 @@ func newPurgeCmd(rootParams *rootParameters) *cobra.Command {
140144
deletedManifestsCount += singleDeletedManifestsCount
141145
} else {
142146
// No tag or manifest will be deleted but the counters still will be updated.
143-
singleDeletedTagsCount, singleDeletedManifestsCount, err := dryRunPurge(ctx, acrClient, loginURL, repoName, purgeParams.ago, tagRegex, purgeParams.untagged, purgeParams.keep, purgeParams.filterTimeout)
147+
singleDeletedTagsCount, singleDeletedManifestsCount, err := dryRunPurge(ctx, acrClient, loginURL, repoName, purgeParams.ago, tagRegex, purgeParams.untagged, purgeParams.keep, purgeParams.filterTimeout, purgeParams.excludeFilter)
144148
if err != nil {
145149
return errors.Wrap(err, "failed to dry-run purge")
146150
}
@@ -165,6 +169,7 @@ func newPurgeCmd(rootParams *rootParameters) *cobra.Command {
165169
cmd.Flags().StringVar(&purgeParams.ago, "ago", "", "The tags that were last updated before this duration will be deleted, the format is [number]d[string] where the first number represents an amount of days and the string is in a Go duration format (e.g. 2d3h6m selects images older than 2 days, 3 hours and 6 minutes)")
166170
cmd.Flags().IntVar(&purgeParams.keep, "keep", 0, "Number of latest to-be-deleted tags to keep, use this when you want to keep at least x number of latest tags that could be deleted meeting all other filter criteria")
167171
cmd.Flags().StringArrayVarP(&purgeParams.filters, "filter", "f", nil, "Specify the repository and a regular expression filter for the tag name, if a tag matches the filter and is older than the duration specified in ago it will be deleted. Note: If backtracking is used in the regexp it's possible for the expression to run into an infinite loop. The default timeout is set to 1 minute for evaluation of any filter expression. Use the '--filter-timeout-seconds' option to set a different value.")
172+
cmd.Flags().StringVar(&purgeParams.excludeFilter, "exclude-filter", "", "Specify a regular expression filter for the tag name (without repository!) to be excluded from purge.")
168173
cmd.Flags().StringArrayVarP(&purgeParams.configs, "config", "c", nil, "Authentication config paths (e.g. C://Users/docker/config.json)")
169174
cmd.Flags().Int64Var(&purgeParams.filterTimeout, "filter-timeout-seconds", defaultRegexpMatchTimeoutSeconds, "This limits the evaluation of the regex filter, and will return a timeout error if this duration is exceeded during a single evaluation. If written incorrectly a regexp filter with backtracking can result in an infinite loop.")
170175
cmd.Flags().IntVar(&purgeParams.concurrency, "concurrency", defaultPoolSize, concurrencyDescription)
@@ -176,8 +181,19 @@ func newPurgeCmd(rootParams *rootParameters) *cobra.Command {
176181
}
177182

178183
// purgeTags deletes all tags that are older than the ago value and that match the tagFilter string.
179-
func purgeTags(ctx context.Context, acrClient api.AcrCLIClientInterface, poolSize int, loginURL string, repoName string, ago string, tagFilter string, keep int, regexpMatchTimeoutSeconds int64) (int, error) {
184+
func purgeTags(ctx context.Context, acrClient api.AcrCLIClientInterface, poolSize int, loginURL string, repoName string, ago string, tagFilter string, keep int, regexpMatchTimeoutSeconds int64, excludeFilter string) (int, error) {
180185
fmt.Printf("Deleting tags for repository: %s\n", repoName)
186+
187+
var excludeRegex *regexp2.Regexp = nil
188+
189+
if excludeFilter != "" {
190+
res, err := common.BuildRegexFilter(excludeFilter, regexpMatchTimeoutSeconds)
191+
if err != nil {
192+
return -1, err
193+
}
194+
excludeRegex = res
195+
}
196+
181197
agoDuration, err := parseDuration(ago)
182198
if err != nil {
183199
return -1, err
@@ -197,8 +213,9 @@ func purgeTags(ctx context.Context, acrClient api.AcrCLIClientInterface, poolSiz
197213
// In order to only have a limited amount of http requests, a purger is used that will start goroutines to delete tags.
198214
purger := worker.NewPurger(poolSize, acrClient, loginURL, repoName)
199215
// GetTagsToDelete will return an empty lastTag when there are no more tags.
216+
200217
for {
201-
tagsToDelete, newLastTag, newSkippedTagsCount, err := getTagsToDelete(ctx, acrClient, repoName, tagRegex, timeToCompare, lastTag, keep, skippedTagsCount)
218+
tagsToDelete, newLastTag, newSkippedTagsCount, err := getTagsToDelete(ctx, acrClient, repoName, tagRegex, timeToCompare, lastTag, keep, skippedTagsCount, excludeRegex)
202219
if err != nil {
203220
return -1, err
204221
}
@@ -257,7 +274,8 @@ func getTagsToDelete(ctx context.Context,
257274
timeToCompare time.Time,
258275
lastTag string,
259276
keep int,
260-
skippedTagsCount int) (*[]acr.TagAttributesBase, string, int, error) {
277+
skippedTagsCount int,
278+
excludeFilter *regexp2.Regexp) (*[]acr.TagAttributesBase, string, int, error) {
261279

262280
var matches bool
263281
var lastUpdateTime time.Time
@@ -275,6 +293,18 @@ func getTagsToDelete(ctx context.Context,
275293
tags := *resultTags.TagsAttributes
276294
tagsEligibleForDeletion := []acr.TagAttributesBase{}
277295
for _, tag := range tags {
296+
if excludeFilter != nil {
297+
matches, err = excludeFilter.MatchString(*tag.Name)
298+
if err != nil {
299+
// The only error that regexp2 will return is a timeout error
300+
return nil, "", skippedTagsCount, err
301+
}
302+
if matches {
303+
fmt.Printf("Tag \"%s\" skipped due to exclude filter\n", *tag.Name)
304+
continue
305+
}
306+
}
307+
278308
matches, err = filter.MatchString(*tag.Name)
279309
if err != nil {
280310
// The only error that regexp2 will return is a timeout error
@@ -337,7 +367,18 @@ func purgeDanglingManifests(ctx context.Context, acrClient api.AcrCLIClientInter
337367
}
338368

339369
// dryRunPurge outputs everything that would be deleted if the purge command was executed
340-
func dryRunPurge(ctx context.Context, acrClient api.AcrCLIClientInterface, loginURL string, repoName string, ago string, filter string, untagged bool, keep int, regexMatchTimeout int64) (int, int, error) {
370+
func dryRunPurge(
371+
ctx context.Context,
372+
acrClient api.AcrCLIClientInterface,
373+
loginURL string,
374+
repoName string,
375+
ago string,
376+
filter string,
377+
untagged bool,
378+
keep int,
379+
regexMatchTimeout int64,
380+
excludeFilter string) (int, int, error) {
381+
341382
deletedTagsCount := 0
342383
deletedManifestsCount := 0
343384
// In order to keep track if a manifest would get deleted a map is defined that as a key has the manifest
@@ -355,11 +396,21 @@ func dryRunPurge(ctx context.Context, acrClient api.AcrCLIClientInterface, login
355396
return -1, -1, err
356397
}
357398

399+
var excludeRegex *regexp2.Regexp = nil
400+
401+
if excludeFilter != "" {
402+
res, err := common.BuildRegexFilter(excludeFilter, regexMatchTimeout)
403+
if err != nil {
404+
return -1, -1, err
405+
}
406+
excludeRegex = res
407+
}
408+
358409
lastTag := ""
359410
skippedTagsCount := 0
360411
// The loop to get the deleted tags follows the same logic as the one in the purgeTags function
361412
for {
362-
tagsToDelete, newLastTag, newSkippedTagsCount, err := getTagsToDelete(ctx, acrClient, repoName, regex, timeToCompare, lastTag, keep, skippedTagsCount)
413+
tagsToDelete, newLastTag, newSkippedTagsCount, err := getTagsToDelete(ctx, acrClient, repoName, regex, timeToCompare, lastTag, keep, skippedTagsCount, excludeRegex)
363414
if err != nil {
364415
return -1, -1, err
365416
}

0 commit comments

Comments
 (0)