Skip to content

Commit

Permalink
Merge pull request #126 from marceldiass/improve-from-args
Browse files Browse the repository at this point in the history
Add ARG Dockerfile values to the buildArgs collection
  • Loading branch information
abayer authored Nov 20, 2018
2 parents 50ad50b + d38f912 commit d1e67b5
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@
*/
package org.jenkinsci.plugins.docker.workflow;

import java.util.AbstractMap.SimpleEntry;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.tools.ant.types.Commandline;

Expand All @@ -36,16 +33,19 @@ private DockerUtils() {
// utility class
}

public static Map<String, String> parseBuildArgs(String commandLine) {
public static Map<String, String> parseBuildArgs(final Dockerfile dockerfile, final String commandLine) {
// this also accounts for quote, escapes, ...
Commandline parsed = new Commandline(commandLine);
Map<String, String> result = new HashMap<>();
if (dockerfile != null) {
result.putAll(dockerfile.getArgs());
}

String[] arguments = parsed.getArguments();
for (int i = 0; i < arguments.length; i++) {
String arg = arguments[i];
if (arg.equals("--build-arg")) {
if (arguments.length < i + 1) {
if (arguments.length <= i + 1) {
throw new IllegalArgumentException("Missing parameter for --build-arg: " + commandLine);
}
String keyVal = arguments[i+1];
Expand All @@ -62,20 +62,4 @@ public static Map<String, String> parseBuildArgs(String commandLine) {
}
return result;
}

public static SimpleEntry<String, String> splitArgs(String argString) {
//TODO: support complex single/double quotation marks
Pattern p = Pattern.compile("^['\"]?(\\w+)=(.*?)['\"]?$");

Matcher matcher = p.matcher(argString.trim());
if (!matcher.matches()) {
throw new IllegalArgumentException("Illegal --build-arg parameter syntax: " + argString);
}

String key = matcher.group(1);
String value = matcher.group(2);

return new SimpleEntry<>(key, value);
}

}
101 changes: 101 additions & 0 deletions src/main/java/org/jenkinsci/plugins/docker/workflow/Dockerfile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* The MIT License
*
* Copyright (c) 2015, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jenkinsci.plugins.docker.workflow;

import hudson.FilePath;
import org.kohsuke.stapler.DataBoundConstructor;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

public final class Dockerfile {

private static final String ARG = "ARG";
private static final String FROM = "FROM ";

private FilePath dockerfilePath;
private LinkedList<String> froms;
private Map<String,String> args;

@DataBoundConstructor public Dockerfile(FilePath dockerfilePath) throws IOException, InterruptedException {
this.dockerfilePath = dockerfilePath;
this.froms = new LinkedList<>();
this.args = new HashMap<>();
parse();
}

public LinkedList<String> getFroms() {
return froms;
}

public Map<String, String> getArgs() {
return args;
}

private void parse() throws IOException, InterruptedException {
InputStream is = dockerfilePath.read();
try {
// encoding probably irrelevant since image/tag names must be ASCII
BufferedReader r = new BufferedReader(new InputStreamReader(is, "ISO-8859-1"));
try {
String line;
while ((line = r.readLine()) != null) {
line = line.trim();
if (line.startsWith("#")) {
continue;
}
if (line.startsWith(ARG)) {
String[] keyVal = parseDockerfileArg(line.substring(4));
args.put(keyVal[0], keyVal[1]);
continue;
}

if (line.startsWith(FROM)) {
froms.add(line.substring(5));
continue;
}
}
} finally {
r.close();
}
} finally {
is.close();
}
}

protected String[] parseDockerfileArg(String argLine) {
String[] keyValue = argLine.split("=", 2);
String key = keyValue[0];
String value = "";
if (keyValue.length > 1) {
value = keyValue[1];
}
return new String[]{key, value};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@
*/
package org.jenkinsci.plugins.docker.workflow;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;

import com.google.inject.Inject;
Expand All @@ -48,10 +45,12 @@

public class FromFingerprintStep extends AbstractStepImpl {

private static final String FIELD_ID = ".Id";

private final String dockerfile;
private final String image;
private String toolName;
private Map<String, String> buildArgs;
private String commandLine;

@DataBoundConstructor public FromFingerprintStep(String dockerfile, String image) {
this.dockerfile = dockerfile;
Expand All @@ -74,12 +73,12 @@ public String getToolName() {
this.toolName = Util.fixEmpty(toolName);
}

public Map<String, String> getBuildArgs() {
return buildArgs;
public String getCommandLine() {
return commandLine;
}

@DataBoundSetter public void setBuildArgs(Map<String, String> buildArgs) {
this.buildArgs = buildArgs;
@DataBoundSetter public void setCommandLine(String commandLine) {
this.commandLine = commandLine;
}

public static class Execution extends AbstractSynchronousNonBlockingStepExecution<Void> {
Expand All @@ -95,42 +94,24 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio
@StepContextParameter private transient Node node;

@Override protected Void run() throws Exception {
String fromImage = null;
FilePath dockerfile = workspace.child(step.dockerfile);
InputStream is = dockerfile.read();
try {
BufferedReader r = new BufferedReader(new InputStreamReader(is, "ISO-8859-1")); // encoding probably irrelevant since image/tag names must be ASCII
try {
String line;
while ((line = r.readLine()) != null) {
line = line.trim();
if (line.startsWith("#")) {
continue;
}
if (line.startsWith("FROM ")) {
fromImage = line.substring(5);
continue;
}
}
} finally {
r.close();
}
} finally {
is.close();
}
if (fromImage == null) {
throw new AbortException("could not find FROM instruction in " + dockerfile);
FilePath dockerfilePath = workspace.child(step.dockerfile);
Dockerfile dockerfile = new Dockerfile(dockerfilePath);
Map<String, String> buildArgs = DockerUtils.parseBuildArgs(dockerfile, step.commandLine);
String fromImage = dockerfile.getFroms().getLast();

if (dockerfile.getFroms().isEmpty()) {
throw new AbortException("could not find FROM instruction in " + dockerfilePath);
}
if (step.getBuildArgs() != null) {
if (buildArgs != null) {
// Fortunately, Docker uses the same EnvVar syntax as Jenkins :)
fromImage = Util.replaceMacro(fromImage, step.getBuildArgs());
fromImage = Util.replaceMacro(fromImage, buildArgs);
}
DockerClient client = new DockerClient(launcher, node, step.toolName);
String descendantImageId = client.inspectRequiredField(env, step.image, ".Id");
String descendantImageId = client.inspectRequiredField(env, step.image, FIELD_ID);
if (fromImage.equals("scratch")) { // we just made a base image
DockerFingerprints.addFromFacet(null, descendantImageId, run);
} else {
DockerFingerprints.addFromFacet(client.inspectRequiredField(env, fromImage, ".Id"), descendantImageId, run);
DockerFingerprints.addFromFacet(client.inspectRequiredField(env, fromImage, FIELD_ID), descendantImageId, run);
ImageAction.add(fromImage, run);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,9 @@ class Docker implements Serializable {
}

def commandLine = "docker build -t ${image} ${args}"
def buildArgs = DockerUtils.parseBuildArgs(commandLine)

script.sh commandLine
script.dockerFingerprintFrom dockerfile: dockerfile, image: image, toolName: script.env.DOCKER_TOOL_NAME, buildArgs: buildArgs
script.dockerFingerprintFrom dockerfile: dockerfile, image: image, toolName: script.env.DOCKER_TOOL_NAME, commandLine: commandLine
this.image(image)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* The MIT License
*
* Copyright (c) 2015, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jenkinsci.plugins.docker.workflow;

import hudson.FilePath;
import org.hamcrest.collection.IsCollectionWithSize;
import org.hamcrest.core.IsCollectionContaining;
import org.hamcrest.core.IsEqual;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import java.io.File;
import java.io.IOException;
import java.util.Map;

public class DockerUtilsTest {

@Rule public final ExpectedException exception = ExpectedException.none();

@Test public void parseBuildArgs() throws IOException, InterruptedException {

FilePath dockerfilePath = new FilePath(new File("src/test/resources/Dockerfile-withArgs"));
Dockerfile dockerfile = new Dockerfile(dockerfilePath);

final String imageToUpdate = "hello-world:latest";
final String key = "IMAGE_TO_UPDATE";
final String commangLine = "docker build -t hello-world --build-arg "+key+"="+imageToUpdate;
Map<String, String> buildArgs = DockerUtils.parseBuildArgs(dockerfile, commangLine);

Assert.assertThat(buildArgs.keySet(), IsCollectionWithSize.hasSize(1));
Assert.assertThat(buildArgs.keySet(), IsCollectionContaining.hasItems(key));
Assert.assertThat(buildArgs.get(key), IsEqual.equalTo(imageToUpdate));
}

@Test public void parseBuildArgsWithDefaults() throws IOException, InterruptedException {

Dockerfile dockerfile = getDockerfileDefaultArgs();

final String registry = "";
final String key_registry = "REGISTRY_URL";
final String key_tag = "TAG";
final String commangLine = "docker build -t hello-world";
Map<String, String> buildArgs = DockerUtils.parseBuildArgs(dockerfile, commangLine);

Assert.assertThat(buildArgs.keySet(), IsCollectionWithSize.hasSize(2));
Assert.assertThat(buildArgs.keySet(), IsCollectionContaining.hasItems(key_registry, key_tag));
Assert.assertThat(buildArgs.get(key_registry), IsEqual.equalTo(registry));
Assert.assertThat(buildArgs.get(key_tag), IsEqual.equalTo("latest"));
}

@Test public void parseBuildArgsOverridingDefaults() throws IOException, InterruptedException {

Dockerfile dockerfile = getDockerfileDefaultArgs();

final String registry = "http://private.registry:5000/";
final String key_registry = "REGISTRY_URL";
final String key_tag = "TAG";
final String tag = "1.2.3";
final String commangLine = "docker build -t hello-world --build-arg "+key_tag+"="+tag+
" --build-arg "+key_registry+"="+registry;
Map<String, String> buildArgs = DockerUtils.parseBuildArgs(dockerfile, commangLine);

Assert.assertThat(buildArgs.keySet(), IsCollectionWithSize.hasSize(2));
Assert.assertThat(buildArgs.keySet(), IsCollectionContaining.hasItems(key_registry, key_tag));
Assert.assertThat(buildArgs.get(key_registry), IsEqual.equalTo(registry));
Assert.assertThat(buildArgs.get(key_tag), IsEqual.equalTo(tag));
}

@Test public void parseBuildArgWithKeyAndEqual() throws IOException, InterruptedException {
final String commangLine = "docker build -t hello-world --build-arg key=";

Map<String, String> buildArgs = DockerUtils.parseBuildArgs(null, commangLine);

Assert.assertThat(buildArgs.keySet(), IsCollectionWithSize.hasSize(1));
Assert.assertThat(buildArgs.keySet(), IsCollectionContaining.hasItems("key"));
Assert.assertThat(buildArgs.get("key"), IsEqual.equalTo(""));
}

@Test public void parseInvalidBuildArg() throws IOException, InterruptedException {
final String commangLine = "docker build -t hello-world --build-arg";

exception.expect(IllegalArgumentException.class);
DockerUtils.parseBuildArgs(null, commangLine);
}

@Test public void parseInvalidBuildArgWithKeyOnly() throws IOException, InterruptedException {
final String commangLine = "docker build -t hello-world --build-arg key";

exception.expect(IllegalArgumentException.class);
DockerUtils.parseBuildArgs(null, commangLine);
}

private Dockerfile getDockerfileDefaultArgs() throws IOException, InterruptedException {
FilePath dockerfilePath = new FilePath(new File("src/test/resources/Dockerfile-defaultArgs"));
return new Dockerfile(dockerfilePath);
}
}


Loading

0 comments on commit d1e67b5

Please sign in to comment.