Artifactory Clean-up

Boris Tsigelman
Develeap
Published in
14 min readJul 29, 2021

Over time, during the development process, many artifacts are pushed into Artifactory. A best practice (with artifact servers) is to delete old and unused artifacts. This helps in reducing clutter, disk space and can help with performance.

The cleanup process is the actual implementation of what is called a Data Retention Policy. Each organization defines his data retention policy based on their needs and development workflows. And, although the data retention policy is individual to each organization, there are some main similarities. For example, most data retention policies will include (or address) the following:

  • Reduce artifacts storage space.
  • Delete old and/or unused artifacts.
  • Keep only a limited number of non production artifacts (e.g. snapshots or nightly builds).

The following will describe how to delete old artifacts from an Artifactory’s repository using Jenkins, groovy scripts and REST API to automate the process.

This process uses what JFrog calls User Plugins. You can read more about it here: https://www.jfrog.com/confluence/display/JFROG/User+Plugins

There are sample user plugins here: https://github.com/JFrog/artifactory-user-plugins

I have used some of the sample user plugins as a bases for my own implementation.

Please note: This tutorial is using a specific data retention policy that was custom made for one of my clients. You will probably implement a different data retention policy, but the process will remain similar.

The process outline

The process outline is as follows:

  1. Create administrative user in Artifactory
  2. Mark existing artifacts to be ignored by the cleanup plugin
  3. Create the Groovy script that will become the user plugins
  4. Copy the Groovy scripts (plugins) into plugin directory
  5. Load plugins into Artifactory using REST API
  6. Create a new logger for clean-up process
  7. Define a Jenkins pipeline to:
  • Execute the plugins (with different options)
  • Trigger Artifactory’s empty trash function
  • Trigger Artifactory’s Garbage collector process

Please note: In this scenario, I’ve chosen to control the plugins using Jenkins and setup the pipeline to run once a day using cron. It is possible to set the cron inside the plugin’s groovy script and let it run internally inside Artifactory without Jenkins.

Also: The user plugins can only be run on an Artifactory Pro server.

For code demo, assume you have an Artifactory Pro 7.3.2 running as Docker container on a Ubuntu 18.04.4 LTS machine.

Artifactory Docker run command

docker run --name artifactory --restart=always -v /art_pro_data:/var/opt/jfrog/artifactory -d -p 8081:8081 -p 8082:8082 docker.bintray.io/jfrog/artifactory-pro:7.3.2

Create administrative user in Artifactory

If you’re planning on controlling these plugins using Jenkins, you have to setup an Admin user in Artifactory and use it’s credentials in Jenkins’ pipeline. Only an Admin user can run certain actions like loading plugins into the system.

Mark existing artifacts to be ignored by the cleanup plugin

If you already have a repository filled with artifacts, it’s easier to set the initial ‘do not delete’ property using Artifactory’s UI. After that, it’s better to add this property, automatically (using Jenkins), to each upload of an artifact. This will ensure that the artifact(s) will never be deleted using the artifactCleanup plugin.

In Artifactory’s UI

  1. Select the artifact or path you wish to exclude from the delete (cleanup) process.
  2. Click the Properties tab
  3. Under Add: Property
    Type cleanup.skip in the Name field
    Type true in the Value field
  4. If you are setting this property to a path and you want all artifacts under this path to be set as well, check the Recursive option.
  5. Click Add

In Jenkins Pipeline

Add the props property with value cleanup.skip=true to the File Spec.

def upload_spec = """{
"files": [{
"pattern": "$archive_filename",
"target": "$upload_dir/",
"props": "cleanup.skip=true"
}]
}"""

Create the Groovy script that will become the user plugins

For the process of artifacts cleanup we will need two groovy scripts:

  • artifactCleanup.groovy — This script will delete the artifacts.
  • deleteEmptyDirs.groovy — This script will delete the empty directories or paths left after the artifacts deletion.

I’ve started with downloading the above script from the user plugins github and then slightly modified them to suite the customer needs.

The full script code is at the end of this readme.

Copy the Groovy scripts (plugins) into plugin directory

After you have updated the groovy scripts as needed (if it was necessary), copy them into the following path inside Artifactory’s file system:

$JFROG_HOME/artifactory/var/etc/artifactory/plugins

For example, in my production server the external (not inside the docker container) path is:

/mnt/Jfrog_Dev/artifactory/etc/artifactory/plugins

Load plugins into Artifactory using REST API

To load the plugins into Artifactory, use the following REST API command. If you make changes to the scripts, use this command to reload the plugin and the new changes.

curl -u admin:pass -X POST http://artifactory:8081/artifactory/api/plugins/reload

Note: If you setup a Jenkins pipeline to control this process, you can reload the plugins using Jenkins and don’t have to run the above command manually.

Create a new logger for clean-up process

All the logs created by the plugins are written into the master log (called console.log). It's more convenient to have a separate logger for the cleanup plugins. Here are the instructions for creating such a log.

All logging settings are in file logback.xml. It's an XML file and it's located here:

$JFROG_HOME/artifactory/var/etc/artifactory/logback.xml

Open the file in a text editor, locate the block that starts with:

<appender name="ACCESS" class="ch.qos.logback.core.rolling.RollingFileAppender">

and add the following after the end of the block:

<!-- User Plugins Appenders -->
<appender name="CLEANUP" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${log.dir}/artifactory-cleanup.log</File>
<rollingPolicy class="org.jfrog.common.logging.logback.rolling.FixedWindowWithDateRollingPolicy">
<FileNamePattern>${log.dir.archived}/artifactory-cleanup.%i.log.gz</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>25MB</MaxFileSize>
</triggeringPolicy>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.jfrog.common.logging.logback.layout.BackTracePatternLayout">
<pattern>%date{yyyy-MM-dd'T'HH:mm:ss.SSS, UTC}Z [jfrt ] - %m%n</pattern>
</layout>
</encoder>
</appender>

Then, in the same file locate the section marked as <!-- specialized appenders --> and add the following just above it:

<!-- user plugins loggers -->
<logger name="artifactCleanup" level="info">
<appender-ref ref="CLEANUP"/>
</logger>
<logger name="deleteEmptyDirs" level="info"/>

Note: If you have chosen to not create a separate logger, you can skip the part of adding the <appender> section. And if that is the case, then you need to add the following <logger> section instead:

<!-- user plugins loggers -->
<logger name="artifactCleanup" level="info"/>
<logger name="deleteEmptyDirs" level="info"/>

This will add the logging for the cleanup into the main logger only.

Location of logs in Artifactory

All logs are located in the following path:

$JFROG_HOME/artifactory/var/log

If you created a custom log it will be here:

$JFROG_HOME/artifactory/var/log/artifactory-cleanup.log

If not, the logs will be in the following path:

$JFROG_HOME/artifactory/var/log/console.log

Define a Jenkins pipeline

It’s possible to set up the cleanup plugins internally in Artifactory, but when controlling the plugins with Jenkins, you get more control over the plugins with Jenkins parameters combined with a pipeline.

The full Jenkins pipeline code is listed at the end of this readme file.

A little information on the pipeline

Because the cleanup plugin has many parameters that control how it executes, I’ve created a parameters in the pipeline to correspond to each parameter in the cleanup script. All the parameter can be adjusted except the repos parameter. The reason this parameter is hard-coded to a specific repository is because, in my production server, there are two development teams that use the same Artifactory server and I don't want to delete the other team’s artifacts by mistake. I did set up the pipeline in such a way that you can easily make the necessary changes to make this option editable.

Note: All the parameters have default values, so the job can run using Jenkins cron trigger and perform the default clean periodically.

Groovy Scripts

artifactCleanup.groovy

import org.apache.commons.lang3.StringUtils
import org.artifactory.api.repo.exception.ItemNotFoundRuntimeException
import org.artifactory.exception.CancelException
import groovy.json.JsonSlurper
import groovy.time.TimeCategory
import groovy.time.TimeDuration
import groovy.transform.Field
import java.text.SimpleDateFormat@Field final String CONFIG_FILE_PATH = "plugins/${this.class.name}.json"
@Field final String PROPERTIES_FILE_PATH = "plugins/${this.class.name}.properties"
@Field final String DEFAULT_TIME_UNIT = "day"
@Field final int DEFAULT_TIME_INTERVAL = 21 // 21 days = 3 weeks
class Global {
static Boolean stopCleaning = false
static Boolean pauseCleaning = false
static int paceTimeMS = 0
}
def pluginGroup = 'cleaners'executions {
cleanup(groups: [pluginGroup]) { params ->
def timeUnit = params['timeUnit'] ? params['timeUnit'][0] as String : DEFAULT_TIME_UNIT
def timeInterval = params['timeInterval'] ? params['timeInterval'][0] as int : DEFAULT_TIME_INTERVAL
def repos = params['repos'] as String[]
def dryRun = params['dryRun'] ? new Boolean(params['dryRun'][0]) : true
def disablePropertiesSupport = params['disablePropertiesSupport'] ? new Boolean(params['disablePropertiesSupport'][0]) : false
def paceTimeMS = params['paceTimeMS'] ? params['paceTimeMS'][0] as int : 0

// Enable fallback support for deprecated month parameter
if ( params['months'] && !params['timeInterval'] ) {
log.info('Deprecated month parameter is still in use, please use the new timeInterval parameter instead!', properties)
timeInterval = params['months'][0] as int
} else if ( params['months'] ) {
log.warn('Deprecated month parameter and the new timeInterval are used in parallel: month has been ignored.', properties)
}
artifactCleanup(timeUnit, timeInterval, repos, log, paceTimeMS, dryRun, disablePropertiesSupport)
}
cleanupCtl(groups: [pluginGroup]) { params ->
def command = params['command'] ? params['command'][0] as String : ''
switch ( command ) {
case "stop":
Global.stopCleaning = true
log.info "Stop request detected"
break
case "adjustPaceTimeMS":
def adjustPaceTimeMS = params['value'] ? params['value'][0] as int : 0
Global.paceTimeMS += adjustPaceTimeMS
log.info "Pacing adjustment request detected, adjusting pace time by $adjustPaceTimeMS to new value of $Global.paceTimeMS"
break
case "pause":
Global.pauseCleaning = true
log.info "Pause request detected"
break
case "resume":
Global.pauseCleaning = false
log.info "Resume request detected"
break
default:
log.info "Missing or invalid command, '$command'"
}
}
}
def deprecatedConfigFile = new File(ctx.artifactoryHome.etcDir, PROPERTIES_FILE_PATH)
def configFile = new File(ctx.artifactoryHome.etcDir, CONFIG_FILE_PATH)
if ( deprecatedConfigFile.exists() ) { if ( !configFile.exists() ) {
def config = new ConfigSlurper().parse(deprecatedConfigFile.toURL())
log.info "Schedule job policy list: $config.policies"
config.policies.each{ policySettings ->
def cron = policySettings[ 0 ] ? policySettings[ 0 ] as String : ["0 0 5 ? * 1"]
def repos = policySettings[ 1 ] ? policySettings[ 1 ] as String[] : ["__none__"]
def months = policySettings[ 2 ] ? policySettings[ 2 ] as int : 6
def paceTimeMS = policySettings[ 3 ] ? policySettings[ 3 ] as int : 0
def dryRun = policySettings[ 4 ] ? policySettings[ 4 ] as Boolean : false
def disablePropertiesSupport = policySettings[ 5 ] ? policySettings[ 5 ] as Boolean : false
jobs {
"scheduledCleanup_$cron"(cron: cron) {
log.info "Policy settings for scheduled run at($cron): repo list($repos), timeUnit(month), timeInterval($months), paceTimeMS($paceTimeMS) dryrun($dryRun) disablePropertiesSupport($disablePropertiesSupport)"
artifactCleanup( "month", months, repos, log, paceTimeMS, dryRun, disablePropertiesSupport )
}
}
}
} else {
log.warn "Deprecated 'artifactCleanup.properties' file is still present, but ignored. You should remove the file."
}
}
if ( configFile.exists() ) {

def config = new JsonSlurper().parse(configFile.toURL())
log.info "Schedule job policy list: $config.policies"
config.policies.each{ policySettings ->
def cron = policySettings.containsKey("cron") ? policySettings.cron as String : ["0 0 5 ? * 1"]
def repos = policySettings.containsKey("repos") ? policySettings.repos as String[] : ["__none__"]
def timeUnit = policySettings.containsKey("timeUnit") ? policySettings.timeUnit as String : DEFAULT_TIME_UNIT
def timeInterval = policySettings.containsKey("timeInterval") ? policySettings.timeInterval as int : DEFAULT_TIME_INTERVAL
def paceTimeMS = policySettings.containsKey("paceTimeMS") ? policySettings.paceTimeMS as int : 0
def dryRun = policySettings.containsKey("dryRun") ? new Boolean(policySettings.dryRun) : false
def disablePropertiesSupport = policySettings.containsKey("disablePropertiesSupport") ? new Boolean(policySettings.disablePropertiesSupport) : false
jobs {
"scheduledCleanup_$cron"(cron: cron) {
log.info "Policy settings for scheduled run at($cron): repo list($repos), timeUnit($timeUnit), timeInterval($timeInterval), paceTimeMS($paceTimeMS) dryrun($dryRun) disablePropertiesSupport($disablePropertiesSupport)"
artifactCleanup( timeUnit, timeInterval, repos, log, paceTimeMS, dryRun, disablePropertiesSupport )
}
}
}
}
if ( deprecatedConfigFile.exists() && configFile.exists() ) {
log.warn "The deprecated artifactCleanup.properties and the new artifactCleanup.json are defined in parallel. You should migrate the old file and remove it."
}
private def artifactCleanup(String timeUnit, int timeInterval, String[] repos, log, paceTimeMS, dryRun = false, disablePropertiesSupport = false) {
log.info "Starting artifact cleanup for repositories $repos, until $timeInterval ${timeUnit}s ago with pacing interval $paceTimeMS ms, dryrun: $dryRun, disablePropertiesSupport: $disablePropertiesSupport"
// Create Map(repo, paths) of skiped paths (or others properties supported in future ...)
def skip = [:]
if ( ! disablePropertiesSupport && repos){
skip = getSkippedPaths(repos)
}
def calendarUntil = Calendar.getInstance() calendarUntil.add(mapTimeUnitToCalendar(timeUnit), -timeInterval) def calendarUntilFormatted = new SimpleDateFormat("yyyy/MM/dd HH:mm").format(calendarUntil.getTime());
log.info "Removing all artifacts not downloaded since $calendarUntilFormatted"
Global.stopCleaning = false
int cntFoundArtifacts = 0
int cntNoDeletePermissions = 0
long bytesFound = 0
long bytesFoundWithNoDeletePermission = 0

//def artifactsCleanedUp = searches.artifactsNotDownloadedSince(calendarUntil, calendarUntil, repos)
def artifactsCleanedUp = searches.artifactsCreatedOrModifiedInRange(null, calendarUntil, repos)
artifactsCleanedUp.find {
try {
while ( Global.pauseCleaning ) {
log.info "Pausing by request"
sleep( 60000 )
}
if ( Global.stopCleaning ) {
log.info "Stopping by request, ending loop"
return true
}
if ( ! disablePropertiesSupport && skip[ it.repoKey ] && StringUtils.startsWithAny(it.path, skip[ it.repoKey ])){
if (log.isDebugEnabled()){
log.debug "Skip $it"
}
return false
}
bytesFound += repositories.getItemInfo(it)?.getSize()
cntFoundArtifacts++
if (!security.canDelete(it)) {
bytesFoundWithNoDeletePermission += repositories.getItemInfo(it)?.getSize()
cntNoDeletePermissions++
}
if (dryRun) {
log.info "Found $it, $cntFoundArtifacts/$artifactsCleanedUp.size total $bytesFound bytes"
log.info "\t==> currentUser: ${security.currentUser().getUsername()}"
log.info "\t==> canDelete: ${security.canDelete(it)}"
} else {
if (security.canDelete(it)) {
log.info "Deleting $it, $cntFoundArtifacts/$artifactsCleanedUp.size total $bytesFound bytes"
repositories.delete it
} else {
log.info "Can't delete $it (user ${security.currentUser().getUsername()} has no delete permissions), " +
"$cntFoundArtifacts/$artifactsCleanedUp.size total $bytesFound bytes"
}
}
} catch (ItemNotFoundRuntimeException ex) {
log.info "Failed to find $it, skipping"
}
def sleepTime = (Global.paceTimeMS > 0) ? Global.paceTimeMS : paceTimeMS
if (sleepTime > 0) {
sleep( sleepTime )
}
return false
}
if (dryRun) {
log.info "Dry run - nothing deleted. Found $cntFoundArtifacts artifacts consuming $bytesFound bytes"
if (cntNoDeletePermissions > 0) {
log.info "$cntNoDeletePermissions artifacts cannot be deleted due to lack of permissions ($bytesFoundWithNoDeletePermission bytes)"
}
} else {
log.info "Finished cleanup, deleting $cntFoundArtifacts artifacts that took up $bytesFound bytes"
if (cntNoDeletePermissions > 0) {
log.info "$cntNoDeletePermissions artifacts could not be deleted due to lack of permissions ($bytesFoundWithNoDeletePermission bytes)"
}
}
}
private def getSkippedPaths(String[] repos) {
def timeStart = new Date()
def skip = [:]
for (String repoKey : repos){
def pathsTmp = []
def aql = "items.find({\"repo\":\"" + repoKey + "\",\"type\": \"any\",\"@cleanup.skip\":\"true\"}).include(\"repo\", \"path\", \"name\", \"type\")"
searches.aql(aql.toString()) {
for (item in it) {
def path = item.path + '/' + item.name
// Root path case behavior
if ('.' == item.path){
path = item.name
}
if ('folder' == item.type){
path += '/'
}
if (log.isTraceEnabled()){
log.trace "skip found for " + repoKey + ":" + path
}
pathsTmp.add(path)
}
}
// Simplify list to have only parent paths
def paths = []
for (path in pathsTmp.sort{ it }) {
if (paths.size == 0 || ! path.startsWith(paths[-1])) {
if (log.isTraceEnabled()){
log.trace "skip added for " + repoKey + ":" + path
}
paths.add(path)
}
}
if (paths.size > 0){
skip[repoKey] = paths.toArray(new String[paths.size])
}
}
def timeStop = new Date()
TimeDuration duration = TimeCategory.minus(timeStop, timeStart)
log.info "Elapsed time to retrieve paths to skip: " + duration
return skip
}
private def mapTimeUnitToCalendar (String timeUnit) {
switch ( timeUnit ) {
case "minute":
return Calendar.MINUTE
case "hour":
return Calendar.HOUR
case "day":
return Calendar.DAY_OF_YEAR
case "month":
return Calendar.MONTH
case "year":
return Calendar.YEAR
default:
def errorMessage = "$timeUnit is not a valid time unit. Please check your request or scheduled policy."
log.error errorMessage
throw new CancelException(errorMessage, 400)
}
}

deleteEmptyDirs.groovy

import groovy.transform.Field
import org.artifactory.repo.RepoPath
import static java.lang.Thread.sleep
import static org.artifactory.repo.RepoPathFactory.create
/**
*
* @originalAuthor jbaruch
* @since 16/08/12
*
* @adaptedForClient boris_t
* @since 14/04/20
*
*/
executions { deleteEmptyDirsPlugin(version: '1.1', description: 'Deletes empty directories', users: ['admin'].toSet()) { params ->
if (!params || !params.paths) {
def errorMessage = 'Paths parameter is mandatory, please supply it.'
log.error errorMessage
status = 400
message = errorMessage
} else {
deleteEmptyDirectories(params.paths as String[])
}
}
}
private def deleteEmptyDirectories(String[] paths) {
def totalDeletedDirs = 0
paths.each {
log.info "Beginning deleting empty directories for path($it)"
def deletedDirs = deleteEmptyDirsRecursively create(it)
log.info "Deleted($deletedDirs) empty directories for given path($it)"
totalDeletedDirs += deletedDirs
}
log.info "Finished deleting total($totalDeletedDirs) directories"
}
def deleteEmptyDirsRecursively(RepoPath path) {
def deletedDirs = 0
// let's let other threads to do something.
sleep 50
// if not folder - we're done, nothing to do here
if (repositories.getItemInfo(path).folder) {
def children = repositories.getChildren path
children.each {
deletedDirs += deleteEmptyDirsRecursively it.repoPath
}
// now let's check again
if (repositories.getChildren(path).empty) {
// it is folder, and no children - delete!
log.info "Deleting empty directory($path)"
repositories.delete path
deletedDirs += 1
}
}
return deletedDirs
}

Full Jenkins pipeline code

properties([
parameters([
[$class: 'ChoiceParameter',
name: 'Reload_Plugins',
choiceType: 'PT_SINGLE_SELECT',
description: '<font size=3>Reload Plugins? Select Yes or No.</font>',
filterLength: 1,
filterable: false,
randomName: 'choice-parameter-108460119586981',
script: [
$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: false,
script:
''
],
script: [
classpath: [],
sandbox: false,
script:
'return ["YES","NO:selected"]'
]
]
],
[$class: 'DynamicReferenceParameter',
name: 'Dry_Run',
choiceType: 'ET_FORMATTED_HTML',
description: '',
randomName: 'choice-parameter-108440134895850',
referencedParameters: 'Reload_Plugins',
omitValueField: true,
script: [
$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: false,
script: ''
],
script: [
classpath: [],
sandbox: false,
script: '''
if (Reload_Plugins.equals("YES")) {
return "<table style=width:100%><tr><td align=left><input name=value type=checkbox disabled></td><td align=left width=100%><font size=3>N/A</font></td></tr><tr><td colspan=2><font size=3>Dry Run? Check this option if you wish to run without <b>deleting</b></span> artifacts.</font></td></tr></table>"
}
return "<table style=width:100%><tr><td align=left><input name=value type=checkbox></td></tr><tr><td><font size=3>Dry Run? Check this option if you wish to run without <b>deleting</b></span> artifacts.</font></td></tr></table>"
'''
]
]
],
[$class: 'CascadeChoiceParameter',
choiceType: 'PT_SINGLE_SELECT',
description: '<font size=3>The unit (type) of the time interval.</font>',
filterLength: 1,
filterable: false,
name: 'Time_Unit',
randomName: 'choice-parameter-2930846449195033',
referencedParameters: 'Reload_Plugins',
script:
[$class: 'GroovyScript',
fallbackScript:
[classpath: [],
sandbox: false,
script: ''
],
script:
[classpath: [],
sandbox: false,
script: '''

if (Reload_Plugins.equals("YES")) {
return ["N/A:selected"]
}
return ["Year", "Month", "Day:selected", "Hour", "Minute"]
'''
]
]
],
[$class: 'DynamicReferenceParameter',
name: 'Time_Interval',
choiceType: 'ET_FORMATTED_HTML',
description: '',
randomName: 'choice-parameter-108460134875850',
referencedParameters: 'Reload_Plugins',
omitValueField: true,
script: [
$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: false,
script: ''
],
script: [
classpath: [],
sandbox: false,
script:
'''
if (Reload_Plugins.equals("YES")) {
return "<table style=width:100%><tr><td align=left><input name=value type=text value='N/A' disabled></td></tr><tr><td><font size=3><font size=3>The time interval to look back before deleting an artifact.</font></font></td></tr></table>"
}
return "<table style=width:100%><tr><td align=left><input name=value type=text value='21'></td></tr><tr><td><font size=3><font size=3>The time interval to look back before deleting an artifact.</font></font></td></tr></table>"
'''
]
]
],
[$class: 'DynamicReferenceParameter',
name: 'Target_Repositories',
choiceType: 'ET_FORMATTED_HTML',
description: '',
randomName: 'choice-parameter-108461134575850',
referencedParameters: 'Reload_Plugins',
omitValueField: true,
script: [
$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: false,
script: ''
],
script: [
classpath: [],
sandbox: false,
script:
'''
if (Reload_Plugins.equals("YES")) {
return "<table style=width:100%><tr><td align=left><input name=value type=text value='N/A' disabled></td></tr><tr><td><font size=3>A list of repositories to clean. This parameter is hardcoded to Segway repo.</font></td></tr></table>"
}
return "<table style=width:100%><tr><td align=left><input name=value type=text value='Segway' disabled></td></tr><tr><td><font size=3>A list of repositories to clean. This parameter is hardcoded to Segway repo.</font></td></tr></table>"
'''
]
]
],
[$class: 'DynamicReferenceParameter',
name: 'Pace_Time_ms',
choiceType: 'ET_FORMATTED_HTML',
description: '',
randomName: 'choice-parameter-108260134873850',
referencedParameters: 'Reload_Plugins',
omitValueField: true,
script: [
$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: false,
script: ''
],
script: [
classpath: [],
sandbox: false,
script:
'''
if (Reload_Plugins.equals("YES")) {
return "<table style=width:100%><tr><td align=left><input name=value type=text value='N/A' disabled></td></tr><tr><td><font size=3>The number of milliseconds to delay between delete operations</font></td></tr></table>"
}
return "<table style=width:100%><tr><td align=left><input name=value type=text value='0'></td></tr><tr><td><font size=3>The number of milliseconds to delay between delete operations</font></td></tr></table>"
'''
]
]
],
[$class: 'DynamicReferenceParameter',
name: 'Ignore_Properties',
choiceType: 'ET_FORMATTED_HTML',
description: '',
randomName: 'choice-parameter-118460134875857',
referencedParameters: 'Reload_Plugins',
omitValueField: true,
script: [
$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: false,
script: ''
],
script: [
classpath: [],
sandbox: false,
script:
'''
if (Reload_Plugins.equals("YES")) {
return "<table style=width:100%><tr><td align=left><input name=value type=checkbox disabled></td><td align=left width=100%><font size=3>N/A</font></td></tr><tr><td colspan=2><font size=3>Ignore Artifacts Properties? Check this option if you wish to <b>ignore aertifactory properties</b></span>. <b>NOT RECOMMENDET!</b></font></td></tr></table>"
}
return "<table style=width:100%><tr><td align=left><input name=value type=checkbox></td></tr><tr><td><font size=3>Ignore Artifacts Properties? Check this option if you wish to <b>ignore aertifactory properties</b></span>. <b>NOT RECOMMENDET!</b></font></td></tr></table>"
'''
]
]
],
])
])
// Get/Set Parameters values
def reload_plugins = (env.Reload_Plugins == "YES") ? true : false
def dry_run = (env.Dry_Run == "true") ? true : false
def time_unit = env.Time_Unit.toLowerCase()
def time_interval = env.Time_Interval.isInteger() ? env.Time_Interval.toInteger() : 21 // default = 21
def target_repositories = env.Target_Repositories
def pace_time_ms = env.Pace_Time_ms.isInteger() ? env.Pace_Time_ms.toInteger() : 0 // default = 0
def ignore_properties = (env.Ignore_Properties == "true") ? true : false
// Artifactory API vars
def artifactory_api = "http://artifactory:8081/artifactory/api"
def execute_cleaup = artifactory_api + "/plugins/execute/cleanup?params="
def execute_del_dir = artifactory_api + "/plugins/execute/deleteEmptyDirsPlugin?params="
pipeline {
agent any
stages {
stage('Reload Plugins') {
when { expression { return reload_plugins } }
steps {
script {
withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'jenkins_artifactory_admin_user', usernameVariable: 'username', passwordVariable: 'password']]) {
def encoded_pass = URLEncoder.encode(password, "UTF-8")
sh """
curl -u $username:$encoded_pass -X POST "$artifactory_api/plugins/reload"
"""
}
}
}
} // End stage Reload Plugins
stage('Cleanup Plugin') {
when { expression { return !reload_plugins } }
steps {
script {
def curl_uri = execute_cleaup
// Add params
curl_uri += "timeUnit=$time_unit;"
curl_uri += "timeInterval=$time_interval;"
curl_uri += "repos=$target_repositories;"
curl_uri += "dryRun=$dry_run;"
curl_uri += "paceTimeMS=$pace_time_ms;"
curl_uri += "disablePropertiesSupport=$ignore_properties"
withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'jenkins_artifactory_admin_user', usernameVariable: 'username', passwordVariable: 'password']]) {
def encoded_pass = URLEncoder.encode(password, "UTF-8")
sh """
curl -i -u $username:$encoded_pass -X POST "$curl_uri"
"""
}
}
}
} // End stage Cleanup Plugin
stage('Delete Empty Dirs Plugin') {
// Run this stage only if reload_plugins=false and dry_run=false
when { expression { return (!reload_plugins && !dry_run) } }
steps {
script {
def curl_uri = execute_del_dir
// Add params
curl_uri += "paths=$target_repositories"
withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'jenkins_artifactory_admin_user', usernameVariable: 'username', passwordVariable: 'password']]) {
def encoded_pass = URLEncoder.encode(password, "UTF-8")
sh """
curl -i -u $username:$encoded_pass -X POST "$curl_uri"
"""
}
}
}
} // End stage Delete Empty Dirs Plugin
stage('Empty Trash Can') {
// Run this stage only if reload_plugins=false and dry_run=false
when { expression { return (!reload_plugins && !dry_run) } }
steps {
script {
withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'jenkins_artifactory_admin_user', usernameVariable: 'username', passwordVariable: 'password']]) {
def encoded_pass = URLEncoder.encode(password, "UTF-8")
sh """
curl -u $username:$encoded_pass -X POST "$artifactory_api/trash/empty"
"""
}
}
}
} // End stage Empty Trash Can
stage('Garbage Collector') {
// Run this stage only if reload_plugins=false and dry_run=false
when { expression { return (!reload_plugins && !dry_run) } }
steps {
script {
withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'jenkins_artifactory_admin_user', usernameVariable: 'username', passwordVariable: 'password']]) {
def encoded_pass = URLEncoder.encode(password, "UTF-8")
sh """
curl -u $username:$encoded_pass -X POST "$artifactory_api/system/storage/gc"
"""
}
}
}
} // End stage Garbage Collector
}
}

Related Artifactory REST API Commands

Reload Plugins

curl -u admin:pass -X POST http://artifactory:8081/artifactory/api/plugins/reload

Retrieve Plugin Info

curl -u admin:pass -X GET http://artifactory:8081/artifactory/api/plugins

Execute Cleanup Artifacts Plugin (several variations)

curl -i -u admin:pass -X POST "http://artifactory:8081/artifactory/api/plugins/execute/cleanup?params=timeUnit=month;timeInterval=20;repos=Segway;dryRun=true;paceTimeMS=2;disablePropertiesSupport=false"curl -i -u admin:pass -X POST "http://artifactory:8081/artifactory/api/plugins/execute/cleanup?params=timeUnit=hour;timeInterval=1;repos=Segway;dryRun=true;paceTimeMS=2;disablePropertiesSupport=false"curl -i -u admin:pass -X POST "http://artifactory:8081/artifactory/api/plugins/execute/cleanup?params=timeUnit=month;timeInterval=3;repos=Segway;dryRun=true;disablePropertiesSupport=false"curl -i -u admin:pass -X POST "http://artifactory:8081/artifactory/api/plugins/execute/cleanup?params=timeUnit=month;timeInterval=20;repos=Segway;dryRun=true"curl -i -u admin:pass -X POST "http://artifactory:8081/artifactory/api/plugins/execute/cleanup?params=timeUnit=month;timeInterval=5;repos=Segway;dryRun=true"curl -i -u admin:pass -X POST "http://artifactory:8081/artifactory/api/plugins/execute/cleanup?params=repos=Segway;dryRun=true;disablePropertiesSupport=false"

Execute Delete Empty Dirs Plugin

curl -i -u admin:pass -X POST "http://artifactory:8081/artifactory/api/plugins/execute/deleteEmptyDirsPlugin?params=paths=Segway"

Empty Trash Can

curl -i -u admin:pass -X POST "http://artifactory:8081/artifactory/api/trash/empty"

Run Garbage Collection

curl -i -u admin:pass -X POST "http://artifactory:8081/artifactory/api/system/storage/gc"

--

--