1
0
Fork 0
mirror of synced 2024-07-01 20:50:49 +12:00
appwrite/app/executor.php

850 lines
31 KiB
PHP
Raw Normal View History

2021-08-24 21:32:27 +12:00
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Appwrite\Utopia\Response;
use Swoole\ConnectionPool;
use Swoole\Coroutine as Co;
2021-08-24 21:32:27 +12:00
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
2022-01-24 06:21:23 +13:00
use Swoole\Http\Server;
use Swoole\Process;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
2022-01-19 22:33:48 +13:00
use Utopia\Logger\Log;
2022-01-24 06:21:23 +13:00
use Utopia\Orchestration\Adapter\DockerCLI;
use Utopia\Orchestration\Orchestration;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Utopia\Swoole\Request;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
2022-01-24 06:21:23 +13:00
use Utopia\Validator\JSON;
use Utopia\Validator\Range as ValidatorRange;
2022-01-24 06:21:23 +13:00
use Utopia\Validator\Text;
2021-08-27 21:21:28 +12:00
2021-10-13 00:54:50 +13:00
require_once __DIR__ . '/init.php';
2021-08-24 21:32:27 +12:00
2022-01-21 23:42:12 +13:00
Authorization::disable();
2022-01-21 00:34:50 +13:00
Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
2022-01-21 23:42:12 +13:00
function logError(Throwable $error, string $action, Utopia\Route $route = null)
{
global $register;
2021-08-24 21:32:27 +12:00
2022-01-21 23:42:12 +13:00
$logger = $register->get('logger');
2022-01-21 00:34:50 +13:00
2022-01-21 23:42:12 +13:00
if ($logger) {
2022-01-19 05:55:53 +13:00
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$log = new Log();
$log->setNamespace("executor");
$log->setServer(\gethostname());
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
2021-08-27 21:21:28 +12:00
2022-01-21 23:42:12 +13:00
if ($route) {
2022-01-19 05:55:53 +13:00
$log->addTag('method', $route->getMethod());
$log->addTag('url', $route->getPath());
}
2022-01-19 05:55:53 +13:00
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
2022-01-19 05:55:53 +13:00
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->setAction($action);
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
$responseCode = $logger->addLog($log);
2022-01-21 23:42:12 +13:00
Console::info('Executor log pushed with status code: ' . $responseCode);
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
Console::error('[Error] Type: ' . get_class($error));
Console::error('[Error] Message: ' . $error->getMessage());
Console::error('[Error] File: ' . $error->getFile());
Console::error('[Error] Line: ' . $error->getLine());
};
2022-01-17 23:18:31 +13:00
$orchestrationPool = new ConnectionPool(function () {
2022-01-19 05:55:53 +13:00
$dockerUser = App::getEnv('DOCKERHUB_PULL_USERNAME', null);
$dockerPass = App::getEnv('DOCKERHUB_PULL_PASSWORD', null);
$orchestration = new Orchestration(new DockerCLI($dockerUser, $dockerPass));
2022-01-17 23:09:29 +13:00
return $orchestration;
2022-01-22 07:56:37 +13:00
}, 6);
2022-01-24 06:21:23 +13:00
try {
2022-01-19 05:55:53 +13:00
$runtimes = Config::getParam('runtimes');
2022-01-17 23:09:29 +13:00
2022-01-19 05:55:53 +13:00
// Warmup: make sure images are ready to run fast 🚀
Co\run(function () use ($runtimes, $orchestrationPool) {
2022-01-19 05:55:53 +13:00
foreach ($runtimes as $runtime) {
go(function () use ($runtime, $orchestrationPool) {
try {
$orchestration = $orchestrationPool->get();
Console::info('Warming up ' . $runtime['name'] . ' ' . $runtime['version'] . ' environment...');
2021-08-24 21:32:27 +12:00
$response = $orchestration->pull($runtime['image']);
2021-08-24 21:32:27 +12:00
if ($response) {
Console::success("Successfully Warmed up {$runtime['name']} {$runtime['version']}!");
} else {
Console::warning("Failed to Warmup {$runtime['name']} {$runtime['version']}!");
}
} catch (\Throwable $th) {
} finally {
$orchestrationPool->put($orchestration);
2022-01-19 05:55:53 +13:00
}
});
2021-08-24 21:32:27 +12:00
}
2022-01-19 05:55:53 +13:00
});
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$activeFunctions = new Swoole\Table(1024);
$activeFunctions->column('id', Swoole\Table::TYPE_STRING, 512);
$activeFunctions->column('name', Swoole\Table::TYPE_STRING, 512);
$activeFunctions->column('status', Swoole\Table::TYPE_STRING, 512);
$activeFunctions->column('key', Swoole\Table::TYPE_STRING, 4096);
$activeFunctions->create();
Co\run(function () use ($orchestrationPool, $activeFunctions) {
try {
$orchestration = $orchestrationPool->get();
$executionStart = \microtime(true);
$residueList = $orchestration->list(['label' => 'appwrite-type=function']);
} catch (\Throwable $th) {
} finally {
$orchestrationPool->put($orchestration);
}
2022-01-19 05:55:53 +13:00
foreach ($residueList as $value) {
2022-01-21 23:42:12 +13:00
go(fn () => $activeFunctions->set($value->getName(), [
2022-01-19 05:55:53 +13:00
'id' => $value->getId(),
'name' => $value->getName(),
'status' => $value->getStatus(),
'private-key' => ''
2022-01-21 23:42:12 +13:00
]));
2022-01-19 05:55:53 +13:00
}
2022-01-19 05:55:53 +13:00
$executionEnd = \microtime(true);
Console::info(count($activeFunctions) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds');
});
} catch (\Throwable $error) {
call_user_func($logError, $error, "startupError");
}
function createRuntimeServer(string $projectId, string $deploymentId, array $build, array $vars, string $baseImage, string $runtime): array
2022-01-19 05:55:53 +13:00
{
global $orchestrationPool;
2022-01-19 05:55:53 +13:00
global $activeFunctions;
$orchestration = $orchestrationPool->get();
try {
$container = 'appwrite-function-' . $deploymentId;
if ($activeFunctions->exists($container) && !(\substr($activeFunctions->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online
// If container is online then stop and remove it
2022-01-19 05:55:53 +13:00
try {
$orchestration->remove($container, true);
} catch (Exception $e) {
throw new Exception('Failed to remove container: ' . $e->getMessage());
2022-01-19 05:55:53 +13:00
}
$activeFunctions->del($container);
}
2021-08-24 21:32:27 +12:00
/** Storage stuff */
$deploymentPath = $build['outputPath'];
$deploymentPathTarget = '/tmp/project-' . $projectId . '/' . $build['$id'] . '/builtCode/code.tar.gz';
2022-01-25 13:36:33 +13:00
$deploymentPathTargetDir = \pathinfo($deploymentPathTarget, PATHINFO_DIRNAME);
2022-01-19 05:55:53 +13:00
$device = Storage::getDevice('builds');
2022-01-25 13:36:33 +13:00
if (!\file_exists($deploymentPathTargetDir)) {
if (@\mkdir($deploymentPathTargetDir, 0777, true)) {
\chmod($deploymentPathTargetDir, 0777);
} else {
2022-01-25 13:36:33 +13:00
throw new Exception('Can\'t create directory ' . $deploymentPathTargetDir);
}
2022-01-19 05:55:53 +13:00
}
2022-01-25 13:36:33 +13:00
if (!\file_exists($deploymentPathTarget)) {
if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
2022-01-25 13:36:33 +13:00
if (!\copy($deploymentPath, $deploymentPathTarget)) {
throw new Exception('Can\'t create temporary code file ' . $deploymentPathTarget);
}
} else {
2022-01-25 13:36:33 +13:00
$buffer = $device->read($deploymentPath);
\file_put_contents($deploymentPathTarget, $buffer);
2022-01-19 05:55:53 +13:00
}
};
/** End Storage stuff */
// Generate random secret key
$secret = \bin2hex(\random_bytes(16));
$vars = \array_merge($vars, [
// 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''),
// 'APPWRITE_FUNCTION_ID' => $function->getId(),
// 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
// 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
// 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'],
// 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'],
'APPWRITE_FUNCTION_PROJECT_ID' => $projectId,
'INTERNAL_RUNTIME_KEY' => $secret
]);
2022-01-19 05:55:53 +13:00
/** Launch Runtime */
if (!$activeFunctions->exists($container)) {
$executionStart = \microtime(true);
$executionTime = \time();
2022-01-19 05:55:53 +13:00
$vars = array_map(fn ($v) => strval($v), $vars);
$orchestration
->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', '1'))
->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', '256'))
->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', '256'));
2022-01-19 05:55:53 +13:00
$id = $orchestration->run(
image: $baseImage,
name: $container,
vars: $vars,
labels: [
'appwrite-type' => 'function',
'appwrite-created' => strval($executionTime),
'appwrite-runtime' => $runtime,
'appwrite-project' => $projectId,
'appwrite-deployment' => $deploymentId,
],
hostname: $container,
2022-01-25 13:36:33 +13:00
mountFolder: $deploymentPathTargetDir,
);
if (empty($id)) {
throw new Exception('Failed to create container');
}
2022-01-19 05:55:53 +13:00
// Add to network
$orchestration->networkConnect($container, App::getEnv('_APP_EXECUTOR_RUNTIME_NETWORK', 'appwrite_runtimes'));
2022-01-19 05:55:53 +13:00
$executionEnd = \microtime(true);
2022-01-19 05:55:53 +13:00
$activeFunctions->set($container, [
'id' => $id,
'name' => $container,
'status' => 'Up ' . \round($executionEnd - $executionStart, 2) . 's',
'key' => $secret,
]);
}
/** End Launch Runtime */
Console::success('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds');
} catch (\Throwable $th) {
$build['status'] = 'failed';
Console::error('Runtime Server Creation Failed: '. $th->getMessage());
} finally {
$orchestrationPool->put($orchestration);
return $build;
2022-01-19 05:55:53 +13:00
}
};
2022-02-06 08:49:57 +13:00
function execute(string $projectId, string $functionId, string $deploymentId, array $build, array $vars, string $data, string $userId, string $baseImage, string $runtime, string $entrypoint, int $timeout, array $webhooks = []): array
2022-01-19 05:55:53 +13:00
{
2022-01-19 05:55:53 +13:00
Console::info('Executing function: ' . $functionId);
global $activeFunctions;
$container = 'appwrite-function-' . $deploymentId;
2022-01-19 05:55:53 +13:00
/** Create a new runtime server if there's none running */
if (!$activeFunctions->exists($container)) {
2022-02-06 22:37:50 +13:00
Console::info("Runtime server for $deploymentId not running. Creating new one...");
createRuntimeServer($projectId, $deploymentId, $build, $vars, $baseImage, $runtime);
2022-01-19 05:55:53 +13:00
}
2021-08-24 21:32:27 +12:00
$key = $activeFunctions->get('appwrite-function-' . $deploymentId, 'key');
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$stdout = '';
$stderr = '';
2022-01-19 05:55:53 +13:00
$executionStart = \microtime(true);
2022-01-19 05:55:53 +13:00
$statusCode = 0;
2022-01-19 05:55:53 +13:00
$errNo = -1;
$attempts = 0;
$max = 5;
2022-01-19 05:55:53 +13:00
$executorResponse = '';
2022-01-19 05:55:53 +13:00
// cURL request to runtime
do {
$attempts++;
$ch = \curl_init();
2022-01-19 05:55:53 +13:00
$body = \json_encode([
'path' => '/usr/code',
2022-02-06 08:49:57 +13:00
'file' => $entrypoint,
2022-01-19 05:55:53 +13:00
'env' => $vars,
'payload' => $data,
'timeout' => $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)
2022-01-19 05:55:53 +13:00
]);
2022-01-19 05:55:53 +13:00
\curl_setopt($ch, CURLOPT_URL, "http://" . $container . ":3000/");
\curl_setopt($ch, CURLOPT_POST, true);
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
2022-01-19 05:55:53 +13:00
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ?? (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900));
2022-01-19 05:55:53 +13:00
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
2022-01-19 05:55:53 +13:00
\curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . \strlen($body),
'x-internal-challenge: ' . $key,
'host: null'
]);
2022-01-19 05:55:53 +13:00
$executorResponse = \curl_exec($ch);
2022-01-19 05:55:53 +13:00
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
2022-01-19 05:55:53 +13:00
$error = \curl_error($ch);
$errNo = \curl_errno($ch);
\curl_close($ch);
if ($errNo != CURLE_COULDNT_CONNECT && $errNo != 111) {
break;
}
2022-01-19 05:55:53 +13:00
sleep(1);
} while ($attempts < $max);
2022-01-19 05:55:53 +13:00
if ($attempts >= 5) {
$stderr = 'Failed to connect to executor runtime after 5 attempts.';
$statusCode = 124;
}
2022-01-19 05:55:53 +13:00
// If timeout error
2022-01-21 23:42:12 +13:00
if (in_array($errNo, [CURLE_OPERATION_TIMEDOUT, 110])) {
2022-01-19 05:55:53 +13:00
$statusCode = 124;
}
2021-11-30 22:39:50 +13:00
2022-01-19 05:55:53 +13:00
// 110 is the Swoole error code for timeout, see: https://www.swoole.co.uk/docs/swoole-error-code
2022-01-21 03:41:27 +13:00
if ($errNo !== 0 && $errNo !== CURLE_COULDNT_CONNECT && $errNo !== CURLE_OPERATION_TIMEDOUT && $errNo !== 110) {
2022-01-19 05:55:53 +13:00
throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 500);
}
2022-01-19 05:55:53 +13:00
$executionData = [];
2022-01-19 05:55:53 +13:00
if (!empty($executorResponse)) {
$executionData = json_decode($executorResponse, true);
}
2022-01-19 05:55:53 +13:00
if (isset($executionData['code'])) {
$statusCode = $executionData['code'];
}
if ($statusCode === 500) {
if (isset($executionData['message'])) {
$stderr = $executionData['message'];
} else {
$stderr = 'Internal Runtime error';
}
2022-01-19 05:55:53 +13:00
} else if ($statusCode === 124) {
$stderr = 'Execution timed out.';
} else if ($statusCode === 0) {
$stderr = 'Execution failed.';
} else if ($statusCode >= 200 && $statusCode < 300) {
$stdout = $executorResponse;
} else {
$stderr = 'Execution failed.';
}
2022-01-19 05:55:53 +13:00
$executionEnd = \microtime(true);
$executionTime = ($executionEnd - $executionStart);
$functionStatus = ($statusCode >= 200 && $statusCode < 300) ? 'completed' : 'failed';
Console::success('Function executed in ' . $executionTime . ' seconds, status: ' . $functionStatus);
$execution = [
'status' => $functionStatus,
'statusCode' => $statusCode,
'stdout' => \utf8_encode(\mb_substr($stdout, -8000)),
'stderr' => \utf8_encode(\mb_substr($stderr, -8000)),
'time' => $executionTime,
];
return $execution;
2022-01-19 05:55:53 +13:00
};
function runBuildStage(string $buildId, string $projectID, string $path, array $vars, string $baseImage, string $runtime): array
2022-01-19 05:55:53 +13:00
{
global $orchestrationPool;
$orchestration = $orchestrationPool->get();
$build = [];
$id = '';
2022-01-19 05:55:53 +13:00
$buildStdout = '';
$buildStderr = '';
2022-01-26 23:09:11 +13:00
$buildStart = \time();
$buildEnd = 0;
2022-01-26 23:09:11 +13:00
2022-01-19 05:55:53 +13:00
try {
Console::info('Running build stage: ' . $buildId);
// Grab Deployment Files
$deploymentPath = $path;
2022-01-19 05:55:53 +13:00
$device = Storage::getDevice('builds');
2022-01-28 01:50:22 +13:00
$deploymentPathTarget = '/tmp/project-' . $projectID . '/' . $buildId . '/code.tar.gz';
$deploymentPathTargetDir = \pathinfo($deploymentPathTarget, PATHINFO_DIRNAME);
2022-01-28 01:50:22 +13:00
$container = 'build-stage-' . $buildId;
2022-01-19 05:55:53 +13:00
// Perform various checks
if (!\file_exists($deploymentPathTargetDir)) {
if (@\mkdir($deploymentPathTargetDir, 0777, true)) {
\chmod($deploymentPathTargetDir, 0777);
} else {
throw new Exception('Can\'t create directory ' . $deploymentPathTargetDir);
2022-01-19 05:55:53 +13:00
}
}
if (!\file_exists($deploymentPathTarget)) {
2022-01-19 05:55:53 +13:00
if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
if (!\copy($deploymentPath, $deploymentPathTarget)) {
throw new Exception('Can\'t create temporary code file ' . $deploymentPathTarget);
2022-01-19 05:55:53 +13:00
}
} else {
$buffer = $device->read($deploymentPath);
\file_put_contents($deploymentPathTarget, $buffer);
2022-01-19 05:55:53 +13:00
}
}
if (!$device->exists($deploymentPath)) {
throw new Exception('Code is not readable: ' . $path);
2022-01-26 23:49:02 +13:00
}
$vars = array_map(fn ($v) => strval($v), $vars);
2022-01-28 01:50:22 +13:00
$path = '/tmp/project-' . $projectID . '/' . $buildId . '/builtCode';
if (!\file_exists($path)) {
2022-01-24 10:01:20 +13:00
if (@\mkdir($path, 0777, true)) {
\chmod($path, 0777);
} else {
2022-01-28 01:50:22 +13:00
throw new Exception('Can\'t create directory /tmp/project-' . $projectID . '/' . $buildId . '/builtCode');
2022-01-19 05:55:53 +13:00
}
}
$orchestration
->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0))
->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256))
->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256));
2022-01-19 05:55:53 +13:00
$id = $orchestration->run(
image: $baseImage,
2022-01-19 05:55:53 +13:00
name: $container,
vars: $vars,
workdir: '/usr/code',
labels: [
'appwrite-type' => 'function',
'appwrite-created' => strval($buildStart),
'appwrite-runtime' => $runtime,
2022-01-19 05:55:53 +13:00
'appwrite-project' => $projectID,
2022-01-28 01:50:22 +13:00
'appwrite-build' => $buildId,
2022-01-19 05:55:53 +13:00
],
command: [
'tail',
'-f',
'/dev/null'
],
hostname: $container,
2022-01-25 13:36:33 +13:00
mountFolder: $deploymentPathTargetDir,
2022-01-19 05:55:53 +13:00
volumes: [
2022-01-28 01:50:22 +13:00
'/tmp/project-' . $projectID . '/' . $buildId . '/builtCode' . ':/usr/builtCode:rw'
2022-01-19 05:55:53 +13:00
]
);
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
if (empty($id)) {
throw new Exception('Failed to start build container');
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
// Extract user code into build container
$untarStdout = '';
$untarStderr = '';
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$untarSuccess = $orchestration->execute(
name: $container,
command: [
'sh',
'-c',
'mkdir -p /usr/code && cp /tmp/code.tar.gz /usr/workspace/code.tar.gz && cd /usr/workspace/ && tar -zxf /usr/workspace/code.tar.gz -C /usr/code && rm /usr/workspace/code.tar.gz'
],
stdout: $untarStdout,
stderr: $untarStderr,
timeout: 60
);
2021-08-27 22:55:22 +12:00
2022-01-19 05:55:53 +13:00
if (!$untarSuccess) {
throw new Exception('Failed to extract tar: ' . $untarStderr);
}
2021-08-27 22:55:22 +12:00
2022-01-19 05:55:53 +13:00
// Build Code / Install Dependencies
$buildSuccess = $orchestration->execute(
name: $container,
command: ['sh', '-c', 'cd /usr/local/src && ./build.sh'],
stdout: $buildStdout,
stderr: $buildStderr,
timeout: App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900)
);
if (!$buildSuccess) {
throw new Exception('Failed to build dependencies: ' . $buildStderr);
}
2021-09-21 03:52:12 +12:00
2022-01-19 05:55:53 +13:00
// Repackage Code and Save.
$compressStdout = '';
$compressStderr = '';
2021-09-21 03:52:12 +12:00
$builtCodePath = '/tmp/project-' . $projectID . '/' . $buildId . '/builtCode/code.tar.gz';
2021-09-01 21:48:56 +12:00
2022-01-19 05:55:53 +13:00
$compressSuccess = $orchestration->execute(
name: $container,
command: [
'tar', '-C', '/usr/code', '-czvf', '/usr/builtCode/code.tar.gz', './'
],
stdout: $compressStdout,
stderr: $compressStderr,
timeout: 60
);
2022-01-19 05:55:53 +13:00
if (!$compressSuccess) {
throw new Exception('Failed to compress built code: ' . $compressStderr);
}
2021-09-21 03:52:12 +12:00
2022-01-19 05:55:53 +13:00
// Check if the build was successful by checking if file exists
if (!\file_exists($builtCodePath)) {
throw new Exception('Something went wrong during the build process.');
}
2021-10-01 03:36:09 +13:00
2022-01-19 05:55:53 +13:00
// Upload new code
$device = Storage::getDevice('builds');
2022-01-19 05:55:53 +13:00
$path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
2021-08-27 22:55:22 +12:00
2022-01-19 05:55:53 +13:00
if (!\file_exists(\dirname($path))) { // Checks if directory path to file exists
2022-01-24 10:01:20 +13:00
if (@\mkdir(\dirname($path), 0777, true)) {
\chmod(\dirname($path), 0777);
} else {
2022-01-19 05:55:53 +13:00
throw new Exception('Can\'t create directory: ' . \dirname($path));
}
2021-08-27 22:55:22 +12:00
}
2022-01-19 05:55:53 +13:00
if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
if (!$device->move($builtCodePath, $path)) {
throw new Exception('Failed to upload built code upload to storage', 500);
}
} else {
if (!$device->upload($builtCodePath, $path)) {
throw new Exception('Failed to upload built code upload to storage', 500);
}
}
2021-08-27 22:55:22 +12:00
2022-01-31 23:57:57 +13:00
if ($buildStdout === '') {
2022-01-19 05:55:53 +13:00
$buildStdout = 'Build Successful!';
}
$buildEnd = \time();
$build = [
'$id' => $buildId,
'outputPath' => $path,
'status' => 'ready',
'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)),
'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)),
'startTime' => $buildStart,
'endTime' => $buildEnd,
'duration' => $buildEnd - $buildStart,
];
Console::success('Build Stage Ran in ' . ($buildEnd - $buildStart) . ' seconds');
} catch (Throwable $th) {
$buildEnd = \time();
$buildStderr = $th->getMessage();
$build = [
'$id' => $buildId,
'status' => 'failed',
'stdout' => \utf8_encode(\mb_substr($buildStdout, -4096)),
'stderr' => \utf8_encode(\mb_substr($buildStderr, -4096)),
'startTime' => $buildStart,
'endTime' => $buildEnd,
'duration' => $buildEnd - $buildStart,
];
Console::error('Build failed: ' . $th->getMessage());
} finally {
if (!empty($id)) {
2022-01-19 05:55:53 +13:00
$orchestration->remove($id, true);
}
$orchestrationPool->put($orchestration);
return $build;
2022-01-19 05:55:53 +13:00
}
}
2021-08-24 21:32:27 +12:00
2022-02-06 22:25:10 +13:00
App::post('/v1/execution')
2022-02-06 22:25:33 +13:00
->desc('Create a function execution')
->param('functionId', '', new Text(1024), 'The FunctionID to execute')
->param('deploymentId', '', new Text(1024), 'The deployment ID to execute')
->param('buildId', '', new Text(1024), 'The build ID of the function')
->param('path', '', new Text(0), 'Path to built files.', false)
->param('vars', '', new Assoc(), 'Environment Variables required for the build', false)
->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true)
->param('runtime', '', new Text(128), 'Runtime for the cloud function', false)
2022-02-06 08:49:57 +13:00
->param('entrypoint', '', new Text(256), 'Entrypoint of the code file')
->param('timeout', 15, new ValidatorRange(1, 900), 'Function maximum execution time in seconds.', true)
->param('baseImage', '', new Text(128), 'Base image name of the runtime', false)
->param('webhooks', [], new ArrayList(new JSON()), 'Any webhooks that need to be triggered after this execution', true)
->param('userId', '', new Text(1024), 'The UserID of the user who triggered the execution if it was called from a client SDK', true)
->inject('projectId')
2022-01-24 06:21:23 +13:00
->inject('response')
->action(
2022-02-06 08:49:57 +13:00
function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $data, string $runtime, string $entrypoint, $timeout, string $baseImage, array $webhooks, string $userId, string $projectId, Response $response) {
2022-01-24 06:21:23 +13:00
$build = [
'$id' => $buildId,
'outputPath' => $path,
];
2022-01-26 12:45:41 +13:00
// Send both data and vars from the caller
2022-02-06 08:49:57 +13:00
$execution = execute($projectId, $functionId, $deploymentId, $build, $vars, $data, $userId, $baseImage, $runtime, $entrypoint, $timeout, $webhooks);
2022-01-26 12:45:41 +13:00
$response
->setStatusCode(Response::STATUS_CODE_OK)
->json($execution);
2022-01-26 12:45:41 +13:00
}
);
2022-01-26 12:45:41 +13:00
App::delete('/v1/deployments/:deploymentId')
2022-01-27 11:29:49 +13:00
->desc('Delete a deployment')
->param('deploymentId', '', new UID(), 'Deployment unique ID.', false)
->param('buildIds', [], new ArrayList(new Text(0), 100), 'List of build IDs to delete.', false)
2022-01-24 06:21:23 +13:00
->inject('response')
->action(function (string $deploymentId, array $buildIds, Response $response) use ($orchestrationPool) {
2022-02-01 12:44:55 +13:00
Console::info('Deleting deployment: ' . $deploymentId);
$orchestration = $orchestrationPool->get();
// Remove the container of the deployment
$status = $orchestration->remove('appwrite-function-' . $deploymentId , true);
if ($status) {
Console::success('Removed container for deployment: ' . $deploymentId);
} else {
Console::error('Failed to remove container for deployment: ' . $deploymentId);
}
// Remove all the build containers
foreach ($buildIds as $buildId) {
try {
Console::info('Deleting build container : ' . $buildId);
$status = $orchestration->remove('build-stage-' . $buildId, true);
} catch (Throwable $th) {
Console::error($th->getMessage());
2022-01-24 06:21:23 +13:00
}
}
$orchestrationPool->put($orchestration);
2022-01-24 06:21:23 +13:00
$response
->setStatusCode(Response::STATUS_CODE_OK)
->send();
2022-01-24 06:21:23 +13:00
});
2022-01-27 11:29:49 +13:00
App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
->desc("Create a new build")
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('deploymentId', '', new UID(), 'Deployment unique ID.', false)
2022-01-24 06:21:23 +13:00
->param('buildId', '', new UID(), 'Build unique ID.', false)
->param('path', '', new Text(0), 'Path to source files.', false)
->param('vars', '', new Assoc(), 'Environment Variables required for the build', false)
->param('runtime', '', new Text(128), 'Runtime for the cloud function', false)
->param('baseImage', '', new Text(128), 'Base image name of the runtime', false)
2022-01-27 11:29:49 +13:00
->inject('projectId')
->inject('response')
->action(function (string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $runtime, string $baseImage, string $projectId, Response $response) {
2022-01-27 11:29:49 +13:00
$build = runBuildStage($buildId, $projectId, $path, $vars, $baseImage, $runtime);
2022-01-27 11:29:49 +13:00
if ( $build['status'] === 'ready') {
$build = createRuntimeServer($projectId, $deploymentId, $build, $vars, $baseImage, $runtime);
2022-01-27 11:29:49 +13:00
}
$response
->setStatusCode(201)
->json($build);
2022-01-24 06:21:23 +13:00
});
2022-01-19 05:55:53 +13:00
App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode
2022-01-24 06:21:23 +13:00
$http = new Server("0.0.0.0", 80);
2021-08-24 21:32:27 +12:00
2022-02-04 05:22:56 +13:00
// function handleShutdown()
// {
// global $orchestrationPool;
// global $register;
2022-01-21 23:42:12 +13:00
2022-02-04 05:22:56 +13:00
// try {
// Console::info('Cleaning up containers before shutdown...');
2021-08-24 21:32:27 +12:00
2022-02-04 05:22:56 +13:00
// // Remove all containers.
2021-08-24 21:32:27 +12:00
2022-02-04 05:22:56 +13:00
// /** @var Orchestration $orchestration */
// $orchestration = $orchestrationPool->get();
2021-08-24 21:32:27 +12:00
2022-02-04 05:22:56 +13:00
// $functionsToRemove = $orchestration->list(['label' => 'appwrite-type=function']);
2021-08-24 21:32:27 +12:00
2022-02-04 05:22:56 +13:00
// foreach ($functionsToRemove as $container) {
// go(fn () => $orchestration->remove($container->getId(), true));
2021-10-13 00:54:50 +13:00
2022-02-04 05:22:56 +13:00
// // Get a database instance
// $db = $register->get('dbPool')->get();
// $cache = $register->get('redisPool')->get();
2021-10-13 00:54:50 +13:00
2022-02-04 05:22:56 +13:00
// $cache = new Cache(new RedisCache($cache));
// $database = new Database(new MariaDB($db), $cache);
// $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
// $database->setNamespace('_project_' . $container->getLabels()["appwrite-project"]);
2021-10-13 00:54:50 +13:00
2022-02-04 05:22:56 +13:00
// // Get list of all processing executions
// $executions = $database->find('executions', [
// new Query('deploymentId', Query::TYPE_EQUAL, [$container->getLabels()["appwrite-deployment"]]),
// new Query('status', Query::TYPE_EQUAL, ['waiting'])
// ]);
2021-08-24 21:32:27 +12:00
2022-02-04 05:22:56 +13:00
// // Mark all processing executions as failed
// foreach ($executions as $execution) {
// $execution
// ->setAttribute('status', 'failed')
// ->setAttribute('statusCode', 1)
// ->setAttribute('stderr', 'Appwrite was shutdown during execution');
2021-08-24 21:32:27 +12:00
2022-02-04 05:22:56 +13:00
// $database->updateDocument('executions', $execution->getId(), $execution);
// }
2021-08-24 21:32:27 +12:00
2022-02-04 05:22:56 +13:00
// Console::info('Removed container ' . $container->getName());
// }
// } catch (\Throwable $error) {
// logError($error, 'shutdownError');
// } finally {
// $orchestrationPool->put($orchestration);
// }
// };
2021-08-24 21:32:27 +12:00
2022-01-21 23:42:12 +13:00
$http->on('start', function ($http) {
@Process::signal(SIGINT, function () use ($http) {
2022-02-04 05:22:56 +13:00
// handleShutdown();
$http->shutdown();
});
2022-01-21 23:42:12 +13:00
@Process::signal(SIGQUIT, function () use ($http) {
2022-02-04 05:22:56 +13:00
// handleShutdown();
$http->shutdown();
});
2022-01-21 23:42:12 +13:00
@Process::signal(SIGKILL, function () use ($http) {
2022-02-04 05:22:56 +13:00
// handleShutdown();
$http->shutdown();
});
2022-01-21 23:42:12 +13:00
@Process::signal(SIGTERM, function () use ($http) {
2022-02-04 05:22:56 +13:00
// handleShutdown();
$http->shutdown();
});
});
2022-01-21 23:42:12 +13:00
$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) {
2021-08-24 21:32:27 +12:00
$request = new Request($swooleRequest);
$response = new Response($swooleResponse);
$app = new App('UTC');
$projectId = $request->getHeader('x-appwrite-project', '');
Storage::setDevice('functions', new Local(APP_STORAGE_FUNCTIONS . '/app-' . $projectId));
Storage::setDevice('builds', new Local(APP_STORAGE_BUILDS . '/app-' . $projectId));
// Check environment variable key
$secretKey = $request->getHeader('x-appwrite-executor-key', '');
if (empty($secretKey)) {
$swooleResponse->status(401);
return $swooleResponse->end('401: Authentication Error');
}
if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) {
$swooleResponse->status(401);
return $swooleResponse->end('401: Authentication Error');
}
2022-01-21 23:42:12 +13:00
App::error(function ($error, $utopia, $request, $response) {
2021-08-24 21:32:27 +12:00
/** @var Exception $error */
/** @var Utopia\App $utopia */
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
2021-08-24 21:32:27 +12:00
if ($error instanceof PDOException) {
throw $error;
}
2021-08-24 21:32:27 +12:00
$route = $utopia->match($request);
2022-01-21 23:42:12 +13:00
logError($error, "httpError", $route);
2021-08-24 21:32:27 +12:00
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
$code = $error->getCode();
$message = $error->getMessage();
2021-08-24 21:32:27 +12:00
$output = ((App::isDevelopment())) ? [
'message' => $error->getMessage(),
'code' => $error->getCode(),
'file' => $error->getFile(),
'line' => $error->getLine(),
'trace' => $error->getTrace(),
'version' => $version,
] : [
'message' => $message,
'code' => $code,
'version' => $version,
];
2021-08-24 21:32:27 +12:00
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
2022-01-21 00:34:50 +13:00
->setStatusCode(500);
$response->dynamic(
new Document($output),
$utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR
);
2021-08-24 21:32:27 +12:00
}, ['error', 'utopia', 'request', 'response']);
2022-01-27 11:29:49 +13:00
App::setResource('projectId', function () use ($projectId) {
2021-08-24 21:32:27 +12:00
return $projectId;
});
try {
$app->run($request, $response);
} catch (Exception $e) {
2022-01-21 23:42:12 +13:00
logError($e, "serverError");
2021-08-24 21:32:27 +12:00
$swooleResponse->end('500: Server Error');
}
});
$http->start();