|
| 1 | +package org.labkey.gradle.task |
| 2 | + |
| 3 | + |
| 4 | +import org.apache.commons.lang3.StringUtils |
| 5 | +import org.apache.hc.client5.http.classic.methods.HttpDelete |
| 6 | +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient |
| 7 | +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse |
| 8 | +import org.apache.hc.client5.http.impl.classic.HttpClients |
| 9 | +import org.apache.hc.core5.http.HttpStatus |
| 10 | +import org.gradle.api.DefaultTask |
| 11 | +import org.gradle.api.GradleException |
| 12 | +import org.gradle.api.provider.Property |
| 13 | +import org.gradle.api.tasks.Input |
| 14 | +import org.gradle.api.tasks.TaskAction |
| 15 | +import org.labkey.gradle.util.TaskUtils |
| 16 | + |
| 17 | +abstract class PurgeNpmVersions extends DefaultTask |
| 18 | +{ |
| 19 | + private static final String REPOSITORY_NAME = 'libs-client-local' |
| 20 | + public static final String DRY_RUN_PROPERTY = 'dryRun' |
| 21 | + private static final String PACKAGE_NAME_PROP = "packageName" |
| 22 | + private static final String VERSION_LIST_PROP = "versionList" |
| 23 | + |
| 24 | + @Input |
| 25 | + final abstract Property<String> packageName = project.objects.property(String).convention((project.hasProperty(PACKAGE_NAME_PROP) ? (String) project.property(PACKAGE_NAME_PROP) : null)) |
| 26 | + |
| 27 | + @Input |
| 28 | + final abstract Property<String> versionList = project.objects.property(String).convention((project.hasProperty(VERSION_LIST_PROP) ? (String) project.property(VERSION_LIST_PROP) : null)) |
| 29 | + |
| 30 | + @Input |
| 31 | + final abstract Property<Boolean> isDryRun = project.objects.property(Boolean).convention(project.hasProperty(DRY_RUN_PROPERTY)) |
| 32 | + @Input |
| 33 | + final abstract Property<String> artifactoryUrl = project.objects.property(String).convention((String) project.property('artifactory_contextUrl')) |
| 34 | + @Input |
| 35 | + final abstract Property<String> artifactoryUser = project.objects.property(String).convention((String) project.property('artifactory_user')) |
| 36 | + @Input |
| 37 | + final abstract Property<String> artifactoryPassword = project.objects.property(String).convention((String) project.property('artifactory_password')) |
| 38 | + |
| 39 | + @TaskAction |
| 40 | + void purgeVersions() |
| 41 | + { |
| 42 | + if (!packageName.isPresent() || StringUtils.isEmpty(packageName.get().trim())) |
| 43 | + throw new GradleException("No value provided for packageName.") |
| 44 | + String packageName = "@labkey/" + packageName.get() |
| 45 | + String[] undeletedVersions = [] |
| 46 | + |
| 47 | + logger.quiet("Considering ${packageName}...") |
| 48 | + List<String> versions = readPurgeVersions() |
| 49 | + if (versions.isEmpty()) |
| 50 | + logger.quiet("No versions provided.") |
| 51 | + else { |
| 52 | + logger.quiet("Found ${versions.size()} versions in package ${packageName}") |
| 53 | + versions.forEach(version -> { |
| 54 | + if (isDryRun.get()) |
| 55 | + logger.quiet("Removing version ${version} of package ${packageName} -- Skipped for dry run") |
| 56 | + else { |
| 57 | + logger.quiet("Removing version ${version} of package ${packageName}") |
| 58 | + if (!makeDeleteRequest(packageName, version)) { |
| 59 | + undeletedVersions += "${packageName}: ${version}" |
| 60 | + } |
| 61 | + } |
| 62 | + }) |
| 63 | + } |
| 64 | + |
| 65 | + if (undeletedVersions.size() > 0) |
| 66 | + throw new GradleException("The following versions were not deleted.\n${undeletedVersions}\nCheck the log for more information.") |
| 67 | + } |
| 68 | + |
| 69 | + List<String> readPurgeVersions() |
| 70 | + { |
| 71 | + if (versionList.isPresent() && !StringUtils.isEmpty(versionList.get().trim())) |
| 72 | + return TaskUtils.readInputFile(versionList.get(), "versions", logger) |
| 73 | + else |
| 74 | + throw new GradleException("No " + VERSION_LIST_PROP + " or " + VERSION_LIST_PROP + " property provided."); |
| 75 | + } |
| 76 | + |
| 77 | + |
| 78 | + /** |
| 79 | + * This uses the Artifactory REST Api to request a deletion of a particular package and version. There does |
| 80 | + * not appear to be a way to request deletion of multiple versions at once. Also, though it might seem natural |
| 81 | + * to use "npm unpublish" for this deletion, this does not work with artifactory, possibly due to this long-standing |
| 82 | + * issue: https://github.com/npm/npm-registry-client/issues/41 |
| 83 | + * The command appears to work, returning a 200 status code when you use --verbose logging, but the artifact doesn't |
| 84 | + * go anywhere. |
| 85 | + * |
| 86 | + * Another possibility here would be to use the same action as is used in the Web UI. There, Artifactory sends |
| 87 | + * a POST request to: |
| 88 | + * Request URL: https://artifactory.labkey.com/artifactory/ui/artifactactions/delete |
| 89 | + * with parameters |
| 90 | + * repoKey: libs-client-local |
| 91 | + * path: "@labkey/components/-/@labkey/components-2.14.2-fb-update-react-select.1.tgz" |
| 92 | + * The REST API seems a better approach, though. |
| 93 | + * @param packageName the package whose version is to be deleted, including the scope (e.g., @labkey/components) |
| 94 | + * @param version the version of the package to delete (e.g., 2.14.2-fb-update-react-select.1) |
| 95 | + * @return true if deletion was successful, false otherwise |
| 96 | + * @throws GradleException if the delete request throws an exception |
| 97 | + */ |
| 98 | + protected boolean makeDeleteRequest(String packageName, String version) |
| 99 | + { |
| 100 | + CloseableHttpClient httpClient = HttpClients.createDefault() |
| 101 | + String endpoint = artifactoryUrl.get() |
| 102 | + boolean success = true |
| 103 | + if (!endpoint.endsWith("/")) |
| 104 | + endpoint += "/" |
| 105 | + |
| 106 | + // The coordinates of the packages look like this: "@labkey/components/-/@labkey/components-2.14.2-fb-update-react-select.1.tgz" |
| 107 | + endpoint += REPOSITORY_NAME + "/" + packageName + "/-/" + packageName + "-" + version + ".tgz" |
| 108 | + logger.debug("Making delete request for package ${packageName} and version ${version} via endpoint ${endpoint}") |
| 109 | + try |
| 110 | + { |
| 111 | + HttpDelete httpDelete = new HttpDelete(endpoint) |
| 112 | + // N.B. Using Authorization Bearer with an API token does not currently work |
| 113 | + httpDelete.setHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString("${artifactoryUser.get()}:${artifactoryPassword.get()}".getBytes())) |
| 114 | + CloseableHttpResponse response = httpClient.execute(httpDelete) |
| 115 | + int statusCode = response.getCode() |
| 116 | + if (statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_NO_CONTENT) { |
| 117 | + logger.error("Unable to delete using ${endpoint}: ${statusCode} ${response.getReasonPhrase()}") |
| 118 | + success = false |
| 119 | + } |
| 120 | + response.close() |
| 121 | + return success |
| 122 | + } |
| 123 | + catch (Exception e) |
| 124 | + { |
| 125 | + throw new GradleException("Problem executing delete request with url ${endpoint}", e) |
| 126 | + } |
| 127 | + finally |
| 128 | + { |
| 129 | + httpClient.close() |
| 130 | + } |
| 131 | + } |
| 132 | +} |
0 commit comments