mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch 'feat-functions-refactor' of github.com:appwrite/appwrite into feat-add-builds-worker
This commit is contained in:
@@ -2015,7 +2015,7 @@ $collections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'automaticDeploy',
|
||||
'$id' => 'deploy',
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
@@ -2116,7 +2116,7 @@ $collections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'buildTime',
|
||||
'$id' => 'time',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
@@ -2127,7 +2127,7 @@ $collections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'envVars',
|
||||
'$id' => 'vars',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
@@ -2159,6 +2159,17 @@ $collections = [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'search',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
@@ -2168,6 +2179,13 @@ $collections = [
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_search',
|
||||
'type' => Database::INDEX_FULLTEXT,
|
||||
'attributes' => ['search'],
|
||||
'lengths' => [2048],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -433,7 +433,7 @@ App::delete('/v1/functions/:functionId')
|
||||
|
||||
// Request executor to delete tag containers
|
||||
$ch = \curl_init();
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor:8080/v1/cleanup/function");
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/cleanup/function");
|
||||
\curl_setopt($ch, CURLOPT_POST, true);
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||
'functionId' => $functionId
|
||||
@@ -496,14 +496,14 @@ App::post('/v1/functions/:functionId/tags')
|
||||
->param('functionId', '', new UID(), 'Function ID.')
|
||||
->param('entrypoint', '', new Text('1028'), 'Entrypoint File.')
|
||||
->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', false)
|
||||
->param('automaticDeploy', false, new Boolean(true), 'Automatically deploy the function when it is finished building.', false)
|
||||
->param('deploy', false, new Boolean(true), 'Automatically deploy the function when it is finished building.', false)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('usage')
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->action(function ($functionId, $entrypoint, $file, $automaticDeploy, $request, $response, $dbForProject, $usage, $user, $project) {
|
||||
->action(function ($functionId, $entrypoint, $file, $deploy, $request, $response, $dbForProject, $usage, $user, $project) {
|
||||
/** @var Utopia\Swoole\Request $request */
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Utopia\Database\Database $dbForProject */
|
||||
@@ -552,15 +552,15 @@ App::post('/v1/functions/:functionId/tags')
|
||||
throw new Exception('Failed moving file', 500);
|
||||
}
|
||||
|
||||
if ((bool) $automaticDeploy) {
|
||||
// Remove automaticDeploy for all other tags.
|
||||
if ((bool) $deploy) {
|
||||
// Remove deploy for all other tags.
|
||||
$tags = $dbForProject->find('tags', [
|
||||
new Query('automaticDeploy', Query::TYPE_EQUAL, [true]),
|
||||
new Query('deploy', Query::TYPE_EQUAL, [true]),
|
||||
new Query('functionId', Query::TYPE_EQUAL, [$functionId])
|
||||
]);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$tag->setAttribute('automaticDeploy', false);
|
||||
$tag->setAttribute('deploy', false);
|
||||
$dbForProject->updateDocument('tags', $tag->getId(), $tag);
|
||||
}
|
||||
}
|
||||
@@ -581,7 +581,7 @@ App::post('/v1/functions/:functionId/tags')
|
||||
'status' => 'processing',
|
||||
'buildStdout' => '',
|
||||
'buildStderr' => '',
|
||||
'automaticDeploy' => ($automaticDeploy === 'true'),
|
||||
'deploy' => ($deploy === 'true'),
|
||||
]));
|
||||
|
||||
$usage
|
||||
@@ -743,7 +743,7 @@ App::delete('/v1/functions/:functionId/tags/:tagId')
|
||||
|
||||
// Request executor to delete tag containers
|
||||
$ch = \curl_init();
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor:8080/v1/cleanup/tag");
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/cleanup/tag");
|
||||
\curl_setopt($ch, CURLOPT_POST, true);
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||
'tagId' => $tagId
|
||||
@@ -901,7 +901,7 @@ App::post('/v1/functions/:functionId/executions')
|
||||
|
||||
// Directly execute function.
|
||||
$ch = \curl_init();
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor:8080/v1/execute");
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/execute");
|
||||
\curl_setopt($ch, CURLOPT_POST, true);
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||
'trigger' => 'http',
|
||||
@@ -1029,50 +1029,47 @@ App::get('/v1/functions/:functionId/executions/:executionId')
|
||||
});
|
||||
|
||||
App::get('/v1/builds')
|
||||
->groups(['api', 'functions'])
|
||||
->desc('Get Builds')
|
||||
->label('scope', 'execution.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'functions')
|
||||
->label('sdk.method', 'listBuilds')
|
||||
->label('sdk.description', '/docs/references/functions/list-builds.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_BUILD_LIST)
|
||||
->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
|
||||
->param('offset', 0, new Range(0, 2000), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->param('cursor', '', new UID(), 'ID of the build used as the starting point for the query, excluding the build itself. Should be used for efficient pagination when working with large sets of data.', true)
|
||||
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function ($limit, $offset, $search, $cursor, $cursorDirection, $response, $dbForProject) {
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Utopia\Database\Database $dbForProject */
|
||||
->groups(['api', 'functions'])
|
||||
->desc('List Builds')
|
||||
->label('scope', 'functions.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'functions')
|
||||
->label('sdk.method', 'builds')
|
||||
->label('sdk.description', '/docs/references/functions/list-builds.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_BUILD_LIST)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->param('limit', 25, new Range(0, 100), 'Maximum number of builds to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
|
||||
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
|
||||
->param('cursor', '', new UID(), 'ID of the function used as the starting point for the query, excluding the function itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
|
||||
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
|
||||
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function ($search, $limit, $offset, $cursor, $cursorDirection, $orderType, $response, $dbForProject) {
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Utopia\Database\Database $dbForProject */
|
||||
|
||||
if (!empty($cursor)) {
|
||||
$cursorExecution = $dbForProject->getDocument('builds', $cursor);
|
||||
if (!empty($cursor)) {
|
||||
$cursorFunction = $dbForProject->getDocument('builds', $cursor);
|
||||
|
||||
if ($cursorExecution->isEmpty()) {
|
||||
throw new Exception("Execution '{$cursor}' for the 'cursor' value not found.", 400);
|
||||
}
|
||||
if ($cursorFunction->isEmpty()) {
|
||||
throw new Exception("Build '{$cursor}' for the 'cursor' value not found.", 400);
|
||||
}
|
||||
}
|
||||
|
||||
$queries = [];
|
||||
$queries = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
|
||||
}
|
||||
if (!empty($search)) {
|
||||
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
|
||||
}
|
||||
|
||||
$results = $dbForProject->find('builds', $queries, $limit, $offset, [], [Database::ORDER_DESC], $cursorExecution ?? null, $cursorDirection);
|
||||
|
||||
$sum = $dbForProject->count('builds', $queries, APP_LIMIT_COUNT);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'builds' => $results,
|
||||
'sum' => $sum,
|
||||
]), Response::MODEL_BUILD_LIST);
|
||||
});
|
||||
$response->dynamic(new Document([
|
||||
'builds' => $dbForProject->find('builds', $queries, $limit, $offset, [], [$orderType], $cursorFunction ?? null, $cursorDirection),
|
||||
'sum' => $dbForProject->count('builds', $queries, APP_LIMIT_COUNT),
|
||||
]), Response::MODEL_BUILD_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/builds/:buildId')
|
||||
->groups(['api', 'functions'])
|
||||
|
||||
+432
-323
@@ -1,40 +1,40 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Stats\Stats;
|
||||
use Utopia\App;
|
||||
use Utopia\Swoole\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\CLI\Console;
|
||||
use Swoole\Process;
|
||||
use Swoole\Http\Server;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
use Cron\CronExpression;
|
||||
use Swoole\ConnectionPool;
|
||||
use Swoole\Coroutine as Co;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Swoole\Http\Response as SwooleResponse;
|
||||
use Utopia\Orchestration\Orchestration;
|
||||
use Utopia\Database\Adapter\MariaDB;
|
||||
use Swoole\Http\Server;
|
||||
use Swoole\Process;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Adapter\MariaDB;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Orchestration\Adapter\DockerAPI;
|
||||
use Utopia\Orchestration\Adapter\DockerCLI;
|
||||
use Utopia\Orchestration\Orchestration;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\JSON;
|
||||
use Utopia\Validator\Text;
|
||||
use Cron\CronExpression;
|
||||
use Swoole\ConnectionPool;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Storage;
|
||||
use Swoole\Coroutine as Co;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Orchestration\Adapter\DockerCLI;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Orchestration\Adapter\DockerAPI;
|
||||
use Utopia\Registry\Registry;
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
@@ -91,6 +91,7 @@ $orchestrationPool = new ConnectionPool(function () {
|
||||
|
||||
return $orchestration;
|
||||
}, 6);
|
||||
|
||||
try {
|
||||
$runtimes = Config::getParam('runtimes');
|
||||
|
||||
@@ -98,19 +99,22 @@ try {
|
||||
Co\run(function () use ($runtimes, $orchestrationPool) {
|
||||
foreach ($runtimes as $runtime) {
|
||||
go(function () use ($runtime, $orchestrationPool) {
|
||||
$orchestration = $orchestrationPool->get();
|
||||
try {
|
||||
$orchestration = $orchestrationPool->get();
|
||||
|
||||
Console::info('Warming up ' . $runtime['name'] . ' ' . $runtime['version'] . ' environment...');
|
||||
Console::info('Warming up ' . $runtime['name'] . ' ' . $runtime['version'] . ' environment...');
|
||||
|
||||
$response = $orchestration->pull($runtime['image']);
|
||||
$response = $orchestration->pull($runtime['image']);
|
||||
|
||||
if ($response) {
|
||||
Console::success("Successfully Warmed up {$runtime['name']} {$runtime['version']}!");
|
||||
} else {
|
||||
Console::warning("Failed to Warmup {$runtime['name']} {$runtime['version']}!");
|
||||
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);
|
||||
}
|
||||
|
||||
$orchestrationPool->put($orchestration);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -123,11 +127,15 @@ try {
|
||||
$activeFunctions->create();
|
||||
|
||||
Co\run(function () use ($orchestrationPool, $activeFunctions) {
|
||||
$orchestration = $orchestrationPool->get();
|
||||
$executionStart = \microtime(true);
|
||||
try {
|
||||
$orchestration = $orchestrationPool->get();
|
||||
$executionStart = \microtime(true);
|
||||
$residueList = $orchestration->list(['label' => 'appwrite-type=function']);
|
||||
} catch (\Throwable $th) {
|
||||
} finally {
|
||||
$orchestrationPool->put($orchestration);
|
||||
}
|
||||
|
||||
$residueList = $orchestration->list(['label' => 'appwrite-type=function']);
|
||||
$orchestrationPool->put($orchestration);
|
||||
|
||||
foreach ($residueList as $value) {
|
||||
go(fn () => $activeFunctions->set($value->getName(), [
|
||||
@@ -195,7 +203,7 @@ function createRuntimeServer(string $functionId, string $projectId, string $tagI
|
||||
'INTERNAL_RUNTIME_KEY' => $secret
|
||||
]);
|
||||
|
||||
$vars = \array_merge($vars, $build->getAttribute('envVars', [])); // for gettng endpoint.
|
||||
$vars = \array_merge($vars, $build->getAttribute('vars', [])); // for gettng endpoint.
|
||||
|
||||
$container = 'appwrite-function-' . $tag->getId();
|
||||
|
||||
@@ -234,7 +242,9 @@ function createRuntimeServer(string $functionId, string $projectId, string $tagI
|
||||
$device = Storage::getDevice('builds');
|
||||
|
||||
if (!\file_exists($tagPathTargetDir)) {
|
||||
if (!\mkdir($tagPathTargetDir, 0777, true)) {
|
||||
if (@\mkdir($tagPathTargetDir, 0777, true)) {
|
||||
\chmod($tagPathTargetDir, 0777);
|
||||
} else {
|
||||
throw new Exception('Can\'t create directory ' . $tagPathTargetDir);
|
||||
}
|
||||
}
|
||||
@@ -268,9 +278,7 @@ function createRuntimeServer(string $functionId, string $projectId, string $tagI
|
||||
->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', '256'))
|
||||
->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', '256'));
|
||||
|
||||
foreach ($vars as $key => $value) {
|
||||
$vars[$key] = strval($value);
|
||||
}
|
||||
$vars = array_map(fn ($v) => strval($v), $vars);
|
||||
|
||||
// Launch runtime server
|
||||
$id = $orchestration->run(
|
||||
@@ -312,9 +320,8 @@ function createRuntimeServer(string $functionId, string $projectId, string $tagI
|
||||
var_dump($th->getTraceAsString());
|
||||
$orchestrationPool->put($orchestration ?? null);
|
||||
throw $th;
|
||||
} finally {
|
||||
$orchestrationPool->put($orchestration);
|
||||
}
|
||||
$orchestrationPool->put($orchestration);
|
||||
};
|
||||
|
||||
function execute(string $trigger, string $projectId, string $executionId, string $functionId, Database $database, string $event = '', string $eventData = '', string $data = '', array $webhooks = [], string $userId = '', string $jwt = ''): array
|
||||
@@ -394,7 +401,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $projectId,
|
||||
]);
|
||||
|
||||
$vars = \array_merge($vars, $build->getAttribute('envVars', []));
|
||||
$vars = \array_merge($vars, $build->getAttribute('vars', []));
|
||||
|
||||
$container = 'appwrite-function-' . $tag->getId();
|
||||
|
||||
@@ -405,7 +412,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
|
||||
$database->createDocument('builds', new Document([
|
||||
'$id' => $buildId,
|
||||
'$read' => ($userId !== '') ? ['user:' . $userId] : [],
|
||||
'$write' => ['role:all'],
|
||||
'$write' => [],
|
||||
'dateCreated' => time(),
|
||||
'status' => 'processing',
|
||||
'outputPath' => '',
|
||||
@@ -414,8 +421,8 @@ function execute(string $trigger, string $projectId, string $executionId, string
|
||||
'sourceType' => Storage::DEVICE_LOCAL,
|
||||
'stdout' => '',
|
||||
'stderr' => '',
|
||||
'buildTime' => 0,
|
||||
'envVars' => [
|
||||
'time' => 0,
|
||||
'vars' => [
|
||||
'ENTRYPOINT_NAME' => $tag->getAttribute('entrypoint'),
|
||||
'APPWRITE_FUNCTION_ID' => $function->getId(),
|
||||
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
|
||||
@@ -444,7 +451,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$activeFunctions->exists($container)) { // Create contianer if not ready
|
||||
if (!$activeFunctions->exists($container)) { // Create container if not ready
|
||||
createRuntimeServer($functionId, $projectId, $tag->getId(), $database);
|
||||
} else if ($activeFunctions->get($container)['status'] === 'Down') {
|
||||
sleep(1);
|
||||
@@ -490,7 +497,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $projectId
|
||||
]);
|
||||
|
||||
$vars = \array_merge($vars, $build->getAttribute('envVars', []));
|
||||
$vars = \array_merge($vars, $build->getAttribute('vars', []));
|
||||
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
@@ -512,7 +519,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
|
||||
|
||||
$body = \json_encode([
|
||||
'path' => '/usr/code',
|
||||
'file' => $build->getAttribute('envVars', [])['ENTRYPOINT_NAME'],
|
||||
'file' => $build->getAttribute('vars', [])['ENTRYPOINT_NAME'],
|
||||
'env' => $vars,
|
||||
'payload' => $data,
|
||||
'timeout' => $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))
|
||||
@@ -652,6 +659,261 @@ function execute(string $trigger, string $projectId, string $executionId, string
|
||||
];
|
||||
};
|
||||
|
||||
function runBuildStage(string $buildId, string $projectID): Document
|
||||
{
|
||||
global $runtimes;
|
||||
global $orchestrationPool;
|
||||
global $register;
|
||||
|
||||
/** @var Orchestration $orchestration */
|
||||
$orchestration = $orchestrationPool->get();
|
||||
|
||||
$buildStdout = '';
|
||||
$buildStderr = '';
|
||||
|
||||
$db = $register->get('dbPool')->get();
|
||||
$redis = $register->get('redisPool')->get();
|
||||
$cache = new Cache(new RedisCache($redis));
|
||||
|
||||
$database = new Database(new MariaDB($db), $cache);
|
||||
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
|
||||
$database->setNamespace('_project_' . $projectID);
|
||||
|
||||
// Check if build has already been run
|
||||
$build = $database->getDocument('builds', $buildId);
|
||||
|
||||
try {
|
||||
// If we already have a built package ready there is no need to rebuild.
|
||||
if ($build->getAttribute('status') === 'ready' && \file_exists($build->getAttribute('outputPath'))) {
|
||||
return $build;
|
||||
}
|
||||
|
||||
// Update Tag Status
|
||||
$build->setAttribute('status', 'building');
|
||||
|
||||
$database->updateDocument('builds', $build->getId(), $build);
|
||||
|
||||
// Check if runtime is active
|
||||
$runtime = $runtimes[$build->getAttribute('runtime', '')] ?? null;
|
||||
|
||||
if (\is_null($runtime)) {
|
||||
throw new Exception('Runtime "' . $build->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
|
||||
// Grab Tag Files
|
||||
$tagPath = $build->getAttribute('source', '');
|
||||
$sourceType = $build->getAttribute('sourceType', '');
|
||||
|
||||
$device = Storage::getDevice('builds');
|
||||
|
||||
$tagPathTarget = '/tmp/project-' . $projectID . '/' . $build->getId() . '/code.tar.gz';
|
||||
$tagPathTargetDir = \pathinfo($tagPathTarget, PATHINFO_DIRNAME);
|
||||
|
||||
$container = 'build-stage-' . $build->getId();
|
||||
|
||||
// Perform various checks
|
||||
if (!\file_exists($tagPathTargetDir)) {
|
||||
if (@\mkdir($tagPathTargetDir, 0777, true)) {
|
||||
\chmod($tagPathTargetDir, 0777);
|
||||
} else {
|
||||
throw new Exception('Can\'t create directory ' . $tagPathTargetDir);
|
||||
}
|
||||
}
|
||||
|
||||
if (!\file_exists($tagPathTarget)) {
|
||||
if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
|
||||
if (!\copy($tagPath, $tagPathTarget)) {
|
||||
throw new Exception('Can\'t create temporary code file ' . $tagPathTarget);
|
||||
}
|
||||
} else {
|
||||
$buffer = $device->read($tagPath);
|
||||
\file_put_contents($tagPathTarget, $buffer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$device->exists($tagPath)) {
|
||||
throw new Exception('Code is not readable: ' . $build->getAttribute('source', ''));
|
||||
}
|
||||
|
||||
$vars = $build->getAttribute('vars', []);
|
||||
|
||||
// Start tracking time
|
||||
$buildStart = \microtime(true);
|
||||
$time = \time();
|
||||
|
||||
$orchestration
|
||||
->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0))
|
||||
->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256))
|
||||
->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256));
|
||||
|
||||
$vars = array_map(fn ($v) => strval($v), $vars);
|
||||
$path = '/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode';
|
||||
|
||||
if (!\file_exists($path)) {
|
||||
if (@\mkdir($path, 0777, true)) {
|
||||
\chmod($path, 0777);
|
||||
} else {
|
||||
throw new Exception('Can\'t create directory /tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode');
|
||||
}
|
||||
}
|
||||
|
||||
// Launch build container
|
||||
$id = $orchestration->run(
|
||||
image: $runtime['base'],
|
||||
name: $container,
|
||||
vars: $vars,
|
||||
workdir: '/usr/code',
|
||||
labels: [
|
||||
'appwrite-type' => 'function',
|
||||
'appwrite-created' => strval($time),
|
||||
'appwrite-runtime' => $build->getAttribute('runtime', ''),
|
||||
'appwrite-project' => $projectID,
|
||||
'appwrite-build' => $build->getId(),
|
||||
],
|
||||
command: [
|
||||
'tail',
|
||||
'-f',
|
||||
'/dev/null'
|
||||
],
|
||||
hostname: $container,
|
||||
mountFolder: $tagPathTargetDir,
|
||||
volumes: [
|
||||
'/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode' . ':/usr/builtCode:rw'
|
||||
]
|
||||
);
|
||||
|
||||
if (empty($id)) {
|
||||
throw new Exception('Failed to start build container');
|
||||
}
|
||||
|
||||
// Extract user code into build container
|
||||
$untarStdout = '';
|
||||
$untarStderr = '';
|
||||
|
||||
$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
|
||||
);
|
||||
|
||||
if (!$untarSuccess) {
|
||||
throw new Exception('Failed to extract tar: ' . $untarStderr);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Repackage Code and Save.
|
||||
$compressStdout = '';
|
||||
$compressStderr = '';
|
||||
|
||||
$builtCodePath = '/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode/code.tar.gz';
|
||||
|
||||
$compressSuccess = $orchestration->execute(
|
||||
name: $container,
|
||||
command: [
|
||||
'tar', '-C', '/usr/code', '-czvf', '/usr/builtCode/code.tar.gz', './'
|
||||
],
|
||||
stdout: $compressStdout,
|
||||
stderr: $compressStderr,
|
||||
timeout: 60
|
||||
);
|
||||
|
||||
if (!$compressSuccess) {
|
||||
throw new Exception('Failed to compress built code: ' . $compressStderr);
|
||||
}
|
||||
|
||||
// Remove Container
|
||||
$orchestration->remove($id, true);
|
||||
|
||||
// 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.');
|
||||
}
|
||||
|
||||
// Upload new code
|
||||
$device = Storage::getDevice('builds');
|
||||
|
||||
$path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
|
||||
|
||||
if (!\file_exists(\dirname($path))) { // Checks if directory path to file exists
|
||||
if (@\mkdir(\dirname($path), 0777, true)) {
|
||||
\chmod(\dirname($path), 0777);
|
||||
} else {
|
||||
throw new Exception('Can\'t create directory: ' . \dirname($path));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if ($buildStdout == '') {
|
||||
$buildStdout = 'Build Successful!';
|
||||
}
|
||||
|
||||
$build
|
||||
->setAttribute('outputPath', $path)
|
||||
->setAttribute('status', 'ready')
|
||||
->setAttribute('stdout', \utf8_encode(\mb_substr($buildStdout, -4096)))
|
||||
->setAttribute('stderr', \utf8_encode(\mb_substr($buildStderr, -4096)))
|
||||
->setAttribute('time', $time);
|
||||
|
||||
// Update build with built code attribute
|
||||
$build = $database->updateDocument('builds', $buildId, $build);
|
||||
|
||||
$buildEnd = \microtime(true);
|
||||
|
||||
Console::info('Build Stage Ran in ' . ($buildEnd - $buildStart) . ' seconds');
|
||||
} catch (Exception $e) {
|
||||
$build
|
||||
->setAttribute('status', 'failed')
|
||||
->setAttribute('stdout', \utf8_encode(\mb_substr($buildStdout, -4096)))
|
||||
->setAttribute('stderr', \utf8_encode(\mb_substr($e->getMessage(), -4096)));
|
||||
|
||||
$build = $database->updateDocument('builds', $buildId, $build);
|
||||
|
||||
// also remove the container if it exists
|
||||
if (isset($id)) {
|
||||
$orchestration->remove($id, true);
|
||||
}
|
||||
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($redis);
|
||||
|
||||
throw new Exception('Build failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$orchestrationPool->put($orchestration);
|
||||
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($redis);
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
App::post('/v1/execute') // Define Route
|
||||
->desc('Execute a function')
|
||||
->param('trigger', '', new Text(1024))
|
||||
@@ -683,7 +945,6 @@ App::post('/v1/execute') // Define Route
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Cleanup Endpoints used internally by appwrite when a function or tag gets deleted to also clean up their containers
|
||||
App::post('/v1/cleanup/function')
|
||||
->param('functionId', '', new UID())
|
||||
@@ -788,27 +1049,125 @@ App::post('/v1/cleanup/tag')
|
||||
return $response->json(['success' => true]);
|
||||
});
|
||||
|
||||
|
||||
App::post('/v1/executor/runtime')
|
||||
->desc('Create a new runtime server')
|
||||
App::post('/v1/tag')
|
||||
->param('functionId', '', new UID(), 'Function unique ID.')
|
||||
->param('tagId', '', new UID(), 'Tag unique ID.')
|
||||
->inject('projectID')
|
||||
->param('userId', '', new UID(), 'User unique ID.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $functionId, string $tagId, string $projectID, Response $response, Database $dbForProject) {
|
||||
try {
|
||||
Console::success('Creating runtime for tag ' . $tagId);
|
||||
createRuntimeServer($functionId, $projectID, $tagId, $dbForProject);
|
||||
} catch (\Throwable $th) {
|
||||
$response
|
||||
->setStatusCode(400)
|
||||
->json(['error' => $th->getMessage()]);
|
||||
};
|
||||
->inject('projectID')
|
||||
->inject('register')
|
||||
->action(function (string $functionId, string $tagId, string $userId, Response $response, Database $dbForProject, string $projectID, Registry $register) use ($runtimes) {
|
||||
// Get function document
|
||||
$function = $dbForProject->getDocument('functions', $functionId);
|
||||
// Get tag document
|
||||
$tag = $dbForProject->getDocument('tags', $tagId);
|
||||
|
||||
$response
|
||||
->setStatusCode(201)
|
||||
->noContent();
|
||||
// Check if both documents exist
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception('Function not found', 404);
|
||||
}
|
||||
|
||||
if ($tag->isEmpty()) {
|
||||
throw new Exception('Tag not found', 404);
|
||||
}
|
||||
|
||||
$runtime = $runtimes[$function->getAttribute('runtime')] ?? null;
|
||||
|
||||
if (\is_null($runtime)) {
|
||||
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
|
||||
// Create a new build entry
|
||||
$buildId = $dbForProject->getId();
|
||||
|
||||
if ($tag->getAttribute('buildId')) {
|
||||
$buildId = $tag->getAttribute('buildId');
|
||||
} else {
|
||||
try {
|
||||
$dbForProject->createDocument('builds', new Document([
|
||||
'$id' => $buildId,
|
||||
'$read' => (!empty($userId)) ? ['user:' . $userId] : [],
|
||||
'$write' => ['role:all'],
|
||||
'dateCreated' => time(),
|
||||
'status' => 'processing',
|
||||
'runtime' => $function->getAttribute('runtime'),
|
||||
'outputPath' => '',
|
||||
'source' => $tag->getAttribute('path'),
|
||||
'sourceType' => Storage::DEVICE_LOCAL,
|
||||
'stdout' => '',
|
||||
'stderr' => '',
|
||||
'time' => 0,
|
||||
'vars' => [
|
||||
'ENTRYPOINT_NAME' => $tag->getAttribute('entrypoint'),
|
||||
'APPWRITE_FUNCTION_ID' => $function->getId(),
|
||||
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
|
||||
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'],
|
||||
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'],
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $projectID,
|
||||
]
|
||||
]));
|
||||
|
||||
$tag->setAttribute('buildId', $buildId);
|
||||
|
||||
$dbForProject->updateDocument('tags', $tag->getId(), $tag);
|
||||
} catch (\Throwable $th) {
|
||||
var_dump($tag->getArrayCopy());
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
// Build Code
|
||||
go(function () use ($projectID, $tagId, $buildId, $functionId, $function, $register) {
|
||||
try {
|
||||
$db = $register->get('dbPool')->get();
|
||||
$redis = $register->get('redisPool')->get();
|
||||
$cache = new Cache(new RedisCache($redis));
|
||||
|
||||
$dbForProject = new Database(new MariaDB($db), $cache);
|
||||
$dbForProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
|
||||
$dbForProject->setNamespace('_project_' . $projectID);
|
||||
// Build Code
|
||||
runBuildStage($buildId, $projectID);
|
||||
|
||||
// Update the schedule
|
||||
$schedule = $function->getAttribute('schedule', '');
|
||||
$cron = (empty($function->getAttribute('tag')) && !empty($schedule)) ? new CronExpression($schedule) : null;
|
||||
$next = (empty($function->getAttribute('tag')) && !empty($schedule)) ? $cron->getNextRunDate()->format('U') : 0;
|
||||
|
||||
// Grab tag
|
||||
$tag = $dbForProject->getDocument('tags', $tagId);
|
||||
|
||||
// Grab build
|
||||
$build = $dbForProject->getDocument('builds', $buildId);
|
||||
|
||||
// If the build failed, it won't be possible to deploy
|
||||
if ($build->getAttribute('status') !== 'ready') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($tag->getAttribute('automaticDeploy') === true) {
|
||||
// Update the function document setting the tag as the active one
|
||||
$function
|
||||
->setAttribute('tag', $tag->getId())
|
||||
->setAttribute('scheduleNext', (int)$next);
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
}
|
||||
|
||||
// Deploy Runtime Server
|
||||
createRuntimeServer($functionId, $projectID, $tagId, $dbForProject);
|
||||
} catch (\Throwable $th) {
|
||||
} finally {
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($redis);
|
||||
}
|
||||
});
|
||||
|
||||
if (false === $function) {
|
||||
throw new Exception('Failed saving function to DB', 500);
|
||||
}
|
||||
|
||||
$response->dynamic($function, Response::MODEL_FUNCTION);
|
||||
});
|
||||
|
||||
App::get('/v1/')
|
||||
@@ -849,9 +1208,10 @@ App::post('/v1/build/:buildId') // Start a Build
|
||||
throw new Exception('Build is already finished', 409);
|
||||
}
|
||||
|
||||
Console::success('Starting build ' . $buildId);
|
||||
// Build Code
|
||||
runBuildStage($buildId, $projectID, $dbForProject);
|
||||
go(function () use ($buildId, $dbForProject, $projectID) {
|
||||
// Build Code
|
||||
runBuildStage($buildId, $projectID, $dbForProject);
|
||||
});
|
||||
|
||||
// return success
|
||||
return $response->json(['success' => true]);
|
||||
@@ -866,260 +1226,9 @@ App::post('/v1/build/:buildId') // Start a Build
|
||||
}
|
||||
});
|
||||
|
||||
function runBuildStage(string $buildId, string $projectID): Document
|
||||
{
|
||||
global $runtimes;
|
||||
global $orchestrationPool;
|
||||
global $register;
|
||||
|
||||
/** @var Orchestration $orchestration */
|
||||
$orchestration = $orchestrationPool->get();
|
||||
|
||||
$buildStdout = '';
|
||||
$buildStderr = '';
|
||||
|
||||
$db = $register->get('dbPool')->get();
|
||||
$redis = $register->get('redisPool')->get();
|
||||
$cache = new Cache(new RedisCache($redis));
|
||||
|
||||
$database = new Database(new MariaDB($db), $cache);
|
||||
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
|
||||
$database->setNamespace('_project_' . $projectID);
|
||||
|
||||
// Check if build has already been run
|
||||
$build = $database->getDocument('builds', $buildId);
|
||||
|
||||
try {
|
||||
// If we already have a built package ready there is no need to rebuild.
|
||||
if ($build->getAttribute('status') === 'ready' && \file_exists($build->getAttribute('outputPath'))) {
|
||||
return $build;
|
||||
}
|
||||
|
||||
// Update Tag Status
|
||||
$build->setAttribute('status', 'building');
|
||||
|
||||
$database->updateDocument('builds', $build->getId(), $build);
|
||||
|
||||
// Check if runtime is active
|
||||
$runtime = $runtimes[$build->getAttribute('runtime', '')] ?? null;
|
||||
|
||||
if (\is_null($runtime)) {
|
||||
throw new Exception('Runtime "' . $build->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
|
||||
// Grab Tag Files
|
||||
$tagPath = $build->getAttribute('source', '');
|
||||
$sourceType = $build->getAttribute('sourceType', '');
|
||||
|
||||
$device = Storage::getDevice('builds');
|
||||
|
||||
$tagPathTarget = '/tmp/project-' . $projectID . '/' . $build->getId() . '/code.tar.gz';
|
||||
$tagPathTargetDir = \pathinfo($tagPathTarget, PATHINFO_DIRNAME);
|
||||
|
||||
$container = 'build-stage-' . $build->getId();
|
||||
|
||||
// Perform various checks
|
||||
if (!\file_exists($tagPathTargetDir)) {
|
||||
if (!\mkdir($tagPathTargetDir, 0777, true)) {
|
||||
throw new Exception('Can\'t create directory ' . $tagPathTargetDir);
|
||||
}
|
||||
}
|
||||
|
||||
if (!\file_exists($tagPathTarget)) {
|
||||
if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
|
||||
if (!\copy($tagPath, $tagPathTarget)) {
|
||||
throw new Exception('Can\'t create temporary code file ' . $tagPathTarget);
|
||||
}
|
||||
} else {
|
||||
$buffer = $device->read($tagPath);
|
||||
\file_put_contents($tagPathTarget, $buffer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$device->exists($tagPath)) {
|
||||
throw new Exception('Code is not readable: ' . $build->getAttribute('source', ''));
|
||||
}
|
||||
|
||||
$vars = $build->getAttribute('envVars', []);
|
||||
|
||||
// Start tracking time
|
||||
$buildStart = \microtime(true);
|
||||
$buildTime = \time();
|
||||
|
||||
$orchestration
|
||||
->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', 0))
|
||||
->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256))
|
||||
->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256));
|
||||
|
||||
foreach ($vars as &$value) {
|
||||
$value = strval($value);
|
||||
}
|
||||
|
||||
if (!\file_exists('/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode')) {
|
||||
if (!\mkdir('/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode', 0777, true)) {
|
||||
throw new Exception('Can\'t create directory /tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode');
|
||||
}
|
||||
};
|
||||
|
||||
// Launch build container
|
||||
$id = $orchestration->run(
|
||||
image: $runtime['base'],
|
||||
name: $container,
|
||||
vars: $vars,
|
||||
workdir: '/usr/code',
|
||||
labels: [
|
||||
'appwrite-type' => 'function',
|
||||
'appwrite-created' => strval($buildTime),
|
||||
'appwrite-runtime' => $build->getAttribute('runtime', ''),
|
||||
'appwrite-project' => $projectID,
|
||||
'appwrite-build' => $build->getId(),
|
||||
],
|
||||
command: [
|
||||
'tail',
|
||||
'-f',
|
||||
'/dev/null'
|
||||
],
|
||||
hostname: $container,
|
||||
mountFolder: $tagPathTargetDir,
|
||||
volumes: [
|
||||
'/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode' . ':/usr/builtCode:rw'
|
||||
]
|
||||
);
|
||||
|
||||
if (empty($id)) {
|
||||
throw new Exception('Failed to start build container');
|
||||
}
|
||||
|
||||
// Extract user code into build container
|
||||
$untarStdout = '';
|
||||
$untarStderr = '';
|
||||
|
||||
$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
|
||||
);
|
||||
|
||||
if (!$untarSuccess) {
|
||||
throw new Exception('Failed to extract tar: ' . $untarStderr);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Repackage Code and Save.
|
||||
$compressStdout = '';
|
||||
$compressStderr = '';
|
||||
|
||||
$builtCodePath = '/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode/code.tar.gz';
|
||||
|
||||
$compressSuccess = $orchestration->execute(
|
||||
name: $container,
|
||||
command: [
|
||||
'tar', '-C', '/usr/code', '-czvf', '/usr/builtCode/code.tar.gz', './'
|
||||
],
|
||||
stdout: $compressStdout,
|
||||
stderr: $compressStderr,
|
||||
timeout: 60
|
||||
);
|
||||
|
||||
if (!$compressSuccess) {
|
||||
throw new Exception('Failed to compress built code: ' . $compressStderr);
|
||||
}
|
||||
|
||||
// Remove Container
|
||||
$orchestration->remove($id, true);
|
||||
|
||||
// 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.');
|
||||
}
|
||||
|
||||
// Upload new code
|
||||
$device = Storage::getDevice('builds');
|
||||
|
||||
$path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
|
||||
|
||||
if (!\file_exists(\dirname($path))) { // Checks if directory path to file exists
|
||||
if (!@\mkdir(\dirname($path), 0777, true)) {
|
||||
throw new Exception('Can\'t create directory: ' . \dirname($path));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if ($buildStdout == '') {
|
||||
$buildStdout = 'Build Successful!';
|
||||
}
|
||||
|
||||
$build
|
||||
->setAttribute('outputPath', $path)
|
||||
->setAttribute('status', 'ready')
|
||||
->setAttribute('stdout', \utf8_encode(\mb_substr($buildStdout, -4096)))
|
||||
->setAttribute('stderr', \utf8_encode(\mb_substr($buildStderr, -4096)))
|
||||
->setAttribute('buildTime', $buildTime);
|
||||
|
||||
// Update build with built code attribute
|
||||
$build = $database->updateDocument('builds', $buildId, $build);
|
||||
|
||||
$buildEnd = \microtime(true);
|
||||
|
||||
Console::info('Build Stage Ran in ' . ($buildEnd - $buildStart) . ' seconds');
|
||||
} catch (Exception $e) {
|
||||
$build
|
||||
->setAttribute('status', 'failed')
|
||||
->setAttribute('stdout', \utf8_encode(\mb_substr($buildStdout, -4096)))
|
||||
->setAttribute('stderr', \utf8_encode(\mb_substr($e->getMessage(), -4096)));
|
||||
|
||||
$build = $database->updateDocument('builds', $buildId, $build);
|
||||
|
||||
// also remove the container if it exists
|
||||
if (isset($id)) {
|
||||
$orchestration->remove($id, true);
|
||||
}
|
||||
$orchestrationPool->put(null);
|
||||
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($redis);
|
||||
|
||||
throw new Exception('Build failed: ' . $e->getMessage());
|
||||
} finally {
|
||||
$orchestrationPool->put($orchestration);
|
||||
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($redis);
|
||||
}
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode
|
||||
|
||||
$http = new Server("0.0.0.0", 8080);
|
||||
$http = new Server("0.0.0.0", 80);
|
||||
|
||||
function handleShutdown()
|
||||
{
|
||||
@@ -1299,4 +1408,4 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
||||
}
|
||||
});
|
||||
|
||||
$http->start();
|
||||
$http->start();
|
||||
@@ -685,7 +685,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
|
||||
<input type="file" name="code" id="tag-code" size="1" required accept="application/x-gzip,.gz">
|
||||
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">(Max file size allowed: <?php echo $fileLimitHuman; ?>)</div>
|
||||
|
||||
<label for="tag-automaticDeploy" class="margin-bottom-large">Auto Deploy Tag after build <input type="checkbox" class="margin-start-small" id="tag-automaticDeploy" name="automaticDeploy" /></label>
|
||||
<label for="tag-deploy" class="margin-bottom-large">Auto Deploy Tag after build <input type="checkbox" class="margin-start-small" id="tag-deploy" name="deploy" /></label>
|
||||
|
||||
<footer>
|
||||
<button type="submit">Create</button> <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
|
||||
|
||||
@@ -194,10 +194,10 @@ services:
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_STATSD_HOST
|
||||
- _APP_STATSD_PORT
|
||||
- DOCKERHUB_PULL_USERNAME
|
||||
- DOCKERHUB_PULL_PASSWORD
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- DOCKERHUB_PULL_USERNAME
|
||||
- DOCKERHUB_PULL_PASSWORD
|
||||
|
||||
appwrite-worker-database:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -324,10 +324,40 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
- appwrite-executor
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_USAGE_STATS
|
||||
|
||||
appwrite-executor:
|
||||
container_name: appwrite-executor
|
||||
entrypoint: executor
|
||||
stop_signal: SIGINT
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
networks:
|
||||
appwrite:
|
||||
runtimes:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
- /tmp:/tmp:rw
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- ./dev:/usr/local/dev
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
@@ -343,12 +373,18 @@ services:
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CONTAINERS
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_FUNCTIONS_MEMORY_SWAP
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_STATSD_HOST
|
||||
- _APP_STATSD_PORT
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- DOCKERHUB_PULL_USERNAME
|
||||
- DOCKERHUB_PULL_PASSWORD
|
||||
|
||||
appwrite-worker-mails:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -485,6 +521,7 @@ services:
|
||||
networks:
|
||||
gateway:
|
||||
appwrite:
|
||||
runtimes:
|
||||
|
||||
volumes:
|
||||
appwrite-mariadb:
|
||||
@@ -495,3 +532,4 @@ volumes:
|
||||
appwrite-functions:
|
||||
appwrite-influxdb:
|
||||
appwrite-config:
|
||||
appwrite-executor:
|
||||
+2
-105
@@ -1,10 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Appwrite\Stats\Stats;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
use Cron\CronExpression;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\App;
|
||||
@@ -13,11 +9,8 @@ use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Orchestration\Orchestration;
|
||||
use Utopia\Orchestration\Adapter\DockerAPI;
|
||||
use Utopia\Orchestration\Container;
|
||||
use Utopia\Orchestration\Exception\Orchestration as OrchestrationException;
|
||||
use Utopia\Orchestration\Exception\Timeout as TimeoutException;
|
||||
use Utopia\Orchestration\Orchestration;
|
||||
|
||||
require_once __DIR__.'/../init.php';
|
||||
|
||||
@@ -38,41 +31,6 @@ $warmupTime = $warmupEnd - $warmupStart;
|
||||
|
||||
Console::success('Finished warmup in ' . $warmupTime . ' seconds');
|
||||
|
||||
/**
|
||||
* List function servers
|
||||
*/
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
|
||||
$executionStart = \microtime(true);
|
||||
|
||||
$response = $orchestration->list(['label' => 'appwrite-type=function']);
|
||||
/** @var Container[] $list */
|
||||
$list = [];
|
||||
|
||||
foreach ($response as $value) {
|
||||
$list[$value->getName()] = $value;
|
||||
}
|
||||
|
||||
$executionEnd = \microtime(true);
|
||||
|
||||
Console::info(count($list) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds');
|
||||
|
||||
/**
|
||||
* 1. Get event args - DONE
|
||||
* 2. Unpackage code in the isolated container - DONE
|
||||
* 3. Execute in container with timeout
|
||||
* + messure execution time - DONE
|
||||
* + pass env vars - DONE
|
||||
* + pass one-time api key
|
||||
* 4. Update execution status - DONE
|
||||
* 5. Update execution stdout & stderr - DONE
|
||||
* 6. Trigger audit log - DONE
|
||||
* 7. Trigger usage log - DONE
|
||||
*/
|
||||
|
||||
// TODO avoid scheduled execution if delay is bigger than X offest
|
||||
|
||||
class FunctionsV1 extends Worker
|
||||
{
|
||||
public array $args = [];
|
||||
@@ -266,7 +224,7 @@ class FunctionsV1 extends Worker
|
||||
public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $eventData = '', string $data = '', array $webhooks = [], string $userId = '', string $jwt = ''): void
|
||||
{
|
||||
$ch = \curl_init();
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor:8080/v1/execute");
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/execute");
|
||||
\curl_setopt($ch, CURLOPT_POST, true);
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||
'trigger' => $trigger,
|
||||
@@ -299,67 +257,6 @@ class FunctionsV1 extends Worker
|
||||
\curl_close($ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup any hanging containers above the allowed max containers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function cleanup(): void
|
||||
{
|
||||
/** @var Container[] $list */
|
||||
global $list;
|
||||
/** @var Orchestration $orchestration */
|
||||
global $orchestration;
|
||||
|
||||
Console::success(count($list) . ' running containers counted');
|
||||
|
||||
$max = (int) App::getEnv('_APP_FUNCTIONS_CONTAINERS');
|
||||
|
||||
if (\count($list) > $max) {
|
||||
Console::info('Starting containers cleanup');
|
||||
|
||||
\uasort($list, function (Container $item1, Container $item2) {
|
||||
return (int)($item1->getLabels['appwrite-created'] ?? 0) <=> (int)($item2->getLabels['appwrite-created'] ?? 0);
|
||||
});
|
||||
|
||||
while (\count($list) > $max) {
|
||||
$first = \array_shift($list);
|
||||
|
||||
try {
|
||||
$orchestration->remove($first->getName(), true);
|
||||
Console::info('Removed container: ' . $first->getName());
|
||||
} catch (Exception $e) {
|
||||
Console::error('Failed to remove container: ' . $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter ENV vars
|
||||
*
|
||||
* @param string $string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function filterEnvKey(string $string): string
|
||||
{
|
||||
if (empty($this->allowed)) {
|
||||
$this->allowed = array_fill_keys(\str_split('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'), true);
|
||||
}
|
||||
|
||||
$string = \str_split($string);
|
||||
$output = '';
|
||||
|
||||
foreach ($string as $char) {
|
||||
if (\array_key_exists($char, $this->allowed)) {
|
||||
$output .= $char;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user