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 tag task name for 'tags' #205

Merged
merged 9 commits into from
Jan 7, 2019
Merged
Show file tree
Hide file tree
Changes from 7 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
9 changes: 6 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ automatically include outputs of task dependencies in the Docker build context.

**Docker Configuration Parameters**
- `name` the name to use for this container, may include a tag
- `tags` (optional) an argument list of tags to create; any tag in `name` will
- `tags` (deprecated) (optional) an argument list of tags to create; any tag in `name` will
be stripped before applying a specific tag; defaults to the empty set
- `tag` (optional) defined a tag with task name
devkanro marked this conversation as resolved.
Show resolved Hide resolved
- `dockerfile` (optional) the dockerfile to use for building the image; defaults to
`project.file('Dockerfile')` and must be a file object
- `files` (optional) an argument list of files to be included in the Docker build context, evaluated per `Project#files`. For example, `files tasks.distTar.outputs` adds the TAR/TGZ file produced by the `distTar` tasks, and `files tasks.distTar.outputs, 'my-file.txt'` adds the archive in addition to file `my-file.txt` from the project root directory. The specified files are collected in a Gradle CopySpec which may be copied `into` the Docker build context directory. The underlying CopySpec may also be used to copy entire directories into the build context. The following example adds the aforementioned archive and text file to the CopySpec, uses the CopySpec to add all files `from` `src/myDir` into the CopySpec, then finally executes the copy `into` the docker build context directory `myDir`
Expand All @@ -53,7 +54,7 @@ docker {
To build a docker container, run the `docker` task. To push that container to a
docker repository, run the `dockerPush` task.

Tag and Push tasks for each tag will be generated for each provided `tags` entry.
Tag and Push tasks for each tag will be generated for each provided `tag` and `tags` entry.

**Examples**

Expand All @@ -80,7 +81,8 @@ Configuration specifying all parameters:
```gradle
docker {
name 'hub.docker.com/username/my-app:version'
tags 'latest'
tags 'latest' // deprecated, use 'tag'
tag 'privateRepo', 'my.repo.com/username/my-app:version'
devkanro marked this conversation as resolved.
Show resolved Hide resolved
dockerfile file('Dockerfile')
files tasks.distTar.outputs, 'file1.txt', 'file2.txt'
buildArgs([BUILD_VERSION: 'version'])
Expand Down Expand Up @@ -257,6 +259,7 @@ Tasks
* `dockerTag<tag>`: tag the docker image with `<tag>`
* `dockerPush`: push the specified image to a docker repository
* `dockerPush<tag>`: push the `<tag>` docker image to a docker repository
* `dockerTagsPush`: push all tagged Docker images to configured Docker Hub
devkanro marked this conversation as resolved.
Show resolved Hide resolved
* `dockerPrepare`: prepare to build a docker image by copying
dependent task outputs, referenced files, and `dockerfile` into a temporary
directory
Expand Down
16 changes: 16 additions & 0 deletions src/main/groovy/com/palantir/gradle/docker/DockerExtension.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ import com.google.common.base.Preconditions
import com.google.common.base.Strings
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.CopySpec
import org.gradle.internal.logging.text.StyledTextOutput
import org.gradle.internal.logging.text.StyledTextOutputFactory

class DockerExtension {
Project project
Expand All @@ -33,6 +36,7 @@ class DockerExtension {
private String dockerComposeFile = 'docker-compose.yml'
private Set<Task> dependencies = ImmutableSet.of()
private Set<String> tags = ImmutableSet.of()
private Map<String, String> namedTags = new HashMap<>()
private Map<String, String> labels = ImmutableMap.of()
private Map<String, String> buildArgs = ImmutableMap.of()
private boolean pull = false
Expand Down Expand Up @@ -89,10 +93,22 @@ class DockerExtension {
return tags
}

@Deprecated
public void tags(String... args) {
this.tags = ImmutableSet.copyOf(args)
}

public Map<String, String> getNamedTags() {
return ImmutableMap.copyOf(namedTags)
}

public void tag(String taskName, String tag) {
if (namedTags.putIfAbsent(taskName, tag) != null) {
StyledTextOutput o = project.services.get(StyledTextOutputFactory.class).create(DockerExtension)
o.withStyle(StyledTextOutput.Style.Error).println("WARNING: Task name '${taskName}' is existed.")
}
}

public Map<String, String> getLabels() {
return labels
}
Expand Down
118 changes: 89 additions & 29 deletions src/main/groovy/com/palantir/gradle/docker/PalantirDockerPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.bundling.Zip
import org.gradle.internal.logging.text.StyledTextOutput
import org.gradle.internal.logging.text.StyledTextOutputFactory

import javax.inject.Inject
import java.util.regex.Pattern
Expand Down Expand Up @@ -74,9 +76,9 @@ class PalantirDockerPlugin implements Plugin<Project> {
})

Task tag = project.tasks.create('dockerTag', {
group = 'Docker'
description = 'Applies all tags to the Docker image.'
dependsOn exec
group = 'Docker'
description = 'Applies all tags to the Docker image.'
dependsOn exec
})

Exec push = project.tasks.create('dockerPush', Exec, {
Expand All @@ -85,6 +87,11 @@ class PalantirDockerPlugin implements Plugin<Project> {
dependsOn tag
})

Task pushAllTags = project.tasks.create('dockerTagsPush', {
group = 'Docker'
description = 'Pushes all tagged Docker images to configured Docker Hub.'
})

Zip dockerfileZip = project.tasks.create('dockerfileZip', Zip, {
group = 'Docker'
description = 'Bundles the configured Dockerfile in a zip file'
Expand Down Expand Up @@ -119,32 +126,54 @@ class PalantirDockerPlugin implements Plugin<Project> {
logging.captureStandardError LogLevel.ERROR
}

Map<String, Object> tags = ext.namedTags.collectEntries { taskName, tagName ->
[generateTagTaskName(taskName), [
tagName: tagName,
tagTask: { -> tagName }
]]
}

if (!ext.tags.isEmpty()) {
ext.tags.each { unresolvedTagName ->
String taskName = generateTagTaskName(unresolvedTagName)

if (tags.containsKey(taskName)) {
StyledTextOutput o = project.services.get(StyledTextOutputFactory.class).create(DockerExtension)
o.withStyle(StyledTextOutput.Style.Error).println("WARNING: Task name '${taskName}' is existed.")
devkanro marked this conversation as resolved.
Show resolved Hide resolved
}else{
devkanro marked this conversation as resolved.
Show resolved Hide resolved
tags[taskName] = [
tagName: unresolvedTagName,
tagTask: { -> computeName(ext.name, unresolvedTagName) }
]
}
}
}

ext.tags.each { tagName ->
String taskTagName = ucfirst(tagName)
Exec subTask = project.tasks.create('dockerTag' + taskTagName, Exec, {
if (!tags.isEmpty()) {
devkanro marked this conversation as resolved.
Show resolved Hide resolved
tags.each { taskName, tagConfig ->
Exec tagSubTask = project.tasks.create('dockerTag' + taskName, Exec, {
group = 'Docker'
description = "Tags Docker image with tag '${tagName}'"
description = "Tags Docker image with tag '${tagConfig.tagName}'"
workingDir dockerDir
commandLine 'docker', 'tag', "${ -> ext.name}", "${ -> computeName(ext.name, tagName)}"
commandLine 'docker', 'tag', "${-> ext.name}", "${-> tagConfig.tagTask()}"
dependsOn exec
})
tag.dependsOn subTask
tag.dependsOn tagSubTask

project.tasks.create('dockerPush' + taskTagName, Exec, {
Exec pushSubTask = project.tasks.create('dockerPush' + taskName, Exec, {
group = 'Docker'
description = "Pushes the Docker image with tag '${tagName}' to configured Docker Hub"
description = "Pushes the Docker image with tag '${tagConfig.tagName}' to configured Docker Hub"
workingDir dockerDir
commandLine 'docker', 'push', "${ -> computeName(ext.name, tagName)}"
dependsOn subTask
commandLine 'docker', 'push', "${-> tagConfig.tagTask()}"
dependsOn tagSubTask
})
pushAllTags.dependsOn pushSubTask
}
}

push.with {
workingDir dockerDir
commandLine 'docker', 'push', "${ -> ext.name}"
commandLine 'docker', 'push', "${-> ext.name}"
}

dockerfileZip.with {
Expand Down Expand Up @@ -176,29 +205,60 @@ class PalantirDockerPlugin implements Plugin<Project> {
if (ext.pull) {
buildCommandLine.add '--pull'
}
buildCommandLine.addAll(['-t', "${ -> ext.name}", '.'])
buildCommandLine.addAll(['-t', "${-> ext.name}", '.'])
return buildCommandLine
}

@Deprecated
private static String computeName(String name, String tag) {
int lastColon = name.lastIndexOf(':')
int lastSlash = name.lastIndexOf('/')
int firstAt = tag.indexOf("@")

int endIndex;
String tagValue
if (firstAt > 0) {
tagValue = tag.substring(firstAt + 1, tag.length())
} else {
tagValue = tag
}

// image_name -> this should remain
// host:port/image_name -> this should remain.
// host:port/image_name:v1 -> v1 should be replaced
if (lastColon > lastSlash) endIndex = lastColon
else endIndex = name.length()
if (tagValue.contains(':') || tagValue.contains('/')) {
// tag with ':' or '/' -> force use the tag value
return tagValue
} else {
// tag without ':' and '/' -> replace the tag part of original name
int lastColon = name.lastIndexOf(':')
int lastSlash = name.lastIndexOf('/')

return name.substring(0, endIndex) + ":" + tag
}
int endIndex;

private static String ucfirst(String str) {
StringBuffer sb = new StringBuffer(str);
sb.replace(0, 1, str.substring(0, 1).toUpperCase());
return sb.toString();
// image_name -> this should remain
// host:port/image_name -> this should remain.
// host:port/image_name:v1 -> v1 should be replaced
if (lastColon > lastSlash) endIndex = lastColon
else endIndex = name.length()

return name.substring(0, endIndex) + ":" + tagValue
}
}

@Deprecated
devkanro marked this conversation as resolved.
Show resolved Hide resolved
private static String generateTagTaskName(String name) {
String tagTaskName = name
int firstAt = name.indexOf("@")

if (firstAt > 0) {
// Get substring of task name
tagTaskName = name.substring(0, firstAt)
} else if (firstAt == 0) {
// Task name must not be empty
throw new GradleException("Task name of docker tag '${name}' must not be empty.")
} else if (name.contains(':') || name.contains('/')) {
// Tags which with repo or name must have a task name
throw new GradleException("Docker tag '${name}' must have a task name.")
}

StringBuffer sb = new StringBuffer(tagTaskName)
// Uppercase the first letter of task name
sb.replace(0, 1, tagTaskName.substring(0, 1).toUpperCase());
return sb.toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ class PalantirDockerPluginTests extends AbstractPluginTest {

docker {
name '${id}'
tags 'latest', 'another'
tags 'latest', 'another', 'withTaskName@2.0', 'newImageName@${id}-new:latest'
tag 'withTaskNameByTag', '${id}:new-latest'
}
""".stripIndent()

Expand All @@ -251,11 +252,16 @@ class PalantirDockerPluginTests extends AbstractPluginTest {
then:
buildResult.output.contains('dockerTagLatest')
buildResult.output.contains('dockerTagAnother')
buildResult.output.contains('dockerTagWithTaskName')
buildResult.output.contains('dockerTagNewImageName')
buildResult.output.contains('dockerTagWithTaskNameByTag')
buildResult.output.contains('dockerPushLatest')
buildResult.output.contains('dockerPushAnother')
buildResult.output.contains('dockerPushWithTaskName')
buildResult.output.contains('dockerPushNewImageName')
buildResult.output.contains('dockerPushWithTaskNameByTag')
}


def 'does not throw if name is configured after evaluation phase'() {
given:
String id = 'id6'
Expand All @@ -269,7 +275,8 @@ class PalantirDockerPluginTests extends AbstractPluginTest {
}

docker {
tags 'latest', 'another'
tags 'latest', 'another', 'withTaskName@2.0', 'newImageName@${id}-new:latest'
tag 'withTaskNameByTag', '${id}:new-latest'
}

afterEvaluate {
Expand All @@ -287,10 +294,15 @@ class PalantirDockerPluginTests extends AbstractPluginTest {
exec("docker inspect --format '{{.Author}}' ${id}") == "'${id}'\n"
exec("docker inspect --format '{{.Author}}' ${id}:latest") == "'${id}'\n"
exec("docker inspect --format '{{.Author}}' ${id}:another") == "'${id}'\n"
exec("docker inspect --format '{{.Author}}' ${id}:2.0") == "'${id}'\n"
exec("docker inspect --format '{{.Author}}' ${id}-new:latest") == "'${id}'\n"
exec("docker inspect --format '{{.Author}}' ${id}:new-latest") == "'${id}'\n"
execCond("docker rmi -f ${id}")
execCond("docker rmi -f ${id}:another")
execCond("docker rmi -f ${id}:latest")

execCond("docker rmi -f ${id}:2.0")
execCond("docker rmi -f ${id}-new:latest")
execCond("docker rmi -f ${id}:new-latest")
}

def 'running tag task creates images with specified tags'() {
Expand All @@ -307,7 +319,8 @@ class PalantirDockerPluginTests extends AbstractPluginTest {

docker {
name 'fake-service-name'
tags 'latest', 'another'
tags 'latest', 'another', 'withTaskName@2.0', 'newImageName@${id}-new:latest'
tag 'withTaskNameByTag', '${id}:new-latest'
}

afterEvaluate {
Expand All @@ -318,6 +331,9 @@ class PalantirDockerPluginTests extends AbstractPluginTest {
doLast {
println "LATEST: \${tasks.dockerTagLatest.commandLine}"
println "ANOTHER: \${tasks.dockerTagAnother.commandLine}"
println "WITH_TASK_NAME: \${tasks.dockerTagWithTaskName.commandLine}"
println "NEW_IMAGE_NAME: \${tasks.dockerTagNewImageName.commandLine}"
println "WITH_TASK_NAME_BY_TAG: \${tasks.dockerTagWithTaskNameByTag.commandLine}"
}
}
""".stripIndent()
Expand All @@ -326,17 +342,26 @@ class PalantirDockerPluginTests extends AbstractPluginTest {
BuildResult buildResult = with('dockerTag', 'printInfo').build()

then:
buildResult.output.contains("LATEST: [docker, tag, id6, id6:latest]")
buildResult.output.contains("ANOTHER: [docker, tag, id6, id6:another]")
buildResult.output.contains("LATEST: [docker, tag, ${id}, ${id}:latest]")
buildResult.output.contains("ANOTHER: [docker, tag, ${id}, ${id}:another]")
buildResult.output.contains("WITH_TASK_NAME: [docker, tag, ${id}, ${id}:2.0]")
buildResult.output.contains("NEW_IMAGE_NAME: [docker, tag, ${id}, ${id}-new:latest]")
buildResult.output.contains("WITH_TASK_NAME_BY_TAG: [docker, tag, ${id}, ${id}:new-latest]")
buildResult.task(':dockerPrepare').outcome == TaskOutcome.SUCCESS
buildResult.task(':docker').outcome == TaskOutcome.SUCCESS
buildResult.task(':dockerTag').outcome == TaskOutcome.SUCCESS
exec("docker inspect --format '{{.Author}}' ${id}") == "'${id}'\n"
exec("docker inspect --format '{{.Author}}' ${id}:latest") == "'${id}'\n"
exec("docker inspect --format '{{.Author}}' ${id}:another") == "'${id}'\n"
exec("docker inspect --format '{{.Author}}' ${id}:2.0") == "'${id}'\n"
exec("docker inspect --format '{{.Author}}' ${id}-new:latest") == "'${id}'\n"
exec("docker inspect --format '{{.Author}}' ${id}:new-latest") == "'${id}'\n"
execCond("docker rmi -f ${id}")
execCond("docker rmi -f ${id}:latest")
execCond("docker rmi -f ${id}:another")
execCond("docker rmi -f ${id}:2.0")
execCond("docker rmi -f ${id}-new:latest")
execCond("docker rmi -f ${id}:new-latest")
}

def 'build args are correctly processed'() {
Expand Down Expand Up @@ -602,6 +627,36 @@ class PalantirDockerPluginTests extends AbstractPluginTest {
"host/v1:1" | "latest" | "host/v1:latest"
"host:port/v1" | "latest" | "host:port/v1:latest"
"host:port/v1:1" | "latest" | "host:port/v1:latest"
"v1" | "name@latest" | "v1:latest"
"v1:1" | "name@latest" | "v1:latest"
"host/v1" | "name@latest" | "host/v1:latest"
"host/v1:1" | "name@latest" | "host/v1:latest"
"host:port/v1" | "name@latest" | "host:port/v1:latest"
"host:port/v1:1" | "name@latest" | "host:port/v1:latest"
"v1" | "name@v2:latest" | "v2:latest"
"v1:1" | "name@v2:latest" | "v2:latest"
"host/v1" | "name@v2:latest" | "v2:latest"
"host/v1:1" | "name@v2:latest" | "v2:latest"
"host:port/v1" | "name@v2:latest" | "v2:latest"
"host:port/v1:1" | "name@v2:latest" | "v2:latest"
"v1" | "name@host/v2" | "host/v2"
"v1:1" | "name@host/v2" | "host/v2"
"host/v1" | "name@host/v2" | "host/v2"
"host/v1:1" | "name@host/v2" | "host/v2"
"host:port/v1" | "name@host/v2" | "host/v2"
"host:port/v1:1" | "name@host/v2" | "host/v2"
"v1" | "name@host/v2:2" | "host/v2:2"
"v1:1" | "name@host/v2:2" | "host/v2:2"
"host/v1" | "name@host/v2:2" | "host/v2:2"
"host/v1:1" | "name@host/v2:2" | "host/v2:2"
"host:port/v1" | "name@host/v2:2" | "host/v2:2"
"host:port/v1:1" | "name@host/v2:2" | "host/v2:2"
"v1" | "name@host:port/v2:2" | "host:port/v2:2"
"v1:1" | "name@host:port/v2:2" | "host:port/v2:2"
"host/v1" | "name@host:port/v2:2" | "host:port/v2:2"
"host/v1:1" | "name@host:port/v2:2" | "host:port/v2:2"
"host:port/v1" | "name@host:port/v2:2" | "host:port/v2:2"
"host:port/v1:1" | "name@host:port/v2:2" | "host:port/v2:2"
}

def 'can add entire directories via copyspec'() {
Expand Down