Skip to content

Instantly share code, notes, and snippets.

@schisamo
Last active August 29, 2015 14:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save schisamo/89336bd63e2724e87f1b to your computer and use it in GitHub Desktop.
Save schisamo/89336bd63e2724e87f1b to your computer and use it in GitHub Desktop.
/*
* 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 <sethvargo@chef.io>
* @author Yvonne Lam <yvonne@chef.io>
* @author Seth Chisamore <schisamo@chef.io>
*
*/
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