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:
martmull
2024-08-23 12:06:03 +02:00
committed by GitHub
parent 7ca091faa5
commit 6f9aa1e870
42 changed files with 850 additions and 269 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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