Skip to content

Commit 89778a0

Browse files
committed
feat(cleanDockerImages): Add globalMaxDays and globalMaxCount
This PR extends the `cleanDockerImages` plugin to support optional global fallback values for retention policies, eliminating the need to manually label every Docker image with retention policies. Previously, each Docker image had to be explicitly labeled with `com.jfrog.artifactory.retention.maxDays` and/or `com.jfrog.artifactory.retention.maxCount` in its Dockerfile. This approach had limitations: - Requires rebuilding images to change retention policies - Not practical for third-party images that can't be modified - Tedious to manage retention across many images - No way to set repository-wide defaults Added two optional configuration parameters to `cleanDockerImages.properties`: - **`globalMaxDays`**: Global fallback for maximum age retention (in days) - **`globalMaxCount`**: Global fallback for maximum version count retention These values apply to images that don't have explicit labels set. Set to `-1` or omit to disable. - Global settings are loaded from the properties file on plugin execution - Image-specific labels always take precedence over global settings - Both `maxDays` and `maxCount` policies work with global fallbacks - Compatible with existing `byDownloadDate` behavior - Modified `checkDaysPassedForDelete()` and `getMaxCountForDelete()` to accept and use global fallback parameters - Updated function signatures throughout the call chain to pass global settings - Added type checking to handle both String (from labels) and Integer (from global config) property values - Enhanced logging to indicate when global values are being used **This change is fully backward compatible:** - Existing configurations without global settings continue to work unchanged - Image-specific labels are still supported and take priority - No breaking changes to the plugin API or execution parameters ```groovy dockerRepos = ["docker-local", "docker-prod"] byDownloadDate = false globalMaxDays = 30 globalMaxCount = 5
1 parent 216ea6d commit 89778a0

3 files changed

Lines changed: 87 additions & 31 deletions

File tree

cleanup/cleanDockerImages/README.md

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,71 @@ The `cleanDockerImages.properties` file has the following fields:
1111

1212
- `dockerRepos`: A list of Docker repositories to clean. If a repo is not in
1313
this list, it will not be cleaned.
14-
- `byDownloadDate`: An optional boolean flag (true/false).
14+
- `byDownloadDate`: An optional boolean flag (true/false).
1515
* **false** (default): retention will take into account only **creation** date of the image
1616
(technically, its manifest file). This is the original behaviour.
1717
* **true**: identify images to remove by their **last download date** or failing that,
1818
last **update** date. This mode of operation has been inspired by the 'artifactCleanup'
1919
plugin.
20+
- `globalMaxDays`: An optional global fallback value (integer) for `maxDays` retention policy.
21+
This value will be applied to all Docker images that don't have an explicit
22+
`com.jfrog.artifactory.retention.maxDays` label. Set to `null` or omit to disable global `maxDays` policy.
23+
- `globalMaxCount`: An optional global fallback value (integer) for `maxCount` retention policy.
24+
This value will be applied to all Docker images that don't have an explicit
25+
`com.jfrog.artifactory.retention.maxCount` label. Set to `null` or omit to disable global `maxCount` policy.
2026

2127
For example:
2228

23-
``` json
29+
``` groovy
2430
dockerRepos = ["example-docker-local", "example-docker-local-2"]
2531
byDownloadDate = false
32+
globalMaxDays = 30
33+
globalMaxCount = 5
2634
```
2735

36+
This configuration will clean Docker images from the two specified repositories, using creation date
37+
for retention checks. Images without explicit labels will be deleted if they are older than 30 days
38+
or if there are more than 5 versions of the same image.
39+
2840
Usage
2941
-----
3042

31-
Cleanup policies are specified as labels on the Docker image. Currently, this
32-
plugin supports the following policies:
43+
Cleanup policies can be specified in two ways:
44+
45+
### 1. Image-Specific Labels (Priority)
46+
47+
Labels on individual Docker images take priority over global settings. Add them to the Dockerfile before building:
48+
49+
``` dockerfile
50+
LABEL com.jfrog.artifactory.retention.maxCount="10"
51+
LABEL com.jfrog.artifactory.retention.maxDays="7"
52+
```
53+
54+
### 2. Global Fallback Values
55+
56+
For images without explicit labels, the plugin will use the global values configured in
57+
`cleanDockerImages.properties` (see Configuration section above).
58+
59+
### Retention Policies
60+
61+
Currently, this plugin supports the following policies:
3362

3463
- `maxDays`: The maximum number of days a Docker image can exist in an
3564
Artifactory repository. Any images older than this will be deleted.
65+
* when `byDownloadDate=false` (default): images created within last `maxDays` will be preserved
3666
* when `byDownloadDate=true`: images downloaded or updated within last `maxDays` will
3767
be preserved
3868
- `maxCount`: The maximum number of versions of a particular image which should
3969
exist. For example, if there are 10 versions of a Docker image and `maxCount`
4070
is set to 6, the oldest 4 versions of the image will be deleted.
71+
* when `byDownloadDate=false` (default): image age determined by creation date
4172
* when `byDownloadDate=true`: image age will be determined by first checking
4273
the _Last Downloaded Date_ and _Modification Date_ will be checked only when this image has never
4374
been downloaded.
4475

45-
To set these labels for an image, add them to the Dockerfile before building:
76+
**Note:** Image-specific labels always take precedence over global fallback values.
4677

47-
``` dockerfile
48-
LABEL com.jfrog.artifactory.retention.maxCount="10"
49-
LABEL com.jfrog.artifactory.retention.maxDays="7"
50-
```
78+
### Execution
5179

5280
When a Docker image is deployed, Artifactory will automatically create
5381
properties reflecting each of its labels. These properties are read by the

cleanup/cleanDockerImages/cleanDockerImages.groovy

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ executions {
2626
cleanDockerImages() { params ->
2727
def deleted = []
2828
def etcdir = ctx.artifactoryHome.etcDir
29-
def propsfile = new File(etcdir, "plugins/cleanDockerImages.properties")
29+
def propsfile = new File(etcdir, 'plugins/cleanDockerImages.properties')
3030
def propConfigData = new ConfigSlurper().parse(propsfile.toURL())
3131
def repos = propConfigData.dockerRepos
3232
def dryRun = params['dryRun'] ? params['dryRun'][0] as boolean : false
@@ -35,10 +35,14 @@ executions {
3535
def byDownloadDate = propConfigData.byDownloadDate ? propConfigData.byDownloadDate : false
3636
byDownloadDate = params['byDownloadDate'] ? params['byDownloadDate'][0] as boolean : byDownloadDate
3737

38-
log.info("cleanDockerImages: Options dryRun=${dryRun}, byDownloadDate=${byDownloadDate}")
38+
// Load global retention settings as fallback values
39+
def globalMaxDays = propConfigData.globalMaxDays ? propConfigData.globalMaxDays as Integer : null
40+
def globalMaxCount = propConfigData.globalMaxCount ? propConfigData.globalMaxCount as Integer : null
41+
42+
log.info("cleanDockerImages: Options dryRun=${dryRun}, byDownloadDate=${byDownloadDate}, globalMaxDays=${globalMaxDays}, globalMaxCount=${globalMaxCount}")
3943
repos.each {
4044
log.debug("Cleaning Docker images in repo: $it")
41-
def del = buildParentRepoPaths(RepoPathFactory.create(it), dryRun, byDownloadDate)
45+
def del = this.buildParentRepoPaths(RepoPathFactory.create(it), dryRun, byDownloadDate, globalMaxDays, globalMaxCount)
4246
deleted.addAll(del)
4347
}
4448
def json = [status: 'okay', dryRun: dryRun, deleted: deleted]
@@ -47,10 +51,10 @@ executions {
4751
}
4852
}
4953

50-
def buildParentRepoPaths(path, dryRun, byDownloadDate) {
54+
def buildParentRepoPaths(path, dryRun, byDownloadDate, globalMaxDays, globalMaxCount) {
5155
def deleted = [], oldSet = [], imagesPathMap = [:], imagesCount = [:]
5256
def parentInfo = repositories.getItemInfo(path)
53-
simpleTraverse(parentInfo, oldSet, imagesPathMap, imagesCount, byDownloadDate)
57+
simpleTraverse(parentInfo, oldSet, imagesPathMap, imagesCount, byDownloadDate, globalMaxDays, globalMaxCount)
5458
for (img in oldSet) {
5559
deleted << img.id
5660
if (!dryRun) repositories.delete(img)
@@ -74,27 +78,27 @@ def buildParentRepoPaths(path, dryRun, byDownloadDate) {
7478
// - delete the images immediately if the maxDays policy applies
7579
// - Aggregate the images that qualify for maxCount policy (to get deleted in
7680
// the execution closure)
77-
def simpleTraverse(parentInfo, oldSet, imagesPathMap, imagesCount, byDownloadDate) {
81+
def simpleTraverse(parentInfo, oldSet, imagesPathMap, imagesCount, byDownloadDate, globalMaxDays, globalMaxCount) {
7882
def maxCount = null
7983
def parentRepoPath = parentInfo.repoPath
8084
for (childItem in repositories.getChildren(parentRepoPath)) {
8185
def currentPath = childItem.repoPath
8286
if (childItem.isFolder()) {
83-
simpleTraverse(childItem, oldSet, imagesPathMap, imagesCount, byDownloadDate)
87+
simpleTraverse(childItem, oldSet, imagesPathMap, imagesCount, byDownloadDate, globalMaxDays, globalMaxCount)
8488
continue
8589
}
8690
log.debug("Scanning File: $currentPath.name")
87-
if (currentPath.name != "manifest.json") continue
91+
if (currentPath.name != 'manifest.json') continue
8892
// get the properties here and delete based on policies:
8993
// - implement daysPassed policy first and delete the images that
9094
// qualify
9195
// - aggregate the image info to group by image and sort by create
9296
// (byDownloadDate=false) or downloaded/updated (byDownloadDate=true)
9397
// date for maxCount policy
94-
if (checkDaysPassedForDelete(childItem, byDownloadDate)) {
98+
if (checkDaysPassedForDelete(childItem, byDownloadDate, globalMaxDays)) {
9599
log.debug("Adding to OLD MAP: $parentRepoPath")
96100
oldSet << parentRepoPath
97-
} else if ((maxCount = getMaxCountForDelete(childItem)) > 0) {
101+
} else if ((maxCount = getMaxCountForDelete(childItem, globalMaxCount)) > 0) {
98102
log.debug("Adding to IMAGES MAP: $parentRepoPath")
99103
def parentId = parentRepoPath.parent.id
100104
def oldmax = maxCount
@@ -133,8 +137,8 @@ def getItemLastUsedDate(item, byDownloadDate) {
133137
def itemLastUse = item.created
134138

135139
if (byDownloadDate) {
136-
lastDownloadedDate = getLastDownloadedDate(item.repoPath)
137-
itemLastUse = (lastDownloadedDate) ? lastDownloadedDate : item.getLastUpdated()
140+
lastDownloadedDate = getLastDownloadedDate(item.repoPath)
141+
itemLastUse = (lastDownloadedDate) ? lastDownloadedDate : item.getLastUpdated()
138142
}
139143

140144
log.debug("itemLastUse = ${itemLastUse} item.created = ${item.created} item.getLastUpdated = ${item.getLastUpdated()}")
@@ -143,14 +147,25 @@ def getItemLastUsedDate(item, byDownloadDate) {
143147

144148
// This method checks if the docker image's manifest has the property
145149
// "com.jfrog.artifactory.retention.maxDays" for purge
146-
def checkDaysPassedForDelete(item, byDownloadDate) {
147-
def maxDaysProp = "docker.label.com.jfrog.artifactory.retention.maxDays"
150+
// Falls back to globalMaxDays if the property is not set on the item
151+
def checkDaysPassedForDelete(item, byDownloadDate, globalMaxDays) {
152+
def maxDaysProp = 'docker.label.com.jfrog.artifactory.retention.maxDays'
148153
def oneday = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS)
149154
def prop = repositories.getProperty(item.repoPath, maxDaysProp)
150-
if (!prop) return false
151155

152-
log.debug("PROPERTY maxDays FOUND = $prop IN MANIFEST FILE ${item.repoPath}")
153-
prop = prop.isInteger() ? prop.toInteger() : null
156+
// Use item-specific property if available, otherwise fall back to global setting
157+
if (!prop && globalMaxDays != null) {
158+
prop = globalMaxDays
159+
log.debug("Using global maxDays = $prop for ${item.repoPath}")
160+
} else if (!prop) {
161+
return false
162+
} else {
163+
log.debug("PROPERTY maxDays FOUND = $prop IN MANIFEST FILE ${item.repoPath}")
164+
if (prop instanceof String) {
165+
prop = prop.isInteger() ? prop.toInteger() : null
166+
}
167+
}
168+
154169
if (prop == null) return false
155170

156171
def fileLastUseDate = getItemLastUsedDate(item, byDownloadDate)
@@ -159,12 +174,23 @@ def checkDaysPassedForDelete(item, byDownloadDate) {
159174

160175
// This method checks if the docker image's manifest has the property
161176
// "com.jfrog.artifactory.retention.maxCount" for purge
162-
def getMaxCountForDelete(item) {
163-
def maxCountProp = "docker.label.com.jfrog.artifactory.retention.maxCount"
177+
// Falls back to globalMaxCount if the property is not set on the item
178+
def getMaxCountForDelete(item, globalMaxCount) {
179+
def maxCountProp = 'docker.label.com.jfrog.artifactory.retention.maxCount'
164180
def prop = repositories.getProperty(item.repoPath, maxCountProp)
165-
if (!prop) return 0
166181

167-
log.debug("PROPERTY maxCount FOUND = $prop IN MANIFEST FILE ${item}")
168-
prop = prop.isInteger() ? prop.toInteger() : 0
182+
// Use item-specific property if available, otherwise fall back to global setting
183+
if (!prop && globalMaxCount != null) {
184+
prop = globalMaxCount
185+
log.debug("Using global maxCount = $prop for ${item.repoPath}")
186+
} else if (!prop) {
187+
return 0
188+
} else {
189+
log.debug("PROPERTY maxCount FOUND = $prop IN MANIFEST FILE ${item}")
190+
if (prop instanceof String) {
191+
prop = prop.isInteger() ? prop.toInteger() : 0
192+
}
193+
}
194+
169195
return prop > 0 ? prop : 0
170196
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
dockerRepos = ["example-docker-local"]
22
byDownloadDate = false
3+
globalMaxDays = null
4+
globalMaxCount = null

0 commit comments

Comments
 (0)