diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/pluginExceptions/StaleConnectionException.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/pluginExceptions/StaleConnectionException.java
index 2c17c9acac..26a46913bf 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/pluginExceptions/StaleConnectionException.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/pluginExceptions/StaleConnectionException.java
@@ -1,4 +1,14 @@
package com.appsmith.external.pluginExceptions;
public class StaleConnectionException extends RuntimeException {
+ public StaleConnectionException() {
+ }
+
+ public StaleConnectionException(String message) {
+ super(message);
+ }
+
+ public StaleConnectionException(String message, Throwable cause) {
+ super(message, cause);
+ }
}
diff --git a/app/server/appsmith-plugins/pom.xml b/app/server/appsmith-plugins/pom.xml
index 4673fe878b..032f02dd81 100644
--- a/app/server/appsmith-plugins/pom.xml
+++ b/app/server/appsmith-plugins/pom.xml
@@ -9,7 +9,6 @@
4.0.0
- com.appsmith
appsmith-plugins
1.0-SNAPSHOT
pom
@@ -22,6 +21,6 @@
mysqlPlugin
elasticSearchPlugin
dynamoPlugin
+ redisPlugin
-
-
+
\ No newline at end of file
diff --git a/app/server/appsmith-plugins/redisPlugin/plugin.properties b/app/server/appsmith-plugins/redisPlugin/plugin.properties
new file mode 100644
index 0000000000..382cc2fbe9
--- /dev/null
+++ b/app/server/appsmith-plugins/redisPlugin/plugin.properties
@@ -0,0 +1,5 @@
+plugin.id=redis-plugin
+plugin.class=com.external.plugins.RedisPlugin
+plugin.version=1.0-SNAPSHOT
+plugin.provider=tech@appsmith.com
+plugin.dependencies=
\ No newline at end of file
diff --git a/app/server/appsmith-plugins/redisPlugin/pom.xml b/app/server/appsmith-plugins/redisPlugin/pom.xml
new file mode 100644
index 0000000000..2be6893d04
--- /dev/null
+++ b/app/server/appsmith-plugins/redisPlugin/pom.xml
@@ -0,0 +1,115 @@
+
+
+
+ 4.0.0
+
+ com.external.plugins
+ redisPlugin
+ 1.0-SNAPSHOT
+
+ redisPlugin
+
+
+ UTF-8
+ 11
+ ${java.version}
+ ${java.version}
+ redis-plugin
+ com.external.plugins.RedisPlugin
+ 1.0-SNAPSHOT
+ tech@appsmith.com
+
+
+
+
+
+
+ org.pf4j
+ pf4j-spring
+ 0.6.0
+ provided
+
+
+
+ com.appsmith
+ interfaces
+ 1.0-SNAPSHOT
+ provided
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.8
+ provided
+
+
+
+ redis.clients
+ jedis
+ 3.3.0
+
+
+
+
+ junit
+ junit
+ 4.11
+ test
+
+
+
+ io.projectreactor
+ reactor-test
+ 3.2.11.RELEASE
+ test
+
+
+ org.mockito
+ mockito-core
+ 3.1.0
+ test
+
+
+ org.testcontainers
+ testcontainers
+ 1.14.1
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.4
+
+ false
+
+
+
+ ${plugin.id}
+ ${plugin.class}
+ ${plugin.version}
+ ${plugin.provider}
+
+
+
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
+
diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java
new file mode 100644
index 0000000000..96695c737f
--- /dev/null
+++ b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java
@@ -0,0 +1,164 @@
+package com.external.plugins;
+
+import com.appsmith.external.models.*;
+import com.appsmith.external.pluginExceptions.AppsmithPluginError;
+import com.appsmith.external.pluginExceptions.AppsmithPluginException;
+import com.appsmith.external.plugins.BasePlugin;
+import com.appsmith.external.plugins.PluginExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.ObjectUtils;
+import org.pf4j.Extension;
+import org.pf4j.PluginWrapper;
+import org.pf4j.util.StringUtils;
+import org.springframework.util.CollectionUtils;
+import reactor.core.publisher.Mono;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.Protocol;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+import redis.clients.jedis.util.SafeEncoder;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class RedisPlugin extends BasePlugin {
+ private static final Integer DEFAULT_PORT = 6379;
+
+ public RedisPlugin(PluginWrapper wrapper) {
+ super(wrapper);
+ }
+
+ @Slf4j
+ @Extension
+ public static class RedisPluginExecutor implements PluginExecutor {
+ @Override
+ public Mono execute(Jedis jedis,
+ DatasourceConfiguration datasourceConfiguration,
+ ActionConfiguration actionConfiguration) {
+ String body = actionConfiguration.getBody();
+ if (StringUtils.isNullOrEmpty(body)) {
+ return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR,
+ String.format("Body is null or empty [%s]", body)));
+ }
+
+ // First value will be the redis command and others are arguments for that command
+ String[] bodySplitted = body.trim().split("\\s+");
+
+ Protocol.Command command;
+ try {
+ // Commands are in upper case
+ command = Protocol.Command.valueOf(bodySplitted[0].toUpperCase());
+ } catch (IllegalArgumentException exc) {
+ return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR,
+ String.format("Not a valid Redis command:%s", bodySplitted[0])));
+ }
+
+ Object commandOutput;
+ if (bodySplitted.length > 1) {
+ commandOutput = jedis.sendCommand(command, Arrays.copyOfRange(bodySplitted, 1, bodySplitted.length));
+ } else {
+ commandOutput = jedis.sendCommand(command);
+ }
+
+ ActionExecutionResult actionExecutionResult = new ActionExecutionResult();
+ actionExecutionResult.setBody(processCommandOutput(commandOutput));
+
+ return Mono.just(actionExecutionResult);
+ }
+
+ // This will be updated as we encounter different outputs.
+ private String processCommandOutput(Object commandOutput) {
+ if (commandOutput == null) {
+ return "null";
+ } else if (commandOutput instanceof byte[]) {
+ return SafeEncoder.encode((byte[]) commandOutput);
+ } else {
+ return String.valueOf(commandOutput);
+ }
+ }
+
+ @Override
+ public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
+ if (datasourceConfiguration.getEndpoints().isEmpty()) {
+ return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "No endpoint(s) configured"));
+ }
+
+ Endpoint endpoint = datasourceConfiguration.getEndpoints().get(0);
+ Integer port = (int) (long) ObjectUtils.defaultIfNull(endpoint.getPort(), DEFAULT_PORT);
+ Jedis jedis = new Jedis(endpoint.getHost(), port);
+
+ AuthenticationDTO auth = datasourceConfiguration.getAuthentication();
+ if (auth != null && AuthenticationDTO.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) {
+ jedis.auth(auth.getUsername(), auth.getPassword());
+ }
+
+ return Mono.just(jedis);
+ }
+
+ @Override
+ public void datasourceDestroy(Jedis jedis) {
+ try {
+ if (jedis != null) {
+ jedis.close();
+ }
+ } catch (JedisConnectionException exc) {
+ log.error("Error closing Redis connection");
+ }
+ }
+
+ @Override
+ public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) {
+ Set invalids = new HashSet<>();
+
+ if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) {
+ invalids.add("Missing endpoint(s)");
+ } else {
+ Endpoint endpoint = datasourceConfiguration.getEndpoints().get(0);
+ if (StringUtils.isNullOrEmpty(endpoint.getHost())) {
+ invalids.add("Missing host for endpoint");
+ }
+ }
+
+ AuthenticationDTO auth = datasourceConfiguration.getAuthentication();
+ if (auth != null && AuthenticationDTO.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) {
+ if (StringUtils.isNullOrEmpty(datasourceConfiguration.getAuthentication().getUsername())) {
+ invalids.add("Missing username for authentication.");
+ }
+
+ if (StringUtils.isNullOrEmpty(datasourceConfiguration.getAuthentication().getPassword())) {
+ invalids.add("Missing password for authentication.");
+ }
+ }
+
+ return invalids;
+ }
+
+ private Mono verifyPing(Jedis jedis) {
+ String pingResponse;
+ try {
+ pingResponse = jedis.ping();
+ } catch (Exception exc) {
+ return Mono.error(exc);
+ }
+
+ if (!"PONG".equals(pingResponse)) {
+ return Mono.error(new RuntimeException(
+ String.format("Expected PONG in response of PING but got %s", pingResponse)));
+ }
+
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) {
+ return datasourceCreate(datasourceConfiguration).
+ map(jedis -> {
+ verifyPing(jedis).block();
+ datasourceDestroy(jedis);
+ return new DatasourceTestResult();
+ }).
+ onErrorResume(error -> Mono.just(new DatasourceTestResult(error.getMessage())));
+ }
+
+ }
+}
diff --git a/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java
new file mode 100644
index 0000000000..8ec264ca6c
--- /dev/null
+++ b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java
@@ -0,0 +1,212 @@
+package com.external.plugins;
+
+import com.appsmith.external.models.*;
+import com.appsmith.external.pluginExceptions.AppsmithPluginException;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.testcontainers.containers.GenericContainer;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import redis.clients.jedis.Jedis;
+
+import java.util.Collections;
+import java.util.Set;
+
+@Slf4j
+public class RedisPluginTest {
+ @ClassRule
+ public static final GenericContainer redis = new GenericContainer("redis:5.0.3-alpine")
+ .withExposedPorts(6379);
+ private static String host;
+ private static Integer port;
+
+ private RedisPlugin.RedisPluginExecutor pluginExecutor = new RedisPlugin.RedisPluginExecutor();
+
+ @BeforeClass
+ public static void setup() {
+ host = redis.getContainerIpAddress();
+ port = redis.getFirstMappedPort();
+ }
+
+ private DatasourceConfiguration createDatasourceConfiguration() {
+ Endpoint endpoint = new Endpoint();
+ endpoint.setHost(host);
+ endpoint.setPort(Long.valueOf(port));
+
+ DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
+ datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint));
+
+ return datasourceConfiguration;
+ }
+
+ @Test
+ public void itShouldCreateDatasource() {
+ DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
+ Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+
+ StepVerifier.create(jedisMono)
+ .assertNext(Assert::assertNotNull)
+ .verifyComplete();
+
+ pluginExecutor.datasourceDestroy(jedisMono.block());
+ }
+
+ @Test
+ public void itShouldValidateDatasourceWithNoEndpoints() {
+ DatasourceConfiguration invalidDatasourceConfiguration = new DatasourceConfiguration();
+
+ Assert.assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration), Set.of("Missing endpoint(s)"));
+ }
+
+ @Test
+ public void itShouldValidateDatasourceWithInvalidEndpoint() {
+ DatasourceConfiguration invalidDatasourceConfiguration = new DatasourceConfiguration();
+
+ Endpoint endpoint = new Endpoint();
+ invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint));
+
+ Assert.assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration), Set.of("Missing host for endpoint"));
+ }
+
+ @Test
+ public void itShouldValidateDatasourceWithInvalidAuth() {
+ DatasourceConfiguration invalidDatasourceConfiguration = new DatasourceConfiguration();
+
+ Endpoint endpoint = new Endpoint();
+ endpoint.setHost("test-host");
+
+ AuthenticationDTO invalidAuth = new AuthenticationDTO();
+ invalidAuth.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD);
+
+ invalidDatasourceConfiguration.setAuthentication(invalidAuth);
+ invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint));
+
+ Assert.assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration),
+ Set.of("Missing username for authentication.", "Missing password for authentication.")
+ );
+ }
+
+ @Test
+ public void itShouldValidateDatasource() {
+ DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
+
+ AuthenticationDTO auth = new AuthenticationDTO();
+ auth.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD);
+ auth.setUsername("test-username");
+ auth.setPassword("test-password");
+
+ Endpoint endpoint = new Endpoint();
+ endpoint.setHost("test-host");
+
+ datasourceConfiguration.setAuthentication(auth);
+ datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint));
+
+ Assert.assertTrue(pluginExecutor.validateDatasource(datasourceConfiguration).isEmpty());
+ }
+
+ @Test
+ public void itShouldTestDatasource() {
+ DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
+ Mono datasourceTestResultMono = pluginExecutor.testDatasource(datasourceConfiguration);
+
+ StepVerifier.create(datasourceTestResultMono)
+ .assertNext(datasourceTestResult -> {
+ Assert.assertNotNull(datasourceTestResult);
+ Assert.assertTrue(datasourceTestResult.isSuccess());
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void itShouldThrowErrorIfEmptyBody() {
+ DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
+ Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+
+ ActionConfiguration actionConfiguration = new ActionConfiguration();
+
+ Mono actionExecutionResultMono = jedisMono
+ .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration));
+
+ StepVerifier.create(actionExecutionResultMono)
+ .expectError(AppsmithPluginException.class)
+ .verify();
+ }
+
+ @Test
+ public void itShouldThrowErrorIfInvalidRedisCommand() {
+ DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
+ Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+
+ ActionConfiguration actionConfiguration = new ActionConfiguration();
+ actionConfiguration.setBody("LOL");
+
+ Mono actionExecutionResultMono = jedisMono
+ .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration));
+
+ StepVerifier.create(actionExecutionResultMono)
+ .expectError(AppsmithPluginException.class)
+ .verify();
+ }
+
+ @Test
+ public void itShouldExecuteCommandWithoutArgs() {
+ DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
+ Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+
+ ActionConfiguration actionConfiguration = new ActionConfiguration();
+ actionConfiguration.setBody("PING");
+
+ Mono actionExecutionResultMono = jedisMono
+ .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration));
+
+ StepVerifier.create(actionExecutionResultMono)
+ .assertNext(actionExecutionResult -> {
+ Assert.assertNotNull(actionExecutionResult);
+ Assert.assertNotNull(actionExecutionResult.getBody());
+ Assert.assertEquals(actionExecutionResult.getBody(), "PONG");
+ }).verifyComplete();
+ }
+
+ @Test
+ public void itShouldExecuteCommandWithArgs() {
+ DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
+ Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+
+ // Getting a non-existent key
+ ActionConfiguration getActionConfiguration = new ActionConfiguration();
+ getActionConfiguration.setBody("GET key");
+ Mono actionExecutionResultMono = jedisMono
+ .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, getActionConfiguration));
+ StepVerifier.create(actionExecutionResultMono)
+ .assertNext(actionExecutionResult -> {
+ Assert.assertNotNull(actionExecutionResult);
+ Assert.assertNotNull(actionExecutionResult.getBody());
+ Assert.assertEquals(actionExecutionResult.getBody(), "null");
+ }).verifyComplete();
+
+ // Setting a key
+ ActionConfiguration setActionConfiguration = new ActionConfiguration();
+ setActionConfiguration.setBody("SET key value");
+ actionExecutionResultMono = jedisMono
+ .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, setActionConfiguration));
+ StepVerifier.create(actionExecutionResultMono)
+ .assertNext(actionExecutionResult -> {
+ Assert.assertNotNull(actionExecutionResult);
+ Assert.assertNotNull(actionExecutionResult.getBody());
+ Assert.assertEquals(actionExecutionResult.getBody(), "OK");
+ }).verifyComplete();
+
+ // Getting the key
+ actionExecutionResultMono = jedisMono
+ .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, getActionConfiguration));
+ StepVerifier.create(actionExecutionResultMono)
+ .assertNext(actionExecutionResult -> {
+ Assert.assertNotNull(actionExecutionResult);
+ Assert.assertNotNull(actionExecutionResult.getBody());
+ Assert.assertEquals(actionExecutionResult.getBody(), "value");
+ }).verifyComplete();
+ }
+}