Skip to content

Commit

Permalink
Merge pull request #22 from oleg-nenashev/JENKINS-28675
Browse files Browse the repository at this point in the history
[JENKINS-28675] - Create fingerprints when creating new images
  • Loading branch information
oleg-nenashev committed Jun 3, 2015
2 parents 899be05 + 30a0bc0 commit 570ab9b
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 29 deletions.
117 changes: 88 additions & 29 deletions src/main/java/com/cloudbees/dockerpublish/DockerBuilder.java
@@ -1,22 +1,20 @@
package com.cloudbees.dockerpublish;

import com.cloudbees.dockerpublish.DockerCLIHelper.InspectImageResponse;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Computer;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.util.FormValidation;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
Expand All @@ -26,12 +24,14 @@
import java.util.regex.Pattern;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.servlet.ServletException;

import org.apache.commons.io.output.TeeOutputStream;
import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterial;
import org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint;
import org.jenkinsci.plugins.docker.commons.credentials.DockerServerEndpoint;
import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints;
import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException;
import org.jenkinsci.plugins.tokenmacro.TokenMacro;
import org.kohsuke.stapler.DataBoundConstructor;
Expand Down Expand Up @@ -63,6 +63,7 @@ public class DockerBuilder extends Builder {
@CheckForNull
private String repoTag;
private boolean skipPush = true;
private boolean createFingerprint = true;
private boolean skipTagLatest;

@Deprecated
Expand Down Expand Up @@ -174,6 +175,15 @@ public boolean isSkipTagLatest() {
return skipTagLatest;
}

@DataBoundSetter
public void setCreateFingerprint(boolean createFingerprint) {
this.createFingerprint = createFingerprint;
}

public boolean isCreateFingerprint() {
return createFingerprint;
}

@DataBoundSetter
public void setSkipTagLatest(boolean skipTagLatest) {
this.skipTagLatest = skipTagLatest;
Expand All @@ -200,15 +210,17 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListener lis

private static class Result {
final boolean result;
final String stdout;
final @Nonnull String stdout;
final @Nonnull String stderr;

private Result() {
this(true, "");
this(true, "", "");
}

private Result(boolean result, String stdout) {
private Result(boolean result, @CheckForNull String stdout, @CheckForNull String stderr) {
this.result = result;
this.stdout = stdout;
this.stdout = hudson.Util.fixNull(stdout);
this.stderr = hudson.Util.fixNull(stderr);
}
}

Expand All @@ -217,7 +229,7 @@ static String getImageBuiltFromStdout(CharSequence stdout) {
Matcher m = IMAGE_BUILT_PATTERN.matcher(stdout);
return m.find() ? m.group(1) : null;
}

private class Perform {
private final AbstractBuild build;
private final Launcher launcher;
Expand Down Expand Up @@ -301,14 +313,16 @@ private boolean buildAndTag() throws MacroEvaluationException, IOException, Inte
// we know the image name so apply the tags directly
while (lastResult.result && i.hasNext()) {
lastResult = executeCmd("docker tag --force=true " + image + " " + i.next());
}
}
processFingerprints(image);
} else {
// we don't know the image name so rebuild the image for each tag
while (lastResult.result && i.hasNext()) {
lastResult = executeCmd("docker build -t " + i.next()
+ ((isNoCache()) ? " --no-cache=true " : "") + " "
+ ((isForcePull()) ? " --pull=true " : "") + " "
+ context);
processFingerprintsFromStdout(lastResult.stdout);
}
}
return lastResult.result;
Expand All @@ -332,10 +346,36 @@ private boolean executeCmd(List<String> cmds) throws IOException, InterruptedExc
return lastResult.result;
}

/**
* Runs Docker command using Docker CLI.
* In this default implementation STDOUT and STDERR outputs will be printed to build logs.
* Use {@link #executeCmd(java.lang.String, boolean, boolean)} to alter the behavior.
* @param cmd Command to be executed
* @return Execution result
* @throws IOException Execution error
* @throws InterruptedException The build has been interrupted
*/
private Result executeCmd(String cmd) throws IOException, InterruptedException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
TeeOutputStream stdout = new TeeOutputStream(listener.getLogger(), baos);
PrintStream stderr = listener.getLogger();
return executeCmd(cmd, true, true);
}

/**
* Runs Docker command using Docker CLI.
* @param cmd Command to be executed
* @param logStdOut If true, propagate STDOUT to the build log
* @param logStdErr If true, propagate STDERR to the build log
* @return Execution result
* @throws IOException Execution error
* @throws InterruptedException The build has been interrupted
*/
private @Nonnull Result executeCmd( @Nonnull String cmd,
boolean logStdOut, boolean logStdErr) throws IOException, InterruptedException {
ByteArrayOutputStream baosStdOut = new ByteArrayOutputStream();
ByteArrayOutputStream baosStdErr = new ByteArrayOutputStream();
OutputStream stdout = logStdOut ?
new TeeOutputStream(listener.getLogger(), baosStdOut) : baosStdOut;
OutputStream stderr = logStdErr ?
new TeeOutputStream(listener.getLogger(), baosStdErr) : baosStdErr;

// get Docker registry credentials
KeyMaterial registryKey = getRegistry().newKeyMaterialFactory(build).materialize();
Expand All @@ -361,22 +401,9 @@ private Result executeCmd(String cmd) throws IOException, InterruptedException {
.start().join() == 0;

// capture the stdout so it can be parsed later on
String stdoutStr = null;
try {
Computer computer = Computer.currentComputer();
if (computer != null) {
Charset charset = computer.getDefaultCharset();
if (charset != null) {
baos.flush();
stdoutStr = baos.toString(charset.name());
}
}
} catch (UnsupportedEncodingException e) {
// we couldn't parse, ignore
logger.log(Level.FINE, "Unable to get stdout from launched command: {}", e);
}

return new Result(result, stdoutStr);
final String stdOutStr = DockerCLIHelper.getConsoleOutput(baosStdOut, logger);
final String stdErrStr = DockerCLIHelper.getConsoleOutput(baosStdErr, logger);
return new Result(result, stdOutStr, stdErrStr);

} finally {
registryKey.close();
Expand All @@ -385,6 +412,38 @@ private Result executeCmd(String cmd) throws IOException, InterruptedException {
}
}
}

void processFingerprintsFromStdout(@Nonnull String stdout) throws IOException, InterruptedException {
if (!createFingerprint) {
return;
}

final String image = getImageBuiltFromStdout(stdout);
if (image == null) {
return;
}
processFingerprints(image);
}

void processFingerprints(@Nonnull String image) throws IOException, InterruptedException {
if (!createFingerprint) {
return;
}

// Retrieve full image ID using another call
final Result response = executeCmd("docker inspect " + image, false, true);
if (!response.result) {
return; // Bad result, cannot do anything
}
final InspectImageResponse rsp = DockerCLIHelper.parseInspectImageResponse(response.stdout);
if (rsp == null) {
return; // Cannot process the data
}

// Create or retrieve the fingerprint
DockerFingerprints.addFromFacet(rsp.getParent(), rsp.getId(), build);

}

private boolean recordException(Exception e) {
listener.error(e.getMessage());
Expand Down
115 changes: 115 additions & 0 deletions src/main/java/com/cloudbees/dockerpublish/DockerCLIHelper.java
@@ -0,0 +1,115 @@
/*
* 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 com.cloudbees.dockerpublish;

import hudson.model.Computer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* Retrieves the data for {@link DockerBuilder} from docker-java datatypes
* @author Oleg Nenashev
* @since TODO
*/
@Restricted(NoExternalUse.class)
public class DockerCLIHelper {

/**
* Parses console output from a {@link ByteArrayOutputStream}.
* @param output Output stream
* @param logger Logger
* @return Retrieved string or null if cannot retrieve it
* @throws IOException Buffer flush error
*/
/*package*/ static @CheckForNull String getConsoleOutput(
@Nonnull ByteArrayOutputStream output, @Nonnull Logger logger) throws IOException {
String res = null;
try {
Computer computer = Computer.currentComputer();
if (computer != null) {
Charset charset = computer.getDefaultCharset();
if (charset != null) {
output.flush();
res = output.toString(charset.name());
}
}
} catch (UnsupportedEncodingException e) {
// we couldn't parse, ignore
logger.log(Level.FINE, "Unable to get a console output from launched command: {}", e);
}
return res;
}

/**
* Retrieves an info about image from command-line outputs.
* @param stdout Data output to be parsed
* @return ImageId or null
* @throws IOException Cannot parse the response
* @since TODO
*/
@CheckForNull
public static InspectImageResponse parseInspectImageResponse(@Nonnull String stdout) throws IOException {
JSONArray array = JSONArray.fromObject(stdout);
if (array != null && array.size() > 0) {
return new InspectImageResponse(array.getJSONObject(0));
} else {
return null;
}
}

@Restricted(NoExternalUse.class)
public static class InspectImageResponse {

private final String parent;
private final String id;

public InspectImageResponse(JSONObject inspectImageResponse) throws IOException {
this.parent = inspectImageResponse.getString("Parent");
this.id = inspectImageResponse.getString("Id");
}

public InspectImageResponse(String parent, String id) {
this.parent = parent;
this.id = id;
}

public String getParent() {
return parent;
}

public String getId() {
return id;
}
}
}
Expand Up @@ -36,6 +36,10 @@
description="Do not build the image">
<f:checkbox />
</f:entry>

<f:entry title="${%Create fingerprints}" field="createFingerprint">
<f:checkbox default="true"/>
</f:entry>

<f:entry title="Skip Decorate" field="skipDecorate"
description="Do not decorate the build name">
Expand Down
@@ -0,0 +1,6 @@
<div>
If enabled, the plugin will create fingerprints after the build of each image.
These fingerprints are being managed by
<a href="https://wiki.jenkins-ci.org/display/JENKINS/Docker+Commons+Plugin">
Docker Commons Plugin</a>.
</div>
47 changes: 47 additions & 0 deletions src/test/java/com/cloudbees/dockerpublish/DockerCLIHelperTest.java
@@ -0,0 +1,47 @@
/*
* 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 com.cloudbees.dockerpublish;

import static org.junit.Assert.*;

import com.google.common.base.Charsets;
import java.net.URL;
import org.junit.Test;
import com.google.common.io.Resources;

/**
* Tests for {@link DockerCLIHelper}.
* @author Oleg Nenashev
*/
public class DockerCLIHelperTest {

@Test
public void parseDockerInspectImageOutput() throws Exception {
URL url = Resources.getResource("dockerInspectImage_response.json");
DockerCLIHelper.InspectImageResponse rsp = DockerCLIHelper.parseInspectImageResponse(Resources.toString(url, Charsets.UTF_8));
assertNotNull(rsp);
assertEquals("5366517d611967756a43c63a3223dbf645c5e9be66d594d86802dee143aad93a", rsp.getId());
assertEquals("4300417211ebb75b48b06ed5640d641778f312072d24b37978682345cbb362b1", rsp.getParent());
}
}

0 comments on commit 570ab9b

Please sign in to comment.