Skip to content

Instantly share code, notes, and snippets.

@double16
Last active January 4, 2019 12:57
Show Gist options
  • Save double16/e9588de0f9cc063751cb390809626e5e to your computer and use it in GitHub Desktop.
Save double16/e9588de0f9cc063751cb390809626e5e to your computer and use it in GitHub Desktop.
Creates a JSON file suitable for deployment using AWS CodePipeline + ECS deploy
dependencies {
compile "com.bmuschko:gradle-docker-plugin:3.6.0"
compile 'org.yaml:snakeyaml:1.17'
}
import com.bmuschko.gradle.docker.DockerRegistryCredentials
import com.bmuschko.gradle.docker.tasks.AbstractDockerRemoteApiTask
import com.bmuschko.gradle.docker.tasks.RegistryCredentialsAware
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Slf4j
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
/**
* Copies Docker images from one registry to another. The input is a JSON file intended for AWS CodeDeploy specifying
* which ECS containers to update.
*/
@Slf4j
class ImageCopyTask extends AbstractDockerRemoteApiTask implements RegistryCredentialsAware {
@InputFile
File imageDefinitions
@OutputFile
@Optional
File copiedDefinitions
/**
* The target Docker registry credentials.
*/
@Nested
@Optional
DockerRegistryCredentials registryCredentials
ImageCopyTask() {
onlyIf { registryCredentials != null }
}
void runRemoteCommand(dockerClient) {
def authConfig = threadContextClassLoader.createAuthConfig(getRegistryCredentials())
URL repoUrl
if (registryCredentials.url.contains('://')) {
repoUrl = new URL(registryCredentials.url)
} else {
repoUrl = new URL('https://'+registryCredentials.url)
}
def images = new JsonSlurper().parse(imageDefinitions)
def copiedImages = []
images.each {
String definitionName = it['name']
String originalImage = it['imageUri']
def match = originalImage =~ /(?:.*\/)(.+)(?::(.*))/
if (match) {
String name = match[0][1]
String tag = match[0][2] ?: 'latest'
String newImage = (repoUrl.port > 0) ? "${repoUrl.host}:${repoUrl.port}/${name}":"${repoUrl.host}/${name}"
log.info "Copying ${originalImage} to ${newImage}:${tag}"
log.info "Pulling ${originalImage}"
def pullImageCmd = dockerClient.pullImageCmd(originalImage-":${tag}").withTag(tag)
pullImageCmd.exec(threadContextClassLoader.createPullImageResultCallback(onNext)).awaitSuccess()
def inspectImage = dockerClient.inspectImageCmd(originalImage).exec()
log.info "Tagging ${originalImage} as ${newImage}:${tag}"
def tagImageCmd = dockerClient.tagImageCmd(inspectImage.id, newImage, tag)
tagImageCmd.exec()
log.info "Pushing ${newImage}:${tag}"
def pushImageCmd = dockerClient.pushImageCmd(newImage)
.withTag(tag)
.withAuthConfig(authConfig)
pushImageCmd.exec(threadContextClassLoader.createPushImageResultCallback(onNext)).awaitSuccess()
copiedImages << [name: definitionName, imageUri: "${newImage}:${tag}"]
}
}
if (copiedDefinitions != null) {
copiedDefinitions.text = JsonOutput.prettyPrint(JsonOutput.toJson(copiedImages))
}
}
}
version: "3"
volumes:
consul:
services:
mongodb:
image: pdouble16/autopilotpattern-mongodb:3.6.3-r3
api:
image: ${PRIVATE_DOCKER_REGISTRY}/api:${APPLICATION_VERSION:-latest}
consul:
image: pdouble16/autopilotpattern-consul:1.0.6-r1
version: "3"
volumes:
consul:
services:
consul:
image: consul:latest
import groovy.json.JsonOutput
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.yaml.snakeyaml.Yaml
import java.util.regex.Pattern
/**
* Creates a JSON file suitable for deployment using AWS CodePipeline + ECS deploy. It describes the docker
* images pushed and the containers that should be updated. The images are taken from a set of docker files.
*/
@CacheableTask
class ImageDefinitionsTask extends DefaultTask {
protected static final Pattern VARIABLE = ~/\$\{(.*?)}/
/** The destination for the JSON file. */
@OutputFile
File output
/** The name to resolve variables in docker files. */
@Input
Map<String, CharSequence> environment = [:]
/** List of services to only include in the result. */
@Input
@Optional
List<String> only = []
@InputFiles
List<File> dockerComposeFiles = []
@TaskAction
void createOutput() {
Map<String, CharSequence> images = [:]
Yaml parser = new Yaml()
dockerComposeFiles.each { File file ->
Map<String, Object> yaml = Collections.emptyMap()
file.withInputStream { yaml = parser.loadAs(it, Map) }
yaml['services']?.each { k,v ->
if (v.containsKey('image')) {
String image = v['image'].toString()
image = image.replaceAll(VARIABLE, { m, x -> interpolateSingle(x as String) } )
images[k.toString()] = image
}
}
}
if (only) {
images = images.findAll { k,v -> only.contains(k) }
}
List model = []
images.each { k, v -> model << [name: k, imageUri: v.toString()] }
output.parentFile.mkdirs()
output.text = JsonOutput.prettyPrint(JsonOutput.toJson(model))
}
protected String interpolateSingle(String variable) {
if (!variable) {
return ''
}
String[] split = variable.split(':-')
String replacement = environment[split[0]]
if (replacement == null) {
replacement = (split.length > 1) ? split[1] : ''
}
replacement
}
}
import groovy.json.JsonSlurper
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Specification
import spock.lang.Unroll
@Unroll
class ImageDefinitionsTaskSpec extends Specification {
@Rule
TemporaryFolder tmp = new TemporaryFolder()
Project project
void setup() {
project = ProjectBuilder.builder().withProjectDir(tmp.newFolder()).build()
}
void "single compose file no environment"() {
given:
File testFile = tmp.newFile()
project.tasks.create('imageDef', ImageDefinitionsTask) {
output = testFile
dockerComposeFiles = [new File('src/test/resources/imagedefinitions-compose1.yml')]
}
when:
project.evaluate()
project.tasks.imageDef.createOutput()
then:
new JsonSlurper().parse(testFile) == [
[
name: 'mongodb',
imageUri: 'pdouble16/autopilotpattern-mongodb:3.6.3-r3',
],
[
name: 'api',
imageUri: '/api:latest',
],
[
name: 'consul',
imageUri: 'pdouble16/autopilotpattern-consul:1.0.6-r1',
]
]
}
void "single compose file with environment"() {
given:
File testFile = tmp.newFile()
project.tasks.create('imageDef', ImageDefinitionsTask) {
output = testFile
dockerComposeFiles = [new File('src/test/resources/imagedefinitions-compose1.yml')]
environment = [ PRIVATE_DOCKER_REGISTRY: 'gatekeeper:5001', APPLICATION_VERSION: '1.2.3' ]
}
when:
project.evaluate()
project.tasks.imageDef.createOutput()
then:
new JsonSlurper().parse(testFile) == [
[
name: 'mongodb',
imageUri: 'pdouble16/autopilotpattern-mongodb:3.6.3-r3',
],
[
name: 'api',
imageUri: 'gatekeeper:5001/api:1.2.3',
],
[
name: 'consul',
imageUri: 'pdouble16/autopilotpattern-consul:1.0.6-r1',
]
]
}
void "multiple compose files"() {
given:
File testFile = tmp.newFile()
project.tasks.create('imageDef', ImageDefinitionsTask) {
output = testFile
dockerComposeFiles = [
new File('src/test/resources/imagedefinitions-compose1.yml'),
new File('src/test/resources/imagedefinitions-compose2.yml')
]
environment = [ PRIVATE_DOCKER_REGISTRY: 'gatekeeper:5001', APPLICATION_VERSION: '1.2.3' ]
}
when:
project.evaluate()
project.tasks.imageDef.createOutput()
then:
new JsonSlurper().parse(testFile) == [
[
name: 'mongodb',
imageUri: 'pdouble16/autopilotpattern-mongodb:3.6.3-r3',
],
[
name: 'api',
imageUri: 'gatekeeper:5001/api:1.2.3',
],
[
name: 'consul',
imageUri: 'consul:latest',
]
]
}
void "single compose file with environment and subset of services"() {
given:
File testFile = tmp.newFile()
project.tasks.create('imageDef', ImageDefinitionsTask) {
output = testFile
only << 'api'
dockerComposeFiles = [new File('src/test/resources/imagedefinitions-compose1.yml')]
environment = [ PRIVATE_DOCKER_REGISTRY: 'gatekeeper:5001', APPLICATION_VERSION: '1.2.3' ]
}
when:
project.evaluate()
project.tasks.imageDef.createOutput()
then:
new JsonSlurper().parse(testFile) == [
[
name: 'api',
imageUri: 'gatekeeper:5001/api:1.2.3',
]
]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment