Skip to content

Commit

Permalink
Merge pull request #607 from bmuschko/ISSUE-606
Browse files Browse the repository at this point in the history
Support multi-stage builds in Dockerfile task
  • Loading branch information
orzeh authored Jun 27, 2018
2 parents 5ad185b + 03dadd0 commit 088ee55
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -387,4 +387,64 @@ task ${DOCKERFILE_TASK_NAME}(type: Dockerfile) {
and:
buildAndFail('dockerFile2')
}

def "supports multi-stage builds"() {
buildFile << """
import com.bmuschko.gradle.docker.tasks.image.Dockerfile
task ${DOCKERFILE_TASK_NAME}(type: Dockerfile) {
from '$TEST_IMAGE_WITH_TAG', 'builder'
maintainer 'Benjamin Muschko "benjamin.muschko@gmail.com"'
copyFile 'http://hsql.sourceforge.net/m2-repo/com/h2database/h2/1.4.184/h2-1.4.184.jar', '/opt/h2.jar'
from({'$TEST_IMAGE_WITH_TAG'}, {'prod'})
copyFile '/opt/h2.jar', '/opt/h2.jar', 'builder'
}
"""
when:
build(DOCKERFILE_TASK_NAME)

then:
File dockerfile = new File(projectDir, 'build/docker/Dockerfile')
dockerfile.exists()
dockerfile.text ==
"""FROM $TEST_IMAGE_WITH_TAG AS builder
MAINTAINER Benjamin Muschko "benjamin.muschko@gmail.com"
COPY http://hsql.sourceforge.net/m2-repo/com/h2database/h2/1.4.184/h2-1.4.184.jar /opt/h2.jar
FROM alpine:3.4 AS prod
COPY --from=builder /opt/h2.jar /opt/h2.jar
"""
}


def "supports multi-stage builds (lazy evaluation)"() {
buildFile << """
import com.bmuschko.gradle.docker.tasks.image.Dockerfile

ext.buildStageName = ''

task ${DOCKERFILE_TASK_NAME}(type: Dockerfile) {
from({'$TEST_IMAGE_WITH_TAG'}, {buildStageName})
maintainer 'Benjamin Muschko "benjamin.muschko@gmail.com"'
copyFile 'http://hsql.sourceforge.net/m2-repo/com/h2database/h2/1.4.184/h2-1.4.184.jar', '/opt/h2.jar'
from({'$TEST_IMAGE_WITH_TAG'}, {'prod'})
copyFile({'/opt/h2.jar'}, {'/opt/h2.jar'}, {buildStageName})
doFirst {
buildStageName = 'builder'
}
}
"""
when:
build(DOCKERFILE_TASK_NAME)
then:
File dockerfile = new File(projectDir, 'build/docker/Dockerfile')
dockerfile.exists()
dockerfile.text ==
"""FROM $TEST_IMAGE_WITH_TAG AS builder
MAINTAINER Benjamin Muschko "benjamin.muschko@gmail.com"
COPY http://hsql.sourceforge.net/m2-repo/com/h2database/h2/1.4.184/h2-1.4.184.jar /opt/h2.jar
FROM alpine:3.4 AS prod
COPY --from=builder /opt/h2.jar /opt/h2.jar
"""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ class Dockerfile extends DefaultTask {
}

private void verifyValidInstructions() {
if(getInstructions().empty) {
if (getInstructions().empty) {
throw new IllegalStateException('Please specify instructions for your Dockerfile')
}

def fromPos = getInstructions().findIndexOf { it.keyword == 'FROM' }
def othersPos = getInstructions().findIndexOf { it.keyword != 'ARG' && it.keyword != 'FROM' }
if(fromPos < 0 || (othersPos >= 0 && fromPos > othersPos)) {
if (fromPos < 0 || (othersPos >= 0 && fromPos > othersPos)) {
throw new IllegalStateException('The first instruction of a Dockerfile has to be FROM (or ARG for Docker later than 17.05)')
}
}
Expand Down Expand Up @@ -112,9 +112,10 @@ class Dockerfile extends DefaultTask {
* subsequent instructions.
*
* @param image Base image name
* @param stageName stage name in case of multi-stage builds (default null)
*/
void from(String image) {
instructions << new FromInstruction(image)
void from(String image, String stageName = null) {
instructions << new FromInstruction(image, stageName)
}

/**
Expand All @@ -132,9 +133,10 @@ class Dockerfile extends DefaultTask {
* subsequent instructions.
*
* @param image Base image name
* @param stageName closure returning stage name in case of multi-stage builds (default null)
*/
void from(Closure image) {
instructions << new FromInstruction(image)
void from(Closure image, Closure stageName = null) {
instructions << new FromInstruction(image, stageName)
}

/**
Expand Down Expand Up @@ -276,9 +278,10 @@ class Dockerfile extends DefaultTask {
*
* @param src Source file
* @param dest Destination path
* @param stageName stage name in case of multi stage build
*/
void copyFile(String src, String dest) {
instructions << new CopyFileInstruction(src, dest)
void copyFile(String src, String dest, String stageName = null) {
instructions << new CopyFileInstruction(src, dest, stageName)
}

/**
Expand All @@ -287,9 +290,10 @@ class Dockerfile extends DefaultTask {
*
* @param src Source file
* @param dest Destination path
* @param stageName closure returning stage name in case of multi stage build
*/
void copyFile(Closure src, Closure dest) {
instructions << new CopyFileInstruction(src, dest)
void copyFile(Closure src, Closure dest, Closure stageName) {
instructions << new CopyFileInstruction(src, dest, stageName)
}

/**
Expand Down Expand Up @@ -428,10 +432,9 @@ class Dockerfile extends DefaultTask {

@Override
String getKeyword() {
if(instruction instanceof String) {
if (instruction instanceof String) {
parseKeyword(instruction)
}
else if(instruction instanceof Closure) {
} else if (instruction instanceof Closure) {
parseKeyword(instruction())
}
}
Expand All @@ -442,10 +445,9 @@ class Dockerfile extends DefaultTask {

@Override
String build() {
if(instruction instanceof String) {
instruction
}
else if(instruction instanceof Closure) {
if (instruction instanceof String) {
instruction
} else if (instruction instanceof Closure) {
instruction()
}
}
Expand All @@ -464,10 +466,9 @@ class Dockerfile extends DefaultTask {

@Override
String build() {
if(command instanceof String) {
if (command instanceof String) {
"$keyword $command"
}
else if(command instanceof Closure) {
} else if (command instanceof Closure) {
"$keyword ${command()}"
}
}
Expand All @@ -486,16 +487,14 @@ class Dockerfile extends DefaultTask {

@Override
String build() {
if(command instanceof String[]) {
if (command instanceof String[]) {
keyword + ' ["' + command.join('", "') + '"]'
}
else if(command instanceof Closure) {
} else if (command instanceof Closure) {
def evaluatedCommand = command()

if(evaluatedCommand instanceof String) {
if (evaluatedCommand instanceof String) {
keyword + ' ["' + evaluatedCommand + '"]'
}
else {
} else {
keyword + ' ["' + command().join('", "') + '"]'
}
}
Expand All @@ -510,7 +509,7 @@ class Dockerfile extends DefaultTask {
@Override
String join(Map<String, String> map) {
map.inject([]) { result, entry ->
def key = ItemJoinerUtil.isUnquotedStringWithWhitespaces(entry.key) ? ItemJoinerUtil.toQuotedString(entry.key) : entry.key
def key = ItemJoinerUtil.isUnquotedStringWithWhitespaces(entry.key) ? ItemJoinerUtil.toQuotedString(entry.key) : entry.key
def value = ItemJoinerUtil.isUnquotedStringWithWhitespaces(entry.value) ? ItemJoinerUtil.toQuotedString(entry.value) : entry.value
value = value.replaceAll("(\r)*\n", "\\\\\n")
result << "$key=$value"
Expand All @@ -522,7 +521,7 @@ class Dockerfile extends DefaultTask {
@Override
String join(Map<String, String> map) {
map.inject([]) { result, entry ->
def key = ItemJoinerUtil.isUnquotedStringWithWhitespaces(entry.key) ? ItemJoinerUtil.toQuotedString(entry.key) : entry.key
def key = ItemJoinerUtil.isUnquotedStringWithWhitespaces(entry.key) ? ItemJoinerUtil.toQuotedString(entry.key) : entry.key
// preserve multiline value in a single item key value instruction but ignore any other whitespaces or quotings
def value = entry.value.replaceAll("(\r)*\n", "\\\\\n")
result << "$key $value"
Expand Down Expand Up @@ -563,15 +562,15 @@ class Dockerfile extends DefaultTask {
@Override
String build() {
Map<String, String> commandToJoin = command
if(commandClosure != null) {
if (commandClosure != null) {
def evaluatedCommand = commandClosure()

if(!(evaluatedCommand instanceof Map<String, String>)) {
if (!(evaluatedCommand instanceof Map<String, String>)) {
throw new IllegalArgumentException("the given evaluated closure is not a valid input for instruction ${keyword} while it doesn't provie a `Map` ([ key: value ]) but a `${evaluatedCommand?.class}` (${evaluatedCommand?.toString()})")
}
commandToJoin = evaluatedCommand as Map<String, String>
}
if(commandToJoin == null) {
if (commandToJoin == null) {
throw new IllegalArgumentException("instruction has to be set for ${keyword}")
}
validateKeysAreNotBlank commandToJoin
Expand All @@ -580,7 +579,7 @@ class Dockerfile extends DefaultTask {

private void validateKeysAreNotBlank(Map<String, String> command) throws IllegalArgumentException {
command.each { entry ->
if(entry.key.trim().length() == 0) {
if (entry.key.trim().length() == 0) {
throw new IllegalArgumentException("blank keys for a key=value pair are not allowed: please check instruction ${keyword} and given pair `${entry}`")
}
}
Expand All @@ -590,41 +589,70 @@ class Dockerfile extends DefaultTask {
static abstract class FileInstruction implements Instruction {
final Object src
final Object dest
final Object flags

FileInstruction(String src, String dest) {
FileInstruction(String src, String dest, String flags = null) {
this.src = src
this.dest = dest
this.flags = flags
}

FileInstruction(Closure src, Closure dest) {
FileInstruction(Closure src, Closure dest, Closure flags = null) {
this.src = src
this.dest = dest
this.flags = flags
}

@Override
String build() {
if(src instanceof String && dest instanceof String) {
"$keyword $src $dest"
String keyword = getKeyword()
if (flags) {
if (flags instanceof String) {
keyword += " $flags"
} else if (flags instanceof Closure) {
keyword += " ${flags()}"
}
}
else if(src instanceof Closure && dest instanceof Closure) {
if (src instanceof String && dest instanceof String) {
"$keyword $src $dest"
} else if (src instanceof Closure && dest instanceof Closure) {
"$keyword ${src()} ${dest()}"
}
}
}

static class FromInstruction extends StringCommandInstruction {
FromInstruction(String image) {
final Object stageName

FromInstruction(String image, String stageName = null) {
super(image)
this.stageName = stageName
}

FromInstruction(Closure image) {
FromInstruction(Closure image, Closure stageName = null) {
super(image)
this.stageName = stageName
}

@Override
String getKeyword() {
"FROM"
}

@Override
String build() {
String result = super.build()

if (stageName) {
if (stageName instanceof String) {
result += " AS $stageName"
} else if (stageName instanceof Closure) {
result += " AS ${stageName()}"
}
}

result
}
}

static class ArgInstruction extends StringCommandInstruction {
Expand Down Expand Up @@ -658,7 +686,7 @@ class Dockerfile extends DefaultTask {
}

static class RunCommandInstruction extends StringCommandInstruction {
RunCommandInstruction(String command) {
RunCommandInstruction(String command) {
super(command)
}

Expand Down Expand Up @@ -705,16 +733,14 @@ class Dockerfile extends DefaultTask {

@Override
String build() {
if(ports instanceof Integer[]) {
if (ports instanceof Integer[]) {
"$keyword ${ports.join(' ')}"
}
else if(ports instanceof Closure) {
} else if (ports instanceof Closure) {
def evaluatedPorts = ports()

if(evaluatedPorts instanceof String || evaluatedPorts instanceof Integer) {
if (evaluatedPorts instanceof String || evaluatedPorts instanceof Integer) {
"$keyword ${evaluatedPorts}"
}
else {
} else {
"$keyword ${evaluatedPorts.join(' ')}"
}
}
Expand Down Expand Up @@ -756,12 +782,12 @@ class Dockerfile extends DefaultTask {
}

static class CopyFileInstruction extends FileInstruction {
CopyFileInstruction(String src, String dest) {
super(src, dest)
CopyFileInstruction(String src, String dest, String stageName = null) {
super(src, dest, stageName ? "--from=$stageName" : null)
}

CopyFileInstruction(Closure src, Closure dest) {
super(src, dest)
CopyFileInstruction(Closure src, Closure dest, Closure stageName = null) {
super(src, dest, stageName ? { "--from=${stageName()}" } : null)
}

@Override
Expand Down Expand Up @@ -887,13 +913,15 @@ class Dockerfile extends DefaultTask {
void defaultCommand(String... command) {
instructions << new DefaultCommandInstruction(command)
}

void defaultCommand(Closure command) {
instructions << new DefaultCommandInstruction(command)
}

void entryPoint(String... entryPoint) {
instructions << new EntryPointInstruction(entryPoint)
}

void entryPoint(Closure entryPoint) {
instructions << new EntryPointInstruction(entryPoint)
}
Expand Down

0 comments on commit 088ee55

Please sign in to comment.