Adding basic structure for plugin integrating with Redis (#1085)

Co-authored-by: nitesh261193 <nitesh261193@gmail.com>
This commit is contained in:
Prashant Chaubey 2020-10-22 08:56:58 +01:00 committed by GitHub
parent a16372fed1
commit bf69c7d66b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 508 additions and 3 deletions

View File

@ -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);
}
}

View File

@ -9,7 +9,6 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.appsmith</groupId>
<artifactId>appsmith-plugins</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
@ -22,6 +21,6 @@
<module>mysqlPlugin</module>
<module>elasticSearchPlugin</module>
<module>dynamoPlugin</module>
<module>redisPlugin</module>
</modules>
</project>
</project>

View File

@ -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=

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.external.plugins</groupId>
<artifactId>redisPlugin</artifactId>
<version>1.0-SNAPSHOT</version>
<name>redisPlugin</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<plugin.id>redis-plugin</plugin.id>
<plugin.class>com.external.plugins.RedisPlugin</plugin.class>
<plugin.version>1.0-SNAPSHOT</plugin.version>
<plugin.provider>tech@appsmith.com</plugin.provider>
<plugin.dependencies/>
</properties>
<dependencies>
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<version>0.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.appsmith</groupId>
<artifactId>interfaces</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<version>3.2.11.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.14.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<minimizeJar>false</minimizeJar>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Plugin-Id>${plugin.id}</Plugin-Id>
<Plugin-Class>${plugin.class}</Plugin-Class>
<Plugin-Version>${plugin.version}</Plugin-Version>
<Plugin-Provider>${plugin.provider}</Plugin-Provider>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -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<Jedis> {
@Override
public Mono<ActionExecutionResult> 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<Jedis> 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<String> validateDatasource(DatasourceConfiguration datasourceConfiguration) {
Set<String> 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<Void> 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<DatasourceTestResult> testDatasource(DatasourceConfiguration datasourceConfiguration) {
return datasourceCreate(datasourceConfiguration).
map(jedis -> {
verifyPing(jedis).block();
datasourceDestroy(jedis);
return new DatasourceTestResult();
}).
onErrorResume(error -> Mono.just(new DatasourceTestResult(error.getMessage())));
}
}
}

View File

@ -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<Jedis> 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<DatasourceTestResult> 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<Jedis> jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
ActionConfiguration actionConfiguration = new ActionConfiguration();
Mono<ActionExecutionResult> actionExecutionResultMono = jedisMono
.flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration));
StepVerifier.create(actionExecutionResultMono)
.expectError(AppsmithPluginException.class)
.verify();
}
@Test
public void itShouldThrowErrorIfInvalidRedisCommand() {
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
Mono<Jedis> jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("LOL");
Mono<ActionExecutionResult> actionExecutionResultMono = jedisMono
.flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration));
StepVerifier.create(actionExecutionResultMono)
.expectError(AppsmithPluginException.class)
.verify();
}
@Test
public void itShouldExecuteCommandWithoutArgs() {
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
Mono<Jedis> jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("PING");
Mono<ActionExecutionResult> 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<Jedis> jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
// Getting a non-existent key
ActionConfiguration getActionConfiguration = new ActionConfiguration();
getActionConfiguration.setBody("GET key");
Mono<ActionExecutionResult> 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();
}
}