|
16 | 16 |
|
17 | 17 | import com.google.common.io.CharStreams;
|
18 | 18 | import com.google.gson.reflect.TypeToken;
|
| 19 | +import io.kubernetes.client.custom.IOTrio; |
19 | 20 | import io.kubernetes.client.openapi.ApiClient;
|
20 | 21 | import io.kubernetes.client.openapi.ApiException;
|
21 | 22 | import io.kubernetes.client.openapi.Configuration;
|
|
34 | 35 | import java.io.UnsupportedEncodingException;
|
35 | 36 | import java.lang.reflect.Type;
|
36 | 37 | import java.net.URLEncoder;
|
| 38 | +import java.util.Arrays; |
37 | 39 | import java.util.HashMap;
|
38 | 40 | import java.util.List;
|
39 | 41 | import java.util.Map;
|
| 42 | +import java.util.concurrent.CompletableFuture; |
40 | 43 | import java.util.concurrent.CountDownLatch;
|
| 44 | +import java.util.concurrent.Future; |
41 | 45 | import java.util.concurrent.TimeUnit;
|
| 46 | +import java.util.function.BiConsumer; |
| 47 | +import java.util.function.Consumer; |
| 48 | +import java.util.function.Supplier; |
42 | 49 | import org.apache.commons.lang3.StringUtils;
|
43 | 50 | import org.slf4j.Logger;
|
44 | 51 | import org.slf4j.LoggerFactory;
|
@@ -191,6 +198,129 @@ public Process exec(
|
191 | 198 | .execute();
|
192 | 199 | }
|
193 | 200 |
|
| 201 | + /** |
| 202 | + * A convenience method. Executes a command remotely on a pod and monitors for events in that |
| 203 | + * execution. The monitored events are: <br> |
| 204 | + * - connection established (onOpen) <br> |
| 205 | + * - connection closed (onClosed) <br> |
| 206 | + * - execution error occurred (onError) <br> |
| 207 | + * This method also allows to specify a MAX timeout for the execution and returns a future in |
| 208 | + * order to monitor the execution flow. <br> |
| 209 | + * onError and onClosed callbacks are invoked asynchronously, in a separate thread. <br> |
| 210 | + * |
| 211 | + * @param namespace a namespace the target pod "lives" in |
| 212 | + * @param podName a name of the pod to exec the command on |
| 213 | + * @param onOpen a callback invoked upon the connection established event. |
| 214 | + * @param onClosed a callback invoked upon the process termination. Return code might not always |
| 215 | + * be there. N.B. this callback is invoked before the returned {@link Future} is completed. |
| 216 | + * @param onError a callback to handle k8s errors (NOT the command errors/stderr!) |
| 217 | + * @param timeoutMs timeout in milliseconds for the execution. I.e. the execution will take this |
| 218 | + * many ms or less. If the timeout command is running longer than the allowed timeout, the |
| 219 | + * command will be "asked" to terminate gracefully. If the command is still running after the |
| 220 | + * grace period, the sigkill will be issued. If null is passed, the timeout will not be used |
| 221 | + * and will wait for process to exit itself. |
| 222 | + * @param tty whether you need tty to pipe the data. TTY mode will trim some binary data in order |
| 223 | + * to make it possible to show on screen (tty) |
| 224 | + * @param command a tokenized command to run on the pod |
| 225 | + * @return a {@link Future} promise representing this execution. Unless something goes south, the |
| 226 | + * promise will contain the process return exit code. If the timeoutMs is non-null and the |
| 227 | + * timeout expires before the process exits, promise will contain {@link Integer#MAX_VALUE}. |
| 228 | + * @throws IOException |
| 229 | + */ |
| 230 | + public Future<Integer> exec( |
| 231 | + String namespace, |
| 232 | + String podName, |
| 233 | + Consumer<IOTrio> onOpen, |
| 234 | + BiConsumer<Integer, IOTrio> onClosed, |
| 235 | + BiConsumer<Throwable, IOTrio> onError, |
| 236 | + Long timeoutMs, |
| 237 | + boolean tty, |
| 238 | + String... command) |
| 239 | + throws IOException { |
| 240 | + CompletableFuture<Integer> future = new CompletableFuture<>(); |
| 241 | + IOTrio io = new IOTrio(); |
| 242 | + String cmdStr = Arrays.toString(command); |
| 243 | + BiConsumer<Throwable, IOTrio> errHandler = |
| 244 | + (err, errIO) -> { |
| 245 | + if (onError != null) { |
| 246 | + onError.accept(err, errIO); |
| 247 | + } |
| 248 | + }; |
| 249 | + try { |
| 250 | + Process process = exec(namespace, podName, command, null, true, tty); |
| 251 | + |
| 252 | + io.onClose( |
| 253 | + (code, timeout) -> { |
| 254 | + process.destroy(); |
| 255 | + waitForProcessToExit(process, timeout, cmdStr, err -> errHandler.accept(err, io)); |
| 256 | + // processWaitingThread will handle the rest |
| 257 | + }); |
| 258 | + io.setStdin(process.getOutputStream()); |
| 259 | + io.setStdout(process.getInputStream()); |
| 260 | + io.setStderr(process.getErrorStream()); |
| 261 | + runAsync( |
| 262 | + "Process-Waiting-Thread-" + command[0] + "-" + podName, |
| 263 | + () -> { |
| 264 | + Supplier<Integer> returnCode = process::exitValue; |
| 265 | + try { |
| 266 | + log.debug("Waiting for process to close in {} ms: {}", timeoutMs, cmdStr); |
| 267 | + boolean beforeTimout = |
| 268 | + waitForProcessToExit( |
| 269 | + process, timeoutMs, cmdStr, err -> errHandler.accept(err, io)); |
| 270 | + if (!beforeTimout) { |
| 271 | + returnCode = () -> Integer.MAX_VALUE; |
| 272 | + } |
| 273 | + } catch (Exception e) { |
| 274 | + errHandler.accept(e, io); |
| 275 | + } |
| 276 | + log.debug("process.onExit({}): {}", returnCode.get(), cmdStr); |
| 277 | + if (onClosed != null) { |
| 278 | + onClosed.accept(returnCode.get(), io); |
| 279 | + } |
| 280 | + future.complete(returnCode.get()); |
| 281 | + }); |
| 282 | + if (onOpen != null) { |
| 283 | + onOpen.accept(io); |
| 284 | + } |
| 285 | + } catch (ApiException e) { |
| 286 | + errHandler.accept(e, io); |
| 287 | + future.completeExceptionally(e); |
| 288 | + } |
| 289 | + return future; |
| 290 | + } |
| 291 | + |
| 292 | + protected void runAsync(String taskName, Runnable task) { |
| 293 | + Thread thread = new Thread(task); |
| 294 | + thread.setName(taskName); |
| 295 | + thread.start(); |
| 296 | + } |
| 297 | + |
| 298 | + private boolean waitForProcessToExit( |
| 299 | + Process process, Long timeoutMs, String cmdStr, Consumer<Exception> onError) { |
| 300 | + boolean beforeTimeout = true; |
| 301 | + if (timeoutMs != null && timeoutMs >= 0) { |
| 302 | + boolean exited = false; |
| 303 | + try { |
| 304 | + exited = process.waitFor(timeoutMs, TimeUnit.MILLISECONDS); |
| 305 | + } catch (InterruptedException e) { |
| 306 | + onError.accept(e); |
| 307 | + } |
| 308 | + log.debug("Process closed={}: {}", exited, cmdStr); |
| 309 | + if (!exited && process.isAlive()) { |
| 310 | + beforeTimeout = false; |
| 311 | + log.warn("Process timed out after {} ms. Shutting down: {}", timeoutMs, cmdStr); |
| 312 | + process.destroy(); |
| 313 | + } |
| 314 | + } else { |
| 315 | + try { |
| 316 | + process.waitFor(); |
| 317 | + } catch (InterruptedException e) { |
| 318 | + onError.accept(e); |
| 319 | + } |
| 320 | + } |
| 321 | + return beforeTimeout; |
| 322 | + } |
| 323 | + |
194 | 324 | public final class ExecutionBuilder {
|
195 | 325 | private final String namespace;
|
196 | 326 | private final String name;
|
|
0 commit comments