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

JNLP Enhancement #654

Merged
merged 3 commits into from
May 29, 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
@@ -1,35 +1,51 @@
package io.jenkins.docker.connector;

import static com.nirima.jenkins.plugins.docker.DockerTemplateBase.splitAndFilterEmpty;

import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.google.common.base.Joiner;
import com.nirima.jenkins.plugins.docker.DockerTemplate;

import hudson.EnvVars;
import hudson.Extension;
import hudson.Util;
import hudson.model.Descriptor;
import hudson.model.TaskListener;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.JNLPLauncher;
import hudson.slaves.NodeProperty;
import hudson.util.LogTaskListener;
import io.jenkins.docker.client.DockerAPI;
import io.jenkins.docker.client.DockerEnvUtils;
import jenkins.model.Jenkins;
import jenkins.slaves.JnlpSlaveAgentProtocol;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.CheckForNull;

/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class DockerComputerJNLPConnector extends DockerComputerConnector {
private static final Logger LOGGER = Logger.getLogger(DockerComputerJNLPConnector.class.getCanonicalName());
private static final TaskListener LOGGER_LISTENER = new LogTaskListener(LOGGER, Level.FINER);

private String user;
private final JNLPLauncher jnlpLauncher;
private String jenkinsUrl;
private String[] entryPointArguments;

@DataBoundConstructor
public DockerComputerJNLPConnector(JNLPLauncher jnlpLauncher) {
Expand All @@ -51,6 +67,30 @@ public void setUser(String user) {
@DataBoundSetter
public void setJenkinsUrl(String jenkinsUrl){ this.jenkinsUrl = jenkinsUrl; }

@CheckForNull
public String[] getEntryPointArguments(){
return entryPointArguments;
}

@CheckForNull
public String getEntryPointArgumentsString() {
if (entryPointArguments == null) return null;
return Joiner.on("\n").join(entryPointArguments);
}

@DataBoundSetter
public void setEntryPointArgumentsString(String entryPointArgumentsString) {
setEntryPointArguments(splitAndFilterEmpty(entryPointArgumentsString, "\n"));
}

public void setEntryPointArguments(String[] entryPointArguments) {
if (entryPointArguments == null || entryPointArguments.length == 0) {
this.entryPointArguments = null;
} else {
this.entryPointArguments = entryPointArguments;
}
}

public DockerComputerJNLPConnector withUser(String user) {
this.user = user;
return this;
Expand All @@ -61,6 +101,11 @@ public DockerComputerJNLPConnector withJenkinsUrl(String jenkinsUrl) {
return this;
}

public DockerComputerJNLPConnector withEntryPointArguments(String... args) {
setEntryPointArguments(args);
return this;
}

public JNLPLauncher getJnlpLauncher() {
return jnlpLauncher;
}
Expand All @@ -71,21 +116,49 @@ protected ComputerLauncher createLauncher(final DockerAPI api, final String work
return new JNLPLauncher();
}

@Override
public void beforeContainerCreated(DockerAPI api, String workdir, CreateContainerCmd cmd) throws IOException, InterruptedException {
List<String> args = new ArrayList<>();
if (StringUtils.isNotBlank(jnlpLauncher.tunnel)) {
args.addAll(Arrays.asList("-tunnel", jnlpLauncher.tunnel));
@Restricted(NoExternalUse.class)
public enum ArgumentVariables {
NodeName("NODE_NAME", "The name assigned to this node"), //
Secret("JNLP_SECRET",
"The secret that must be passed to slave.jar's -secret argument to pass JNLP authentication."), //
JenkinsUrl("JENKINS_URL", "The Jenkins root URL."), //
TunnelArgument("TUNNEL_ARG",
"If a JNLP tunnel has been specified then this evaluates to '-tunnel', otherwise it evaluates to the empty string"), //
TunnelValue("TUNNEL", "The JNLP tunnel value");
private final String name;
private final String description;

private ArgumentVariables(String name, String description) {
this.name = name;
this.description = description;
}

final String nodeName = DockerTemplate.getNodeNameFromContainerConfig(cmd);
public String getName() {
return name;
}

args.addAll(Arrays.asList(
"-url", StringUtils.isEmpty(jenkinsUrl) ? Jenkins.getInstance().getRootUrl() : jenkinsUrl,
JnlpSlaveAgentProtocol.SLAVE_SECRET.mac(nodeName),
nodeName));
public String getDescription() {
return description;
}
}

private static final String DEFAULT_ENTRY_POINT_ARGUMENTS = "${" + ArgumentVariables.TunnelArgument.getName()
+ "}\n${" + ArgumentVariables.TunnelValue.getName() + "}\n-url\n${" + ArgumentVariables.JenkinsUrl.getName()
+ "}\n${" + ArgumentVariables.Secret.getName() + "}\n${" + ArgumentVariables.NodeName.getName() + "}";

@Override
public void beforeContainerCreated(DockerAPI api, String workdir, CreateContainerCmd cmd) throws IOException, InterruptedException {

cmd.withCmd(args.toArray(new String[args.size()]));
final String effectiveJenkinsUrl = StringUtils.isEmpty(jenkinsUrl) ? Jenkins.getInstance().getRootUrl() : jenkinsUrl;
final String nodeName = DockerTemplate.getNodeNameFromContainerConfig(cmd);
final String secret = JnlpSlaveAgentProtocol.SLAVE_SECRET.mac(nodeName);
final EnvVars knownVariables = calculateVariablesForVariableSubstitution(nodeName, secret, jnlpLauncher.tunnel, effectiveJenkinsUrl);
final String configuredArgString = getEntryPointArgumentsString();
final String effectiveConfiguredArgString = StringUtils.isNotBlank(configuredArgString) ? configuredArgString : DEFAULT_ENTRY_POINT_ARGUMENTS;
final String resolvedArgString = Util.replaceMacro(effectiveConfiguredArgString, knownVariables);
final String[] resolvedArgs = splitAndFilterEmpty(resolvedArgString, "\n");

cmd.withCmd(resolvedArgs);
String vmargs = jnlpLauncher.vmargs;
if (StringUtils.isNotBlank(vmargs)) {
DockerEnvUtils.addEnvToCmd("JAVA_OPT", vmargs.trim(), cmd);
Expand All @@ -99,13 +172,74 @@ public void beforeContainerCreated(DockerAPI api, String workdir, CreateContaine
public void afterContainerStarted(DockerAPI api, String workdir, String containerId) throws IOException, InterruptedException {
}

private EnvVars calculateVariablesForVariableSubstitution(final String nodeName, final String secret,
final String jnlpTunnel, final String jenkinsUrl) throws IOException, InterruptedException {
final EnvVars knownVariables = new EnvVars();
final Jenkins j = Jenkins.getInstance();
addEnvVars(knownVariables, j.getGlobalNodeProperties());
for (final ArgumentVariables v : ArgumentVariables.values()) {
// This switch statement MUST handle all possible
// values of v.
final String argValue;
switch (v) {
case JenkinsUrl :
argValue = jenkinsUrl;
break;
case TunnelArgument :
argValue = StringUtils.isNotBlank(jnlpTunnel) ? "-tunnel" : "";
break;
case TunnelValue :
argValue = jnlpTunnel;
break;
case Secret :
argValue = secret;
break;
case NodeName :
argValue = nodeName;
break;
default :
final String msg = "Internal code error: Switch statement is missing \"case " + v.name()
+ " : argValue = ... ; break;\" code.";
// If this line throws an exception then it's because
// someone has added a new variable to the enum without
// adding code above to handle it.
// The two have to be kept in step in order to
// ensure that the help text stays in step.
throw new RuntimeException(msg);
}
addEnvVar(knownVariables, v.getName(), argValue);
}
return knownVariables;
}

private static void addEnvVars(final EnvVars vars, final Iterable<? extends NodeProperty<?>> nodeProperties)
throws IOException, InterruptedException {
if (nodeProperties != null) {
for (final NodeProperty<?> nodeProperty : nodeProperties) {
nodeProperty.buildEnvVars(vars, LOGGER_LISTENER);
}
}
}

private static void addEnvVar(final EnvVars vars, final String name, final Object valueOrNull) {
vars.put(name, valueOrNull == null ? "" : valueOrNull.toString());
}

@Extension @Symbol("jnlp")
public static final class DescriptorImpl extends Descriptor<DockerComputerConnector> {

public Collection<ArgumentVariables> getEntryPointArgumentVariables() {
return Arrays.asList(ArgumentVariables.values());
}

public Collection<String> getDefaultEntryPointArguments() {
final String[] args = splitAndFilterEmpty(DEFAULT_ENTRY_POINT_ARGUMENTS, "\n");
return Arrays.asList(args);
}

@Override
public String getDisplayName() {
return "Connect with JNLP";
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">

<f:block>
<span class="info">Prerequisites:</span> Docker image must have <a href="https://go.java">Java</a> installed. Also entrypoint must be able to accept jenkins slave connection parameters. <a href="https://github.com/jenkinsci/docker-jnlp-slave">See jenkins/docker-jnlp-slave as an example</a>
<span class="info">Prerequisites:</span>
<ul>
<li>
Docker image must have <a href="https://go.java">Java</a> installed.
</li>
<li>
Entrypoint must be able to accept jenkins slave connection parameters.
See
<a href="https://github.com/jenkinsci/docker-jnlp-slave">jenkins/docker-jnlp-slave</a>
as an example.
</li>
</ul>
</f:block>

<f:entry title="${%User}" field="user">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">

<f:block>
<span class="info">Prerequisites:</span> Docker image must have <a href="https://go.java">Java</a> installed,
and Jenkins master has to be accessible over network <em>from</em> container.
<span class="info">Prerequisites:</span>
<ul>
<li>
Jenkins master has to be accessible over network <em>from</em> the container.
</li>
<li>
Docker image must have <a href="https://go.java">Java</a> installed.
</li>
<li>
Docker image must launch <tt>slave.jar</tt> by itself or using the ${%EntryPoint Arguments} below.
</li>
</ul>
</f:block>

<f:entry title="${%User}" field="user">
Expand All @@ -14,5 +24,9 @@
<f:textbox/>
</f:entry>

<f:entry title="${%EntryPoint Arguments}" field="entryPointArgumentsString">
<f:expandableTextbox />
</f:entry>

<f:property field="jnlpLauncher"/>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
<div>
<p>
Arguments to be passed to the container's entry point.
</p>
<p>
<b>NOTE:</b>
This field is a multi-line string.
Each (non-empty) line defines a seperate argument.
If you require more than one argument
(e.g. copying one of the examples below)
then you will need to expand the field,
otherwise you'll end up with one long line instead of multiple lines.
</p>
<p>
Limited variable substitution (using $${VARIABLE_NAME} syntax) is carried out on the configured strings prior to starting the container.
In addition to any globally configured environment variables, the variables that can be used here are:
<dl>
<j:forEach var="entry" items="${app.getDescriptor('io.jenkins.docker.connector.DockerComputerJNLPConnector').entryPointArgumentVariables}">
<dt><tt>${entry.name}</tt></dt>
<dd>${entry.description}</dd>
</j:forEach>
</dl>
</p>
<p>
For example, if you are using a custom container that has
<tt>java</tt>
and
<tt>wget</tt>
installed but does not have
<tt>slave.jar</tt>
pre-installed then you could use the following instead:
<blockquote>
<tt>sh</tt><br/>
<tt>-c</tt><br/>
<tt>wget $${JENKINS_URL}jnlpJars/slave.jar &amp;&amp; java -jar slave.jar -jnlpUrl $${JENKINS_URL}computer/$${NODE_NAME}/slave-agent.jnlp -secret $${JNLP_SECRET}</tt><br/>
</blockquote>
</p>
<p>
If this field is left blank then it defaults to arguments
suitable for the standard
Jenkins JNLP Agent Docker image,
<a href="https://github.com/jenkinsci/docker-jnlp-slave">jenkins/jnlp-slave</a>,
which are:
<blockquote>
<j:forEach var="entry" items="${app.getDescriptor('io.jenkins.docker.connector.DockerComputerJNLPConnector').defaultEntryPointArguments}">
<tt>${entry}</tt><br/>
</j:forEach>
</blockquote>
</p>
</div>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,30 @@
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form" >

<f:block>
<span class="info">Prerequisites:</span> Docker image must have <a href="https://www.openssh.com/">sshd</a> and a JDK installed.
<span class="info">Prerequisites:</span>
<ul>
<li>
The docker container's mapped SSH port,
typically a port on the docker host,
has to be accessible over network <em>from</em> the master.
</li>
<li>
Docker image must have
<a href="https://www.openssh.com/">sshd</a>
installed.
</li>
<li>
Docker image must have
<a href="https://go.java">Java</a>
installed.
</li>
<li>
Log in details configured as per
<a href="https://plugins.jenkins.io/ssh-slaves">ssh-slaves</a>
plugin.
</li>
</ul>

</f:block>

<f:dropdownDescriptorSelector title="SSH key" field="sshKeyStrategy" />
Expand Down