feat(serverless): add basic sandbox isolation and flexible driver options (#17176)
## Overview - Add a DISABLED serverless driver to explicitly turn off execution - Clarify self-hosting docs with driver options and recommended usage - Keep integration coverage for serverless function execution (default + external package example) ## Notes - Local driver remains the default for development usage; Lambda or Disabled recommended for production deployments - No functional changes to Lambda execution <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces flexible serverless execution modes and safer local execution. > > - **New driver:** `DISABLED` serverless driver with wiring in `serverless.interface`, factory, module provider, and GraphQL exception mapping; new exception code `SERVERLESS_FUNCTION_DISABLED`. > - **Local driver hardening:** Strip `NODE_OPTIONS` when spawning child processes; cleanup promise signature; better log capture. > - **Dependency build reliability:** Use `execFile` with bundled Yarn (`.yarn/releases/yarn-4.9.2.cjs`), strip `NODE_OPTIONS`, improved error messages, and parallel cleanup excluding `node_modules`. > - **Docs:** Add serverless section detailing `SERVERLESS_TYPE` options (LOCAL, LAMBDA, DISABLED), security notice, and recommended configs. > - **Config/env:** Default `IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS` set to `true` (examples/tests default `false`); sample envs updated. > - **Tests:** Add integration tests and GraphQL helpers for creating, updating, publishing, executing, and deleting serverless functions, including external package usage and error paths. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 1a2958cc19cff1b0108c51b83095bbf95e75d931. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
type ServerlessDriver,
|
||||
type ServerlessExecuteResult,
|
||||
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
||||
|
||||
import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
|
||||
export class DisabledDriver implements ServerlessDriver {
|
||||
async delete(): Promise<void> {
|
||||
// No-op when disabled
|
||||
}
|
||||
|
||||
async execute(): Promise<ServerlessExecuteResult> {
|
||||
throw new ServerlessFunctionException(
|
||||
'Serverless function execution is disabled. Set SERVERLESS_TYPE to LOCAL or LAMBDA to enable.',
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_DISABLED,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -284,9 +284,13 @@ export class LocalDriver implements ServerlessDriver {
|
||||
stack?: string;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}>((resolve, _) => {
|
||||
}>((resolve) => {
|
||||
// Strip NODE_OPTIONS to prevent tsx loader from being inherited
|
||||
const { NODE_OPTIONS: _n1, ...cleanProcessEnv } = process.env;
|
||||
const { NODE_OPTIONS: _n2, ...cleanUserEnv } = env;
|
||||
|
||||
const child = spawn(process.execPath, [runnerPath], {
|
||||
env: { ...process.env, ...env },
|
||||
env: { ...cleanProcessEnv, ...cleanUserEnv },
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||
});
|
||||
|
||||
|
||||
+28
-16
@@ -1,12 +1,12 @@
|
||||
import { statSync, promises as fs } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
import { execFile } from 'child_process';
|
||||
import { promises as fs, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { getLayerDependenciesDirName } from 'src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name';
|
||||
import type { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
const execFilePromise = promisify(execFile);
|
||||
|
||||
export const copyAndBuildDependencies = async (
|
||||
buildDirectory: string,
|
||||
@@ -32,23 +32,35 @@ export const copyAndBuildDependencies = async (
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
const localYarnPath = join(buildDirectory, '.yarn/releases/yarn-4.9.2.cjs');
|
||||
|
||||
// Strip NODE_OPTIONS to prevent tsx loader from interfering with yarn
|
||||
const { NODE_OPTIONS: _nodeOptions, ...cleanEnv } = process.env;
|
||||
|
||||
try {
|
||||
await execPromise('yarn', { cwd: buildDirectory });
|
||||
await execFilePromise(process.execPath, [localYarnPath], {
|
||||
cwd: buildDirectory,
|
||||
env: cleanEnv,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
throw new Error(error.stdout);
|
||||
const errorMessage =
|
||||
[error?.stdout, error?.stderr].filter(Boolean).join('\n') ||
|
||||
'Failed to install serverless dependencies';
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const objects = await fs.readdir(buildDirectory);
|
||||
|
||||
objects.forEach((object) => {
|
||||
const fullPath = join(buildDirectory, object);
|
||||
await Promise.all(
|
||||
objects
|
||||
.filter((object) => object !== 'node_modules')
|
||||
.map((object) => {
|
||||
const fullPath = join(buildDirectory, object);
|
||||
|
||||
if (object === 'node_modules') return;
|
||||
|
||||
if (statSync(fullPath).isDirectory()) {
|
||||
fs.rm(fullPath, { recursive: true, force: true });
|
||||
} else {
|
||||
fs.rm(fullPath);
|
||||
}
|
||||
});
|
||||
return statSync(fullPath).isDirectory()
|
||||
? fs.rm(fullPath, { recursive: true, force: true })
|
||||
: fs.rm(fullPath);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,6 +15,11 @@ export const serverlessModuleFactory = async (
|
||||
const options = { fileStorageService };
|
||||
|
||||
switch (driverType) {
|
||||
case ServerlessDriverType.DISABLED: {
|
||||
return {
|
||||
type: ServerlessDriverType.DISABLED,
|
||||
};
|
||||
}
|
||||
case ServerlessDriverType.LOCAL: {
|
||||
return {
|
||||
type: ServerlessDriverType.LOCAL,
|
||||
|
||||
@@ -4,10 +4,15 @@ import { type LambdaDriverOptions } from 'src/engine/core-modules/serverless/dri
|
||||
import { type LocalDriverOptions } from 'src/engine/core-modules/serverless/drivers/local.driver';
|
||||
|
||||
export enum ServerlessDriverType {
|
||||
DISABLED = 'DISABLED',
|
||||
LAMBDA = 'LAMBDA',
|
||||
LOCAL = 'LOCAL',
|
||||
}
|
||||
|
||||
export interface DisabledDriverFactoryOptions {
|
||||
type: ServerlessDriverType.DISABLED;
|
||||
}
|
||||
|
||||
export interface LocalDriverFactoryOptions {
|
||||
type: ServerlessDriverType.LOCAL;
|
||||
options: LocalDriverOptions;
|
||||
@@ -19,6 +24,7 @@ export interface LambdaDriverFactoryOptions {
|
||||
}
|
||||
|
||||
export type ServerlessModuleOptions =
|
||||
| DisabledDriverFactoryOptions
|
||||
| LocalDriverFactoryOptions
|
||||
| LambdaDriverFactoryOptions;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type DynamicModule, Global } from '@nestjs/common';
|
||||
|
||||
import { AddPackagesCommand } from 'src/engine/core-modules/serverless/commands/add-packages.command';
|
||||
import { DisabledDriver } from 'src/engine/core-modules/serverless/drivers/disabled.driver';
|
||||
import { LambdaDriver } from 'src/engine/core-modules/serverless/drivers/lambda.driver';
|
||||
import { LocalDriver } from 'src/engine/core-modules/serverless/drivers/local.driver';
|
||||
import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants';
|
||||
@@ -19,9 +20,21 @@ export class ServerlessModule {
|
||||
useFactory: async (...args: any[]) => {
|
||||
const config = await options.useFactory(...args);
|
||||
|
||||
return config?.type === ServerlessDriverType.LOCAL
|
||||
? new LocalDriver(config.options)
|
||||
: new LambdaDriver(config.options);
|
||||
switch (config?.type) {
|
||||
case ServerlessDriverType.DISABLED:
|
||||
return new DisabledDriver();
|
||||
case ServerlessDriverType.LOCAL:
|
||||
return new LocalDriver(config.options);
|
||||
case ServerlessDriverType.LAMBDA:
|
||||
return new LambdaDriver(config.options);
|
||||
default: {
|
||||
const unknownConfig = config as { type?: string };
|
||||
|
||||
throw new Error(
|
||||
`Unknown serverless driver type: ${unknownConfig?.type}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
|
||||
@@ -375,7 +375,7 @@ export class ConfigVariables {
|
||||
type: ConfigVariableType.BOOLEAN,
|
||||
})
|
||||
@IsOptional()
|
||||
IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS = false;
|
||||
IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS = true;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.STORAGE_CONFIG,
|
||||
|
||||
Reference in New Issue
Block a user