feat: AWS Lambda integration (#29792)

## Description

#### PR fixes following issue(s)
Fixes #10073

#### Type of change
- New feature (non-breaking change which adds functionality)

## Testing

#### How Has This Been Tested?
- [x] Manual
- [x] JUnit
- [ ] Jest
- [ ] Cypress

## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Introduced AWS Lambda plugin for executing and managing AWS Lambda
functions.

- **Enhancements**
  - Updated server configuration to support new plugins.

- **Documentation**
  - Added constants for new plugins in the PluginConstants interface.

- **Tests**
  - Added test cases for AWS Lambda plugin functionality.

- **Chores**
- Implemented migrations to add AWS Lambda plugin to existing
workspaces.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Trisha Anand <trisha@appsmith.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Arpit Mohan 2023-12-26 10:07:19 +05:30 committed by GitHub
parent 0331d987de
commit 53172d6d5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 762 additions and 0 deletions

View File

@ -16,6 +16,7 @@ public interface PluginConstants {
String ANTHROPIC_PLUGIN = "anthropic-plugin";
String GOOGLE_AI_PLUGIN = "googleai-plugin";
String DATABRICKS_PLUGIN = "databricks-plugin";
String AWS_LAMBDA_PLUGIN = "aws-lambda-plugin";
}
public static final String DEFAULT_REST_DATASOURCE = "DEFAULT_REST_DATASOURCE";
@ -43,6 +44,7 @@ public interface PluginConstants {
public static final String ANTHROPIC_PLUGIN_NAME = "Anthropic";
public static final String GOOGLE_AI_PLUGIN_NAME = "Google AI";
public static final String DATABRICKS_PLUGIN_NAME = "Databricks";
public static final String AWS_LAMBDA_PLUGIN_NAME = "AWS Lambda";
}
interface HostName {

View File

@ -0,0 +1,81 @@
<?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>
<parent>
<groupId>com.appsmith</groupId>
<artifactId>appsmith-plugins</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.external.plugins</groupId>
<artifactId>awsLambdaPlugin</artifactId>
<version>1.0-SNAPSHOT</version>
<name>awsLambdaPlugin</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-lambda</artifactId>
<version>1.12.622</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-osgi</artifactId>
<version>1.12.622</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Test Dependencies -->
<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>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<goals>
<goal>copy-dependencies</goal>
</goals>
<phase>package</phase>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,242 @@
package com.external.plugins;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.lambda.AWSLambda;
import com.amazonaws.services.lambda.AWSLambdaClientBuilder;
import com.amazonaws.services.lambda.model.AWSLambdaException;
import com.amazonaws.services.lambda.model.FunctionConfiguration;
import com.amazonaws.services.lambda.model.InvokeRequest;
import com.amazonaws.services.lambda.model.InvokeResult;
import com.amazonaws.services.lambda.model.ListFunctionsResult;
import com.amazonaws.services.lambda.model.ResourceNotFoundException;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.TriggerRequestDTO;
import com.appsmith.external.models.TriggerResultDTO;
import com.appsmith.external.plugins.BasePlugin;
import com.appsmith.external.plugins.PluginExecutor;
import com.fasterxml.jackson.databind.node.ArrayNode;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.Extension;
import org.pf4j.PluginWrapper;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import static com.appsmith.external.helpers.PluginUtils.STRING_TYPE;
import static com.appsmith.external.helpers.PluginUtils.getDataValueSafelyFromFormData;
public class AwsLambdaPlugin extends BasePlugin {
public AwsLambdaPlugin(PluginWrapper wrapper) {
super(wrapper);
}
@Slf4j
@Extension
public static class AwsLambdaPluginExecutor implements PluginExecutor<AWSLambda> {
@Override
public Mono<ActionExecutionResult> execute(
AWSLambda connection,
DatasourceConfiguration datasourceConfiguration,
ActionConfiguration actionConfiguration) {
Map<String, Object> formData = actionConfiguration.getFormData();
String command = getDataValueSafelyFromFormData(formData, "command", STRING_TYPE);
return Mono.fromCallable(() -> {
ActionExecutionResult result;
switch (Objects.requireNonNull(command)) {
case "LIST_FUNCTIONS" -> result = listFunctions(actionConfiguration, connection);
case "INVOKE_FUNCTION" -> result = invokeFunction(actionConfiguration, connection);
default -> throw new IllegalStateException("Unexpected value: " + command);
}
return result;
})
.onErrorMap(
IllegalArgumentException.class,
e -> new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR, "Unsupported command: " + command))
.onErrorMap(
ResourceNotFoundException.class,
e -> new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getErrorMessage()))
.onErrorMap(
Exception.class,
e -> new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage()))
.map(obj -> obj)
.subscribeOn(Schedulers.boundedElastic());
}
@Override
public Mono<TriggerResultDTO> trigger(
AWSLambda connection, DatasourceConfiguration datasourceConfiguration, TriggerRequestDTO request) {
if (!StringUtils.hasText(request.getRequestType())) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "request type is missing");
}
ActionExecutionResult actionExecutionResult = listFunctions(null, connection);
ArrayNode body = (ArrayNode) actionExecutionResult.getBody();
List<Map<String, String>> functionNames = StreamSupport.stream(body.spliterator(), false)
.map(function -> function.get("functionName").asText())
.sorted()
.map(functionName -> Map.of("label", functionName, "value", functionName))
.collect(Collectors.toList());
TriggerResultDTO triggerResultDTO = new TriggerResultDTO();
triggerResultDTO.setTrigger(functionNames);
return Mono.just(triggerResultDTO);
}
ActionExecutionResult invokeFunction(ActionConfiguration actionConfiguration, AWSLambda connection) {
InvokeRequest invokeRequest = new InvokeRequest();
invokeRequest.setFunctionName(
getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "functionName", STRING_TYPE));
invokeRequest.setPayload(
getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "body", STRING_TYPE));
invokeRequest.setInvocationType(
getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "invocationType", STRING_TYPE));
InvokeResult invokeResult = connection.invoke(invokeRequest);
ActionExecutionResult result = new ActionExecutionResult();
result.setStatusCode(String.valueOf(invokeResult.getStatusCode()));
Boolean isExecutionSuccess = (invokeResult.getFunctionError() == null);
result.setIsExecutionSuccess(isExecutionSuccess);
ByteBuffer responseBuffer = invokeResult.getPayload();
String responsePayload = ObjectUtils.isEmpty(responseBuffer)
? null
: new String(responseBuffer.array(), StandardCharsets.UTF_8);
result.setBody(responsePayload);
return result;
}
ActionExecutionResult listFunctions(ActionConfiguration actionConfiguration, AWSLambda connection) {
ListFunctionsResult listFunctionsResult = connection.listFunctions();
List<FunctionConfiguration> functions = listFunctionsResult.getFunctions();
ActionExecutionResult result = new ActionExecutionResult();
result.setBody(objectMapper.valueToTree(functions));
result.setIsExecutionSuccess(true);
return result;
}
@Override
public Mono<AWSLambda> datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
String accessKey = authentication.getUsername();
String secretKey = authentication.getPassword();
String authenticationType = authentication.getAuthenticationType();
String region =
(String) datasourceConfiguration.getProperties().get(1).getValue();
if (!StringUtils.hasText(region)) {
region = "us-east-1"; // Default region
}
AWSLambdaClientBuilder awsLambdaClientBuilder =
AWSLambdaClientBuilder.standard().withRegion(Regions.fromName(region));
// If access key and secret key are not provided, use the default credentials provider chain. That will
// pick up the instance role if running on an EC2 instance.
if ("accessKey".equals(authenticationType)) {
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
AWSStaticCredentialsProvider staticCredentials = new AWSStaticCredentialsProvider(awsCreds);
awsLambdaClientBuilder = awsLambdaClientBuilder.withCredentials(staticCredentials);
}
AWSLambda awsLambda = awsLambdaClientBuilder.build();
return Mono.just(awsLambda);
}
@Override
public void datasourceDestroy(AWSLambda connection) {
// No need to do anything here.
}
@Override
public Mono<DatasourceTestResult> testDatasource(AWSLambda connection) {
return Mono.fromCallable(() -> {
/*
* - Please note that as of 28 Jan 2021, the way Amazon client SDK works, creating a connection
* object with wrong credentials does not throw any exception.
* - Hence, adding a listFunctions() method call to test the connection.
*/
connection.listFunctions();
return new DatasourceTestResult();
})
.onErrorResume(error -> {
if (error instanceof AWSLambdaException
&& "AccessDenied".equals(((AWSLambdaException) error).getErrorCode())) {
/*
* Sometimes a valid account credential may not have permission to run listFunctions action
* . In this case `AccessDenied` error is returned.
* That fact that the credentials caused `AccessDenied` error instead of invalid access key
* id or signature mismatch error means that the credentials are valid, we are able to
* establish a connection as well, but the account does not have permission to run
* listFunctions.
*/
return Mono.just(new DatasourceTestResult());
}
return Mono.just(new DatasourceTestResult(error.getMessage()));
});
}
@Override
public Set<String> validateDatasource(DatasourceConfiguration datasourceConfiguration) {
Set<String> invalids = new HashSet<>();
if (datasourceConfiguration == null
|| datasourceConfiguration.getAuthentication() == null
|| !StringUtils.hasText(
datasourceConfiguration.getAuthentication().getAuthenticationType())) {
invalids.add("Invalid authentication mechanism provided. Please choose valid authentication type.");
return invalids;
}
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if ("instanceRole".equals(authentication.getAuthenticationType())
&& "true".equalsIgnoreCase(System.getenv("APPSMITH_CLOUD_HOSTING"))) {
// Instance role is not supported for cloud hosting. It's only supported for self-hosted environments.
// This is to prevent a security risk where a user can use the instance role to access resources in a
// hosted environment.
invalids.add(
"Instance role is not supported for cloud hosting. Please choose a different authentication type.");
} else if ("accessKey".equals(authentication.getAuthenticationType())) {
// Only check for access key and secret key if accessKey authentication is selected.
if (!StringUtils.hasText(authentication.getUsername())) {
invalids.add("Unable to find an AWS access key. Please add a valid access key.");
}
if (!StringUtils.hasText(authentication.getPassword())) {
invalids.add("Unable to find an AWS secret key. Please add a valid secret key.");
}
}
return invalids;
}
}
}

View File

@ -0,0 +1,81 @@
{
"identifier": "INVOKE_FUNCTION",
"controlType": "SECTION",
"conditionals": {
"show": "{{actionConfiguration.formData.command.data === 'INVOKE_FUNCTION'}}"
},
"children": [
{
"controlType": "SECTION",
"label": "Details of lambda function",
"children": [
{
"label": "Function to invoke",
"tooltipText": "This is the name of the AWS lambda function that will be invoked.",
"subtitle": "",
"isRequired": true,
"propertyName": "function_name",
"configProperty": "actionConfiguration.formData.functionName.data",
"controlType": "DROP_DOWN",
"initialValue": "",
"options": [],
"placeholderText": "All function names will be fetched.",
"fetchOptionsConditionally": true,
"setFirstOptionAsDefault": true,
"alternateViewTypes": [
"json"
],
"conditionals": {
"enable": "{{true}}",
"fetchDynamicValues": {
"condition": "{{actionConfiguration.formData.command.data === 'INVOKE_FUNCTION'}}",
"config": {
"params": {
"requestType": "FUNCTION_NAMES",
"displayType": "DROP_DOWN"
}
}
}
}
},
{
"label": "Type of invocation",
"tooltipText": "Should the invocation be synchronous or asynchronous?",
"subtitle": "",
"isRequired": true,
"propertyName": "invocation_type",
"configProperty": "actionConfiguration.formData.invocationType.data",
"controlType": "DROP_DOWN",
"initialValue": "",
"options": [
{
"label": "Synchronous",
"value": "RequestResponse"
},
{
"label": "Asynchronous",
"value": "Event"
},
{
"label": "Dry run",
"value": "DryRun"
}
],
"placeholderText": "",
"fetchOptionsConditionally": false,
"setFirstOptionAsDefault": true,
"alternateViewTypes": [
"json"
]
},
{
"label": "Post body",
"configProperty": "actionConfiguration.formData.body.data",
"controlType": "QUERY_DYNAMIC_TEXT",
"initialValue": "",
"placeHolderText": "{`\"key1\": \"value1\"`}"
}
]
}
]
}

View File

@ -0,0 +1,8 @@
{
"identifier": "LIST_FUNCTIONS",
"controlType": "SECTION",
"conditionals": {
"show": "{{actionConfiguration.formData.command.data === 'LIST_FUNCTIONS'}}"
},
"children": []
}

View File

@ -0,0 +1,31 @@
{
"editor": [
{
"controlType": "SECTION",
"identifier": "SELECTOR",
"children": [
{
"label": "Commands",
"description": "Choose the method you would like to use",
"configProperty": "actionConfiguration.formData.command.data",
"controlType": "DROP_DOWN",
"initialValue": "LIST_FUNCTIONS",
"options": [
{
"label": "List all functions",
"value": "LIST_FUNCTIONS"
},
{
"label": "Invoke a function",
"value": "INVOKE_FUNCTION"
}
]
}
]
}
],
"files": [
"list.json",
"invoke.json"
]
}

View File

@ -0,0 +1,60 @@
{
"form": [
{
"sectionName": "Details",
"id": 1,
"children": [
{
"label": "Authentication type",
"configProperty": "datasourceConfiguration.authentication.authenticationType",
"controlType": "DROP_DOWN",
"initialValue": "accessKey",
"setFirstOptionAsDefault": true,
"options": [
{
"label": "AWS access key",
"value": "accessKey"
},
{
"label": "Instance role",
"value": "instanceRole"
}
]
},
{
"label": "Access key",
"configProperty": "datasourceConfiguration.authentication.username",
"controlType": "INPUT_TEXT",
"isRequired": true,
"initialValue": "",
"hidden": {
"path": "datasourceConfiguration.authentication.authenticationType",
"comparison": "NOT_EQUALS",
"value": "accessKey"
}
},
{
"label": "Secret key",
"configProperty": "datasourceConfiguration.authentication.password",
"controlType": "INPUT_TEXT",
"dataType": "PASSWORD",
"initialValue": "",
"isRequired": true,
"encrypted": true,
"hidden": {
"path": "datasourceConfiguration.authentication.authenticationType",
"comparison": "NOT_EQUALS",
"value": "accessKey"
}
},
{
"label": "Region",
"configProperty": "datasourceConfiguration.properties[1].value",
"controlType": "INPUT_TEXT",
"initialValue": "",
"placeholderText": "us-east-1"
}
]
}
]
}

View File

@ -0,0 +1,5 @@
plugin.id=aws-lambda-plugin
plugin.class=com.external.plugins.AwsLambdaPlugin
plugin.version=1.0-SNAPSHOT
plugin.provider=tech@appsmith.com
plugin.dependencies=

View File

@ -0,0 +1,196 @@
package com.external.plugins;
import com.amazonaws.services.lambda.AWSLambda;
import com.amazonaws.services.lambda.model.FunctionConfiguration;
import com.amazonaws.services.lambda.model.InvokeResult;
import com.amazonaws.services.lambda.model.ListFunctionsResult;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Property;
import com.appsmith.external.models.TriggerRequestDTO;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.appsmith.external.helpers.PluginUtils.setDataValueSafelyInFormData;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@Testcontainers
public class AwsLambdaPluginTest {
private static String accessKey;
private static String secretKey;
private static String region;
AwsLambdaPlugin.AwsLambdaPluginExecutor pluginExecutor = new AwsLambdaPlugin.AwsLambdaPluginExecutor();
@BeforeAll
public static void setUp() {
accessKey = "random_access_key";
secretKey = "random_secret_key";
region = "ap-south-1";
}
private DatasourceConfiguration createDatasourceConfiguration() {
DBAuth authDTO = new DBAuth();
authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD);
authDTO.setUsername(accessKey);
authDTO.setPassword(secretKey);
DatasourceConfiguration dsConfig = new DatasourceConfiguration();
dsConfig.setAuthentication(authDTO);
ArrayList<Property> properties = new ArrayList<>();
properties.add(null); // since index 0 is not used anymore.
properties.add(new Property("region", region));
dsConfig.setProperties(properties);
return dsConfig;
}
@Test
public void testExecuteListFunctions() {
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
Map<String, Object> configMap = new HashMap<>();
setDataValueSafelyInFormData(configMap, "command", "LIST_FUNCTIONS");
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setFormData(configMap);
// Mock the Lambda connection
AWSLambda mockLambda = mock(AWSLambda.class);
ListFunctionsResult mockFunctionsResult = new ListFunctionsResult();
mockFunctionsResult.setFunctions(List.of(new FunctionConfiguration().withFunctionName("test-aws-lambda")));
when(mockLambda.listFunctions()).thenReturn(mockFunctionsResult);
Mono<ActionExecutionResult> resultMono =
pluginExecutor.execute(mockLambda, datasourceConfiguration, actionConfiguration);
StepVerifier.create(resultMono)
.assertNext(result -> {
assertEquals(1, ((ArrayNode) result.getBody()).size());
})
.verifyComplete();
}
@Test
public void testExecuteInvokeFunction() {
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
Map<String, Object> configMap = new HashMap<>();
setDataValueSafelyInFormData(configMap, "command", "INVOKE_FUNCTION");
setDataValueSafelyInFormData(configMap, "body", "{\"data\": \"\"}");
setDataValueSafelyInFormData(configMap, "functionName", "test-aws-lambda");
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setFormData(configMap);
// Mock the Lambda connection
AWSLambda mockLambda = mock(AWSLambda.class);
InvokeResult mockResult = new InvokeResult();
mockResult.setPayload(ByteBuffer.wrap("Hello World".getBytes()));
when(mockLambda.invoke(any())).thenReturn(mockResult);
Mono<ActionExecutionResult> resultMono =
pluginExecutor.execute(mockLambda, datasourceConfiguration, actionConfiguration);
StepVerifier.create(resultMono)
.assertNext(result -> {
assertTrue(result.getIsExecutionSuccess());
assertEquals("Hello World", result.getBody().toString());
})
.verifyComplete();
}
@Test
public void testValidateDatasource_missingDatasourceConfiguration() {
// Test case: Missing datasource configuration
Set<String> invalids = pluginExecutor.validateDatasource(null);
assertEquals(1, invalids.size());
assertTrue(invalids.contains(
"Invalid authentication mechanism provided. Please choose valid authentication type."));
}
@Test
public void testValidateDatasource_missingAccessKey() {
// Test case: Missing AWS access key
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
DBAuth authentication = new DBAuth();
authentication.setAuthenticationType("accessKey");
authentication.setPassword("random_secret_key");
datasourceConfiguration.setAuthentication(authentication);
Set<String> invalids = pluginExecutor.validateDatasource(datasourceConfiguration);
assertEquals(1, invalids.size());
assertTrue(invalids.contains("Missing AWS access key"));
}
@Test
public void testValidateDatasource_missingSecretKey() {
AwsLambdaPlugin.AwsLambdaPluginExecutor pluginExecutor = new AwsLambdaPlugin.AwsLambdaPluginExecutor();
// Test case: Missing AWS secret key
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
DBAuth authentication = new DBAuth();
authentication.setAuthenticationType("accessKey");
authentication.setUsername("random_access_key");
authentication.setPassword(null);
datasourceConfiguration.setAuthentication(authentication);
Set<String> invalids = pluginExecutor.validateDatasource(datasourceConfiguration);
assertEquals(1, invalids.size());
assertTrue(invalids.contains("Missing AWS secret key"));
}
@Test
public void testValidateDatasource_validConfigurationForAccessKey() {
AwsLambdaPlugin.AwsLambdaPluginExecutor pluginExecutor = new AwsLambdaPlugin.AwsLambdaPluginExecutor();
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
DBAuth authentication = new DBAuth();
authentication.setAuthenticationType("accessKey");
authentication.setUsername("random_access_key");
authentication.setPassword("random_secret_key");
datasourceConfiguration.setAuthentication(authentication);
Set<String> invalids = pluginExecutor.validateDatasource(datasourceConfiguration);
assertEquals(0, invalids.size());
}
@Test
public void testValidateDatasource_validConfigurationForInstanceRole() {
AwsLambdaPlugin.AwsLambdaPluginExecutor pluginExecutor = new AwsLambdaPlugin.AwsLambdaPluginExecutor();
// Test case: Valid datasource configuration
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
DBAuth authentication = new DBAuth();
authentication.setAuthenticationType("instanceRole");
datasourceConfiguration.setAuthentication(authentication);
Set<String> invalids = pluginExecutor.validateDatasource(datasourceConfiguration);
assertEquals(0, invalids.size());
}
@Test
public void testTrigger_missingRequestType() {
// Test case: Missing request type
AWSLambda mockLambda = mock(AWSLambda.class);
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
TriggerRequestDTO request = new TriggerRequestDTO();
assertThrows(AppsmithPluginException.class, () -> {
pluginExecutor.trigger(mockLambda, datasourceConfiguration, request).block();
});
}
}

View File

@ -66,6 +66,8 @@
<module>googleAiPlugin</module>
<module>awsLambdaPlugin</module>
<module>databricksPlugin</module>
</modules>

View File

@ -0,0 +1,54 @@
package com.appsmith.server.migrations.db.ce;
import com.appsmith.external.constants.PluginConstants;
import com.appsmith.external.models.PluginType;
import com.appsmith.server.domains.Plugin;
import io.mongock.api.annotations.ChangeUnit;
import io.mongock.api.annotations.Execution;
import io.mongock.api.annotations.RollbackExecution;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.mongodb.core.MongoTemplate;
import static com.appsmith.server.migrations.DatabaseChangelog1.installPluginToAllWorkspaces;
@Slf4j
@ChangeUnit(order = "040", id = "add-aws-lambda-plugin", author = " ")
public class Migration040AddAWSLambdaPlugin {
private final MongoTemplate mongoTemplate;
public Migration040AddAWSLambdaPlugin(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
@RollbackExecution
public void rollbackExecution() {}
@Execution
public void addPluginToDbAndWorkspace() {
Plugin plugin = new Plugin();
plugin.setName(PluginConstants.PluginName.AWS_LAMBDA_PLUGIN_NAME);
plugin.setType(PluginType.REMOTE);
plugin.setPluginName(PluginConstants.PluginName.AWS_LAMBDA_PLUGIN_NAME);
plugin.setPackageName(PluginConstants.PackageName.AWS_LAMBDA_PLUGIN);
plugin.setUiComponent("UQIDbEditorForm");
plugin.setDatasourceComponent("DbEditorForm");
plugin.setResponseType(Plugin.ResponseType.JSON);
plugin.setIconLocation("https://assets.appsmith.com/aws-lambda-logo.svg");
plugin.setDocumentationLink("https://docs.appsmith.com/connect-data/reference/aws-lambda");
plugin.setDefaultInstall(true);
try {
mongoTemplate.insert(plugin);
} catch (DuplicateKeyException e) {
log.warn(plugin.getPackageName() + " already present in database.");
}
if (plugin.getId() == null) {
log.error("Failed to insert the AWS Lambda plugin into the database.");
return;
}
installPluginToAllWorkspaces(mongoTemplate, plugin.getId());
}
}