Last active
August 29, 2015 14:20
-
-
Save schisamo/89336bd63e2724e87f1b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2014 Chef Software, Inc. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
@Grab(group='net.kencochrane.raven', module='raven', version='3.1.2') | |
@Grab(group='org.apache.httpcomponents', module='httpmime', version='4.3.2') | |
@Grab(group='org.codehaus.groovy.modules.http-builder', module='http-builder', version='0.7') | |
import groovy.json.JsonSlurper | |
import groovyx.net.http.AsyncHTTPBuilder | |
import groovyx.net.http.HttpResponseException | |
import static groovyx.net.http.ContentType.* | |
import static groovyx.net.http.Method.* | |
import net.kencochrane.raven.* | |
import net.kencochrane.raven.dsn.* | |
import net.kencochrane.raven.event.* | |
import net.kencochrane.raven.event.interfaces.* | |
import org.apache.commons.io.FilenameUtils | |
import org.apache.http.entity.ContentType | |
import org.apache.http.entity.mime.MultipartEntityBuilder | |
import org.apache.http.entity.mime.content.InputStreamBody | |
import org.artifactory.fs.ItemInfo | |
import org.artifactory.fs.FileInfo | |
import org.artifactory.md.Properties | |
import org.artifactory.repo.RepoPath | |
import org.artifactory.repo.Repositories | |
import org.artifactory.resource.ResourceStreamHandle | |
/** | |
* Artifactory user plugin that publishes all incoming artifacts using the | |
* +package_cloud+ API. (http://packagecloud.io/docs/api) | |
* | |
* Assumptions: | |
* - the packagecloud.json file includes: | |
* - the name of the packagecloud user, | |
* - a valid API token for pushing at the "token" attribute, and | |
* - mapping of Artifactory to PackageCloud repos | |
* - an up-to-date copy of the packagecloud distributions.json file | |
* is available in $ARTIFACTORY_HOME/etc. | |
* | |
* @see http://wiki.jfrog.org/confluence/display/RTF/User+Plugins for background. | |
* | |
* @author Seth Vargo <[email protected]> | |
* @author Yvonne Lam <[email protected]> | |
* @author Seth Chisamore <[email protected]> | |
* | |
*/ | |
storage { | |
// Load the plugin's config json | |
def configFile = new File(ctx.artifactoryHome.etcDir, 'packagecloud.json') | |
def config = new JsonSlurper().parseText(configFile.text) | |
/** | |
* Handle after create events. | |
* | |
* Closure parameters: | |
* item (org.artifactory.fs.ItemInfo) - the original item being created. | |
*/ | |
afterCreate { ItemInfo item -> | |
handlePushToPackageCloud(item, item.repoKey, config) | |
} | |
/** | |
* Handle after copy events. | |
* | |
* Closure parameters: | |
* item (org.artifactory.fs.ItemInfo) - the source item copied. | |
* targetRepoPath (org.artifactory.repo.RepoPath) - the target repoPath for the copy. | |
* properties (org.artifactory.md.Properties) - user specified properties to add to the item being moved. | |
*/ | |
afterCopy { ItemInfo item, RepoPath targetRepoPath, Properties properties -> | |
handlePushToPackageCloud(item, targetRepoPath.repoKey, config) | |
} | |
/** | |
* Handle after move events. | |
* | |
* Closure parameters: | |
* item (org.artifactory.fs.ItemInfo) - the source item moved. | |
* targetRepoPath (org.artifactory.repo.RepoPath) - the target repoPath for the move. | |
* properties (org.artifactory.md.Properties) - user specified properties to add to the item being moved. | |
*/ | |
afterMove { ItemInfo item, RepoPath targetRepoPath, Properties properties -> | |
handlePushToPackageCloud(item, targetRepoPath.repoKey, config) | |
} | |
/** | |
* Handle after delete events. | |
* | |
* Closure parameters: | |
* item (org.artifactory.fs.ItemInfo) - the original item deleted. | |
*/ | |
afterDelete { ItemInfo item -> | |
handleYankFromPackageCloud(item, config) | |
} | |
} | |
/******************************************************************************* | |
* Extracts required metadata and pushes a package to packagecloud.io using the | |
* REST API. | |
*******************************************************************************/ | |
def handlePushToPackageCloud(item, targetRepoKey, config) { | |
def itemPath = item.repoPath.path | |
def itemProperties = repositories.getProperties(item.repoPath) | |
def targetRepoPath = "${targetRepoKey}:${itemPath}" | |
def exclusionFilter = config.exclusion_filter ?: '' | |
def platform = itemProperties.getFirst('omnibus.platform') | |
def platform_version = itemProperties.getFirst('omnibus.platform_version') | |
if (item.isFolder()) { | |
log.debug("Ignoring creation of new folder '${item.repoPath}'") | |
return | |
} else if (itemPath.endsWith(".md5") || itemPath.endsWith(".sha1")) { | |
log.debug("Ignoring creation of checksum files") | |
return | |
} else if (!config.repo_mapping.keySet().contains(targetRepoKey)) { | |
log.debug("Pushing packages to packagecloud.io for ${config.repo_mapping.keySet()} only. Skipping upload of '${targetRepoPath}'.") | |
return | |
} else if (item.repoPath ==~ ~exclusionFilter) { | |
log.debug("Package path matches exclusion filter of /${exclusionFilter}/. Skipping upload of '${targetRepoPath}'.") | |
return | |
} else if (isELCompatible(platform)) { | |
log.debug("Use corresponding EL packages for ${platform} ${platform_version}. Skipping upload of '${targetRepoPath}'.") | |
return | |
} else if (!isSupportedPlatform(platform)) { | |
log.debug("No packagecloud.io repo for platform ${platform} ${platform_version}. Skipping upload of '${targetRepoPath}'.") | |
return | |
} | |
// Command data | |
def package_content = repositories.getContent(item.repoPath) | |
def package_name = FilenameUtils.getName(itemPath) | |
def package_ext = FilenameUtils.getExtension(itemPath) | |
def packagecloud_repo = config.repo_mapping[targetRepoKey] | |
def distro_id = getDistroId(platform, platform_version, package_ext) | |
def packagecloud_url = "https://packagecloud.io/api/v1/repos/${config.user}/${packagecloud_repo}/packages.json" | |
try { | |
http = createHttpClient(packagecloud_url, config.token) | |
log.info("[BEGIN] push of package file '${targetRepoPath}' to '${packagecloud_url}' with distro_id '${distro_id}'.") | |
http.request(POST) { req -> | |
MultipartEntityBuilder mpe = MultipartEntityBuilder.create() | |
mpe.addPart("package[package_file]", new ArtifactoryResourceStreamHandleBody(package_content, ContentType.DEFAULT_BINARY, package_name)) | |
mpe.addTextBody("package[distro_version_id]", Integer.toString(distro_id)) | |
req.entity = mpe.build() | |
response.success = { resp -> | |
log.info("[COMPLETE] push of package file '${targetRepoPath}' to '${packagecloud_url}'.") | |
} | |
} | |
} catch(exception) { | |
message = "An error occurred PUSHING package ${item.repoPath} to packagecloud.io repo '${packagecloud_repo}'" | |
handleException(message, exception, config, [packagecloud_url: packagecloud_url, | |
http_method: "POST", | |
packagecloud_repo: packagecloud_repo, | |
platform: platform, | |
platform_version: platform_version]) | |
} | |
} | |
/******************************************************************************* | |
* Extracts required metadata and yanks a package from packagecloud.io using the | |
* REST API. | |
*******************************************************************************/ | |
def handleYankFromPackageCloud(item, config) { | |
def itemRepoKey = item.repoKey | |
def itemPath = item.repoPath.path | |
def itemProperties = repositories.getProperties(item.repoPath) | |
def platform = itemProperties.getFirst('omnibus.platform') | |
def platform_version = itemProperties.getFirst('omnibus.platform_version') | |
if (item.isFolder()) { | |
log.debug("Ignoring deletion of folder: ${item.repoPath}") | |
return | |
} else if (itemPath.endsWith(".md5") || itemPath.endsWith(".sha1")) { | |
log.debug("Ignoring deletion of checksum files") | |
return | |
} else if (!config.repo_mapping.keySet().contains(itemRepoKey)) { | |
log.debug("Yanking packages from packagecloud.io for ${config.repo_mapping.keySet()} only. Skipping deletion of '${item.repoPath}'.") | |
return | |
} else if (!isSupportedPlatform(platform)) { | |
log.debug("No packagecloud.io repo for ${platform} ${platform_version}, no further cleanup needed.") | |
return | |
} | |
// command properties | |
def package_name = FilenameUtils.getName(itemPath) | |
def package_ext = FilenameUtils.getExtension(itemPath) | |
def packagecloud_repo = config.repo_mapping[itemRepoKey] | |
def packagecloud_distro_version = getPackageCloudDistroVersionName(platform, platform_version, package_ext) | |
def packagecloud_url = "https://packagecloud.io/api/v1/repos/${config.user}/${packagecloud_repo}/${platform}/${packagecloud_distro_version}/${package_name}" | |
try { | |
http = createHttpClient(packagecloud_url, config.token) | |
log.info("[BEGIN] yank of '${item.repoPath}' from '${packagecloud_url}'") | |
http.request(DELETE) { req -> | |
path: packagecloud_url | |
response.success = { resp -> | |
log.info("[COMPLETE] yank of '${item.repoPath}' from '${packagecloud_url}'") | |
} | |
response.'404' = { resp -> | |
log.info("[COMPLETE] package '${item.repoPath}' does not exist in packagecloud.io repo '${packagecloud_repo}', no further cleanup needed.") | |
} | |
} | |
} catch(exception) { | |
message = "An error occurred YANKING package '${item.repoPath}' from packagecloud.io repo '${packagecloud_repo}'" | |
handleException(message, exception, config, [packagecloud_url: packagecloud_url, | |
http_method: "DELETE", | |
packagecloud_repo: packagecloud_repo, | |
platform: platform, | |
platform_version: platform_version]) | |
} | |
} | |
/******************************************************************************* | |
* Platform Helpers | |
*******************************************************************************/ | |
Boolean isELCompatible(platform) { | |
['suse', 'sles'].contains(platform) | |
} | |
Boolean isSupportedPlatform(platform) { | |
['debian', 'ubuntu', 'el', 'fedora'].contains(platform) | |
} | |
/******************************************************************************* | |
* HTTP Client Helpers | |
*******************************************************************************/ | |
AsyncHTTPBuilder createHttpClient(url, token) { | |
AsyncHTTPBuilder http = new AsyncHTTPBuilder(uri: url, | |
poolSize: 10, | |
timeout: 600000) // 10 minutes | |
http.headers['Authorization'] = 'Basic '+"${token}:".getBytes('iso-8859-1').encodeBase64() | |
http | |
} | |
/** | |
* Extend {@link InputStreamBody} and take avantage of the fact that | |
* {@link ResourceStreamHandle} has a known length associated with it. | |
* | |
* See http://www.radomirml.com/blog/2009/02/13/file-upload-with-httpcomponents-successor-of-commons-httpclient/ | |
*/ | |
class ArtifactoryResourceStreamHandleBody extends InputStreamBody { | |
private int length; | |
public ArtifactoryResourceStreamHandleBody(final ResourceStreamHandle handle, | |
final ContentType contentType, | |
final String filename) { | |
super(handle.getInputStream(), contentType, filename); | |
this.length = handle.getSize(); | |
} | |
@Override | |
public long getContentLength() { | |
return this.length; | |
} | |
} | |
/******************************************************************************* | |
* Sentry Helpers | |
*******************************************************************************/ | |
def handleException(message, exception, config, tags) { | |
// Log message locally | |
log.error(message, exception) | |
// Generate a Sentry event | |
EventBuilder eventBuilder = new EventBuilder().setMessage(message ?: exception.message) | |
.setLevel(Event.Level.ERROR) | |
.addSentryInterface(new ExceptionInterface(exception)) | |
.addSentryInterface(new StackTraceInterface(exception)) | |
// Attach tags to the event | |
for ( t in tags ) { | |
eventBuilder.addTag(t.key, t.value) | |
} | |
// Send the event to Sentry | |
sentry_client = new DefaultRavenFactory().createRavenInstance(new Dsn(config.sentry_dsn)) | |
sentry_client.sendEvent(eventBuilder.build()) | |
} | |
/******************************************************************************* | |
* packagecloud.io Helpers | |
*******************************************************************************/ | |
// packagecloud.io's distribution.json as a JSON object | |
// See https://packagecloud.io/docs/api#resource_distributions | |
Map getDistroJSON(platform, platform_version, package_ext) { | |
// Load packagecloud.io's distro-to-repo-name mapping | |
def inputFile = new File(ctx.artifactoryHome.etcDir, 'packagecloud.distributions.json') | |
def inputJSON = new JsonSlurper().parseText(inputFile.text) | |
// packagecloud.io wants platform versions with .'s | |
if (platform_version.contains('.')) { | |
pv = platform_version | |
} else { | |
pv = "${platform_version}.0" | |
} | |
platforms = inputJSON[package_ext].find { it['index_name'] == platform } | |
distro_version = platforms.versions.find { it['version_number'] == pv } | |
distro_version | |
} | |
// Extract distro_id from Packagecloud's distribution.json. | |
Integer getDistroId(platform, platform_version, package_ext) { | |
distro_version = getDistroJSON(platform, platform_version, package_ext) | |
distro_version['id'] | |
} | |
// The name corresponding to the distro version (used by packagecloud.io's 'yank' API) | |
String getPackageCloudDistroVersionName(platform, platform_version, package_ext) { | |
distro_version = getDistroJSON(platform, platform_version, package_ext) | |
distro_version['index_name'] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment