1
1
package io .jenkins .docker .connector ;
2
2
3
+ import static com .nirima .jenkins .plugins .docker .DockerTemplateBase .splitAndFilterEmpty ;
4
+
3
5
import com .github .dockerjava .api .DockerClient ;
4
6
import com .github .dockerjava .api .command .CreateContainerCmd ;
5
7
import com .github .dockerjava .api .command .ExecCreateCmd ;
6
8
import com .github .dockerjava .api .command .ExecCreateCmdResponse ;
7
9
import com .github .dockerjava .api .command .InspectContainerResponse ;
10
+ import com .google .common .base .Joiner ;
11
+
12
+ import hudson .EnvVars ;
8
13
import hudson .Extension ;
14
+ import hudson .Util ;
9
15
import hudson .model .Descriptor ;
10
16
import hudson .model .TaskListener ;
11
17
import hudson .remoting .Channel ;
12
18
import hudson .slaves .ComputerLauncher ;
13
19
import hudson .slaves .SlaveComputer ;
14
20
import io .jenkins .docker .client .DockerAPI ;
15
21
import io .jenkins .docker .client .DockerMultiplexedInputStream ;
22
+ import jenkins .model .Jenkins ;
23
+
16
24
import org .apache .commons .lang .StringUtils ;
17
25
import org .jenkinsci .Symbol ;
26
+ import org .kohsuke .accmod .Restricted ;
27
+ import org .kohsuke .accmod .restrictions .NoExternalUse ;
18
28
import org .kohsuke .stapler .DataBoundConstructor ;
19
29
import org .kohsuke .stapler .DataBoundSetter ;
20
30
25
35
import java .io .PrintWriter ;
26
36
import java .io .Serializable ;
27
37
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 ;
28
44
29
45
/**
30
46
* @author <a href="mailto:[email protected] ">Nicolas De Loof</a>
31
47
*/
32
48
public class DockerComputerAttachConnector extends DockerComputerConnector implements Serializable {
33
49
50
+ @ CheckForNull
34
51
private String user ;
52
+ @ CheckForNull
53
+ private String javaExe ;
54
+ @ CheckForNull
55
+ private String [] jvmArgs ;
56
+ @ CheckForNull
57
+ private String [] entryPointCmd ;
35
58
36
59
@ DataBoundConstructor
37
60
public DockerComputerAttachConnector () {
@@ -41,15 +64,104 @@ public DockerComputerAttachConnector(String user) {
41
64
this .user = user ;
42
65
}
43
66
67
+ @ Nonnull
44
68
public String getUser () {
45
- return user ;
69
+ return user == null ? "" : user ;
46
70
}
47
71
48
72
@ DataBoundSetter
49
73
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 );
51
104
}
52
105
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
+ }
53
165
54
166
@ Override
55
167
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
63
175
}
64
176
}
65
177
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
+ }
69
201
}
70
202
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
+
71
212
@ 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 ());
77
215
}
78
216
217
+
79
218
@ Extension (ordinal = 100 ) @ Symbol ("attach" )
80
219
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
+
81
242
@ Override
82
243
public String getDisplayName () {
83
244
return "Attach Docker container" ;
84
245
}
85
246
}
86
247
87
248
private static class DockerAttachLauncher extends ComputerLauncher {
88
-
89
249
private final DockerAPI api ;
90
250
private final String containerId ;
91
251
private final String user ;
92
252
private final String remoteFs ;
253
+ private final String javaExe ;
254
+ private final String jvmArgs ;
255
+ private final String entryPointCmd ;
93
256
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 ) {
95
258
this .api = api ;
96
259
this .containerId = containerId ;
97
260
this .user = user ;
98
261
this .remoteFs = remoteFs ;
262
+ this .javaExe = javaExe ;
263
+ this .jvmArgs = jvmArgs ;
264
+ this .entryPointCmd = entryPointCmd ;
99
265
}
100
266
101
267
@ Override
102
268
public void launch (final SlaveComputer computer , TaskListener listener ) throws IOException , InterruptedException {
103
269
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 ));
105
278
106
279
final String execId ;
107
280
try (final DockerClient client = api .getClient ()) {
@@ -110,7 +283,7 @@ public void launch(final SlaveComputer computer, TaskListener listener) throws I
110
283
.withAttachStdout (true )
111
284
.withAttachStderr (true )
112
285
.withTty (false )
113
- .withCmd ("java" , "-jar" , remoteFs + '/' + remoting . getName (), "-noReconnect" , "-noKeepAlive" , "-slaveLog" , remoteFs + "/agent.log" );
286
+ .withCmd (resolvedEntryPointCmd );
114
287
if (StringUtils .isNotBlank (user )) {
115
288
cmd .withUser (user );
116
289
}
@@ -160,6 +333,46 @@ public void onClosed(Channel channel, IOException cause) {
160
333
161
334
}
162
335
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
+
163
376
private String readLine (InputStream in ) throws IOException {
164
377
StringBuilder s = new StringBuilder ();
165
378
int c ;
0 commit comments