appwrite/src/Appwrite/Platform/Workers/Builds.php

820 lines
40 KiB
PHP
Raw Normal View History

2023-06-05 16:13:00 +00:00
<?php
namespace Appwrite\Platform\Workers;
2024-08-08 07:38:37 +00:00
use Ahc\Jwt\JWT;
2023-06-05 16:13:00 +00:00
use Appwrite\Event\Event;
use Appwrite\Event\Func;
2024-01-24 11:20:13 +00:00
use Appwrite\Event\Usage;
2023-06-05 16:13:00 +00:00
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Utopia\Response\Model\Deployment;
2023-09-28 10:45:15 +00:00
use Appwrite\Vcs\Comment;
2023-06-05 16:13:00 +00:00
use Exception;
use Executor\Executor;
2024-03-06 17:34:21 +00:00
use Swoole\Coroutine as Co;
2023-09-28 10:45:15 +00:00
use Utopia\Cache\Cache;
2023-06-05 16:13:00 +00:00
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
2023-10-01 17:39:26 +00:00
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
2023-06-05 16:13:00 +00:00
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
2023-06-05 16:13:00 +00:00
use Utopia\Platform\Action;
use Utopia\Queue\Message;
2024-02-20 11:40:55 +00:00
use Utopia\Storage\Device;
2023-09-28 10:45:15 +00:00
use Utopia\Storage\Device\Local;
2024-04-01 11:02:47 +00:00
use Utopia\System\System;
2023-09-28 10:45:15 +00:00
use Utopia\VCS\Adapter\Git\GitHub;
2023-06-05 16:13:00 +00:00
class Builds extends Action
{
public static function getName(): string
{
return 'builds';
}
/**
* @throws Exception
*/
public function __construct()
{
$this
->desc('Builds worker')
->inject('message')
2023-06-11 10:29:04 +00:00
->inject('dbForConsole')
2023-06-05 16:13:00 +00:00
->inject('queueForEvents')
->inject('queueForFunctions')
2023-10-25 07:39:59 +00:00
->inject('queueForUsage')
2023-09-28 10:45:15 +00:00
->inject('cache')
2023-10-17 03:44:36 +00:00
->inject('dbForProject')
2024-02-20 14:10:51 +00:00
->inject('deviceForFunctions')
->inject('log')
2024-10-08 07:54:40 +00:00
->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log));
2023-06-05 16:13:00 +00:00
}
/**
2023-10-01 17:39:26 +00:00
* @param Message $message
* @param Database $dbForConsole
* @param Event $queueForEvents
* @param Func $queueForFunctions
2023-10-25 07:39:59 +00:00
* @param Usage $queueForUsage
2023-10-01 17:39:26 +00:00
* @param Cache $cache
2023-10-17 03:44:36 +00:00
* @param Database $dbForProject
2024-02-20 14:10:51 +00:00
* @param Device $deviceForFunctions
* @param Log $log
2023-10-01 17:39:26 +00:00
* @return void
* @throws \Utopia\Database\Exception
2023-06-05 16:13:00 +00:00
*/
2024-10-08 07:54:40 +00:00
public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void
2023-06-05 16:13:00 +00:00
{
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
2024-03-07 09:59:08 +00:00
throw new \Exception('Missing payload');
2023-06-05 16:13:00 +00:00
}
$type = $payload['type'] ?? '';
$project = new Document($payload['project'] ?? []);
$resource = new Document($payload['resource'] ?? []);
$deployment = new Document($payload['deployment'] ?? []);
2023-09-28 10:45:15 +00:00
$template = new Document($payload['template'] ?? []);
2023-06-05 16:13:00 +00:00
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
2023-06-05 16:13:00 +00:00
switch ($type) {
case BUILD_TYPE_DEPLOYMENT:
case BUILD_TYPE_RETRY:
Console::info('Creating build for deployment: ' . $deployment->getId());
2023-09-28 10:45:15 +00:00
$github = new GitHub($cache);
2024-10-08 07:54:40 +00:00
$this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
2023-06-05 16:13:00 +00:00
break;
default:
throw new \Exception('Invalid build type');
}
}
/**
2024-02-20 14:10:51 +00:00
* @param Device $deviceForFunctions
2023-10-01 17:39:26 +00:00
* @param Func $queueForFunctions
* @param Event $queueForEvents
2023-10-25 07:39:59 +00:00
* @param Usage $queueForUsage
2023-10-01 17:39:26 +00:00
* @param Database $dbForConsole
2023-10-17 03:44:36 +00:00
* @param Database $dbForProject
2023-10-01 17:39:26 +00:00
* @param GitHub $github
* @param Document $project
* @param Document $function
* @param Document $deployment
* @param Document $template
* @param Log $log
2023-10-01 17:39:26 +00:00
* @return void
* @throws \Utopia\Database\Exception
2023-10-02 14:02:48 +00:00
* @throws Exception
2023-06-05 16:13:00 +00:00
*/
2024-10-08 07:54:40 +00:00
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
2023-06-05 16:13:00 +00:00
{
2024-04-01 11:02:47 +00:00
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
2023-06-05 16:13:00 +00:00
$functionId = $function->getId();
$log->addTag('functionId', $function->getId());
$function = $dbForProject->getDocument('functions', $functionId);
2023-06-05 16:13:00 +00:00
if ($function->isEmpty()) {
2024-03-07 09:59:08 +00:00
throw new \Exception('Function not found', 404);
2023-06-05 16:13:00 +00:00
}
$deploymentId = $deployment->getId();
$log->addTag('deploymentId', $deploymentId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
2023-06-05 16:13:00 +00:00
if ($deployment->isEmpty()) {
2024-03-07 09:59:08 +00:00
throw new \Exception('Deployment not found', 404);
2023-06-05 16:13:00 +00:00
}
2023-09-28 10:45:15 +00:00
if (empty($deployment->getAttribute('entrypoint', ''))) {
2024-03-07 09:59:08 +00:00
throw new \Exception('Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".', 500);
2023-09-28 10:45:15 +00:00
}
$version = $function->getAttribute('version', 'v2');
2024-08-20 04:23:11 +00:00
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specifications', APP_FUNCTION_SPECIFICATION_DEFAULT)];
2023-09-28 10:45:15 +00:00
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
2023-06-05 16:13:00 +00:00
$key = $function->getAttribute('runtime');
$runtime = $runtimes[$key] ?? null;
if (\is_null($runtime)) {
2024-03-07 09:59:08 +00:00
throw new \Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
2023-06-05 16:13:00 +00:00
}
2023-09-28 10:45:15 +00:00
// Realtime preparation
$allEvents = Event::generateEvents('functions.[functionId].deployments.[deploymentId].update', [
'functionId' => $function->getId(),
'deploymentId' => $deployment->getId()
]);
2023-06-05 16:13:00 +00:00
$startTime = DateTime::now();
2023-09-28 10:45:15 +00:00
$durationStart = \microtime(true);
$buildId = $deployment->getAttribute('buildId', '');
2024-04-29 18:49:14 +00:00
$build = $dbForProject->getDocument('builds', $buildId);
2023-09-28 10:45:15 +00:00
$isNewBuild = empty($buildId);
2024-04-29 18:49:14 +00:00
if ($build->isEmpty()) {
2023-06-05 16:13:00 +00:00
$buildId = ID::unique();
$build = $dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$permissions' => [],
'startTime' => $startTime,
2023-06-11 10:29:04 +00:00
'deploymentInternalId' => $deployment->getInternalId(),
2023-06-05 16:13:00 +00:00
'deploymentId' => $deployment->getId(),
'status' => 'processing',
2023-09-28 10:45:15 +00:00
'path' => '',
2023-06-05 16:13:00 +00:00
'runtime' => $function->getAttribute('runtime'),
2023-09-28 10:45:15 +00:00
'source' => $deployment->getAttribute('path', ''),
2024-02-20 14:10:51 +00:00
'sourceType' => strtolower($deviceForFunctions->getType()),
2023-09-28 10:45:15 +00:00
'logs' => '',
'endTime' => null,
'duration' => 0,
'size' => 0
2023-06-05 16:13:00 +00:00
]));
2023-09-28 10:45:15 +00:00
$deployment->setAttribute('buildId', $build->getId());
2023-06-11 10:29:04 +00:00
$deployment->setAttribute('buildInternalId', $build->getInternalId());
2023-06-05 16:13:00 +00:00
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
2024-06-12 10:21:48 +00:00
} elseif ($build->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
return;
2023-06-05 16:13:00 +00:00
} else {
$build = $dbForProject->getDocument('builds', $buildId);
}
2023-09-28 10:45:15 +00:00
$source = $deployment->getAttribute('path', '');
$installationId = $deployment->getAttribute('installationId', '');
$providerRepositoryId = $deployment->getAttribute('providerRepositoryId', '');
$providerCommitHash = $deployment->getAttribute('providerCommitHash', '');
2024-02-20 11:40:55 +00:00
$isVcsEnabled = !empty($providerRepositoryId);
2023-09-28 10:45:15 +00:00
$owner = '';
$repositoryName = '';
if ($isVcsEnabled) {
$installation = $dbForConsole->getDocument('installations', $installationId);
$providerInstallationId = $installation->getAttribute('providerInstallationId');
2024-04-01 11:02:47 +00:00
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
2023-09-28 10:45:15 +00:00
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
}
2023-06-05 16:13:00 +00:00
try {
2024-09-04 18:52:01 +00:00
if ($isNewBuild && !$isVcsEnabled) {
2024-08-13 10:50:58 +00:00
// Non-vcs+Template
$templateRepositoryName = $template->getAttribute('repositoryName', '');
$templateOwnerName = $template->getAttribute('ownerName', '');
2024-08-14 16:00:13 +00:00
$templateVersion = $template->getAttribute('version', '');
2024-08-13 10:50:58 +00:00
$templateRootDirectory = $template->getAttribute('rootDirectory', '');
$templateRootDirectory = \rtrim($templateRootDirectory, '/');
$templateRootDirectory = \ltrim($templateRootDirectory, '.');
$templateRootDirectory = \ltrim($templateRootDirectory, '/');
2024-08-14 16:00:13 +00:00
if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) {
2024-10-08 07:54:40 +00:00
$stdout = '';
$stderr = '';
2024-08-13 10:50:58 +00:00
// Clone template repo
2024-08-14 16:00:13 +00:00
$tmpTemplateDirectory = '/tmp/builds/' . $buildId . '-template';
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory);
2024-10-08 07:54:40 +00:00
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
2024-08-13 10:50:58 +00:00
if ($exit !== 0) {
2024-10-08 07:54:40 +00:00
throw new \Exception('Unable to clone code repository: ' . $stderr);
2024-08-13 10:50:58 +00:00
}
// Ensure directories
2024-10-08 07:54:40 +00:00
Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr);
2024-08-13 10:50:58 +00:00
$tmpPathFile = $tmpTemplateDirectory . '/code.tar.gz';
$localDevice = new Local();
if (substr($tmpTemplateDirectory, -1) !== '/') {
$tmpTemplateDirectory .= '/';
}
2024-08-20 11:38:42 +00:00
$tarParamDirectory = \escapeshellarg($tmpTemplateDirectory . (empty($templateRootDirectory) ? '' : '/' . $templateRootDirectory));
2024-10-08 07:54:40 +00:00
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
2024-08-13 10:50:58 +00:00
2024-08-14 16:00:13 +00:00
$source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions);
2024-08-13 10:50:58 +00:00
if (!$result) {
throw new \Exception("Unable to move file");
}
2024-10-08 07:54:40 +00:00
Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $stdout, $stderr);
2024-08-13 10:50:58 +00:00
2024-08-20 11:38:42 +00:00
$directorySize = $deviceForFunctions->getFileSize($source);
2024-08-13 10:50:58 +00:00
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
2024-08-19 14:18:57 +00:00
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize));
2024-08-13 10:50:58 +00:00
}
} elseif ($isNewBuild && $isVcsEnabled) {
// VCS and VCS+Temaplte
2023-09-28 10:45:15 +00:00
$tmpDirectory = '/tmp/builds/' . $buildId . '/code';
$rootDirectory = $function->getAttribute('providerRootDirectory', '');
$rootDirectory = \rtrim($rootDirectory, '/');
$rootDirectory = \ltrim($rootDirectory, '.');
$rootDirectory = \ltrim($rootDirectory, '/');
$owner = $github->getOwnerName($providerInstallationId);
$repositoryName = $github->getRepositoryName($providerRepositoryId);
$cloneOwner = $deployment->getAttribute('providerRepositoryOwner', $owner);
$cloneRepository = $deployment->getAttribute('providerRepositoryName', $repositoryName);
$branchName = $deployment->getAttribute('providerBranch');
$commitHash = $deployment->getAttribute('providerCommitHash', '');
2024-08-13 10:50:58 +00:00
2024-07-29 17:28:15 +00:00
$cloneVersion = $branchName;
$cloneType = GitHub::CLONE_TYPE_BRANCH;
2024-09-04 18:52:01 +00:00
if (!empty($commitHash)) {
2024-07-29 17:28:15 +00:00
$cloneVersion = $commitHash;
$cloneType = GitHub::CLONE_TYPE_COMMIT;
}
2024-10-08 07:54:40 +00:00
2024-07-29 17:28:15 +00:00
$gitCloneCommand = $github->generateCloneCommand($cloneOwner, $cloneRepository, $cloneVersion, $cloneType, $tmpDirectory, $rootDirectory);
2024-10-08 07:54:40 +00:00
$stdout = '';
$stderr = '';
Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $stdout, $stderr);
2024-02-23 18:43:11 +00:00
2024-06-12 10:21:48 +00:00
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
2024-02-23 18:43:11 +00:00
return;
}
2024-10-08 07:54:40 +00:00
$exit = Console::execute($gitCloneCommand, '', $stdout, $stderr);
2023-09-28 10:45:15 +00:00
if ($exit !== 0) {
2024-10-08 07:54:40 +00:00
throw new \Exception('Unable to clone code repository: ' . $stderr);
2023-09-28 10:45:15 +00:00
}
// Local refactoring for function folder with spaces
if (str_contains($rootDirectory, ' ')) {
$rootDirectoryWithoutSpaces = str_replace(' ', '', $rootDirectory);
$from = $tmpDirectory . '/' . $rootDirectory;
$to = $tmpDirectory . '/' . $rootDirectoryWithoutSpaces;
2024-10-08 07:54:40 +00:00
$exit = Console::execute('mv "' . \escapeshellarg($from) . '" "' . \escapeshellarg($to) . '"', '', $stdout, $stderr);
if ($exit !== 0) {
2024-10-08 07:54:40 +00:00
throw new \Exception('Unable to move function with spaces' . $stderr);
}
$rootDirectory = $rootDirectoryWithoutSpaces;
}
2023-09-28 10:45:15 +00:00
// Build from template
$templateRepositoryName = $template->getAttribute('repositoryName', '');
$templateOwnerName = $template->getAttribute('ownerName', '');
2024-08-14 16:00:13 +00:00
$templateVersion = $template->getAttribute('version', '');
2023-09-28 10:45:15 +00:00
$templateRootDirectory = $template->getAttribute('rootDirectory', '');
$templateRootDirectory = \rtrim($templateRootDirectory, '/');
$templateRootDirectory = \ltrim($templateRootDirectory, '.');
$templateRootDirectory = \ltrim($templateRootDirectory, '/');
2024-08-14 16:00:13 +00:00
if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) {
2023-09-28 10:45:15 +00:00
// Clone template repo
2024-08-14 16:00:13 +00:00
$tmpTemplateDirectory = '/tmp/builds/' . $buildId . '/template';
2024-07-29 17:28:15 +00:00
2024-08-14 16:00:13 +00:00
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory);
2024-10-08 07:54:40 +00:00
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
2023-09-28 10:45:15 +00:00
if ($exit !== 0) {
2024-10-08 07:54:40 +00:00
throw new \Exception('Unable to clone code repository: ' . $stderr);
2023-09-28 10:45:15 +00:00
}
// Ensure directories
2024-10-08 07:54:40 +00:00
Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr);
Console::execute('mkdir -p ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr);
2023-09-28 10:45:15 +00:00
// Merge template into user repo
2024-10-08 07:54:40 +00:00
Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr);
2023-09-28 10:45:15 +00:00
// Commit and push
2024-10-08 07:54:40 +00:00
$exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr);
2023-09-28 10:45:15 +00:00
if ($exit !== 0) {
2024-10-08 07:54:40 +00:00
throw new \Exception('Unable to push code repository: ' . $stderr);
2023-09-28 10:45:15 +00:00
}
2024-10-08 07:54:40 +00:00
$exit = Console::execute('cd ' . \escapeshellarg($tmpDirectory) . ' && git rev-parse HEAD', '', $stdout, $stderr);
2023-09-28 10:45:15 +00:00
if ($exit !== 0) {
2024-10-08 07:54:40 +00:00
throw new \Exception('Unable to get vcs commit SHA: ' . $stderr);
2023-09-28 10:45:15 +00:00
}
2024-10-08 07:54:40 +00:00
$providerCommitHash = \trim($stdout);
2023-09-28 10:45:15 +00:00
$authorUrl = "https://github.com/$cloneOwner";
$deployment->setAttribute('providerCommitHash', $providerCommitHash ?? '');
$deployment->setAttribute('providerCommitAuthorUrl', $authorUrl);
$deployment->setAttribute('providerCommitAuthor', 'Appwrite');
$deployment->setAttribute('providerCommitMessage', "Create '" . $function->getAttribute('name', '') . "' function");
$deployment->setAttribute('providerCommitUrl', "https://github.com/$cloneOwner/$cloneRepository/commit/$providerCommitHash");
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
/**
* Send realtime Event
*/
$target = Realtime::fromPayload(
2024-03-06 17:34:21 +00:00
// Pass first, most verbose event pattern
2023-09-28 10:45:15 +00:00
event: $allEvents[0],
payload: $build,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
}
2024-08-14 16:00:13 +00:00
$tmpPath = '/tmp/builds/' . $buildId;
2023-09-28 10:45:15 +00:00
$tmpPathFile = $tmpPath . '/code.tar.gz';
2023-10-03 08:40:34 +00:00
$localDevice = new Local();
if (substr($tmpDirectory, -1) !== '/') {
$tmpDirectory .= '/';
}
$directorySize = $localDevice->getDirectorySize($tmpDirectory);
$functionsSizeLimit = (int)System::getEnv('_APP_FUNCTIONS_SIZE_LIMIT', '30000000');
2023-10-03 08:40:34 +00:00
if ($directorySize > $functionsSizeLimit) {
2024-03-07 09:59:08 +00:00
throw new \Exception('Repository directory size should be less than ' . number_format($functionsSizeLimit / 1048576, 2) . ' MBs.');
2023-10-03 08:40:34 +00:00
}
2023-09-28 10:45:15 +00:00
2024-08-14 16:00:13 +00:00
$tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory);
2024-10-08 07:54:40 +00:00
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
2023-09-28 10:45:15 +00:00
2024-08-14 16:00:13 +00:00
$source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions);
2023-09-28 10:45:15 +00:00
if (!$result) {
throw new \Exception("Unable to move file");
}
2024-10-08 07:54:40 +00:00
Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $stdout, $stderr);
2023-09-28 10:45:15 +00:00
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
2024-09-07 17:08:09 +00:00
$directorySize = $deviceForFunctions->getFileSize($source);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize));
2023-09-28 10:45:15 +00:00
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
}
/** Request the executor to build the code... */
$build->setAttribute('status', 'building');
$build = $dbForProject->updateDocument('builds', $buildId, $build);
if ($isVcsEnabled) {
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
}
/** Trigger Webhook */
2023-09-28 17:37:07 +00:00
$deploymentModel = new Deployment();
2023-09-28 10:45:15 +00:00
$deploymentUpdate =
$queueForEvents
->setQueue(Event::WEBHOOK_QUEUE_NAME)
->setClass(Event::WEBHOOK_CLASS_NAME)
->setProject($project)
->setEvent('functions.[functionId].deployments.[deploymentId].update')
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId())
->setPayload($deployment->getArrayCopy(array_keys($deploymentModel->getRules())));
2023-09-28 10:45:15 +00:00
$deploymentUpdate->trigger();
/** Trigger Functions */
$queueForFunctions
->from($deploymentUpdate)
->trigger();
/** Trigger Realtime */
$target = Realtime::fromPayload(
2024-03-06 17:34:21 +00:00
// Pass first, most verbose event pattern
2023-09-28 10:45:15 +00:00
event: $allEvents[0],
payload: $build,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
2023-06-05 16:13:00 +00:00
);
2023-09-28 10:45:15 +00:00
$vars = [];
2023-06-11 10:29:04 +00:00
2023-09-28 10:45:15 +00:00
// Shared vars
foreach ($function->getAttribute('varsProject', []) as $var) {
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
// Function vars
foreach ($function->getAttribute('vars', []) as $var) {
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
2024-08-20 04:23:11 +00:00
$cpus = $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT;
$memory = max($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT, 1024); // We have a minimum of 1024MB here because some runtimes can't compile with less memory than this.
$jwtExpiry = (int)System::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900);
2024-08-08 07:38:37 +00:00
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$apiKey = $jwtObj->encode([
'projectId' => $project->getId(),
'scopes' => $function->getAttribute('scopes', [])
]);
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
2023-09-28 10:45:15 +00:00
// Appwrite vars
$vars = \array_merge($vars, [
2024-08-08 07:38:37 +00:00
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_API_KEY' => API_KEY_DYNAMIC . '_' . $apiKey,
2023-09-28 10:45:15 +00:00
'APPWRITE_FUNCTION_ID' => $function->getId(),
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_FUNCTION_CPUS' => $cpus,
'APPWRITE_FUNCTION_MEMORY' => $memory,
2024-07-15 07:10:11 +00:00
'APPWRITE_VERSION' => APP_VERSION_STABLE,
'APPWRITE_REGION' => $project->getAttribute('region'),
'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''),
'APPWRITE_VCS_REPOSITORY_ID' => $deployment->getAttribute('providerRepositoryId', ''),
'APPWRITE_VCS_REPOSITORY_NAME' => $deployment->getAttribute('providerRepositoryName', ''),
'APPWRITE_VCS_REPOSITORY_OWNER' => $deployment->getAttribute('providerRepositoryOwner', ''),
'APPWRITE_VCS_REPOSITORY_URL' => $deployment->getAttribute('providerRepositoryUrl', ''),
'APPWRITE_VCS_REPOSITORY_BRANCH' => $deployment->getAttribute('providerBranch', ''),
'APPWRITE_VCS_REPOSITORY_BRANCH_URL' => $deployment->getAttribute('providerBranchUrl', ''),
'APPWRITE_VCS_COMMIT_HASH' => $deployment->getAttribute('providerCommitHash', ''),
'APPWRITE_VCS_COMMIT_MESSAGE' => $deployment->getAttribute('providerCommitMessage', ''),
'APPWRITE_VCS_COMMIT_URL' => $deployment->getAttribute('providerCommitUrl', ''),
'APPWRITE_VCS_COMMIT_AUTHOR_NAME' => $deployment->getAttribute('providerCommitAuthor', ''),
'APPWRITE_VCS_COMMIT_AUTHOR_URL' => $deployment->getAttribute('providerCommitAuthorUrl', ''),
'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''),
2023-09-28 10:45:15 +00:00
]);
$command = $deployment->getAttribute('commands', '');
$response = null;
$err = null;
2024-06-12 10:21:48 +00:00
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
2024-02-18 12:31:25 +00:00
return;
2024-02-18 12:34:16 +00:00
}
2024-02-18 12:31:25 +00:00
2024-08-22 10:51:08 +00:00
$isCanceled = false;
2023-10-02 14:02:48 +00:00
Co::join([
Co\go(function () use ($executor, &$response, $project, $deployment, $source, $function, $runtime, $vars, $command, $cpus, $memory, &$err) {
2023-10-02 14:02:48 +00:00
try {
$version = $function->getAttribute('version', 'v2');
$command = $version === 'v2' ? 'tar -zxf /tmp/code.tar.gz -C /usr/code && cd /usr/local/src/ && ./build.sh' : 'tar -zxf /tmp/code.tar.gz -C /mnt/code && helpers/build.sh "' . \trim(\escapeshellarg($command), "\'") . '"';
2023-10-02 14:02:48 +00:00
$response = $executor->createRuntime(
deploymentId: $deployment->getId(),
projectId: $project->getId(),
source: $source,
image: $runtime['image'],
version: $version,
cpus: $cpus,
memory: $memory,
2023-10-02 14:02:48 +00:00
remove: true,
entrypoint: $deployment->getAttribute('entrypoint'),
destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}",
variables: $vars,
command: $command
);
} catch (\Throwable $error) {
2023-10-02 14:02:48 +00:00
$err = $error;
}
}),
2024-08-22 10:51:08 +00:00
Co\go(function () use ($executor, $project, $deployment, &$response, &$build, $dbForProject, $allEvents, &$err, &$isCanceled) {
2024-03-06 17:34:21 +00:00
try {
$executor->getLogs(
deploymentId: $deployment->getId(),
projectId: $project->getId(),
2024-08-22 10:51:08 +00:00
callback: function ($logs) use (&$response, &$err, &$build, $dbForProject, $allEvents, $project, &$isCanceled) {
2024-09-04 18:52:01 +00:00
if ($isCanceled) {
2024-08-22 10:51:08 +00:00
return;
}
2024-08-07 15:13:24 +00:00
// If we have response or error from concurrent coroutine, we already have latest logs
2024-08-07 08:53:40 +00:00
if ($response === null && $err === null) {
2024-03-06 17:34:21 +00:00
$build = $dbForProject->getDocument('builds', $build->getId());
if ($build->isEmpty()) {
2024-03-07 09:59:08 +00:00
throw new \Exception('Build not found', 404);
2024-03-06 17:34:21 +00:00
}
2024-08-22 10:51:08 +00:00
if ($build->getAttribute('status') === 'canceled') {
$isCanceled = true;
Console::info('Ignoring realtime logs because build has been canceled');
return;
}
2024-05-09 13:40:31 +00:00
$logs = \mb_substr($logs, 0, null, 'UTF-8'); // Get only valid UTF8 part - removes leftover half-multibytes causing SQL errors
2024-05-06 15:05:13 +00:00
2024-03-06 17:34:21 +00:00
$build = $build->setAttribute('logs', $build->getAttribute('logs', '') . $logs);
$build = $dbForProject->updateDocument('builds', $build->getId(), $build);
/**
2024-04-29 13:46:17 +00:00
* Send realtime Event
*/
2024-03-06 17:34:21 +00:00
$target = Realtime::fromPayload(
2023-09-28 10:45:15 +00:00
// Pass first, most verbose event pattern
2024-03-06 17:34:21 +00:00
event: $allEvents[0],
payload: $build,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
2023-09-28 10:45:15 +00:00
}
}
2024-03-06 17:34:21 +00:00
);
} catch (\Throwable $error) {
if (empty($err)) {
$err = $error;
2023-09-28 10:45:15 +00:00
}
2024-03-06 17:34:21 +00:00
}
}),
]);
2023-09-28 10:45:15 +00:00
2024-08-20 11:38:42 +00:00
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
return;
}
2023-09-28 10:45:15 +00:00
if ($err) {
throw $err;
}
$endTime = DateTime::now();
$durationEnd = \microtime(true);
2023-06-05 16:13:00 +00:00
$buildSizeLimit = (int)System::getEnv('_APP_FUNCTIONS_BUILD_SIZE_LIMIT', '2000000000');
2024-07-15 09:53:29 +00:00
if ($response['size'] > $buildSizeLimit) {
throw new \Exception('Build size should be less than ' . number_format($buildSizeLimit / 1048576, 2) . ' MBs.');
}
2023-09-28 10:45:15 +00:00
/** Update the build document */
$build->setAttribute('startTime', DateTime::format((new \DateTime())->setTimestamp(floor($response['startTime']))));
2023-09-28 10:45:15 +00:00
$build->setAttribute('endTime', $endTime);
$build->setAttribute('duration', \intval(\ceil($durationEnd - $durationStart)));
$build->setAttribute('status', 'ready');
$build->setAttribute('path', $response['path']);
$build->setAttribute('size', $response['size']);
$build->setAttribute('logs', $response['output']);
2024-08-22 10:51:08 +00:00
$build = $dbForProject->updateDocument('builds', $buildId, $build);
2023-09-28 10:45:15 +00:00
if ($isVcsEnabled) {
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
}
2023-06-05 16:13:00 +00:00
Console::success("Build id: $buildId created");
/** Set auto deploy */
if ($deployment->getAttribute('activate') === true) {
2023-06-11 10:29:04 +00:00
$function->setAttribute('deploymentInternalId', $deployment->getInternalId());
2023-06-05 16:13:00 +00:00
$function->setAttribute('deployment', $deployment->getId());
2023-09-28 10:45:15 +00:00
$function->setAttribute('live', true);
2023-06-05 16:13:00 +00:00
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
}
2024-06-12 10:21:48 +00:00
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
2024-05-27 13:01:35 +00:00
return;
}
2023-06-05 16:13:00 +00:00
/** Update function schedule */
2023-09-28 10:45:15 +00:00
// Inform scheduler if function is still active
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
2023-06-05 16:13:00 +00:00
$schedule
2023-09-28 10:45:15 +00:00
->setAttribute('resourceUpdatedAt', DateTime::now())
2023-06-05 16:13:00 +00:00
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
2024-10-08 07:54:40 +00:00
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
2023-06-05 16:13:00 +00:00
} catch (\Throwable $th) {
2024-06-12 10:21:48 +00:00
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
2024-03-04 20:30:06 +00:00
return;
}
2023-06-05 16:13:00 +00:00
$endTime = DateTime::now();
2023-09-28 10:45:15 +00:00
$durationEnd = \microtime(true);
2023-06-11 10:29:04 +00:00
$build->setAttribute('endTime', $endTime);
2023-09-28 10:45:15 +00:00
$build->setAttribute('duration', \intval(\ceil($durationEnd - $durationStart)));
2023-06-05 16:13:00 +00:00
$build->setAttribute('status', 'failed');
2024-08-07 15:13:24 +00:00
$build->setAttribute('logs', $th->getMessage());
2023-09-28 10:45:15 +00:00
2024-08-22 10:51:08 +00:00
$build = $dbForProject->updateDocument('builds', $buildId, $build);
2023-09-28 10:45:15 +00:00
if ($isVcsEnabled) {
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
}
2023-06-05 16:13:00 +00:00
} finally {
/**
* Send realtime Event
*/
$target = Realtime::fromPayload(
2024-03-06 17:34:21 +00:00
// Pass first, most verbose event pattern
2023-06-05 16:13:00 +00:00
event: $allEvents[0],
payload: $build,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
2023-10-25 07:39:59 +00:00
/** Trigger usage queue */
if ($build->getAttribute('status') === 'ready') {
$queueForUsage
->addMetric(METRIC_BUILDS_SUCCESS, 1) // per project
->addMetric(METRIC_BUILDS_COMPUTE_SUCCESS, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_SUCCESS), 1) // per function
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS), (int)$build->getAttribute('duration', 0) * 1000);
} elseif ($build->getAttribute('status') === 'failed') {
$queueForUsage
->addMetric(METRIC_BUILDS_FAILED, 1) // per project
->addMetric(METRIC_BUILDS_COMPUTE_FAILED, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_FAILED), 1) // per function
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_FAILED), (int)$build->getAttribute('duration', 0) * 1000);
}
2023-10-25 07:39:59 +00:00
$queueForUsage
->addMetric(METRIC_BUILDS, 1) // per project
->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0))
->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000)
2024-08-20 04:23:11 +00:00
->addMetric(METRIC_BUILDS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
2023-10-25 07:39:59 +00:00
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS), 1) // per function
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), $build->getAttribute('size', 0))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$build->getAttribute('duration', 0) * 1000)
2024-08-20 04:23:11 +00:00
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
2024-01-24 11:29:36 +00:00
->setProject($project)
2023-10-25 07:39:59 +00:00
->trigger();
2023-06-05 16:13:00 +00:00
}
}
2023-09-28 10:45:15 +00:00
2023-10-01 17:39:26 +00:00
/**
* @param string $status
* @param GitHub $github
* @param string $providerCommitHash
* @param string $owner
* @param string $repositoryName
* @param Document $project
* @param Document $function
* @param string $deploymentId
* @param Database $dbForProject
* @param Database $dbForConsole
* @return void
* @throws Structure
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Conflict
* @throws Restricted
*/
2023-09-28 10:45:15 +00:00
protected function runGitAction(string $status, GitHub $github, string $providerCommitHash, string $owner, string $repositoryName, Document $project, Document $function, string $deploymentId, Database $dbForProject, Database $dbForConsole): void
{
if ($function->getAttribute('providerSilentMode', false) === true) {
return;
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$commentId = $deployment->getAttribute('providerCommentId', '');
if (!empty($providerCommitHash)) {
$message = match ($status) {
'ready' => 'Build succeeded.',
'failed' => 'Build failed.',
'processing' => 'Building...',
default => $status
};
$state = match ($status) {
'ready' => 'success',
'failed' => 'failure',
'processing' => 'pending',
default => $status
};
$functionName = $function->getAttribute('name');
$projectName = $project->getAttribute('name');
$name = "{$functionName} ({$projectName})";
2024-04-01 11:02:47 +00:00
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
2023-09-28 10:45:15 +00:00
$functionId = $function->getId();
$projectId = $project->getId();
$providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$projectId/functions/function-$functionId";
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, $state, $message, $providerTargetUrl, $name);
}
if (!empty($commentId)) {
$retries = 0;
while (true) {
$retries++;
try {
$dbForConsole->createDocument('vcsCommentLocks', new Document([
'$id' => $commentId
]));
break;
} catch (\Throwable $err) {
2023-09-28 10:45:15 +00:00
if ($retries >= 9) {
throw $err;
}
\sleep(1);
}
}
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment();
$comment->parseComment($github->getComment($owner, $repositoryName, $commentId));
$comment->addBuild($project, $function, $status, $deployment->getId(), ['type' => 'logs']);
$github->updateComment($owner, $repositoryName, $commentId, $comment->generateComment());
} finally {
$dbForConsole->deleteDocument('vcsCommentLocks', $commentId);
}
}
}
2023-06-05 16:13:00 +00:00
}