Skip to content
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 @@ -62,6 +62,8 @@
import hudson.AbortException;
import hudson.Launcher;
import hudson.model.TaskListener;
import hudson.slaves.WorkspaceList;
import org.jenkinsci.plugins.docker.commons.tools.DockerTool;

/**
* Encapsulates the endpoint of DockerHub and how to interact with it.
Expand Down Expand Up @@ -185,7 +187,7 @@ DockerRegistryToken getToken(Item context) {
}

/**
* @deprecated Call {@link #newKeyMaterialFactory(Item, VirtualChannel, Launcher, TaskListener)}
* @deprecated Call {@link #newKeyMaterialFactory(Item, FilePath, Launcher, TaskListener, String)}
*/
@Deprecated
public KeyMaterialFactory newKeyMaterialFactory(@Nonnull AbstractBuild build) throws IOException, InterruptedException {
Expand All @@ -197,17 +199,17 @@ public KeyMaterialFactory newKeyMaterialFactory(@Nonnull AbstractBuild build) th
}

/**
* @deprecated Call {@link #newKeyMaterialFactory(Item, VirtualChannel, Launcher, TaskListener)}
* @deprecated Call {@link #newKeyMaterialFactory(Item, FilePath, Launcher, TaskListener, String)}
*/
@Deprecated
public KeyMaterialFactory newKeyMaterialFactory(Item context, @Nonnull VirtualChannel target) throws IOException, InterruptedException {
return newKeyMaterialFactory(context, target, null, TaskListener.NULL);
}

/**
* Makes the credentials available locally and returns {@link KeyMaterialFactory} that gives you the parameters
* needed to access it.
* @deprecated Call {@link #newKeyMaterialFactory(Item, FilePath, Launcher, TaskListener, String)}
*/
@Deprecated
public KeyMaterialFactory newKeyMaterialFactory(@CheckForNull Item context, @Nonnull VirtualChannel target, @CheckForNull Launcher launcher, @Nonnull TaskListener listener) throws IOException, InterruptedException {
if (credentialsId == null) {
return KeyMaterialFactory.NULL; // nothing needed to be done
Expand All @@ -219,6 +221,23 @@ public KeyMaterialFactory newKeyMaterialFactory(@CheckForNull Item context, @Non
return token.newKeyMaterialFactory(getEffectiveUrl(), target, launcher, listener);
}

/**
* Makes the credentials available locally and returns {@link KeyMaterialFactory} that gives you the parameters
* needed to access it.
* @param workspace a workspace being used for operations ({@link WorkspaceList#tempDir} will be applied)
* @param dockerExecutable as in {@link DockerTool#getExecutable}, with a 1.8+ client
*/
public KeyMaterialFactory newKeyMaterialFactory(@CheckForNull Item context, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener, @Nonnull String dockerExecutable) throws IOException, InterruptedException {
if (credentialsId == null) {
return KeyMaterialFactory.NULL; // nothing needed to be done
}
DockerRegistryToken token = getToken(context);
if (token == null) {
throw new AbortException("Could not find credentials matching " + credentialsId);
}
return token.newKeyMaterialFactory(getEffectiveUrl(), workspace, launcher, listener, dockerExecutable);
}

/**
* Decorates the repository ID namespace/name (ie. "jenkinsci/workflow-demo") with registry prefix
* (docker.acme.com:80/jenkinsci/workflow-demo).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,26 @@
package org.jenkinsci.plugins.docker.commons.credentials;

import com.cloudbees.plugins.credentials.Credentials;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import jenkins.authentication.tokens.api.AuthenticationTokens;
import jenkins.security.MasterToSlaveCallable;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;

import javax.annotation.Nonnull;
import hudson.slaves.WorkspaceList;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.authentication.tokens.api.AuthenticationTokens;
import jenkins.security.MasterToSlaveCallable;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;
import org.jenkinsci.plugins.docker.commons.impl.RegistryKeyMaterialFactory;
import org.jenkinsci.plugins.docker.commons.tools.DockerTool;

/**
* Represents an authentication token that docker(1) understands when pushing/pulling
Expand Down Expand Up @@ -65,28 +71,48 @@ public String getToken() {
}

/**
* @deprecated Call {@link #newKeyMaterialFactory(URL, VirtualChannel, Launcher, TaskListener)}
* @deprecated use {@link #newKeyMaterialFactory(URL, FilePath, Launcher, TaskListener, String)}
*/
@Deprecated
public KeyMaterialFactory newKeyMaterialFactory(final URL endpoint, @Nonnull VirtualChannel target) throws InterruptedException, IOException {
return newKeyMaterialFactory(endpoint, target, null, TaskListener.NULL);
}

/**
* Sets up an environment logged in to the specified Docker registry.
* @param dockerExecutable as in {@link DockerTool#getExecutable}, with a 1.8+ client
*/
public KeyMaterialFactory newKeyMaterialFactory(@Nonnull URL endpoint, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener, @Nonnull String dockerExecutable) throws InterruptedException, IOException {
try {
// see UsernamePasswordDockerRegistryTokenSource for example
String usernameColonPassword = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
int colon = usernameColonPassword.indexOf(':');
if (colon > 0) {
return new RegistryKeyMaterialFactory(usernameColonPassword.substring(0, colon), usernameColonPassword.substring(colon + 1), endpoint, launcher, listener, dockerExecutable).
contextualize(new KeyMaterialContext(WorkspaceList.tempDir(workspace)));
}
} catch (IllegalArgumentException x) {
// not Base64-encoded
}
listener.getLogger().println("Warning: authentication token does not look like a username:password; falling back to direct manipulation of Docker configuration files");
return newKeyMaterialFactory(endpoint, workspace.getChannel(), launcher, listener);
}

/**
* Makes the credentials available locally and returns {@link KeyMaterialFactory} that gives you the parameters
* needed to access it.
*
* This is done by inserting the token into {@code ~/.dockercfg}
* @deprecated use {@link #newKeyMaterialFactory(URL, FilePath, Launcher, TaskListener, String)}
*/
@Deprecated
public KeyMaterialFactory newKeyMaterialFactory(final @Nonnull URL endpoint, @Nonnull VirtualChannel target, @CheckForNull Launcher launcher, final @Nonnull TaskListener listener) throws InterruptedException, IOException {
target.call(new MasterToSlaveCallable<Void, IOException>() {
/**
* Insert the token into {@code ~/.dockercfg}
*/
@Override
public Void call() throws IOException {
// TODO: TF: Should this not be done via docker login (possibly preceded by a logout) ?

JSONObject json;
JSONObject auths;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public KeyMaterialFactory newKeyMaterialFactory(@Nonnull Item context, @Nonnull
}

static FilePath dotDocker(@Nonnull VirtualChannel target) throws IOException, InterruptedException {
// TODO this is wrong, should be using WorkspaceList.tempDir
return FilePath.getHomeDirectory(target).child(".docker");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@
*/
package org.jenkinsci.plugins.docker.commons.credentials;

import hudson.FilePath;
import hudson.model.AbstractBuild;
import org.jenkinsci.plugins.docker.commons.impl.CompositeKeyMaterialFactory;
import org.jenkinsci.plugins.docker.commons.impl.NullKeyMaterialFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
* Represents a locally extracted credentials information.
Expand All @@ -45,13 +46,8 @@
* @see DockerServerEndpoint#newKeyMaterialFactory(AbstractBuild)
* @see DockerRegistryEndpoint#newKeyMaterialFactory(AbstractBuild)
*/
public abstract class KeyMaterialFactory implements Serializable {
public abstract class KeyMaterialFactory {

/**
* Ensure consistent serialization.
*/
private static final long serialVersionUID = 1L;

public static final KeyMaterialFactory NULL = new NullKeyMaterialFactory();

private /* write once */ KeyMaterialContext context;
Expand Down Expand Up @@ -92,6 +88,17 @@ protected synchronized KeyMaterialContext getContext() {
*/
public abstract KeyMaterial materialize() throws IOException, InterruptedException;

/**
* Creates a read-protected directory inside {@link KeyMaterialContext#getBaseDir} suitable for storing secret files.
* Be sure to {@link FilePath#deleteRecursive} this in {@link KeyMaterial#close}.
*/
protected final FilePath createSecretsDirectory() throws IOException, InterruptedException {
FilePath dir = new FilePath(getContext().getBaseDir(), UUID.randomUUID().toString());
dir.mkdirs();
dir.chmod(0700);
return dir;
}

/**
* Merge additional {@link KeyMaterialFactory}s into one.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@
@Restricted(NoExternalUse.class)
public final class NullKeyMaterialFactory extends KeyMaterialFactory {

private static final long serialVersionUID = 1L;

@Override
public KeyMaterial materialize() throws IOException, InterruptedException {
return KeyMaterial.NULL;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* The MIT License
*
* Copyright 2018 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.commons.impl;

import hudson.AbortException;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.TaskListener;
import hudson.util.ArgumentListBuilder;
import java.io.IOException;
import java.net.URL;
import javax.annotation.Nonnull;
import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterial;
import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterialFactory;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* Logs you in to a Docker registry.
*/
@Restricted(NoExternalUse.class)
public class RegistryKeyMaterialFactory extends KeyMaterialFactory {

private final @Nonnull String username;
private final @Nonnull String password;
private final @Nonnull URL endpoint;
private final @Nonnull Launcher launcher;
private final @Nonnull TaskListener listener;
private final @Nonnull String dockerExecutable;

public RegistryKeyMaterialFactory(@Nonnull String username, @Nonnull String password, @Nonnull URL endpoint, @Nonnull Launcher launcher, @Nonnull TaskListener listener, @Nonnull String dockerExecutable) {
this.username = username;
this.password = password;
this.endpoint = endpoint;
this.launcher = launcher;
this.listener = listener;
this.dockerExecutable = dockerExecutable;
}

@Override
public KeyMaterial materialize() throws IOException, InterruptedException {
FilePath dockerConfig = createSecretsDirectory();
try {
if (launcher.launch().cmds(new ArgumentListBuilder(dockerExecutable, "login", "-u", username, "-p").add(password, true).add(endpoint)).envs("DOCKER_CONFIG=" + dockerConfig).stdout(listener).join() != 0) {
throw new AbortException("docker login failed");
}
} catch (IOException | InterruptedException x) {
try {
dockerConfig.deleteRecursive();
} catch (Exception x2) {
x.addSuppressed(x2);
}
throw x;
}
return new RegistryKeyMaterial(dockerConfig, new EnvVars("DOCKER_CONFIG", dockerConfig.getRemote()));
}

private static class RegistryKeyMaterial extends KeyMaterial {

private final FilePath dockerConfig;

RegistryKeyMaterial(FilePath dockerConfig, EnvVars envVars) {
super(envVars);
this.dockerConfig = dockerConfig;
}

@Override
public void close() throws IOException {
try {
dockerConfig.deleteRecursive();
} catch (InterruptedException x) {
// TODO would better have been thrown from KeyMaterial.close to begin with
throw new IOException(x);
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@
*/
@Restricted(NoExternalUse.class)
public class ServerHostKeyMaterialFactory extends KeyMaterialFactory{
/**
* Standardize serialization
*/
private static final long serialVersionUID = 1L;

/**
* The host.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@

import javax.annotation.CheckForNull;
import java.io.IOException;
import java.util.UUID;

/**
* {@link org.jenkinsci.plugins.docker.commons.credentials.KeyMaterialFactory} for talking to docker daemon.
Expand Down Expand Up @@ -80,11 +79,7 @@ public KeyMaterial materialize() throws IOException, InterruptedException {
EnvVars e = new EnvVars();

if (key != null && cert != null && ca != null) {
final FilePath tempCredsDir = new FilePath(getContext().getBaseDir(), UUID.randomUUID().toString());
tempCredsDir.mkdirs();

// protect this information from prying eyes
tempCredsDir.chmod(0700);
FilePath tempCredsDir = createSecretsDirectory();

// these file names are defined by convention by docker
copyInto(tempCredsDir, "key.pem", key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,11 @@ public String getToolName() {
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
// prepare the credentials to talk to this docker and make it available for docker you'll be forking
KeyMaterialFactory keyMaterialFactory = server.newKeyMaterialFactory(build).plus(registry.newKeyMaterialFactory(build));
KeyMaterial key = keyMaterialFactory.materialize();
try {
String dockerExecutable = DockerTool.getExecutable(toolName, build.getBuiltOn(), listener, build.getEnvironment(listener));
KeyMaterialFactory keyMaterialFactory = server.newKeyMaterialFactory(build).plus(registry.newKeyMaterialFactory(build.getParent(), build.getWorkspace(), launcher, listener, dockerExecutable));
try (KeyMaterial key = keyMaterialFactory.materialize()) {
// fork docker with appropriate environment to interact with this docker daemon
return launcher.launch().cmds(DockerTool.getExecutable(toolName, build.getBuiltOn(), listener, build.getEnvironment(listener)), "info").envs(key.env()).join() == 0;
} finally {
key.close();
return launcher.launch().cmds(dockerExecutable, "info").envs(key.env()).join() == 0;
}
}

Expand Down