Skip to content

Commit c08dad8

Browse files
authored
Merge pull request #300 from jenkinsci/JENKINS-50392-exit-code
[JENKINS-50392] Get the exit code the correct way
2 parents 32ceb31 + 58ea43c commit c08dad8

File tree

3 files changed

+54
-71
lines changed

3 files changed

+54
-71
lines changed

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<!-- dependency versions -->
4747
<jenkins.version>2.32.1</jenkins.version>
4848

49-
<kubernetes-client.version>3.1.10</kubernetes-client.version>
49+
<kubernetes-client.version>3.2.0</kubernetes-client.version>
5050

5151
<!-- jenkins plugins versions -->
5252
<jenkins-basic-steps.version>2.3</jenkins-basic-steps.version>

src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/ContainerExecDecorator.java

+7-57
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,19 @@
1818

1919
import static org.csanchez.jenkins.plugins.kubernetes.pipeline.Constants.*;
2020

21+
import java.io.ByteArrayOutputStream;
2122
import java.io.Closeable;
2223
import java.io.IOException;
2324
import java.io.InterruptedIOException;
2425
import java.io.OutputStream;
2526
import java.io.PrintStream;
2627
import java.io.Serializable;
27-
import java.nio.ByteBuffer;
2828
import java.nio.charset.StandardCharsets;
2929
import java.util.ArrayList;
3030
import java.util.Arrays;
3131
import java.util.HashMap;
3232
import java.util.List;
3333
import java.util.Map;
34-
import java.util.Objects;
3534
import java.util.concurrent.CountDownLatch;
3635
import java.util.concurrent.TimeUnit;
3736
import java.util.concurrent.atomic.AtomicBoolean;
@@ -268,22 +267,18 @@ private Proc doLaunch(boolean quiet, String[] cmdEnvs, OutputStream outputForCal
268267
printStream = new PrintStream(stream, false, StandardCharsets.UTF_8.toString());
269268
}
270269

271-
// we need to keep the last bytes in the stream to parse the exit code as it is printed there
272-
// so we use a buffer
273-
ExitCodeOutputStream exitCodeOutputStream = new ExitCodeOutputStream();
274-
// send container output to all 3 streams (pid, out, job).
275-
stream = new TeeOutputStream(exitCodeOutputStream, stream);
276270
// Send to proc caller as well if they sent one
277271
if (outputForCaller != null) {
278272
stream = new TeeOutputStream(outputForCaller, stream);
279273
}
274+
ByteArrayOutputStream error = new ByteArrayOutputStream();
280275

281276
String msg = "Executing shell script inside container [" + containerName + "] of pod [" + podName + "]";
282277
LOGGER.log(Level.FINEST, msg);
283278
printStream.println(msg);
284279

285280
Execable<String, ExecWatch> execable = client.pods().inNamespace(namespace).withName(podName).inContainer(containerName)
286-
.redirectingInput().writingOutput(stream).writingError(stream)
281+
.redirectingInput().writingOutput(stream).writingError(stream).writingErrorChannel(error)
287282
.usingListener(new ExecListener() {
288283
@Override
289284
public void onOpen(Response response) {
@@ -374,7 +369,7 @@ public void onClose(int i, String s) {
374369

375370
int pid = readPidFromPidFile(commands);
376371
LOGGER.log(Level.INFO, "Created process inside pod: ["+podName+"], container: ["+containerName+"] with pid:["+pid+"]");
377-
ContainerExecProc proc = new ContainerExecProc(watch, alive, finished, exitCodeOutputStream::getExitCode);
372+
ContainerExecProc proc = new ContainerExecProc(watch, alive, finished, error);
378373
processes.put(pid, proc);
379374
closables.add(proc);
380375
return proc;
@@ -481,10 +476,10 @@ private static void doExec(ExecWatch watch, PrintStream out, boolean[] masks, St
481476

482477
// get the command exit code and print it padded so it is easier to parse in ContainerExecProc
483478
// We need to exit so that we know when the command has finished.
484-
sb.append(ExitCodeOutputStream.EXIT_COMMAND);
485-
out.print(ExitCodeOutputStream.EXIT_COMMAND);
479+
sb.append(EXIT + NEWLINE);
480+
out.print(EXIT + NEWLINE);
486481
LOGGER.log(Level.FINEST, "Executing command: {0}", sb);
487-
watch.getInput().write(ExitCodeOutputStream.EXIT_COMMAND.getBytes(StandardCharsets.UTF_8));
482+
watch.getInput().write((EXIT + NEWLINE).getBytes(StandardCharsets.UTF_8));
488483

489484
out.flush();
490485
watch.getInput().flush();
@@ -569,49 +564,4 @@ private static void closeWatch(ExecWatch watch) {
569564
public void setKubernetesClient(KubernetesClient client) {
570565
this.client = client;
571566
}
572-
573-
/**
574-
* Keeps the last bytes of the output stream to parse the exit code
575-
*/
576-
static class ExitCodeOutputStream extends OutputStream {
577-
578-
public static final String EXIT_COMMAND_TXT = "EXITCODE";
579-
public static final String EXIT_COMMAND = "printf \"" + EXIT_COMMAND_TXT + " %3d\" $?; " + EXIT + NEWLINE;
580-
581-
private EvictingQueue<Integer> queue = EvictingQueue.create(20);
582-
583-
public ExitCodeOutputStream() {
584-
}
585-
586-
@Override
587-
public void write(int b) throws IOException {
588-
queue.add(b);
589-
byte[] bb = new byte[]{(byte) b};
590-
System.out.print(new String(bb, StandardCharsets.UTF_8));
591-
}
592-
593-
public int getExitCode() {
594-
ByteBuffer b = ByteBuffer.allocate(queue.size());
595-
queue.stream().filter(Objects::nonNull).forEach((i) -> b.put((byte) i.intValue()));
596-
// output ends in a 3 digit padded exit code + newline (13 10)
597-
// as defined in ContainerExecDecorator#doExec
598-
// ie. 32 32 49 13 10 for exit code 1
599-
int i = 1;
600-
String s = new String(b.array(), StandardCharsets.UTF_8);
601-
if (s.indexOf(EXIT_COMMAND_TXT) < 0) {
602-
LOGGER.log(Level.WARNING, "Unable to find \"{0}\" in {1}", new Object[]{EXIT_COMMAND_TXT, s});
603-
return i;
604-
}
605-
// parse the exitcode int printed after EXITCODE
606-
int start = s.indexOf(EXIT_COMMAND_TXT) + EXIT_COMMAND_TXT.length();
607-
s = s.substring(start, start + 4).trim();
608-
try {
609-
i = Integer.parseInt(s);
610-
} catch (NumberFormatException e) {
611-
LOGGER.log(Level.WARNING, "Unable to parse exit code as integer: \"{0}\" {1} / {2}",
612-
new Object[]{s, queue.toString(), Arrays.toString(b.array())});
613-
}
614-
return i;
615-
}
616-
}
617567
}

src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/ContainerExecProc.java

+46-13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.csanchez.jenkins.plugins.kubernetes.pipeline.Constants.*;
44

5+
import java.io.ByteArrayOutputStream;
56
import java.io.Closeable;
67
import java.io.IOException;
78
import java.io.InputStream;
@@ -13,6 +14,9 @@
1314
import java.util.logging.Level;
1415
import java.util.logging.Logger;
1516

17+
import com.fasterxml.jackson.databind.JsonNode;
18+
import com.fasterxml.jackson.databind.ObjectMapper;
19+
1620
import hudson.Proc;
1721
import io.fabric8.kubernetes.client.dsl.ExecWatch;
1822

@@ -27,22 +31,20 @@ public class ContainerExecProc extends Proc implements Closeable {
2731
private final AtomicBoolean alive;
2832
private final CountDownLatch finished;
2933
private final ExecWatch watch;
30-
private final Callable<Integer> exitCode;
31-
32-
/**
33-
*
34-
* @param watch
35-
* @param alive
36-
* @param finished
37-
* @param exitCode
38-
* a way to get the exit code
39-
*/
34+
private final ByteArrayOutputStream error;
35+
36+
@Deprecated
4037
public ContainerExecProc(ExecWatch watch, AtomicBoolean alive, CountDownLatch finished,
4138
Callable<Integer> exitCode) {
39+
this(watch, alive, finished, new ByteArrayOutputStream());
40+
}
41+
42+
public ContainerExecProc(ExecWatch watch, AtomicBoolean alive, CountDownLatch finished,
43+
ByteArrayOutputStream error) {
4244
this.watch = watch;
4345
this.alive = alive;
4446
this.finished = finished;
45-
this.exitCode = exitCode;
47+
this.error = error;
4648
}
4749

4850
@Override
@@ -71,7 +73,38 @@ public int join() throws IOException, InterruptedException {
7173
LOGGER.log(Level.FINEST, "Waiting for websocket to close on command finish ({0})", finished);
7274
finished.await();
7375
LOGGER.log(Level.FINEST, "Command is finished ({0})", finished);
74-
return exitCode.call();
76+
if (error.size() == 0) {
77+
return 0;
78+
} else {
79+
// {"metadata":{},"status":"Success"}
80+
// or
81+
// {"metadata":{},"status":"Failure",
82+
// "message":"command terminated with non-zero exit code: Error executing in Docker Container: 127",
83+
// "reason":"NonZeroExitCode",
84+
// "details":{"causes":[{"reason":"ExitCode","message":"127"}]}}
85+
try {
86+
ObjectMapper mapper = new ObjectMapper();
87+
JsonNode errorJson = mapper.readTree(error.toByteArray());
88+
if ("Success".equalsIgnoreCase(errorJson.get("status").asText())) {
89+
return 0;
90+
}
91+
JsonNode causes = errorJson.get("details").get("causes");
92+
if (causes.isArray()) {
93+
for (JsonNode cause : causes) {
94+
if ("ExitCode".equalsIgnoreCase(cause.get("reason").asText(""))) {
95+
return cause.get("message").asInt();
96+
}
97+
}
98+
}
99+
LOGGER.log(Level.WARNING, "Unable to parse exit code from error message: {0}",
100+
error.toString(StandardCharsets.UTF_8.name()));
101+
return -1;
102+
} catch (IOException e) {
103+
LOGGER.log(Level.WARNING, "Unable to parse exit code from error message: "
104+
+ error.toString(StandardCharsets.UTF_8.name()), e);
105+
return -1;
106+
}
107+
}
75108
} catch (Exception e) {
76109
LOGGER.log(Level.WARNING, "Error getting exit code", e);
77110
return -1;
@@ -98,7 +131,7 @@ public OutputStream getStdin() {
98131
@Override
99132
public void close() throws IOException {
100133
try {
101-
//We are calling explicitly close, in order to cleanup websockets and threads (are not closed implicitly).
134+
// We are calling explicitly close, in order to cleanup websockets and threads (are not closed implicitly).
102135
watch.close();
103136
} catch (Exception e) {
104137
LOGGER.log(Level.INFO, "failed to close watch", e);

0 commit comments

Comments
 (0)