Skip to content

Commit 88c0960

Browse files
Added an option to collect init script logs.
1 parent 483cc28 commit 88c0960

19 files changed

+786
-39
lines changed

src/main/java/hudson/plugins/ec2/SlaveTemplate.java

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ public class SlaveTemplate implements Describable<SlaveTemplate> {
254254

255255
private Boolean enclaveEnabled;
256256

257+
private boolean collectInitScriptLogs;
258+
257259
private transient /* almost final */ Set<LabelAtom> labelSet;
258260

259261
private transient /* almost final */ Set<String> securityGroupSet;
@@ -343,7 +345,8 @@ public SlaveTemplate(
343345
Boolean metadataTokensRequired,
344346
Integer metadataHopsLimit,
345347
Boolean metadataSupported,
346-
Boolean enclaveEnabled) {
348+
Boolean enclaveEnabled,
349+
boolean collectInitScriptLogs) {
347350

348351
if (StringUtils.isNotBlank(remoteAdmin) || StringUtils.isNotBlank(jvmopts) || StringUtils.isNotBlank(tmpDir)) {
349352
LOGGER.log(
@@ -434,6 +437,7 @@ public SlaveTemplate(
434437
metadataHopsLimit != null ? metadataHopsLimit : EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT;
435438
this.enclaveEnabled = enclaveEnabled != null ? enclaveEnabled : EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED;
436439
this.associateIPStrategy = associateIPStrategy != null ? associateIPStrategy : AssociateIPStrategy.DEFAULT;
440+
this.collectInitScriptLogs = collectInitScriptLogs;
437441

438442
readResolve(); // initialize
439443
}
@@ -530,7 +534,8 @@ public SlaveTemplate(
530534
metadataTokensRequired,
531535
metadataHopsLimit,
532536
metadataSupported,
533-
enclaveEnabled);
537+
enclaveEnabled,
538+
false);
534539
}
535540

536541
@Deprecated
@@ -609,7 +614,7 @@ public SlaveTemplate(
609614
deleteRootOnTermination,
610615
useEphemeralDevices,
611616
launchTimeoutStr,
612-
associatePublicIp,
617+
AssociateIPStrategy.backwardsCompatible(associatePublicIp),
613618
customDeviceMapping,
614619
connectBySSHProcess,
615620
monitoring,
@@ -624,7 +629,8 @@ public SlaveTemplate(
624629
metadataTokensRequired,
625630
metadataHopsLimit,
626631
metadataSupported,
627-
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED);
632+
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED,
633+
false);
628634
}
629635

630636
@Deprecated
@@ -703,7 +709,7 @@ public SlaveTemplate(
703709
deleteRootOnTermination,
704710
useEphemeralDevices,
705711
launchTimeoutStr,
706-
associatePublicIp,
712+
AssociateIPStrategy.backwardsCompatible(associatePublicIp),
707713
customDeviceMapping,
708714
connectBySSHProcess,
709715
monitoring,
@@ -718,7 +724,8 @@ public SlaveTemplate(
718724
metadataTokensRequired,
719725
metadataHopsLimit,
720726
EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED,
721-
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED);
727+
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED,
728+
false);
722729
}
723730

724731
@Deprecated
@@ -792,7 +799,7 @@ public SlaveTemplate(
792799
deleteRootOnTermination,
793800
useEphemeralDevices,
794801
launchTimeoutStr,
795-
associatePublicIp,
802+
AssociateIPStrategy.backwardsCompatible(associatePublicIp),
796803
customDeviceMapping,
797804
connectBySSHProcess,
798805
monitoring,
@@ -807,7 +814,8 @@ public SlaveTemplate(
807814
EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED,
808815
EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT,
809816
EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED,
810-
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED);
817+
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED,
818+
false);
811819
}
812820

813821
@Deprecated
@@ -2074,6 +2082,15 @@ public Boolean getEnclaveEnabled() {
20742082
return enclaveEnabled;
20752083
}
20762084

2085+
public boolean getCollectInitScriptLogs() {
2086+
return collectInitScriptLogs;
2087+
}
2088+
2089+
@DataBoundSetter
2090+
public void setCollectInitScriptLogs(boolean collectInitScriptLogs) {
2091+
this.collectInitScriptLogs = collectInitScriptLogs;
2092+
}
2093+
20772094
public DescribableList<NodeProperty<?>, NodePropertyDescriptor> getNodeProperties() {
20782095
return Objects.requireNonNull(nodeProperties);
20792096
}

src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,34 +155,110 @@ public void onClosed(Channel channel, IOException cause) {
155155
computer.setChannel(invertedOut, invertedIn, logger, channelListener);
156156
}
157157

158+
protected boolean executeRemote(ClientSession session, String command, OutputStream logger) {
159+
return executeRemote(session, command, logger, false, null);
160+
}
161+
162+
// Add overloaded method with collectOutput parameter
163+
protected boolean executeRemote(
164+
ClientSession session, String command, OutputStream logger, boolean collectOutput, TaskListener listener) {
165+
try {
166+
if (collectOutput && listener != null) {
167+
// Execute with output capture
168+
ChannelExec channelExec = session.createExecChannel(command);
169+
java.io.ByteArrayOutputStream stdout = new java.io.ByteArrayOutputStream();
170+
java.io.ByteArrayOutputStream stderr = new java.io.ByteArrayOutputStream();
171+
172+
// Send to both the original logger and our capture streams
173+
java.io.OutputStream combinedOut = new java.io.OutputStream() {
174+
@Override
175+
public void write(int b) throws IOException {
176+
logger.write(b);
177+
stdout.write(b);
178+
}
179+
180+
@Override
181+
public void write(byte[] b, int off, int len) throws IOException {
182+
logger.write(b, off, len);
183+
stdout.write(b, off, len);
184+
}
185+
};
186+
187+
java.io.OutputStream combinedErr = new java.io.OutputStream() {
188+
@Override
189+
public void write(int b) throws IOException {
190+
logger.write(b);
191+
stderr.write(b);
192+
}
193+
194+
@Override
195+
public void write(byte[] b, int off, int len) throws IOException {
196+
logger.write(b, off, len);
197+
stderr.write(b, off, len);
198+
}
199+
};
200+
201+
channelExec.setOut(combinedOut);
202+
channelExec.setErr(combinedErr);
203+
channelExec.open();
204+
channelExec.waitFor(java.util.EnumSet.of(org.apache.sshd.client.channel.ClientChannelEvent.CLOSED), 0);
205+
206+
// Log the captured output to Logger
207+
String stdoutStr = stdout.toString(java.nio.charset.StandardCharsets.UTF_8);
208+
String stderrStr = stderr.toString(java.nio.charset.StandardCharsets.UTF_8);
209+
210+
if (!stdoutStr.trim().isEmpty()) {
211+
// Replace all line breaks with "||" so that it appears as a single line in the logs
212+
LOGGER.info("Init script STDOUT for command '" + command + "': "
213+
+ stdoutStr.replaceAll("\\r\\n|\\r|\\n", "||"));
214+
}
215+
if (!stderrStr.trim().isEmpty()) {
216+
// Replace all line breaks with "||" so that it appears as a single line in the logs
217+
LOGGER.warning("Init script STDERR for command '" + command + "': "
218+
+ stderrStr.replaceAll("\\r\\n|\\r|\\n", "||"));
219+
}
220+
221+
return channelExec.getExitStatus() == 0;
222+
} else {
223+
// Use existing implementation
224+
session.executeRemoteCommand(command, logger, logger, null);
225+
return true;
226+
}
227+
} catch (IOException e) {
228+
LOGGER.log(Level.FINE, "Failed to execute remote command: " + command, e);
229+
return false;
230+
}
231+
}
232+
158233
protected boolean executeRemote(
159234
EC2Computer computer,
160235
ClientSession clientSession,
161236
String checkCommand,
162237
String command,
163238
PrintStream logger,
164239
TaskListener listener) {
240+
return executeRemote(computer, clientSession, checkCommand, command, logger, listener, false);
241+
}
242+
243+
protected boolean executeRemote(
244+
EC2Computer computer,
245+
ClientSession clientSession,
246+
String checkCommand,
247+
String command,
248+
PrintStream logger,
249+
TaskListener listener,
250+
boolean collectOutput) {
165251
logInfo(computer, listener, "Verifying: " + checkCommand);
166-
if (!executeRemote(clientSession, checkCommand, logger)) {
252+
if (!executeRemote(clientSession, checkCommand, logger, collectOutput, listener)) {
167253
logInfo(computer, listener, "Installing: " + command);
168-
if (!executeRemote(clientSession, command, logger)) {
254+
if (!executeRemote(clientSession, command, logger, collectOutput, listener)) {
169255
logWarning(computer, listener, "Failed to install: " + command);
170256
return false;
171257
}
172258
}
173259
return true;
174260
}
175261

176-
protected boolean executeRemote(ClientSession session, String command, OutputStream logger) {
177-
try {
178-
session.executeRemoteCommand(command, logger, logger, null);
179-
return true;
180-
} catch (IOException e) {
181-
LOGGER.log(Level.FINE, "Failed to execute remote command: " + command, e);
182-
return false;
183-
}
184-
}
185-
186262
protected File createIdentityKeyFile(EC2Computer computer) throws IOException {
187263
EC2PrivateKey ec2PrivateKey = computer.getCloud().resolvePrivateKey();
188264
String privateKey = "";

src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,12 @@ protected void launchScript(EC2Computer computer, TaskListener listener)
181181

182182
logInfo(computer, listener, "Executing init script");
183183
String initCommand = buildUpCommand(computer, tmpDir + "/init.sh");
184+
185+
// Check if collectInitScriptLogs flag is set
186+
boolean collectInitScriptLogs = template.getCollectInitScriptLogs();
187+
184188
// Set the flag only when init script executed successfully.
185-
if (executeRemote(clientSession, initCommand, logger)) {
189+
if (executeRemote(clientSession, initCommand, logger, collectInitScriptLogs, listener)) {
186190
log(
187191
Level.FINE,
188192
computer,

src/main/resources/hudson/plugins/ec2/SlaveTemplate/config.jelly

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ THE SOFTWARE.
251251
<f:checkbox default="false"/>
252252
</f:entry>
253253

254+
<f:entry title="${%Collect Init Script Logs}" field="collectInitScriptLogs">
255+
<f:checkbox />
256+
</f:entry>
257+
254258
</f:advanced>
255259

256260
<f:entry title="">
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<div>
2+
When checked, the output (stdout and stderr) from the init script execution will be collected and displayed in the Jenkins build logs.
3+
This can be useful for debugging init script issues, but may increase log verbosity.
4+
<br/><br/>
5+
<strong>Note:</strong> This only affects the logging of init script output. The init script will still execute regardless of this setting.
6+
</div>

src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,4 +415,37 @@ void testWindowsSSHConfigAsCodeWithAltEndpointAndJavaPathExport(JenkinsConfigure
415415
String expected = Util.toStringFromYamlFile(this, "WindowsSSHDataExport-withAltEndpointAndJavaPath.yml");
416416
assertEquals(expected, exported);
417417
}
418+
419+
@Test
420+
@ConfiguredWithCode("UnixDataWithCollectInitScriptLogs.yml")
421+
void testUnixDataWithInitLogs(JenkinsConfiguredWithCodeRule j) throws Exception {
422+
ConfiguratorRegistry registry = ConfiguratorRegistry.get();
423+
ConfigurationContext context = new ConfigurationContext(registry);
424+
CNode clouds = Util.getJenkinsRoot(context).get("clouds");
425+
String exported = Util.toYamlString(clouds);
426+
String expected = Util.toStringFromYamlFile(this, "UnixDataExportWithCollectInitScriptLogs.yml");
427+
assertEquals(expected, exported);
428+
}
429+
430+
@Test
431+
@ConfiguredWithCode("MacDataWithCollectInitScriptLogs.yml")
432+
void testMacDataWithInitLogs(JenkinsConfiguredWithCodeRule j) throws Exception {
433+
ConfiguratorRegistry registry = ConfiguratorRegistry.get();
434+
ConfigurationContext context = new ConfigurationContext(registry);
435+
CNode clouds = Util.getJenkinsRoot(context).get("clouds");
436+
String exported = Util.toYamlString(clouds);
437+
String expected = Util.toStringFromYamlFile(this, "MacDataExportWithCollectInitScriptLogs.yml");
438+
assertEquals(expected, exported);
439+
}
440+
441+
@Test
442+
@ConfiguredWithCode("WindowsSSHDataWithCollectInitScriptLogs.yml")
443+
void testWindowsDataWithInitLogs(JenkinsConfiguredWithCodeRule j) throws Exception {
444+
ConfiguratorRegistry registry = ConfiguratorRegistry.get();
445+
ConfigurationContext context = new ConfigurationContext(registry);
446+
CNode clouds = Util.getJenkinsRoot(context).get("clouds");
447+
String exported = Util.toYamlString(clouds);
448+
String expected = Util.toStringFromYamlFile(this, "WindowsSSHDataExportWithCollectInitScriptLogs.yml");
449+
assertEquals(expected, exported);
450+
}
418451
}

0 commit comments

Comments
 (0)