6654 serverless functions add a deploy button disable deploy when autosave (#6715)
- improvements on serverless function behavior (autosave performances, deploy on execution only) - add versioning to serverless functions - add a publish endpoint to create a new version of a serverless function - add deploy and reset to lastVersion button in the settings section: <img width="736" alt="image" src="https://github.com/user-attachments/assets/2001f8d2-07a4-4f79-84dd-ec74b6f301d3">
This commit is contained in:
@ -1,27 +1,19 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
||||
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
|
||||
import { compileTypescript } from 'src/engine/integrations/serverless/drivers/utils/compile-typescript';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
|
||||
|
||||
export class BaseServerlessDriver {
|
||||
getFolderPath(serverlessFunction: ServerlessFunctionEntity) {
|
||||
return join(
|
||||
'workspace-' + serverlessFunction.workspaceId,
|
||||
FileFolder.ServerlessFunction,
|
||||
serverlessFunction.id,
|
||||
);
|
||||
}
|
||||
|
||||
async getCompiledCode(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
fileStorageService: FileStorageService,
|
||||
) {
|
||||
const folderPath = this.getFolderPath(serverlessFunction);
|
||||
const folderPath = getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
const fileStream = await fileStorageService.read({
|
||||
folderPath,
|
||||
filename: SOURCE_FILE_NAME,
|
||||
|
||||
@ -16,9 +16,14 @@ export type ServerlessExecuteResult = {
|
||||
|
||||
export interface ServerlessDriver {
|
||||
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
||||
build(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
||||
build(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
version: string,
|
||||
): Promise<void>;
|
||||
publish(serverlessFunction: ServerlessFunctionEntity): Promise<string>;
|
||||
execute(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
payload: object | undefined,
|
||||
version: string,
|
||||
): Promise<ServerlessExecuteResult>;
|
||||
}
|
||||
|
||||
@ -9,6 +9,9 @@ import {
|
||||
UpdateFunctionCodeCommand,
|
||||
DeleteFunctionCommand,
|
||||
ResourceNotFoundException,
|
||||
waitUntilFunctionUpdatedV2,
|
||||
PublishVersionCommandInput,
|
||||
PublishVersionCommand,
|
||||
} from '@aws-sdk/client-lambda';
|
||||
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
|
||||
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
|
||||
@ -24,6 +27,10 @@ import { FileStorageService } from 'src/engine/integrations/file-storage/file-st
|
||||
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
||||
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
|
||||
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||
import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
|
||||
export interface LambdaDriverOptions extends LambdaClientConfig {
|
||||
fileStorageService: FileStorageService;
|
||||
@ -51,12 +58,10 @@ export class LambdaDriver
|
||||
this.buildDirectoryManagerService = options.buildDirectoryManagerService;
|
||||
}
|
||||
|
||||
private async checkFunctionExists(
|
||||
serverlessFunctionId: string,
|
||||
): Promise<boolean> {
|
||||
private async checkFunctionExists(functionName: string): Promise<boolean> {
|
||||
try {
|
||||
const getFunctionCommand = new GetFunctionCommand({
|
||||
FunctionName: serverlessFunctionId,
|
||||
FunctionName: functionName,
|
||||
});
|
||||
|
||||
await this.lambdaClient.send(getFunctionCommand);
|
||||
@ -132,42 +137,85 @@ export class LambdaDriver
|
||||
await this.lambdaClient.send(command);
|
||||
}
|
||||
|
||||
const waitParams = { FunctionName: serverlessFunction.id };
|
||||
|
||||
await waitUntilFunctionUpdatedV2(
|
||||
{ client: this.lambdaClient, maxWaitTime: 5 },
|
||||
waitParams,
|
||||
);
|
||||
|
||||
await this.buildDirectoryManagerService.clean();
|
||||
}
|
||||
|
||||
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
||||
await this.build(serverlessFunction);
|
||||
const params: PublishVersionCommandInput = {
|
||||
FunctionName: serverlessFunction.id,
|
||||
};
|
||||
|
||||
const command = new PublishVersionCommand(params);
|
||||
|
||||
const result = await this.lambdaClient.send(command);
|
||||
const newVersion = result.Version;
|
||||
|
||||
if (!newVersion) {
|
||||
throw new Error('New published version is undefined');
|
||||
}
|
||||
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
async execute(
|
||||
functionToExecute: ServerlessFunctionEntity,
|
||||
payload: object | undefined = undefined,
|
||||
version: string,
|
||||
): Promise<ServerlessExecuteResult> {
|
||||
const computedVersion =
|
||||
version === 'latest' ? functionToExecute.latestVersion : version;
|
||||
|
||||
const functionName =
|
||||
computedVersion === 'draft'
|
||||
? functionToExecute.id
|
||||
: `${functionToExecute.id}:${computedVersion}`;
|
||||
const startTime = Date.now();
|
||||
const params = {
|
||||
FunctionName: functionToExecute.id,
|
||||
FunctionName: functionName,
|
||||
Payload: JSON.stringify(payload),
|
||||
};
|
||||
|
||||
const command = new InvokeCommand(params);
|
||||
|
||||
const result = await this.lambdaClient.send(command);
|
||||
try {
|
||||
const result = await this.lambdaClient.send(command);
|
||||
|
||||
const parsedResult = result.Payload
|
||||
? JSON.parse(result.Payload.transformToString())
|
||||
: {};
|
||||
const parsedResult = result.Payload
|
||||
? JSON.parse(result.Payload.transformToString())
|
||||
: {};
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (result.FunctionError) {
|
||||
return {
|
||||
data: null,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||
error: parsedResult,
|
||||
};
|
||||
}
|
||||
|
||||
if (result.FunctionError) {
|
||||
return {
|
||||
data: null,
|
||||
data: parsedResult,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||
error: parsedResult,
|
||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ResourceNotFoundException) {
|
||||
throw new ServerlessFunctionException(
|
||||
`Function Version '${version}' does not exist`,
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
data: parsedResult,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
ServerlessExecuteError,
|
||||
ServerlessExecuteResult,
|
||||
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
||||
import { FileStorageExceptionCode } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
||||
@ -17,6 +18,11 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless
|
||||
import { BUILD_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/build-file-name';
|
||||
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
||||
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||
import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
|
||||
|
||||
export interface LocalDriverOptions {
|
||||
fileStorageService: FileStorageService;
|
||||
@ -33,11 +39,7 @@ export class LocalDriver
|
||||
this.fileStorageService = options.fileStorageService;
|
||||
}
|
||||
|
||||
async delete(serverlessFunction: ServerlessFunctionEntity) {
|
||||
await this.fileStorageService.delete({
|
||||
folderPath: this.getFolderPath(serverlessFunction),
|
||||
});
|
||||
}
|
||||
async delete() {}
|
||||
|
||||
async build(serverlessFunction: ServerlessFunctionEntity) {
|
||||
const javascriptCode = await this.getCompiledCode(
|
||||
@ -49,20 +51,48 @@ export class LocalDriver
|
||||
file: javascriptCode,
|
||||
name: BUILD_FILE_NAME,
|
||||
mimeType: undefined,
|
||||
folder: this.getFolderPath(serverlessFunction),
|
||||
folder: getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version: 'draft',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
||||
await this.build(serverlessFunction);
|
||||
|
||||
return serverlessFunction.latestVersion
|
||||
? `${parseInt(serverlessFunction.latestVersion, 10) + 1}`
|
||||
: '1';
|
||||
}
|
||||
|
||||
async execute(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
payload: object | undefined = undefined,
|
||||
version: string,
|
||||
): Promise<ServerlessExecuteResult> {
|
||||
const startTime = Date.now();
|
||||
const fileStream = await this.fileStorageService.read({
|
||||
folderPath: this.getFolderPath(serverlessFunction),
|
||||
filename: BUILD_FILE_NAME,
|
||||
});
|
||||
const fileContent = await readFileContent(fileStream);
|
||||
let fileContent = '';
|
||||
|
||||
try {
|
||||
const fileStream = await this.fileStorageService.read({
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version,
|
||||
}),
|
||||
filename: BUILD_FILE_NAME,
|
||||
});
|
||||
|
||||
fileContent = await readFileContent(fileStream);
|
||||
} catch (error) {
|
||||
if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) {
|
||||
throw new ServerlessFunctionException(
|
||||
`Function Version '${version}' does not exist`,
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const tmpFilePath = join(tmpdir(), `${v4()}.js`);
|
||||
|
||||
|
||||
@ -16,14 +16,22 @@ export class ServerlessService implements ServerlessDriver {
|
||||
return this.driver.delete(serverlessFunction);
|
||||
}
|
||||
|
||||
async build(serverlessFunction: ServerlessFunctionEntity): Promise<void> {
|
||||
return this.driver.build(serverlessFunction);
|
||||
async build(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
version: string,
|
||||
): Promise<void> {
|
||||
return this.driver.build(serverlessFunction, version);
|
||||
}
|
||||
|
||||
async publish(serverlessFunction: ServerlessFunctionEntity): Promise<string> {
|
||||
return this.driver.publish(serverlessFunction);
|
||||
}
|
||||
|
||||
async execute(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
payload: object | undefined = undefined,
|
||||
version: string,
|
||||
): Promise<ServerlessExecuteResult> {
|
||||
return this.driver.execute(serverlessFunction, payload);
|
||||
return this.driver.execute(serverlessFunction, payload, version);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
|
||||
export const getServerlessFolder = ({
|
||||
serverlessFunction,
|
||||
version,
|
||||
}: {
|
||||
serverlessFunction: ServerlessFunctionEntity;
|
||||
version?: string;
|
||||
}) => {
|
||||
const computedVersion =
|
||||
version === 'latest' ? serverlessFunction.latestVersion : version;
|
||||
|
||||
return join(
|
||||
'workspace-' + serverlessFunction.workspaceId,
|
||||
FileFolder.ServerlessFunction,
|
||||
serverlessFunction.id,
|
||||
computedVersion || '',
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user