Skip to content

Commit 0057d45

Browse files
authored
Enhance attach connection method
* Enhance "Attach" connect method - Added javaExe, jvmArgs and entryPointCmd fields. * Fixed some code warnings * DockerNodeStep's connector is now configurable in the UI * Added -Xmx250m JVM arg to all slaves used in DockerNodeStepTest * Updated changelog.
1 parent 597fdc3 commit 0057d45

File tree

14 files changed

+510
-96
lines changed

14 files changed

+510
-96
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
A pre-release can be downloaded from https://ci.jenkins.io/job/Plugins/job/docker-plugin/job/master/
55
* Enhancement: container stop timeout now configurable [#732](https://github.com/jenkinsci/docker-plugin/issues/732)
66
* Fix possible resource leak [#786](https://github.com/jenkinsci/docker-plugin/issues/786)
7-
* Enhancement: can now add/drop capabilites [#696](https://github.com/jenkinsci/docker-plugin/issues/696)
7+
* Enhancement: can now add/drop docker capabilites [#696](https://github.com/jenkinsci/docker-plugin/issues/696)
8+
* Enhancement: can now customise "attach" connections [#790](https://github.com/jenkinsci/docker-plugin/issues/790)
89

910
## 1.2.0
1011
_2020-04-02_

src/main/java/com/nirima/jenkins/plugins/docker/strategy/DockerOnceRetentionStrategy.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
import static java.util.concurrent.TimeUnit.MINUTES;
2424

25+
import java.util.Objects;
26+
2527
/**
2628
* Mix of {@link org.jenkinsci.plugins.durabletask.executors.OnceRetentionStrategy} (1.3) and {@link CloudRetentionStrategy}
2729
* that allows configure it parameters and has Descriptor.
@@ -115,13 +117,16 @@ private synchronized void done(final DockerComputer c) {
115117
});
116118
}
117119

120+
@Override
121+
public int hashCode() {
122+
return Objects.hash(idleMinutes);
123+
}
124+
118125
@Override
119126
public boolean equals(Object o) {
120127
if (this == o) return true;
121128
if (o == null || getClass() != o.getClass()) return false;
122-
123129
DockerOnceRetentionStrategy that = (DockerOnceRetentionStrategy) o;
124-
125130
return idleMinutes == that.idleMinutes;
126131
}
127132

src/main/java/io/jenkins/docker/FastNodeProvisionerStrategy.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,9 @@ private StrategyDecision applyFoCloud(@Nonnull NodeProvisioner.StrategyState sta
8080
if (availableCapacity >= currentDemand) {
8181
LOGGER.log(FINE, "Provisioning completed");
8282
return PROVISIONING_COMPLETED;
83-
} else {
84-
LOGGER.log(FINE, "Provisioning not complete, consulting remaining strategies");
85-
return CONSULT_REMAINING_STRATEGIES;
8683
}
84+
LOGGER.log(FINE, "Provisioning not complete, consulting remaining strategies");
85+
return CONSULT_REMAINING_STRATEGIES;
8786
}
8887

8988
/**

src/main/java/io/jenkins/docker/connector/DockerComputerAttachConnector.java

Lines changed: 227 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
package io.jenkins.docker.connector;
22

3+
import static com.nirima.jenkins.plugins.docker.DockerTemplateBase.splitAndFilterEmpty;
4+
35
import com.github.dockerjava.api.DockerClient;
46
import com.github.dockerjava.api.command.CreateContainerCmd;
57
import com.github.dockerjava.api.command.ExecCreateCmd;
68
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
79
import com.github.dockerjava.api.command.InspectContainerResponse;
10+
import com.google.common.base.Joiner;
11+
12+
import hudson.EnvVars;
813
import hudson.Extension;
14+
import hudson.Util;
915
import hudson.model.Descriptor;
1016
import hudson.model.TaskListener;
1117
import hudson.remoting.Channel;
1218
import hudson.slaves.ComputerLauncher;
1319
import hudson.slaves.SlaveComputer;
1420
import io.jenkins.docker.client.DockerAPI;
1521
import io.jenkins.docker.client.DockerMultiplexedInputStream;
22+
import jenkins.model.Jenkins;
23+
1624
import org.apache.commons.lang.StringUtils;
1725
import org.jenkinsci.Symbol;
26+
import org.kohsuke.accmod.Restricted;
27+
import org.kohsuke.accmod.restrictions.NoExternalUse;
1828
import org.kohsuke.stapler.DataBoundConstructor;
1929
import org.kohsuke.stapler.DataBoundSetter;
2030

@@ -25,13 +35,26 @@
2535
import java.io.PrintWriter;
2636
import java.io.Serializable;
2737
import java.net.Socket;
38+
import java.util.Arrays;
39+
import java.util.Collection;
40+
import java.util.Objects;
41+
42+
import javax.annotation.CheckForNull;
43+
import javax.annotation.Nonnull;
2844

2945
/**
3046
* @author <a href="mailto:[email protected]">Nicolas De Loof</a>
3147
*/
3248
public class DockerComputerAttachConnector extends DockerComputerConnector implements Serializable {
3349

50+
@CheckForNull
3451
private String user;
52+
@CheckForNull
53+
private String javaExe;
54+
@CheckForNull
55+
private String[] jvmArgs;
56+
@CheckForNull
57+
private String[] entryPointCmd;
3558

3659
@DataBoundConstructor
3760
public DockerComputerAttachConnector() {
@@ -41,15 +64,104 @@ public DockerComputerAttachConnector(String user) {
4164
this.user = user;
4265
}
4366

67+
@Nonnull
4468
public String getUser() {
45-
return user;
69+
return user==null ? "" : user;
4670
}
4771

4872
@DataBoundSetter
4973
public void setUser(String user) {
50-
this.user = user;
74+
if ( user==null || user.trim().isEmpty()) {
75+
this.user = null;
76+
} else {
77+
this.user = user;
78+
}
79+
}
80+
81+
@Nonnull
82+
public String getJavaExe() {
83+
return javaExe==null ? "" : javaExe;
84+
}
85+
86+
@DataBoundSetter
87+
public void setJavaExe(String javaExe) {
88+
if ( javaExe==null || javaExe.trim().isEmpty()) {
89+
this.javaExe = null;
90+
} else {
91+
this.javaExe = javaExe;
92+
}
93+
}
94+
95+
@CheckForNull
96+
public String[] getEntryPointCmd(){
97+
return entryPointCmd;
98+
}
99+
100+
@Nonnull
101+
public String getEntryPointCmdString() {
102+
if (entryPointCmd == null) return "";
103+
return Joiner.on("\n").join(entryPointCmd);
51104
}
52105

106+
@DataBoundSetter
107+
public void setEntryPointCmdString(String entryPointCmdString) {
108+
setEntryPointCmd(splitAndFilterEmpty(entryPointCmdString, "\n"));
109+
}
110+
111+
public void setEntryPointCmd(String[] entryPointCmd) {
112+
if (entryPointCmd == null || entryPointCmd.length == 0) {
113+
this.entryPointCmd = null;
114+
} else {
115+
this.entryPointCmd = entryPointCmd;
116+
}
117+
}
118+
119+
@CheckForNull
120+
public String[] getJvmArgs(){
121+
return jvmArgs;
122+
}
123+
124+
@Nonnull
125+
public String getJvmArgsString() {
126+
if (jvmArgs == null) return "";
127+
return Joiner.on("\n").join(jvmArgs);
128+
}
129+
130+
@DataBoundSetter
131+
public void setJvmArgsString(String jvmArgsString) {
132+
setJvmArgs(splitAndFilterEmpty(jvmArgsString, "\n"));
133+
}
134+
135+
public void setJvmArgs(String[] jvmArgs) {
136+
if (jvmArgs == null || jvmArgs.length == 0) {
137+
this.jvmArgs = null;
138+
} else {
139+
this.jvmArgs = jvmArgs;
140+
}
141+
}
142+
143+
@Override
144+
public int hashCode() {
145+
final int prime = 31;
146+
int result = super.hashCode();
147+
result = prime * result + Arrays.hashCode(entryPointCmd);
148+
result = prime * result + Arrays.hashCode(jvmArgs);
149+
result = prime * result + Objects.hash(javaExe, user);
150+
return result;
151+
}
152+
153+
@Override
154+
public boolean equals(Object obj) {
155+
if (this == obj) {
156+
return true;
157+
}
158+
if (!super.equals(obj)) {
159+
return false;
160+
}
161+
DockerComputerAttachConnector other = (DockerComputerAttachConnector) obj;
162+
return Arrays.equals(entryPointCmd, other.entryPointCmd) && Objects.equals(javaExe, other.javaExe)
163+
&& Arrays.equals(jvmArgs, other.jvmArgs) && Objects.equals(user, other.user);
164+
}
53165

54166
@Override
55167
public void beforeContainerCreated(DockerAPI api, String workdir, CreateContainerCmd cmd) throws IOException, InterruptedException {
@@ -63,45 +175,106 @@ public void afterContainerStarted(DockerAPI api, String workdir, String containe
63175
}
64176
}
65177

66-
@Override
67-
protected ComputerLauncher createLauncher(DockerAPI api, String workdir, InspectContainerResponse inspect, TaskListener listener) throws IOException, InterruptedException {
68-
return new DockerAttachLauncher(api, inspect.getId(), user, workdir);
178+
@Restricted(NoExternalUse.class)
179+
public enum ArgumentVariables {
180+
JavaExe("JAVA", "The java binary, e.g. java, /usr/bin/java etc."), //
181+
JvmArgs("JVM_ARGS", "Any arguments for the JVM itself, e.g. -Xmx250m."), //
182+
JarName("JAR_NAME", "The name of the jar file the node must run, e.g. slave.jar."), //
183+
RemoteFs("FS_DIR",
184+
"The filesystem folder in which the slave process is to be run."), //
185+
JenkinsUrl("JENKINS_URL", "The Jenkins root URL.");
186+
private final String name;
187+
private final String description;
188+
189+
ArgumentVariables(String name, String description) {
190+
this.name = name;
191+
this.description = description;
192+
}
193+
194+
public String getName() {
195+
return name;
196+
}
197+
198+
public String getDescription() {
199+
return description;
200+
}
69201
}
70202

203+
private static final String DEFAULT_ENTRY_POINT_CMD_STRING = "${" + ArgumentVariables.JavaExe.getName() + "}\n"
204+
+ "${" + ArgumentVariables.JvmArgs.getName() + "}\n"
205+
+ "-jar\n"
206+
+ "${" + ArgumentVariables.RemoteFs.getName() + "}/${" + ArgumentVariables.JarName.getName() + "}\n"
207+
+ "-noReconnect\n"
208+
+ "-noKeepAlive\n"
209+
+ "-slaveLog\n"
210+
+ "${" + ArgumentVariables.RemoteFs.getName() + "}/agent.log";
211+
71212
@Override
72-
public boolean equals(Object o) {
73-
if (this == o) return true;
74-
if (o == null || getClass() != o.getClass()) return false;
75-
DockerComputerAttachConnector that = (DockerComputerAttachConnector) o;
76-
return user != null ? user.equals(that.user) : that.user == null;
213+
protected ComputerLauncher createLauncher(DockerAPI api, String workdir, InspectContainerResponse inspect, TaskListener listener) throws IOException, InterruptedException {
214+
return new DockerAttachLauncher(api, inspect.getId(), getUser(), workdir, getJavaExe(), getJvmArgsString(), getEntryPointCmdString());
77215
}
78216

217+
79218
@Extension(ordinal = 100) @Symbol("attach")
80219
public static class DescriptorImpl extends Descriptor<DockerComputerConnector> {
220+
221+
public String getDefaultJavaExe() {
222+
return "java";
223+
}
224+
225+
public String getJavaExeVariableName() {
226+
return ArgumentVariables.JavaExe.name;
227+
}
228+
229+
public String getJvmArgsVariableName() {
230+
return ArgumentVariables.JvmArgs.name;
231+
}
232+
233+
public Collection<ArgumentVariables> getEntryPointCmdVariables() {
234+
return Arrays.asList(ArgumentVariables.values());
235+
}
236+
237+
public Collection<String> getDefaultEntryPointCmd() {
238+
final String[] args = splitAndFilterEmpty(DEFAULT_ENTRY_POINT_CMD_STRING, "\n");
239+
return Arrays.asList(args);
240+
}
241+
81242
@Override
82243
public String getDisplayName() {
83244
return "Attach Docker container";
84245
}
85246
}
86247

87248
private static class DockerAttachLauncher extends ComputerLauncher {
88-
89249
private final DockerAPI api;
90250
private final String containerId;
91251
private final String user;
92252
private final String remoteFs;
253+
private final String javaExe;
254+
private final String jvmArgs;
255+
private final String entryPointCmd;
93256

94-
private DockerAttachLauncher(DockerAPI api, String containerId, String user, String remoteFs) {
257+
private DockerAttachLauncher(DockerAPI api, String containerId, String user, String remoteFs, String javaExe, String jvmArgs, String entryPointCmd) {
95258
this.api = api;
96259
this.containerId = containerId;
97260
this.user = user;
98261
this.remoteFs = remoteFs;
262+
this.javaExe = javaExe;
263+
this.jvmArgs = jvmArgs;
264+
this.entryPointCmd = entryPointCmd;
99265
}
100266

101267
@Override
102268
public void launch(final SlaveComputer computer, TaskListener listener) throws IOException, InterruptedException {
103269
final PrintStream logger = computer.getListener().getLogger();
104-
logger.println("Connecting to docker container "+containerId);
270+
final String jenkinsUrl = Jenkins.getInstance().getRootUrl();
271+
final String effectiveJavaExe = javaExe.isEmpty() ? "java" : javaExe;
272+
final String effectiveJvmArgs = jvmArgs.isEmpty() ? "" : jvmArgs;
273+
final EnvVars knownVariables = calculateVariablesForVariableSubstitution(effectiveJavaExe, effectiveJvmArgs, remoting.getName(), remoteFs, jenkinsUrl);
274+
final String effectiveEntryPointCmdString = StringUtils.isNotBlank(entryPointCmd) ? entryPointCmd : DEFAULT_ENTRY_POINT_CMD_STRING;
275+
final String resolvedEntryPointCmdString = Util.replaceMacro(effectiveEntryPointCmdString, knownVariables);
276+
final String[] resolvedEntryPointCmd = splitAndFilterEmpty(resolvedEntryPointCmdString, "\n");
277+
logger.println("Connecting to docker container " + containerId + ", running command " + Joiner.on(" ").join(resolvedEntryPointCmd));
105278

106279
final String execId;
107280
try(final DockerClient client = api.getClient()) {
@@ -110,7 +283,7 @@ public void launch(final SlaveComputer computer, TaskListener listener) throws I
110283
.withAttachStdout(true)
111284
.withAttachStderr(true)
112285
.withTty(false)
113-
.withCmd("java", "-jar", remoteFs + '/' + remoting.getName(), "-noReconnect", "-noKeepAlive", "-slaveLog", remoteFs + "/agent.log");
286+
.withCmd(resolvedEntryPointCmd);
114287
if (StringUtils.isNotBlank(user)) {
115288
cmd.withUser(user);
116289
}
@@ -160,6 +333,46 @@ public void onClosed(Channel channel, IOException cause) {
160333

161334
}
162335

336+
private static EnvVars calculateVariablesForVariableSubstitution(final String javaExe, final String jvmArgs, final String jarName, final String remoteFs,
337+
final String jenkinsUrl) throws IOException, InterruptedException {
338+
final EnvVars knownVariables = new EnvVars();
339+
final Jenkins j = Jenkins.getInstance();
340+
addEnvVars(knownVariables, j.getGlobalNodeProperties());
341+
for (final ArgumentVariables v : ArgumentVariables.values()) {
342+
// This switch statement MUST handle all possible
343+
// values of v.
344+
final String argValue;
345+
switch (v) {
346+
case JavaExe :
347+
argValue = javaExe;
348+
break;
349+
case JvmArgs :
350+
argValue = jvmArgs;
351+
break;
352+
case JarName :
353+
argValue = jarName;
354+
break;
355+
case RemoteFs :
356+
argValue = remoteFs;
357+
break;
358+
case JenkinsUrl :
359+
argValue = jenkinsUrl;
360+
break;
361+
default :
362+
final String msg = "Internal code error: Switch statement is missing \"case " + v.name()
363+
+ " : argValue = ... ; break;\" code.";
364+
// If this line throws an exception then it's because
365+
// someone has added a new variable to the enum without
366+
// adding code above to handle it.
367+
// The two have to be kept in step in order to
368+
// ensure that the help text stays in step.
369+
throw new RuntimeException(msg);
370+
}
371+
addEnvVar(knownVariables, v.getName(), argValue);
372+
}
373+
return knownVariables;
374+
}
375+
163376
private String readLine(InputStream in) throws IOException {
164377
StringBuilder s = new StringBuilder();
165378
int c;

0 commit comments

Comments
 (0)