Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multi-stage builds in Dockerfile task #607

Merged
merged 1 commit into from
Jun 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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