-
Notifications
You must be signed in to change notification settings - Fork 320
/
DockerComputerJNLPConnector.java
285 lines (248 loc) · 10.5 KB
/
DockerComputerJNLPConnector.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package io.jenkins.docker.connector;
import static com.nirima.jenkins.plugins.docker.utils.JenkinsUtils.bldToString;
import static com.nirima.jenkins.plugins.docker.utils.JenkinsUtils.endToString;
import static com.nirima.jenkins.plugins.docker.utils.JenkinsUtils.fixEmpty;
import static com.nirima.jenkins.plugins.docker.utils.JenkinsUtils.makeCopy;
import static com.nirima.jenkins.plugins.docker.utils.JenkinsUtils.splitAndFilterEmpty;
import static com.nirima.jenkins.plugins.docker.utils.JenkinsUtils.startToString;
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 edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
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 io.jenkins.docker.DockerTransientNode;
import io.jenkins.docker.client.DockerAPI;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import jenkins.model.Jenkins;
import jenkins.slaves.JnlpAgentReceiver;
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;
/**
* @author <a href="mailto:[email protected]">Nicolas De Loof</a>
*/
public class DockerComputerJNLPConnector extends DockerComputerConnector {
@CheckForNull
private String user;
private final JNLPLauncher jnlpLauncher;
@CheckForNull
private String jenkinsUrl;
@CheckForNull
private String[] entryPointArguments;
@Restricted(NoExternalUse.class)
public DockerComputerJNLPConnector() {
this(new JNLPLauncher(false));
}
@DataBoundConstructor
public DockerComputerJNLPConnector(JNLPLauncher jnlpLauncher) {
this.jnlpLauncher = jnlpLauncher;
}
@CheckForNull
public String getUser() {
return Util.fixEmptyAndTrim(user);
}
@DataBoundSetter
public void setUser(String user) {
this.user = Util.fixEmptyAndTrim(user);
}
@CheckForNull
public String getJenkinsUrl() {
return Util.fixEmptyAndTrim(jenkinsUrl);
}
@DataBoundSetter
public void setJenkinsUrl(String jenkinsUrl) {
this.jenkinsUrl = Util.fixEmptyAndTrim(jenkinsUrl);
}
@NonNull
public String getEntryPointArgumentsString() {
if (entryPointArguments == null) {
return "";
}
return Joiner.on("\n").join(entryPointArguments);
}
@DataBoundSetter
public void setEntryPointArgumentsString(String entryPointArgumentsString) {
setEntryPointArguments(splitAndFilterEmpty(entryPointArgumentsString, "\n"));
}
private void setEntryPointArguments(String[] entryPointArguments) {
this.entryPointArguments = fixEmpty(entryPointArguments);
}
public DockerComputerJNLPConnector withUser(String value) {
setUser(value);
return this;
}
public DockerComputerJNLPConnector withJenkinsUrl(String value) {
setJenkinsUrl(value);
return this;
}
public DockerComputerJNLPConnector withEntryPointArguments(String... args) {
setEntryPointArguments(args);
return this;
}
public JNLPLauncher getJnlpLauncher() {
return jnlpLauncher;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Arrays.hashCode(entryPointArguments);
result = prime * result + Objects.hash(jenkinsUrl, jnlpLauncher, user);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
DockerComputerJNLPConnector other = (DockerComputerJNLPConnector) obj;
return Arrays.equals(entryPointArguments, other.entryPointArguments)
&& Objects.equals(jenkinsUrl, other.jenkinsUrl)
&& Objects.equals(jnlpLauncher, other.jnlpLauncher)
&& Objects.equals(user, other.user);
}
@Override
public String toString() {
final StringBuilder sb = startToString(this);
bldToString(sb, "user", user);
bldToString(sb, "jnlpLauncher", jnlpLauncher);
bldToString(sb, "jenkinsUrl", jenkinsUrl);
bldToString(sb, "entryPointArguments", entryPointArguments);
endToString(sb);
return sb.toString();
}
@Override
protected ComputerLauncher createLauncher(
final DockerAPI api, final String workdir, final InspectContainerResponse inspect, TaskListener listener)
throws IOException, InterruptedException {
return makeCopy(jnlpLauncher);
}
@Restricted(NoExternalUse.class)
enum ArgumentVariables {
NodeName("NODE_NAME", "The name assigned to this node"), //
Secret(
"JNLP_SECRET",
"The secret that must be passed to agent.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;
ArgumentVariables(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
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 {
final String effectiveJenkinsUrl =
StringUtils.isEmpty(jenkinsUrl) ? Jenkins.get().getRootUrl() : jenkinsUrl;
final String nodeName = DockerTemplate.getNodeNameFromContainerConfig(cmd);
final String secret = JnlpAgentReceiver.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);
if (StringUtils.isNotBlank(user)) {
cmd.withUser(user);
}
}
@Override
public void beforeContainerStarted(DockerAPI api, String workdir, DockerTransientNode node)
throws IOException, InterruptedException {
// For JNLP, we need to have the Jenkins Node known to Jenkins as a valid JNLP
// node before the container starts, otherwise it might get started before
// Jenkins is ready for it.
// That's why we explicitly add the node here instead of allowing the cloud
// provisioning process to add it later.
ensureNodeIsKnown(node);
}
private static 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.get();
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;
}
@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";
}
}
}