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:
parent
0331d987de
commit
53172d6d5b
|
|
@ -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 {
|
||||
|
|
|
|||
81
app/server/appsmith-plugins/awsLambdaPlugin/pom.xml
Normal file
81
app/server/appsmith-plugins/awsLambdaPlugin/pom.xml
Normal 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>
|
||||
242
app/server/appsmith-plugins/awsLambdaPlugin/src/main/java/com/external/plugins/AwsLambdaPlugin.java
vendored
Normal file
242
app/server/appsmith-plugins/awsLambdaPlugin/src/main/java/com/external/plugins/AwsLambdaPlugin.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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\"`}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"identifier": "LIST_FUNCTIONS",
|
||||
"controlType": "SECTION",
|
||||
"conditionals": {
|
||||
"show": "{{actionConfiguration.formData.command.data === 'LIST_FUNCTIONS'}}"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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=
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,8 @@
|
|||
|
||||
<module>googleAiPlugin</module>
|
||||
|
||||
<module>awsLambdaPlugin</module>
|
||||
|
||||
<module>databricksPlugin</module>
|
||||
|
||||
</modules>
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user