Skip to content
Open
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
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,17 @@ acr purge \
--ago 30d
```

You can pair `--ago` with the `--untagged` flag to apply the same age threshold to manifest cleanup, ensuring that only manifests
older than the specified cutoff are removed:

```sh
acr purge \
--registry <Registry Name> \
--filter <Repository Filter/Name>:<Regex Filter> \
--ago 7d \
--untagged
```

The following table further explains the functionality of this flag.

| Intention | Flag |
Expand All @@ -152,7 +163,7 @@ The duration should be of the form \[integer\]d\[string\] where the first intege

#### Untagged flag

To delete all the manifests that do not have any tags linked to them, the `--untagged` flag should be set.
To delete all the manifests that do not have any tags linked to them, the `--untagged` flag should be set. The manifest cleanup respects the same `--ago` cutoff that is used for tag deletions, so recently-untagged images that are newer than the configured age threshold are preserved.
Copy link
Member

@FeynmanZhou FeynmanZhou Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also update the command examples for the flag --ago to tell users about how to use --ago with --untagged together to configure a fine-grained age threshold of the purge policy?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add the examples to the consts at the top of purge.go? Also updating the command info for --ago there as it currently states:

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)")

which wouldn't reflect the new changes. I think beyond that the PR is ready.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing, I'll make this small update as soon as I can


```sh
acr purge \
Expand Down
2 changes: 1 addition & 1 deletion cmd/acr/annotate.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ func annotateUntaggedManifests(ctx context.Context,
// Contrary to getTagsToAnnotate, getManifests gets all the manifests at once.
// This was done because if there is a manifest that has no tag but is referenced by a multiarch manifest that has tags then it
// should not be annotated.
manifestsToAnnotate, err := repository.GetUntaggedManifests(ctx, poolSize, acrClient, repoName, true, nil, dryRun, includeLocked)
manifestsToAnnotate, err := repository.GetUntaggedManifests(ctx, poolSize, acrClient, repoName, true, nil, dryRun, includeLocked, nil)
if err != nil {
return -1, err
}
Expand Down
11 changes: 8 additions & 3 deletions cmd/acr/purge.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func purge(ctx context.Context,
singleDeletedManifestsCount := 0
// If the untagged flag is set then also manifests are deleted.
if removeUtaggedManifests {
singleDeletedManifestsCount, err = purgeDanglingManifests(ctx, acrClient, repoParallelism, loginURL, repoName, manifestToTagsCountMap, dryRun, includeLocked)
singleDeletedManifestsCount, err = purgeDanglingManifests(ctx, acrClient, repoParallelism, loginURL, repoName, tagDeletionSince, manifestToTagsCountMap, dryRun, includeLocked)
if err != nil {
return deletedTagsCount, deletedManifestsCount, fmt.Errorf("failed to purge manifests: %w", err)
}
Expand Down Expand Up @@ -360,16 +360,21 @@ func getTagsToDelete(ctx context.Context,

// purgeDanglingManifests deletes all manifests that do not have any tags associated with them.
// except the ones that are referenced by a multiarch manifest or that have subject.
func purgeDanglingManifests(ctx context.Context, acrClient api.AcrCLIClientInterface, repoParallelism int, loginURL string, repoName string, manifestToTagsCountMap map[string]int, dryRun bool, includeLocked bool) (int, error) {
func purgeDanglingManifests(ctx context.Context, acrClient api.AcrCLIClientInterface, repoParallelism int, loginURL string, repoName string, tagDeletionSince string, manifestToTagsCountMap map[string]int, dryRun bool, includeLocked bool) (int, error) {
if dryRun {
fmt.Printf("Would delete manifests for repository: %s\n", repoName)
} else {
fmt.Printf("Deleting manifests for repository: %s\n", repoName)
}
agoDuration, err := parseDuration(tagDeletionSince)
if err != nil {
return -1, err
}
timeToCompare := time.Now().UTC().Add(agoDuration)
// Contrary to getTagsToDelete, getManifestsToDelete gets all the Manifests at once, this was done because if there is a manifest that has no
// tag but is referenced by a multiarch manifest that has tags then it should not be deleted. Or if a manifest has no tag, but it has subject,
// then it should not be deleted.
manifestsToDelete, err := repository.GetUntaggedManifests(ctx, repoParallelism, acrClient, repoName, false, manifestToTagsCountMap, dryRun, includeLocked)
manifestsToDelete, err := repository.GetUntaggedManifests(ctx, repoParallelism, acrClient, repoName, false, manifestToTagsCountMap, dryRun, includeLocked, &timeToCompare)
if err != nil {
return -1, err
}
Expand Down
Loading