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
12 changes: 7 additions & 5 deletions docs/docs/en/guide/alert/script.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ The following shows the `Script` configuration example:

## Parameter Configuration

| **Parameter** | **Description** |
|---------------|-------------------------------------------------------------|
| User Params | User defined parameters will pass to the script. |
| Script Path | The file location path in the server, only support .sh file |
| Type | Support `Shell` script. |
| **Parameter** | **Description** |
|---------------|-------------------------------------------------------------------------------------|
| User Params | User defined parameters will pass to the script. |
| Script Path | The file location path in the server, only support .sh file |
| Type | Support `Shell` script. |
| Timeout | Script execution timeout in seconds, default is `60`, and must be greater than `0`. |

### Note

1.Consider the script file access privileges with the executing tenant.
2.Script alerts will execute the corresponding shell script. The platform will not verify the script content and whether it has been tampered with. There is a need to have a high degree of trust in this shell script and trust that users will not abuse this function.
3.The script exit code is returned directly when execution completes. If the execution times out, the alert reports a timeout failure instead of a script exit code.
5 changes: 5 additions & 0 deletions docs/docs/zh/guide/alert/script.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@

> 支持`Shell`脚本

* 超时时间

> 脚本执行超时时间(秒),默认值为`60`,且必须大于`0`

**_注意:_**
1.请注意脚本的读写权限与执行租户的关系
2.脚本告警会执行对应shell脚本,平台不会校验脚本内容和是否被篡改,需要高度信任该shell脚本,并且信任用户不会滥用此功能
3.脚本执行完成后会直接返回脚本退出码;如果执行超时,告警会返回超时失败信息,而不是脚本退出码
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@
<artifactId>dolphinscheduler-common</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,127 @@
package org.apache.dolphinscheduler.plugin.alert.script;

import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public final class ProcessUtils {

public static final class ProcessExecutionResult {

private final Integer exitCode;
private final boolean timedOut;

private ProcessExecutionResult(Integer exitCode, boolean timedOut) {
this.exitCode = exitCode;
this.timedOut = timedOut;
}

public Integer getExitCode() {
return exitCode;
}

public boolean isTimedOut() {
return timedOut;
}

static ProcessExecutionResult success(int exitCode) {
return new ProcessExecutionResult(exitCode, false);
}

static ProcessExecutionResult timeout() {
return new ProcessExecutionResult(null, true);
}

static ProcessExecutionResult error() {
return new ProcessExecutionResult(null, false);
}
}

private ProcessUtils() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}

/**
* executeScript
* executeScript with timeout
*
* @param timeoutSeconds timeout in seconds, must be greater than 0
* @param cmd cmd params
* @return exit code
* @return execution result
*/
static Integer executeScript(String... cmd) {

int exitCode = -1;
static ProcessExecutionResult executeScript(long timeoutSeconds, String... cmd) {

ProcessBuilder processBuilder = new ProcessBuilder(cmd);
Process process = null;
StreamGobbler inputStreamGobbler = null;
StreamGobbler errorStreamGobbler = null;
try {
Process process = processBuilder.start();
StreamGobbler inputStreamGobbler = new StreamGobbler(process.getInputStream());
StreamGobbler errorStreamGobbler = new StreamGobbler(process.getErrorStream());
process = processBuilder.start();
inputStreamGobbler = new StreamGobbler(process.getInputStream());
errorStreamGobbler = new StreamGobbler(process.getErrorStream());

inputStreamGobbler.setDaemon(true);
errorStreamGobbler.setDaemon(true);
inputStreamGobbler.start();
errorStreamGobbler.start();
return process.waitFor();
} catch (IOException | InterruptedException e) {

if (timeoutSeconds > 0) {
boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
if (!finished) {
log.error("script execution timed out after {} seconds, destroying process", timeoutSeconds);
process.destroyForcibly();
return ProcessExecutionResult.timeout();
}
} else {
process.waitFor();
}
return ProcessExecutionResult.success(process.exitValue());
} catch (IOException e) {
log.error("execute alert script error {}", e.getMessage());
return ProcessExecutionResult.error();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("execute alert script interrupted", e);
return ProcessExecutionResult.error();
} finally {
closeProcessStreams(process);
joinGobbler(inputStreamGobbler);
joinGobbler(errorStreamGobbler);
}
}

return exitCode;
private static void closeProcessStreams(Process process) {
if (Objects.isNull(process)) {
return;
}
try {
process.getOutputStream().close();
} catch (IOException e) {
log.warn("Failed to close process output stream", e);
}
try {
process.getInputStream().close();
} catch (IOException e) {
log.warn("Failed to close process input stream after timeout", e);
}
try {
process.getErrorStream().close();
} catch (IOException e) {
log.warn("Failed to close process error stream after timeout", e);
}
}

private static void joinGobbler(StreamGobbler gobbler) {
if (gobbler == null) {
return;
}
try {
gobbler.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Interrupted while waiting for stream gobbler to finish", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
import org.apache.dolphinscheduler.alert.api.AlertChannelFactory;
import org.apache.dolphinscheduler.alert.api.AlertInputTips;
import org.apache.dolphinscheduler.common.utils.JSONUtils;
import org.apache.dolphinscheduler.spi.params.base.DataType;
import org.apache.dolphinscheduler.spi.params.base.ParamsOptions;
import org.apache.dolphinscheduler.spi.params.base.PluginParams;
import org.apache.dolphinscheduler.spi.params.base.Validate;
import org.apache.dolphinscheduler.spi.params.input.InputParam;
import org.apache.dolphinscheduler.spi.params.input.number.InputNumberParam;
import org.apache.dolphinscheduler.spi.params.radio.RadioParam;

import java.util.Arrays;
Expand Down Expand Up @@ -66,7 +68,18 @@ public List<PluginParams> params() {
.addValidate(Validate.newBuilder().setRequired(true).build())
.build();

return Arrays.asList(scriptUserParam, scriptPathParam, scriptTypeParams);
InputNumberParam scriptTimeoutParam =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is best to tell the user the meaning of the exit code in the document.

InputNumberParam.newBuilder(ScriptParamsConstants.NAME_SCRIPT_TIMEOUT,
ScriptParamsConstants.SCRIPT_TIMEOUT)
.setValue(ScriptParamsConstants.DEFAULT_SCRIPT_TIMEOUT)
.addValidate(Validate.newBuilder()
.setType(DataType.NUMBER.getDataType())
.setRequired(false)
.setMin(1D)
.build())
.build();

return Arrays.asList(scriptUserParam, scriptPathParam, scriptTypeParams, scriptTimeoutParam);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public final class ScriptParamsConstants {

static final String NAME_SCRIPT_USER_PARAMS = "userParams";

static final String SCRIPT_TIMEOUT = "$t('timeout')";

static final String NAME_SCRIPT_TIMEOUT = "timeout";

static final int DEFAULT_SCRIPT_TIMEOUT = 60;

private ScriptParamsConstants() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public final class ScriptSender {
private final String scriptPath;
private final String scriptType;
private final String userParams;
private final String timeoutConfig;

ScriptSender(Map<String, String> config) {
scriptPath = StringUtils.isNotBlank(config.get(ScriptParamsConstants.NAME_SCRIPT_PATH))
Expand All @@ -47,6 +48,7 @@ public final class ScriptSender {
userParams = StringUtils.isNotBlank(config.get(ScriptParamsConstants.NAME_SCRIPT_USER_PARAMS))
? config.get(ScriptParamsConstants.NAME_SCRIPT_USER_PARAMS)
: "";
timeoutConfig = config.get(ScriptParamsConstants.NAME_SCRIPT_TIMEOUT);
}

AlertResult sendScriptAlert(String title, String content) {
Expand Down Expand Up @@ -106,18 +108,55 @@ private AlertResult executeShellScript(String title, String content) {
return alertResult;
}

Long timeout = parseTimeout(alertResult);
if (timeout == null) {
return alertResult;
}

String[] cmd = {"/bin/sh", "-c", scriptPath + ALERT_TITLE_OPTION + "'" + title + "'" + ALERT_CONTENT_OPTION
+ "'" + content + "'" + ALERT_USER_PARAMS_OPTION + "'" + userParams + "'"};
int exitCode = ProcessUtils.executeScript(cmd);
ProcessUtils.ProcessExecutionResult executionResult = ProcessUtils.executeScript(timeout, cmd);

if (executionResult.isTimedOut()) {
alertResult.setMessage("send script alert msg error, script execution timed out after " + timeout
+ " seconds");
log.error("send script alert msg error, script execution timed out after {} seconds", timeout);
return alertResult;
}

if (exitCode == 0) {
Integer exitCode = executionResult.getExitCode();
if (exitCode != null && exitCode == 0) {
alertResult.setSuccess(true);
alertResult.setMessage("send script alert msg success");
return alertResult;
}
if (exitCode == null) {
alertResult.setMessage("send script alert msg error");
log.info("send script alert msg error, execute alert script failed");
return alertResult;
}
alertResult.setMessage("send script alert msg error,exitCode is " + exitCode);
log.info("send script alert msg error,exitCode is {}", exitCode);
return alertResult;
}

private Long parseTimeout(AlertResult alertResult) {
if (StringUtils.isNotBlank(timeoutConfig)) {
try {
long parsedTimeout = Long.parseLong(timeoutConfig.trim());
if (parsedTimeout <= 0) {
log.warn("Invalid script timeout config value: '{}'", timeoutConfig);
alertResult.setMessage("script timeout config must be greater than 0");
return null;
}
return parsedTimeout;
} catch (NumberFormatException ex) {
log.warn("Invalid script timeout config value: '{}'", timeoutConfig, ex);
alertResult.setMessage("script timeout config is invalid, should be a number");
return null;
}
}
return (long) ScriptParamsConstants.DEFAULT_SCRIPT_TIMEOUT;
}
Comment thread
SbloodyS marked this conversation as resolved.

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,54 @@

package org.apache.dolphinscheduler.plugin.alert.script;

import java.io.File;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
* ProcessUtilsTest
*/
public class ProcessUtilsTest {

private static final String rootPath = System.getProperty("user.dir");
@Test
public void testExecuteScript() {
String javaBin = getJavaBin();
String[] cmd = {javaBin, "-cp", System.getProperty("java.class.path"),
ProcessUtilsTest.class.getName() + "$SimpleMain"};
ProcessUtils.ProcessExecutionResult executionResult = ProcessUtils.executeScript(60, cmd);
Assertions.assertFalse(executionResult.isTimedOut());
Assertions.assertEquals(0, executionResult.getExitCode());
}

private static final String shellFilPath =
rootPath + "/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/script/shell/test.sh";
@Test
public void testExecuteScriptTimeout() {
String javaBin = getJavaBin();
String[] sleepCmd = {javaBin, "-cp", System.getProperty("java.class.path"),
ProcessUtilsTest.class.getName() + "$SleepMain"};
ProcessUtils.ProcessExecutionResult executionResult = ProcessUtils.executeScript(2, sleepCmd);
Assertions.assertTrue(executionResult.isTimedOut());
Assertions.assertNull(executionResult.getExitCode());
}
Comment thread
SbloodyS marked this conversation as resolved.

private String[] cmd = {"/bin/sh", "-c", shellFilPath + " -t 1"};
private static String getJavaBin() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

String executableName = System.getProperty("os.name").toLowerCase().contains("win")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

? "java.exe"
: "java";
return System.getProperty("java.home") + File.separator + "bin" + File.separator + executableName;
}

@Test
public void testExecuteScript() {
int code = ProcessUtils.executeScript(cmd);
assert code != -1;
public static class SimpleMain {

public static void main(String[] args) {
// Intentionally empty, returning with 0 means success.
}
}

public static class SleepMain {

public static void main(String[] args) throws InterruptedException {
Thread.sleep(30_000L);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class ScriptAlertChannelFactoryTest {
public void testGetParams() {
ScriptAlertChannelFactory scriptAlertChannelFactory = new ScriptAlertChannelFactory();
List<PluginParams> params = scriptAlertChannelFactory.params();
Assertions.assertEquals(3, params.size());
Assertions.assertEquals(4, params.size());
}

@Test
Expand Down
Loading
Loading