Skip to content

Commit c9c8db7

Browse files
committed
filter exit code from command output
filter exit code from stdout on join removed lefover public sleep 5 sleep on ssh-add and ssh-agent
1 parent 8900cb1 commit c9c8db7

File tree

5 files changed

+259
-22
lines changed

5 files changed

+259
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.csanchez.jenkins.plugins.kubernetes.pipeline;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.util.Arrays;
5+
6+
import org.apache.commons.lang.ArrayUtils;
7+
8+
public class ContainerExecCutExitCodeUtil {
9+
private static byte SPACE = " ".getBytes(StandardCharsets.UTF_8)[0];
10+
11+
public static byte[] getPartToWriteOut(byte[] q, byte[] exitCommandTxt) {
12+
int exitCodeStartIndex = getExitCodeIndex(q, exitCommandTxt);
13+
int endIndex = exitCodeStartIndex != -1 ? exitCodeStartIndex : q.length;
14+
return partToWriteOut(q, endIndex);
15+
}
16+
17+
private static byte[] partToWriteOut(byte[] q, int endIndex) {
18+
return ArrayUtils.subarray(q, 0, endIndex);
19+
}
20+
21+
private static int getExitCodeIndex(byte[] q, byte[] exitCommandTxt) {
22+
if (!endsWithNumber(q)) {
23+
return -1;
24+
} else {
25+
int numbersStartIndex = getNumbersStartIndex(q);
26+
return getExitCodeStartIndex(q, numbersStartIndex, exitCommandTxt);
27+
}
28+
}
29+
30+
private static int getExitCodeStartIndex(byte[] q, int numbersStartIndex, byte[] exitCommandTxt) {
31+
int possibleStartIndex = getPossibleStartIndex(q, numbersStartIndex, exitCommandTxt);
32+
byte[] exitCodeTxtSubarray = getExitCodeTxtSubarray(q, possibleStartIndex, numbersStartIndex);
33+
if (Arrays.equals(exitCodeTxtSubarray, exitCommandTxt)) {
34+
return possibleStartIndex;
35+
}
36+
return -1;
37+
}
38+
39+
private static int getPossibleStartIndex(byte[] q, int numbersStartIndex, byte[] exitCommandTxt) {
40+
return q.length - (q.length - numbersStartIndex) - exitCommandTxt.length;
41+
}
42+
43+
private static byte[] getExitCodeTxtSubarray(byte[] q, int possibleStartIndex, int numbersStartIndex) {
44+
return ArrayUtils.subarray(q, possibleStartIndex, numbersStartIndex);
45+
}
46+
47+
private static int getNumbersStartIndex(byte[] ba) {
48+
int i = ba.length - 1;
49+
for (; isNumber(ba[i]); i--)
50+
;
51+
for (; ba[i] == SPACE; i--)
52+
;
53+
return ++i;
54+
}
55+
56+
private static boolean endsWithNumber(byte[] ba) {
57+
return isNumber(ba[ba.length - 1]);
58+
}
59+
60+
private static boolean isNumber(byte b) {
61+
String s = new String(new byte[] { (byte) b }, StandardCharsets.UTF_8);
62+
try {
63+
Integer.parseInt(s);
64+
} catch (NumberFormatException e) {
65+
return false;
66+
}
67+
return true;
68+
}
69+
}

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

+67-18
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
import java.io.Serializable;
2727
import java.nio.ByteBuffer;
2828
import java.nio.charset.StandardCharsets;
29+
import java.util.ArrayDeque;
2930
import java.util.ArrayList;
3031
import java.util.Arrays;
3132
import java.util.List;
3233
import java.util.Map;
3334
import java.util.Objects;
35+
import java.util.Queue;
3436
import java.util.concurrent.CountDownLatch;
3537
import java.util.concurrent.TimeUnit;
3638
import java.util.concurrent.atomic.AtomicBoolean;
@@ -42,6 +44,7 @@
4244
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4345
import io.fabric8.kubernetes.client.KubernetesClientTimeoutException;
4446
import org.apache.commons.io.output.TeeOutputStream;
47+
import org.apache.commons.lang.ArrayUtils;
4548

4649
import com.google.common.io.NullOutputStream;
4750

@@ -124,21 +127,22 @@ public Proc launch(ProcStarter starter) throws IOException {
124127
for (String cmd : cmdEnvs) {
125128
if (cmd.startsWith(JENKINS_HOME)) {
126129
cmdEnvs = new String[0];
127-
LOGGER.info("Skipping injection of procstarter cmdenvs due to JENKINS_HOME present");
130+
LOGGER.fine("Skipping injection of procstarter cmdenvs due to JENKINS_HOME present");
128131
break;
129132
}
130133
}
131134
String [] commands = getCommands(starter);
132-
return doLaunch(quiet, cmdEnvs, starter.stdout(), pwd, commands);
135+
return doLaunch(quiet, cmdEnvs, starter.stdout(), starter.stderr(), pwd, commands);
133136
}
134137

135-
private Proc doLaunch(boolean quiet, String [] cmdEnvs, OutputStream outputForCaller, FilePath pwd, String... commands) throws IOException {
138+
private Proc doLaunch(boolean quiet, String [] cmdEnvs, OutputStream outputForCaller, OutputStream errorForCaller, FilePath pwd, String... commands) throws IOException {
136139
waitUntilContainerIsReady();
137140

138141
final CountDownLatch started = new CountDownLatch(1);
139142
final CountDownLatch finished = new CountDownLatch(1);
140143
final AtomicBoolean alive = new AtomicBoolean(false);
141144

145+
final List<FilterOutExitCodeOutputStream> streamsToFilter = new ArrayList<FilterOutExitCodeOutputStream>(5);
142146

143147
PrintStream printStream = launcher.getListener().getLogger();
144148
OutputStream stream = printStream;
@@ -148,22 +152,31 @@ private Proc doLaunch(boolean quiet, String [] cmdEnvs, OutputStream outputForC
148152
printStream = new PrintStream(stream, false, StandardCharsets.UTF_8.toString());
149153
}
150154

155+
stream = new FilterOutExitCodeOutputStream(stream, streamsToFilter);
156+
151157
// we need to keep the last bytes in the stream to parse the exit code as it is printed there
152158
// so we use a buffer
153159
ExitCodeOutputStream exitCodeOutputStream = new ExitCodeOutputStream();
154160
// send container output both to the job output and our buffer
155161
stream = new TeeOutputStream(exitCodeOutputStream, stream);
162+
163+
// don't throw away error, but don't let it interrupt the parsing of output
164+
OutputStream errorStream = stream;
165+
if(errorForCaller != null) {
166+
errorStream = new TeeOutputStream(errorForCaller, stream);
167+
}
168+
156169
// Send to proc caller as well if they sent one
157170
if (outputForCaller != null) {
158-
stream = new TeeOutputStream(outputForCaller, stream);
171+
stream = new TeeOutputStream(new FilterOutExitCodeOutputStream(outputForCaller, streamsToFilter), stream);
159172
}
160173

161174
String msg = "Executing shell script inside container [" + containerName + "] of pod [" + podName + "]";
162175
LOGGER.log(Level.FINEST, msg);
163176
printStream.println(msg);
164177

165178
Execable<String, ExecWatch> execable = client.pods().inNamespace(namespace).withName(podName).inContainer(containerName)
166-
.redirectingInput().writingOutput(stream).writingError(stream)
179+
.redirectingInput().writingOutput(stream).writingError(errorStream)
167180
.usingListener(new ExecListener() {
168181
@Override
169182
public void onOpen(Response response) {
@@ -238,7 +251,7 @@ public void onClose(int i, String s) {
238251

239252
this.setupEnvironmentVariable(envVars, watch);
240253
doExec(watch, printStream, commands);
241-
ContainerExecProc proc = new ContainerExecProc(watch, alive, finished, exitCodeOutputStream::getExitCode);
254+
ContainerExecProc proc = new ContainerExecProc(watch, alive, finished, exitCodeOutputStream::getExitCode, streamsToFilter);
242255
closables.add(proc);
243256
return proc;
244257
} catch (InterruptedException ie) {
@@ -256,7 +269,7 @@ public void kill(Map<String, String> modelEnvVars) throws IOException, Interrupt
256269
String cookie = modelEnvVars.get(COOKIE_VAR);
257270

258271
int exitCode = doLaunch(
259-
true, null, null, null,
272+
true, null, null, null, null,
260273
"sh", "-c", "kill \\`grep -l '" + COOKIE_VAR + "=" + cookie +"' /proc/*/environ | cut -d / -f 3 \\`"
261274
).join();
262275

@@ -322,32 +335,37 @@ public void close() throws IOException {
322335
}
323336
}
324337

325-
private static void doExec(ExecWatch watch, PrintStream out, String... statements) {
338+
private static void doExec(ExecWatch watch, PrintStream out, String... statements) throws IOException {
326339
try {
327-
out.print("Executing command: ");
340+
LOGGER.log(Level.FINE, "Executing command: ");
328341
StringBuilder sb = new StringBuilder();
329342
for (String stmt : statements) {
330343
String s = String.format("\"%s\" ", stmt);
331344
sb.append(s);
332-
out.print(s);
345+
LOGGER.log(Level.FINE, s);
333346
watch.getInput().write(s.getBytes(StandardCharsets.UTF_8));
334347
}
335348
sb.append(NEWLINE);
336-
out.println();
337349
watch.getInput().write(NEWLINE.getBytes(StandardCharsets.UTF_8));
338350

339351
// get the command exit code and print it padded so it is easier to parse in ContainerExecProc
340352
// We need to exit so that we know when the command has finished.
341353
sb.append(ExitCodeOutputStream.EXIT_COMMAND);
342-
out.print(ExitCodeOutputStream.EXIT_COMMAND);
354+
LOGGER.log(Level.FINE, ExitCodeOutputStream.EXIT_COMMAND);
343355
LOGGER.log(Level.FINEST, "Executing command: {0}", sb);
344-
watch.getInput().write(ExitCodeOutputStream.EXIT_COMMAND.getBytes(StandardCharsets.UTF_8));
356+
// a hack only for sshagent. it writes out output after the exitcode has been printed if we don't wait a bit
357+
if ((statements.length >= 1 && statements[0].equals("ssh-add"))
358+
|| (statements.length == 2 && statements[0].equals("ssh-agent") && statements[1].equals("-k"))) {
359+
String sleepExitCommand = "tmp_exit_status=$?; sleep 3; printf \"" + ExitCodeOutputStream.EXIT_COMMAND_TXT + " %3d\" $tmp_exit_status; " + EXIT + NEWLINE;
360+
watch.getInput().write(sleepExitCommand.getBytes(StandardCharsets.UTF_8));
361+
} else {
362+
watch.getInput().write(ExitCodeOutputStream.EXIT_COMMAND.getBytes(StandardCharsets.UTF_8));
363+
}
345364

346-
out.flush();
347365
watch.getInput().flush();
348366
} catch (IOException e) {
349-
e.printStackTrace(out);
350-
throw new RuntimeException(e);
367+
LOGGER.log(Level.WARNING, "IOException during executing command", e);
368+
throw e;
351369
}
352370
}
353371

@@ -394,8 +412,6 @@ public ExitCodeOutputStream() {
394412
@Override
395413
public void write(int b) throws IOException {
396414
queue.add(b);
397-
byte[] bb = new byte[]{(byte) b};
398-
System.out.print(new String(bb, StandardCharsets.UTF_8));
399415
}
400416

401417
public int getExitCode() {
@@ -422,4 +438,37 @@ public int getExitCode() {
422438
return i;
423439
}
424440
}
441+
442+
static class FilterOutExitCodeOutputStream extends OutputStream {
443+
444+
public FilterOutExitCodeOutputStream(OutputStream sink, List<FilterOutExitCodeOutputStream> streamsToFilter) {
445+
this.sink = sink;
446+
streamsToFilter.add(this);
447+
}
448+
449+
public final static byte[] EXIT_COMMAND_TXT_BYTES;
450+
451+
private final static int QUEUE_SIZE = 20;
452+
private final OutputStream sink;
453+
private final Queue<Byte> queue = new ArrayDeque<Byte>(QUEUE_SIZE);
454+
455+
static {
456+
byte[] newLine = new byte[1];
457+
Arrays.fill(newLine, "\n".getBytes(StandardCharsets.UTF_8)[0]);
458+
EXIT_COMMAND_TXT_BYTES = ArrayUtils.addAll(newLine, ExitCodeOutputStream.EXIT_COMMAND_TXT.getBytes(StandardCharsets.UTF_8));
459+
}
460+
461+
@Override
462+
public void write(int b) throws IOException {
463+
if (queue.size() >= QUEUE_SIZE)
464+
sink.write(queue.poll());
465+
queue.offer((byte) b);
466+
}
467+
468+
public void writeOutBuffer() throws IOException {
469+
byte[] q = ArrayUtils.toPrimitive(queue.toArray(new Byte[queue.size()]));
470+
byte[] partToWriteOut = ContainerExecCutExitCodeUtil.getPartToWriteOut(q, EXIT_COMMAND_TXT_BYTES);
471+
sink.write(partToWriteOut);
472+
}
473+
}
425474
}

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
import java.io.InputStream;
88
import java.io.OutputStream;
99
import java.nio.charset.StandardCharsets;
10+
import java.util.List;
1011
import java.util.concurrent.Callable;
1112
import java.util.concurrent.CountDownLatch;
1213
import java.util.concurrent.atomic.AtomicBoolean;
1314
import java.util.logging.Level;
1415
import java.util.logging.Logger;
1516

17+
import org.csanchez.jenkins.plugins.kubernetes.pipeline.ContainerExecDecorator.FilterOutExitCodeOutputStream;
18+
1619
import hudson.Proc;
1720
import io.fabric8.kubernetes.client.dsl.ExecWatch;
1821

@@ -28,6 +31,7 @@ public class ContainerExecProc extends Proc implements Closeable {
2831
private final CountDownLatch finished;
2932
private final ExecWatch watch;
3033
private final Callable<Integer> exitCode;
34+
private final List<FilterOutExitCodeOutputStream> streamsToFilter;
3135

3236
/**
3337
*
@@ -38,11 +42,12 @@ public class ContainerExecProc extends Proc implements Closeable {
3842
* a way to get the exit code
3943
*/
4044
public ContainerExecProc(ExecWatch watch, AtomicBoolean alive, CountDownLatch finished,
41-
Callable<Integer> exitCode) {
45+
Callable<Integer> exitCode, List<FilterOutExitCodeOutputStream> streamsToFilter) {
4246
this.watch = watch;
4347
this.alive = alive;
4448
this.finished = finished;
4549
this.exitCode = exitCode;
50+
this.streamsToFilter = streamsToFilter;
4651
}
4752

4853
@Override
@@ -71,6 +76,9 @@ public int join() throws IOException, InterruptedException {
7176
LOGGER.log(Level.FINEST, "Waiting for websocket to close on command finish ({0})", finished);
7277
finished.await();
7378
LOGGER.log(Level.FINEST, "Command is finished ({0})", finished);
79+
for(FilterOutExitCodeOutputStream stream : streamsToFilter) {
80+
stream.writeOutBuffer();
81+
}
7482
return exitCode.call();
7583
} catch (Exception e) {
7684
LOGGER.log(Level.WARNING, "Error getting exit code", e);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.csanchez.jenkins.plugins.kubernetes.pipeline;
2+
3+
import static org.csanchez.jenkins.plugins.kubernetes.pipeline.ContainerExecCutExitCodeUtil.getPartToWriteOut;
4+
import static org.csanchez.jenkins.plugins.kubernetes.pipeline.ContainerExecDecorator.FilterOutExitCodeOutputStream.EXIT_COMMAND_TXT_BYTES;
5+
import static org.junit.Assert.assertEquals;
6+
7+
import java.nio.charset.StandardCharsets;
8+
9+
import org.junit.Test;
10+
11+
public class ContainerExecCutExitCodeUtilTest {
12+
13+
final static String EXITCODE_GOOD = "\nEXITCODE 0";
14+
final static String EXITCODE_ERR = "\nEXITCODE 127";
15+
final static String EXITCODE_BAD_NONUM = "\nEXITCODE ";
16+
final static String LONG_STR = "this is a very long string. followd by something. or not. who knows?";
17+
final static String SHORT_STR = "a";
18+
final static byte[] EMPTY = "".getBytes(StandardCharsets.UTF_8);
19+
final static byte[] GOOD_LONG = (LONG_STR + EXITCODE_GOOD).getBytes(StandardCharsets.UTF_8);
20+
final static byte[] GOOD_SHORT = (SHORT_STR + EXITCODE_GOOD).getBytes(StandardCharsets.UTF_8);
21+
final static byte[] BAD_LONG = (LONG_STR + EXITCODE_BAD_NONUM).getBytes(StandardCharsets.UTF_8);
22+
final static byte[] BAD_SHORT = (SHORT_STR + EXITCODE_BAD_NONUM).getBytes(StandardCharsets.UTF_8);
23+
final static byte[] BAD_LONG_NOEXIT = LONG_STR.getBytes(StandardCharsets.UTF_8);
24+
final static byte[] BAD_SHORT_NOEXIT = SHORT_STR.getBytes(StandardCharsets.UTF_8);
25+
final static byte[] ERR_LONG = (LONG_STR + EXITCODE_ERR).getBytes(StandardCharsets.UTF_8);
26+
final static byte[] ERR_SHORT = (SHORT_STR + EXITCODE_ERR).getBytes(StandardCharsets.UTF_8);
27+
28+
@Test
29+
public void testGoodLong() {
30+
byte[] p = getPartToWriteOut(GOOD_LONG, EXIT_COMMAND_TXT_BYTES);
31+
String s = new String(p, StandardCharsets.UTF_8);
32+
assertEquals(LONG_STR, s);
33+
}
34+
35+
@Test
36+
public void testGoodShort() {
37+
byte[] p = getPartToWriteOut(GOOD_SHORT, EXIT_COMMAND_TXT_BYTES);
38+
String s = new String(p, StandardCharsets.UTF_8);
39+
assertEquals(SHORT_STR, s);
40+
}
41+
42+
@Test
43+
public void testErrLong() {
44+
byte[] p = getPartToWriteOut(ERR_LONG, EXIT_COMMAND_TXT_BYTES);
45+
String s = new String(p, StandardCharsets.UTF_8);
46+
assertEquals(LONG_STR, s);
47+
}
48+
49+
@Test
50+
public void testErrShort() {
51+
byte[] p = getPartToWriteOut(ERR_SHORT, EXIT_COMMAND_TXT_BYTES);
52+
String s = new String(p, StandardCharsets.UTF_8);
53+
assertEquals(SHORT_STR, s);
54+
}
55+
56+
@Test
57+
public void testBadShort() {
58+
byte[] p = getPartToWriteOut(BAD_SHORT, EXIT_COMMAND_TXT_BYTES);
59+
String s = new String(p, StandardCharsets.UTF_8);
60+
assertEquals(new String(BAD_SHORT, StandardCharsets.UTF_8), s);
61+
}
62+
63+
@Test
64+
public void testBadLong() {
65+
byte[] p = getPartToWriteOut(BAD_LONG, EXIT_COMMAND_TXT_BYTES);
66+
String s = new String(p, StandardCharsets.UTF_8);
67+
assertEquals(new String(BAD_LONG, StandardCharsets.UTF_8), s);
68+
}
69+
70+
@Test
71+
public void testBadShortNoExit() {
72+
byte[] p = getPartToWriteOut(BAD_SHORT_NOEXIT, EXIT_COMMAND_TXT_BYTES);
73+
String s = new String(p, StandardCharsets.UTF_8);
74+
assertEquals(new String(BAD_SHORT_NOEXIT, StandardCharsets.UTF_8), s);
75+
}
76+
77+
@Test
78+
public void testBadLongNoExit() {
79+
byte[] p = getPartToWriteOut(BAD_LONG_NOEXIT, EXIT_COMMAND_TXT_BYTES);
80+
String s = new String(p, StandardCharsets.UTF_8);
81+
assertEquals(new String(BAD_LONG_NOEXIT, StandardCharsets.UTF_8), s);
82+
}
83+
}

0 commit comments

Comments
 (0)