Skip to content

Commit

Permalink
filter exit code from command output
Browse files Browse the repository at this point in the history
filter exit code from stdout on join

removed lefover public

sleep 5

sleep on ssh-add and ssh-agent
  • Loading branch information
balihb committed Nov 14, 2017
1 parent 8900cb1 commit c9c8db7
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.csanchez.jenkins.plugins.kubernetes.pipeline;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

import org.apache.commons.lang.ArrayUtils;

public class ContainerExecCutExitCodeUtil {
private static byte SPACE = " ".getBytes(StandardCharsets.UTF_8)[0];

public static byte[] getPartToWriteOut(byte[] q, byte[] exitCommandTxt) {
int exitCodeStartIndex = getExitCodeIndex(q, exitCommandTxt);
int endIndex = exitCodeStartIndex != -1 ? exitCodeStartIndex : q.length;
return partToWriteOut(q, endIndex);
}

private static byte[] partToWriteOut(byte[] q, int endIndex) {
return ArrayUtils.subarray(q, 0, endIndex);
}

private static int getExitCodeIndex(byte[] q, byte[] exitCommandTxt) {
if (!endsWithNumber(q)) {
return -1;
} else {
int numbersStartIndex = getNumbersStartIndex(q);
return getExitCodeStartIndex(q, numbersStartIndex, exitCommandTxt);
}
}

private static int getExitCodeStartIndex(byte[] q, int numbersStartIndex, byte[] exitCommandTxt) {
int possibleStartIndex = getPossibleStartIndex(q, numbersStartIndex, exitCommandTxt);
byte[] exitCodeTxtSubarray = getExitCodeTxtSubarray(q, possibleStartIndex, numbersStartIndex);
if (Arrays.equals(exitCodeTxtSubarray, exitCommandTxt)) {
return possibleStartIndex;
}
return -1;
}

private static int getPossibleStartIndex(byte[] q, int numbersStartIndex, byte[] exitCommandTxt) {
return q.length - (q.length - numbersStartIndex) - exitCommandTxt.length;
}

private static byte[] getExitCodeTxtSubarray(byte[] q, int possibleStartIndex, int numbersStartIndex) {
return ArrayUtils.subarray(q, possibleStartIndex, numbersStartIndex);
}

private static int getNumbersStartIndex(byte[] ba) {
int i = ba.length - 1;
for (; isNumber(ba[i]); i--)
;
for (; ba[i] == SPACE; i--)
;
return ++i;
}

private static boolean endsWithNumber(byte[] ba) {
return isNumber(ba[ba.length - 1]);
}

private static boolean isNumber(byte b) {
String s = new String(new byte[] { (byte) b }, StandardCharsets.UTF_8);
try {
Integer.parseInt(s);
} catch (NumberFormatException e) {
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
Expand All @@ -42,6 +44,7 @@
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.fabric8.kubernetes.client.KubernetesClientTimeoutException;
import org.apache.commons.io.output.TeeOutputStream;
import org.apache.commons.lang.ArrayUtils;

import com.google.common.io.NullOutputStream;

Expand Down Expand Up @@ -124,21 +127,22 @@ public Proc launch(ProcStarter starter) throws IOException {
for (String cmd : cmdEnvs) {
if (cmd.startsWith(JENKINS_HOME)) {
cmdEnvs = new String[0];
LOGGER.info("Skipping injection of procstarter cmdenvs due to JENKINS_HOME present");
LOGGER.fine("Skipping injection of procstarter cmdenvs due to JENKINS_HOME present");
break;
}
}
String [] commands = getCommands(starter);
return doLaunch(quiet, cmdEnvs, starter.stdout(), pwd, commands);
return doLaunch(quiet, cmdEnvs, starter.stdout(), starter.stderr(), pwd, commands);
}

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

final CountDownLatch started = new CountDownLatch(1);
final CountDownLatch finished = new CountDownLatch(1);
final AtomicBoolean alive = new AtomicBoolean(false);

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

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

stream = new FilterOutExitCodeOutputStream(stream, streamsToFilter);

// we need to keep the last bytes in the stream to parse the exit code as it is printed there
// so we use a buffer
ExitCodeOutputStream exitCodeOutputStream = new ExitCodeOutputStream();
// send container output both to the job output and our buffer
stream = new TeeOutputStream(exitCodeOutputStream, stream);

// don't throw away error, but don't let it interrupt the parsing of output
OutputStream errorStream = stream;
if(errorForCaller != null) {
errorStream = new TeeOutputStream(errorForCaller, stream);
}

// Send to proc caller as well if they sent one
if (outputForCaller != null) {
stream = new TeeOutputStream(outputForCaller, stream);
stream = new TeeOutputStream(new FilterOutExitCodeOutputStream(outputForCaller, streamsToFilter), stream);
}

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

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

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

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

Expand Down Expand Up @@ -322,32 +335,37 @@ public void close() throws IOException {
}
}

private static void doExec(ExecWatch watch, PrintStream out, String... statements) {
private static void doExec(ExecWatch watch, PrintStream out, String... statements) throws IOException {
try {
out.print("Executing command: ");
LOGGER.log(Level.FINE, "Executing command: ");
StringBuilder sb = new StringBuilder();
for (String stmt : statements) {
String s = String.format("\"%s\" ", stmt);
sb.append(s);
out.print(s);
LOGGER.log(Level.FINE, s);
watch.getInput().write(s.getBytes(StandardCharsets.UTF_8));
}
sb.append(NEWLINE);
out.println();
watch.getInput().write(NEWLINE.getBytes(StandardCharsets.UTF_8));

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

out.flush();
watch.getInput().flush();
} catch (IOException e) {
e.printStackTrace(out);
throw new RuntimeException(e);
LOGGER.log(Level.WARNING, "IOException during executing command", e);
throw e;
}
}

Expand Down Expand Up @@ -394,8 +412,6 @@ public ExitCodeOutputStream() {
@Override
public void write(int b) throws IOException {
queue.add(b);
byte[] bb = new byte[]{(byte) b};
System.out.print(new String(bb, StandardCharsets.UTF_8));
}

public int getExitCode() {
Expand All @@ -422,4 +438,37 @@ public int getExitCode() {
return i;
}
}

static class FilterOutExitCodeOutputStream extends OutputStream {

public FilterOutExitCodeOutputStream(OutputStream sink, List<FilterOutExitCodeOutputStream> streamsToFilter) {
this.sink = sink;
streamsToFilter.add(this);
}

public final static byte[] EXIT_COMMAND_TXT_BYTES;

private final static int QUEUE_SIZE = 20;
private final OutputStream sink;
private final Queue<Byte> queue = new ArrayDeque<Byte>(QUEUE_SIZE);

static {
byte[] newLine = new byte[1];
Arrays.fill(newLine, "\n".getBytes(StandardCharsets.UTF_8)[0]);
EXIT_COMMAND_TXT_BYTES = ArrayUtils.addAll(newLine, ExitCodeOutputStream.EXIT_COMMAND_TXT.getBytes(StandardCharsets.UTF_8));
}

@Override
public void write(int b) throws IOException {
if (queue.size() >= QUEUE_SIZE)
sink.write(queue.poll());
queue.offer((byte) b);
}

public void writeOutBuffer() throws IOException {
byte[] q = ArrayUtils.toPrimitive(queue.toArray(new Byte[queue.size()]));
byte[] partToWriteOut = ContainerExecCutExitCodeUtil.getPartToWriteOut(q, EXIT_COMMAND_TXT_BYTES);
sink.write(partToWriteOut);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.csanchez.jenkins.plugins.kubernetes.pipeline.ContainerExecDecorator.FilterOutExitCodeOutputStream;

import hudson.Proc;
import io.fabric8.kubernetes.client.dsl.ExecWatch;

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

/**
*
Expand All @@ -38,11 +42,12 @@ public class ContainerExecProc extends Proc implements Closeable {
* a way to get the exit code
*/
public ContainerExecProc(ExecWatch watch, AtomicBoolean alive, CountDownLatch finished,
Callable<Integer> exitCode) {
Callable<Integer> exitCode, List<FilterOutExitCodeOutputStream> streamsToFilter) {
this.watch = watch;
this.alive = alive;
this.finished = finished;
this.exitCode = exitCode;
this.streamsToFilter = streamsToFilter;
}

@Override
Expand Down Expand Up @@ -71,6 +76,9 @@ public int join() throws IOException, InterruptedException {
LOGGER.log(Level.FINEST, "Waiting for websocket to close on command finish ({0})", finished);
finished.await();
LOGGER.log(Level.FINEST, "Command is finished ({0})", finished);
for(FilterOutExitCodeOutputStream stream : streamsToFilter) {
stream.writeOutBuffer();
}
return exitCode.call();
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error getting exit code", e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.csanchez.jenkins.plugins.kubernetes.pipeline;

import static org.csanchez.jenkins.plugins.kubernetes.pipeline.ContainerExecCutExitCodeUtil.getPartToWriteOut;
import static org.csanchez.jenkins.plugins.kubernetes.pipeline.ContainerExecDecorator.FilterOutExitCodeOutputStream.EXIT_COMMAND_TXT_BYTES;
import static org.junit.Assert.assertEquals;

import java.nio.charset.StandardCharsets;

import org.junit.Test;

public class ContainerExecCutExitCodeUtilTest {

final static String EXITCODE_GOOD = "\nEXITCODE 0";
final static String EXITCODE_ERR = "\nEXITCODE 127";
final static String EXITCODE_BAD_NONUM = "\nEXITCODE ";
final static String LONG_STR = "this is a very long string. followd by something. or not. who knows?";
final static String SHORT_STR = "a";
final static byte[] EMPTY = "".getBytes(StandardCharsets.UTF_8);
final static byte[] GOOD_LONG = (LONG_STR + EXITCODE_GOOD).getBytes(StandardCharsets.UTF_8);
final static byte[] GOOD_SHORT = (SHORT_STR + EXITCODE_GOOD).getBytes(StandardCharsets.UTF_8);
final static byte[] BAD_LONG = (LONG_STR + EXITCODE_BAD_NONUM).getBytes(StandardCharsets.UTF_8);
final static byte[] BAD_SHORT = (SHORT_STR + EXITCODE_BAD_NONUM).getBytes(StandardCharsets.UTF_8);
final static byte[] BAD_LONG_NOEXIT = LONG_STR.getBytes(StandardCharsets.UTF_8);
final static byte[] BAD_SHORT_NOEXIT = SHORT_STR.getBytes(StandardCharsets.UTF_8);
final static byte[] ERR_LONG = (LONG_STR + EXITCODE_ERR).getBytes(StandardCharsets.UTF_8);
final static byte[] ERR_SHORT = (SHORT_STR + EXITCODE_ERR).getBytes(StandardCharsets.UTF_8);

@Test
public void testGoodLong() {
byte[] p = getPartToWriteOut(GOOD_LONG, EXIT_COMMAND_TXT_BYTES);
String s = new String(p, StandardCharsets.UTF_8);
assertEquals(LONG_STR, s);
}

@Test
public void testGoodShort() {
byte[] p = getPartToWriteOut(GOOD_SHORT, EXIT_COMMAND_TXT_BYTES);
String s = new String(p, StandardCharsets.UTF_8);
assertEquals(SHORT_STR, s);
}

@Test
public void testErrLong() {
byte[] p = getPartToWriteOut(ERR_LONG, EXIT_COMMAND_TXT_BYTES);
String s = new String(p, StandardCharsets.UTF_8);
assertEquals(LONG_STR, s);
}

@Test
public void testErrShort() {
byte[] p = getPartToWriteOut(ERR_SHORT, EXIT_COMMAND_TXT_BYTES);
String s = new String(p, StandardCharsets.UTF_8);
assertEquals(SHORT_STR, s);
}

@Test
public void testBadShort() {
byte[] p = getPartToWriteOut(BAD_SHORT, EXIT_COMMAND_TXT_BYTES);
String s = new String(p, StandardCharsets.UTF_8);
assertEquals(new String(BAD_SHORT, StandardCharsets.UTF_8), s);
}

@Test
public void testBadLong() {
byte[] p = getPartToWriteOut(BAD_LONG, EXIT_COMMAND_TXT_BYTES);
String s = new String(p, StandardCharsets.UTF_8);
assertEquals(new String(BAD_LONG, StandardCharsets.UTF_8), s);
}

@Test
public void testBadShortNoExit() {
byte[] p = getPartToWriteOut(BAD_SHORT_NOEXIT, EXIT_COMMAND_TXT_BYTES);
String s = new String(p, StandardCharsets.UTF_8);
assertEquals(new String(BAD_SHORT_NOEXIT, StandardCharsets.UTF_8), s);
}

@Test
public void testBadLongNoExit() {
byte[] p = getPartToWriteOut(BAD_LONG_NOEXIT, EXIT_COMMAND_TXT_BYTES);
String s = new String(p, StandardCharsets.UTF_8);
assertEquals(new String(BAD_LONG_NOEXIT, StandardCharsets.UTF_8), s);
}
}
Loading

0 comments on commit c9c8db7

Please sign in to comment.