Skip to content
Open
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
33 changes: 25 additions & 8 deletions src/main/java/hudson/plugins/ec2/SlaveTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@

private Boolean enclaveEnabled;

private boolean collectInitScriptLogs;

private transient /* almost final */ Set<LabelAtom> labelSet;

private transient /* almost final */ Set<String> securityGroupSet;
Expand Down Expand Up @@ -343,7 +345,8 @@
Boolean metadataTokensRequired,
Integer metadataHopsLimit,
Boolean metadataSupported,
Boolean enclaveEnabled) {
Boolean enclaveEnabled,
boolean collectInitScriptLogs) {

if (StringUtils.isNotBlank(remoteAdmin) || StringUtils.isNotBlank(jvmopts) || StringUtils.isNotBlank(tmpDir)) {
LOGGER.log(
Expand Down Expand Up @@ -434,6 +437,7 @@
metadataHopsLimit != null ? metadataHopsLimit : EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT;
this.enclaveEnabled = enclaveEnabled != null ? enclaveEnabled : EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED;
this.associateIPStrategy = associateIPStrategy != null ? associateIPStrategy : AssociateIPStrategy.DEFAULT;
this.collectInitScriptLogs = collectInitScriptLogs;

readResolve(); // initialize
}
Expand Down Expand Up @@ -530,7 +534,8 @@
metadataTokensRequired,
metadataHopsLimit,
metadataSupported,
enclaveEnabled);
enclaveEnabled,
false);
}

@Deprecated
Expand Down Expand Up @@ -609,190 +614,192 @@
deleteRootOnTermination,
useEphemeralDevices,
launchTimeoutStr,
associatePublicIp,
AssociateIPStrategy.backwardsCompatible(associatePublicIp),
customDeviceMapping,
connectBySSHProcess,
monitoring,
t2Unlimited,
connectionStrategy,
maxTotalUses,
nodeProperties,
hostKeyVerificationStrategy,
tenancy,
ebsEncryptRootVolume,
metadataEndpointEnabled,
metadataTokensRequired,
metadataHopsLimit,
metadataSupported,
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED);
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED,
false);
}

@Deprecated
public SlaveTemplate(
String ami,
String zone,
SpotConfiguration spotConfig,
String securityGroups,
String remoteFS,
com.amazonaws.services.ec2.model.InstanceType type,
boolean ebsOptimized,
String labelString,
Node.Mode mode,
String description,
String initScript,
String tmpDir,
String userData,
String numExecutors,
String remoteAdmin,
AMITypeData amiType,
String javaPath,
String jvmopts,
boolean stopOnTerminate,
String subnetId,
List<EC2Tag> tags,
String idleTerminationMinutes,
int minimumNumberOfInstances,
int minimumNumberOfSpareInstances,
String instanceCapStr,
String iamInstanceProfile,
boolean deleteRootOnTermination,
boolean useEphemeralDevices,
String launchTimeoutStr,
boolean associatePublicIp,
String customDeviceMapping,
boolean connectBySSHProcess,
boolean monitoring,
boolean t2Unlimited,
ConnectionStrategy connectionStrategy,
int maxTotalUses,
List<? extends NodeProperty<?>> nodeProperties,
HostKeyVerificationStrategyEnum hostKeyVerificationStrategy,
Tenancy tenancy,
EbsEncryptRootVolume ebsEncryptRootVolume,
Boolean metadataSupported,
Boolean metadataEndpointEnabled,
Boolean metadataTokensRequired,
Integer metadataHopsLimit) {
this(
ami,
zone,
spotConfig,
securityGroups,
remoteFS,
InstanceType.fromValue(type.toString()).toString(),
ebsOptimized,
labelString,
mode,
description,
initScript,
tmpDir,
userData,
numExecutors,
remoteAdmin,
amiType,
EC2AbstractSlave.DEFAULT_JAVA_PATH,
jvmopts,
stopOnTerminate,
subnetId,
tags,
idleTerminationMinutes,
minimumNumberOfInstances,
minimumNumberOfSpareInstances,
instanceCapStr,
iamInstanceProfile,
deleteRootOnTermination,
useEphemeralDevices,
launchTimeoutStr,
associatePublicIp,
AssociateIPStrategy.backwardsCompatible(associatePublicIp),
customDeviceMapping,
connectBySSHProcess,
monitoring,
t2Unlimited,
connectionStrategy,
maxTotalUses,
nodeProperties,
hostKeyVerificationStrategy,
tenancy,
null,
metadataEndpointEnabled,
metadataTokensRequired,
metadataHopsLimit,
EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED,
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED);
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED,
false);
}

@Deprecated
public SlaveTemplate(
String ami,
String zone,
SpotConfiguration spotConfig,
String securityGroups,
String remoteFS,
com.amazonaws.services.ec2.model.InstanceType type,
boolean ebsOptimized,
String labelString,
Node.Mode mode,
String description,
String initScript,
String tmpDir,
String userData,
String numExecutors,
String remoteAdmin,
AMITypeData amiType,
String jvmopts,
boolean stopOnTerminate,
String subnetId,
List<EC2Tag> tags,
String idleTerminationMinutes,
int minimumNumberOfInstances,
int minimumNumberOfSpareInstances,
String instanceCapStr,
String iamInstanceProfile,
boolean deleteRootOnTermination,
boolean useEphemeralDevices,
String launchTimeoutStr,
boolean associatePublicIp,
String customDeviceMapping,
boolean connectBySSHProcess,
boolean monitoring,
boolean t2Unlimited,
ConnectionStrategy connectionStrategy,
int maxTotalUses,
List<? extends NodeProperty<?>> nodeProperties,
HostKeyVerificationStrategyEnum hostKeyVerificationStrategy,
Tenancy tenancy,
EbsEncryptRootVolume ebsEncryptRootVolume) {
this(
ami,
zone,
spotConfig,
securityGroups,
remoteFS,
InstanceType.fromValue(type.toString()).toString(),
ebsOptimized,
labelString,
mode,
description,
initScript,
tmpDir,
userData,
numExecutors,
remoteAdmin,
amiType,
EC2AbstractSlave.DEFAULT_JAVA_PATH,
jvmopts,
stopOnTerminate,
subnetId,
tags,
idleTerminationMinutes,
minimumNumberOfInstances,
minimumNumberOfSpareInstances,
instanceCapStr,
iamInstanceProfile,
deleteRootOnTermination,
useEphemeralDevices,
launchTimeoutStr,
associatePublicIp,
AssociateIPStrategy.backwardsCompatible(associatePublicIp),

Check warning on line 802 in src/main/java/hudson/plugins/ec2/SlaveTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 617-802 are not covered by tests
customDeviceMapping,
connectBySSHProcess,
monitoring,
Expand All @@ -807,7 +814,8 @@
EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED,
EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT,
EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED,
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED);
EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED,
false);
}

@Deprecated
Expand Down Expand Up @@ -2074,6 +2082,15 @@
return enclaveEnabled;
}

public boolean getCollectInitScriptLogs() {
return collectInitScriptLogs;
}

@DataBoundSetter
public void setCollectInitScriptLogs(boolean collectInitScriptLogs) {
this.collectInitScriptLogs = collectInitScriptLogs;
}

public DescribableList<NodeProperty<?>, NodePropertyDescriptor> getNodeProperties() {
return Objects.requireNonNull(nodeProperties);
}
Expand Down
100 changes: 88 additions & 12 deletions src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,34 +155,110 @@
computer.setChannel(invertedOut, invertedIn, logger, channelListener);
}

protected boolean executeRemote(ClientSession session, String command, OutputStream logger) {
return executeRemote(session, command, logger, false, null);
}

// Add overloaded method with collectOutput parameter
protected boolean executeRemote(
ClientSession session, String command, OutputStream logger, boolean collectOutput, TaskListener listener) {
try {
if (collectOutput && listener != null) {

Check warning on line 166 in src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 166 is only partially covered, one branch is missing
// Execute with output capture
ChannelExec channelExec = session.createExecChannel(command);
java.io.ByteArrayOutputStream stdout = new java.io.ByteArrayOutputStream();
java.io.ByteArrayOutputStream stderr = new java.io.ByteArrayOutputStream();

// Send to both the original logger and our capture streams
java.io.OutputStream combinedOut = new java.io.OutputStream() {
@Override
public void write(int b) throws IOException {
logger.write(b);
stdout.write(b);
}

Check warning on line 178 in src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 176-178 are not covered by tests

@Override
public void write(byte[] b, int off, int len) throws IOException {
logger.write(b, off, len);
stdout.write(b, off, len);
}
};

java.io.OutputStream combinedErr = new java.io.OutputStream() {
@Override
public void write(int b) throws IOException {
logger.write(b);
stderr.write(b);
}

Check warning on line 192 in src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 190-192 are not covered by tests

@Override
public void write(byte[] b, int off, int len) throws IOException {
logger.write(b, off, len);
stderr.write(b, off, len);
}
};

channelExec.setOut(combinedOut);
channelExec.setErr(combinedErr);
channelExec.open();
channelExec.waitFor(java.util.EnumSet.of(org.apache.sshd.client.channel.ClientChannelEvent.CLOSED), 0);

// Log the captured output to Logger
String stdoutStr = stdout.toString(java.nio.charset.StandardCharsets.UTF_8);
String stderrStr = stderr.toString(java.nio.charset.StandardCharsets.UTF_8);

if (!stdoutStr.trim().isEmpty()) {

Check warning on line 210 in src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 210 is only partially covered, one branch is missing
// Replace all line breaks with "||" so that it appears as a single line in the logs
LOGGER.info("Init script STDOUT for command '" + command + "': "
+ stdoutStr.replaceAll("\\r\\n|\\r|\\n", "||"));
}
if (!stderrStr.trim().isEmpty()) {

Check warning on line 215 in src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 215 is only partially covered, one branch is missing
// Replace all line breaks with "||" so that it appears as a single line in the logs
LOGGER.warning("Init script STDERR for command '" + command + "': "
+ stderrStr.replaceAll("\\r\\n|\\r|\\n", "||"));
}

return channelExec.getExitStatus() == 0;

Check warning on line 221 in src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 221 is only partially covered, one branch is missing
} else {
// Use existing implementation
session.executeRemoteCommand(command, logger, logger, null);
return true;
}
} catch (IOException e) {
LOGGER.log(Level.FINE, "Failed to execute remote command: " + command, e);
return false;
}
}

protected boolean executeRemote(
EC2Computer computer,
ClientSession clientSession,
String checkCommand,
String command,
PrintStream logger,
TaskListener listener) {
return executeRemote(computer, clientSession, checkCommand, command, logger, listener, false);
}

protected boolean executeRemote(
EC2Computer computer,
ClientSession clientSession,
String checkCommand,
String command,
PrintStream logger,
TaskListener listener,
boolean collectOutput) {
logInfo(computer, listener, "Verifying: " + checkCommand);
if (!executeRemote(clientSession, checkCommand, logger)) {
if (!executeRemote(clientSession, checkCommand, logger, collectOutput, listener)) {

Check warning on line 252 in src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 252 is only partially covered, one branch is missing
logInfo(computer, listener, "Installing: " + command);
if (!executeRemote(clientSession, command, logger)) {
if (!executeRemote(clientSession, command, logger, collectOutput, listener)) {

Check warning on line 254 in src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 254 is not covered by tests
logWarning(computer, listener, "Failed to install: " + command);
return false;
}
}
return true;
}

protected boolean executeRemote(ClientSession session, String command, OutputStream logger) {
try {
session.executeRemoteCommand(command, logger, logger, null);
return true;
} catch (IOException e) {
LOGGER.log(Level.FINE, "Failed to execute remote command: " + command, e);
return false;
}
}

protected File createIdentityKeyFile(EC2Computer computer) throws IOException {
EC2PrivateKey ec2PrivateKey = computer.getCloud().resolvePrivateKey();
String privateKey = "";
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,12 @@ protected void launchScript(EC2Computer computer, TaskListener listener)

logInfo(computer, listener, "Executing init script");
String initCommand = buildUpCommand(computer, tmpDir + "/init.sh");

// Check if collectInitScriptLogs flag is set
boolean collectInitScriptLogs = template.getCollectInitScriptLogs();

// Set the flag only when init script executed successfully.
if (executeRemote(clientSession, initCommand, logger)) {
if (executeRemote(clientSession, initCommand, logger, collectInitScriptLogs, listener)) {
log(
Level.FINE,
computer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ THE SOFTWARE.
<f:checkbox default="false"/>
</f:entry>

<f:entry title="${%Collect Init Script Logs}" field="collectInitScriptLogs">
<f:checkbox />
</f:entry>

</f:advanced>

<f:entry title="">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
When checked, the output (stdout and stderr) from the init script execution will be collected and displayed in the Jenkins build logs.
This can be useful for debugging init script issues, but may increase log verbosity.
<br/><br/>
<strong>Note:</strong> This only affects the logging of init script output. The init script will still execute regardless of this setting.
</div>
33 changes: 33 additions & 0 deletions src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,37 @@ void testWindowsSSHConfigAsCodeWithAltEndpointAndJavaPathExport(JenkinsConfigure
String expected = Util.toStringFromYamlFile(this, "WindowsSSHDataExport-withAltEndpointAndJavaPath.yml");
assertEquals(expected, exported);
}

@Test
@ConfiguredWithCode("UnixDataWithCollectInitScriptLogs.yml")
void testUnixDataWithInitLogs(JenkinsConfiguredWithCodeRule j) throws Exception {
ConfiguratorRegistry registry = ConfiguratorRegistry.get();
ConfigurationContext context = new ConfigurationContext(registry);
CNode clouds = Util.getJenkinsRoot(context).get("clouds");
String exported = Util.toYamlString(clouds);
String expected = Util.toStringFromYamlFile(this, "UnixDataExportWithCollectInitScriptLogs.yml");
assertEquals(expected, exported);
}

@Test
@ConfiguredWithCode("MacDataWithCollectInitScriptLogs.yml")
void testMacDataWithInitLogs(JenkinsConfiguredWithCodeRule j) throws Exception {
ConfiguratorRegistry registry = ConfiguratorRegistry.get();
ConfigurationContext context = new ConfigurationContext(registry);
CNode clouds = Util.getJenkinsRoot(context).get("clouds");
String exported = Util.toYamlString(clouds);
String expected = Util.toStringFromYamlFile(this, "MacDataExportWithCollectInitScriptLogs.yml");
assertEquals(expected, exported);
}

@Test
@ConfiguredWithCode("WindowsSSHDataWithCollectInitScriptLogs.yml")
void testWindowsDataWithInitLogs(JenkinsConfiguredWithCodeRule j) throws Exception {
ConfiguratorRegistry registry = ConfiguratorRegistry.get();
ConfigurationContext context = new ConfigurationContext(registry);
CNode clouds = Util.getJenkinsRoot(context).get("clouds");
String exported = Util.toYamlString(clouds);
String expected = Util.toStringFromYamlFile(this, "WindowsSSHDataExportWithCollectInitScriptLogs.yml");
assertEquals(expected, exported);
}
}
Loading
Loading