diff --git a/composer.json b/composer.json index 6a02be820b..d3ceb8b7a9 100644 --- a/composer.json +++ b/composer.json @@ -45,13 +45,13 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.16.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.49.*", + "utopia-php/abuse": "0.50.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.49.*", + "utopia-php/audit": "0.51.*", "utopia-php/cache": "0.11.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.58.5", + "utopia-php/database": "0.59.0", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", diff --git a/composer.lock b/composer.lock index 69bcd972d0..11d2ba4c2f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a7081f31ed2111c7b3a5a9c25b69bf80", + "content-hash": "b17c58729c4380afcba7714e9bced863", "packages": [ { "name": "adhocore/jwt", @@ -3377,16 +3377,16 @@ }, { "name": "utopia-php/abuse", - "version": "0.49.0", + "version": "0.50.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "76612c274b895aa3d4d1fa27557a6402463eea99" + "reference": "3ff67819e9de61506c5ca070a70552f7ebe99f80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/76612c274b895aa3d4d1fa27557a6402463eea99", - "reference": "76612c274b895aa3d4d1fa27557a6402463eea99", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/3ff67819e9de61506c5ca070a70552f7ebe99f80", + "reference": "3ff67819e9de61506c5ca070a70552f7ebe99f80", "shasum": "" }, "require": { @@ -3394,7 +3394,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "0.58.*" + "utopia-php/database": "0.59.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3422,9 +3422,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.49.0" + "source": "https://github.com/utopia-php/abuse/tree/0.50.0" }, - "time": "2025-02-04T07:33:59+00:00" + "time": "2025-02-12T09:13:59+00:00" }, { "name": "utopia-php/analytics", @@ -3474,21 +3474,21 @@ }, { "name": "utopia-php/audit", - "version": "0.49.0", + "version": "0.51.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "9d5c5e0cf0f6d9157b911fc3971da4331d71c96d" + "reference": "a5a4b73a57e27a0fac8025b1d6038e145a1ca04e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/9d5c5e0cf0f6d9157b911fc3971da4331d71c96d", - "reference": "9d5c5e0cf0f6d9157b911fc3971da4331d71c96d", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/a5a4b73a57e27a0fac8025b1d6038e145a1ca04e", + "reference": "a5a4b73a57e27a0fac8025b1d6038e145a1ca04e", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "0.58.*" + "utopia-php/database": "0.59.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3515,9 +3515,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.49.0" + "source": "https://github.com/utopia-php/audit/tree/0.51.0" }, - "time": "2025-02-04T07:27:18+00:00" + "time": "2025-02-12T09:12:44+00:00" }, { "name": "utopia-php/cache", @@ -3717,16 +3717,16 @@ }, { "name": "utopia-php/database", - "version": "0.58.5", + "version": "0.59.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "55308014c697639c6cb270cb5a33cc88fc1ef839" + "reference": "0eed7f1ad3eb66ff4a7d73b68dd9d3e05089eb18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/55308014c697639c6cb270cb5a33cc88fc1ef839", - "reference": "55308014c697639c6cb270cb5a33cc88fc1ef839", + "url": "https://api.github.com/repos/utopia-php/database/zipball/0eed7f1ad3eb66ff4a7d73b68dd9d3e05089eb18", + "reference": "0eed7f1ad3eb66ff4a7d73b68dd9d3e05089eb18", "shasum": "" }, "require": { @@ -3767,9 +3767,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.58.5" + "source": "https://github.com/utopia-php/database/tree/0.59.0" }, - "time": "2025-02-10T08:26:52+00:00" + "time": "2025-02-12T08:08:29+00:00" }, { "name": "utopia-php/domains", @@ -4170,16 +4170,16 @@ }, { "name": "utopia-php/migration", - "version": "0.6.17", + "version": "0.6.19", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "677a5c4688d7f54d1631a91f76a35d51346cf96b" + "reference": "3c9497f7a54ef88b1077c48d8326893133ad78eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/677a5c4688d7f54d1631a91f76a35d51346cf96b", - "reference": "677a5c4688d7f54d1631a91f76a35d51346cf96b", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/3c9497f7a54ef88b1077c48d8326893133ad78eb", + "reference": "3c9497f7a54ef88b1077c48d8326893133ad78eb", "shasum": "" }, "require": { @@ -4187,7 +4187,7 @@ "ext-curl": "*", "ext-openssl": "*", "php": ">=8.1", - "utopia-php/database": "0.58.*", + "utopia-php/database": "0.59.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "0.33.*", "utopia-php/storage": "0.18.*" @@ -4220,9 +4220,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.6.17" + "source": "https://github.com/utopia-php/migration/tree/0.6.19" }, - "time": "2025-02-05T05:27:29+00:00" + "time": "2025-02-13T07:50:21+00:00" }, { "name": "utopia-php/mongo", @@ -5560,16 +5560,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", "shasum": "" }, "require": { @@ -5608,7 +5608,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" }, "funding": [ { @@ -5616,7 +5616,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-02-12T12:17:51+00:00" }, { "name": "nikic/php-parser", @@ -6190,16 +6190,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299" + "reference": "72e51f7c32c5aef7c8b462195b8c599b11199893" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/c00d78fb6b29658347f9d37ebe104bffadf36299", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/72e51f7c32c5aef7c8b462195b8c599b11199893", + "reference": "72e51f7c32c5aef7c8b462195b8c599b11199893", "shasum": "" }, "require": { @@ -6231,9 +6231,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.1" }, - "time": "2024-10-13T11:29:49+00:00" + "time": "2025-02-13T12:25:43+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index c0bcab1c3a..f1ae46eea7 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -6,15 +6,33 @@ use Appwrite\Auth\Auth; use Exception; use Throwable; use Utopia\Audit\Audit; +use Utopia\CLI\Console; use Utopia\Database\Database; +use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Structure; use Utopia\Platform\Action; use Utopia\Queue\Message; +use Utopia\System\System; class Audits extends Action { + private const BATCH_SIZE_DEVELOPMENT = 1; // smaller batch size for development + private const BATCH_SIZE_PRODUCTION = 5_000; + private const BATCH_AGGREGATION_INTERVAL = 60; // in seconds + + private int $lastTriggeredTime = 0; + + private array $logs = []; + + private function getBatchSize(): int + { + return System::getEnv('_APP_ENV', 'development') === 'development' + ? self::BATCH_SIZE_DEVELOPMENT + : self::BATCH_SIZE_PRODUCTION; + } + public static function getName(): string { return 'audits'; @@ -30,6 +48,8 @@ class Audits extends Action ->inject('message') ->inject('dbForProject') ->callback(fn ($message, $dbForProject) => $this->action($message, $dbForProject)); + + $this->lastTriggeredTime = time(); } @@ -44,13 +64,14 @@ class Audits extends Action */ public function action(Message $message, Database $dbForProject): void { - $payload = $message->getPayload() ?? []; if (empty($payload)) { throw new Exception('Missing payload'); } + Console::info('Aggregating audit logs'); + $event = $payload['event'] ?? ''; $auditPayload = $payload['payload'] ?? ''; $mode = $payload['mode'] ?? ''; @@ -63,23 +84,48 @@ class Audits extends Action $userEmail = $user->getAttribute('email', ''); $userType = $user->getAttribute('type', Auth::ACTIVITY_TYPE_USER); - $audit = new Audit($dbForProject); - $audit->log( - userId: $user->getInternalId(), - // Pass first, most verbose event pattern - event: $event, - resource: $resource, - userAgent: $userAgent, - ip: $ip, - location: '', - data: [ + // Create event data + $eventData = [ + 'userId' => $user->getInternalId(), + 'event' => $event, + 'resource' => $resource, + 'userAgent' => $userAgent, + 'ip' => $ip, + 'location' => '', + 'data' => [ 'userId' => $user->getId(), 'userName' => $userName, 'userEmail' => $userEmail, 'userType' => $userType, 'mode' => $mode, 'data' => $auditPayload, - ] - ); + ], + 'timestamp' => DateTime::formatTz(DateTime::now()) + ]; + + $this->logs[] = $eventData; + + // Check if we should process the batch by checking both for the batch size and the elapsed time + $batchSize = $this->getBatchSize(); + $shouldProcessBatch = count($this->logs) >= $batchSize; + if (!$shouldProcessBatch && count($this->logs) > 0) { + $shouldProcessBatch = (time() - $this->lastTriggeredTime) >= self::BATCH_AGGREGATION_INTERVAL; + } + + if ($shouldProcessBatch) { + Console::log('Processing batch with ' . count($this->logs) . ' events'); + $audit = new Audit($dbForProject); + + try { + $audit->logBatch($this->logs); + Console::success('Audit logs processed successfully'); + } catch (Throwable $e) { + Console::error('Error processing audit logs: ' . $e->getMessage()); + } finally { + // Clear the pending events after successful batch processing + $this->logs = []; + $this->lastTriggeredTime = time(); + } + } } } diff --git a/src/Appwrite/Platform/Workers/Builds.php b/src/Appwrite/Platform/Workers/Builds.php index e7cbbd5088..c9833adcfb 100644 --- a/src/Appwrite/Platform/Workers/Builds.php +++ b/src/Appwrite/Platform/Workers/Builds.php @@ -54,8 +54,10 @@ class Builds extends Action ->inject('cache') ->inject('dbForProject') ->inject('deviceForFunctions') + ->inject('isResourceBlocked') ->inject('log') - ->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log)); + ->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log) => + $this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $isResourceBlocked, $log)); } /** @@ -72,7 +74,7 @@ class Builds extends Action * @return void * @throws \Utopia\Database\Exception */ - public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void + public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log): void { $payload = $message->getPayload() ?? []; @@ -93,7 +95,7 @@ class Builds extends Action case BUILD_TYPE_RETRY: Console::info('Creating build for deployment: ' . $deployment->getId()); $github = new GitHub($cache); - $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $log); + $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log); break; default: @@ -118,7 +120,7 @@ class Builds extends Action * @throws \Utopia\Database\Exception * @throws Exception */ - protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void + protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, callable $isResourceBlocked, Log $log): void { $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); @@ -127,7 +129,11 @@ class Builds extends Action $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { - throw new \Exception('Function not found', 404); + throw new \Exception('Function not found'); + } + + if ($isResourceBlocked($project, RESOURCE_TYPE_FUNCTIONS, $functionId)) { + throw new \Exception('Function blocked'); } $deploymentId = $deployment->getId(); @@ -135,15 +141,15 @@ class Builds extends Action $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->isEmpty()) { - throw new \Exception('Deployment not found', 404); + throw new \Exception('Deployment not found'); } if (empty($deployment->getAttribute('entrypoint', ''))) { - 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); + 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".'); } $version = $function->getAttribute('version', 'v2'); - $spec = Config::getParam('runtime-specifications')[$function->getAttribute('specifications', APP_FUNCTION_SPECIFICATION_DEFAULT)]; + $spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT)]; $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); $key = $function->getAttribute('runtime'); $runtime = $runtimes[$key] ?? null; @@ -571,7 +577,7 @@ class Builds extends Action $build = $dbForProject->getDocument('builds', $build->getId()); if ($build->isEmpty()) { - throw new \Exception('Build not found', 404); + throw new \Exception('Build not found'); } if ($build->getAttribute('status') === 'canceled') { diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index 639e90a867..a6101522fb 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -214,47 +214,17 @@ class StatsResources extends Action protected function countImageTransformations(Database $dbForProject, Database $dbForLogs, string $region) { $totalImageTransformations = 0; - $totalDailyImageTransformations = 0; - $totalHourlyImageTransformations = 0; - $this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $dbForLogs, $region, &$totalDailyImageTransformations, &$totalHourlyImageTransformations, &$totalImageTransformations) { + $last30Days = (new \DateTime())->sub(\DateInterval::createFromDateString('30 days'))->format('Y-m-d 00:00:00'); + $this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $last30Days, $region, &$totalImageTransformations) { $imageTransformations = $dbForProject->count('bucket_' . $bucket->getInternalId(), [ - Query::isNotNull('transformedAt') + Query::greaterThanEqual('transformedAt', $last30Days), ]); $metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED); - $this->createStatsDocuments($region, $metric, $imageTransformations, 'inf'); + $this->createStatsDocuments($region, $metric, $imageTransformations); $totalImageTransformations += $imageTransformations; - - // hourly - $time = \date($this->periods['1h'], \time()); - $start = $time; - $end = (new \DateTime($start))->format('Y-m-d H:59:59'); - $hourlyImageTransformations = $dbForProject->count('bucket_' . $bucket->getInternalId(), [ - Query::greaterThanEqual('transformedAt', $start), - Query::lessThanEqual('transformedAt', $end), - ]); - $metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED); - $this->createStatsDocuments($region, $metric, $hourlyImageTransformations, '1h'); - $totalHourlyImageTransformations += $hourlyImageTransformations; - - // daily - $time = \date($this->periods['1d'], \time()); - $start = $time; - $end = (new \DateTime($start))->format('Y-m-d 11:59:59'); - - $dailyImageTransformations = $dbForProject->count('bucket_' . $bucket->getInternalId(), [ - Query::greaterThanEqual('transformedAt', $start), - Query::lessThanEqual('transformedAt', $end), - ]); - $metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED); - $this->createStatsDocuments($region, $metric, $dailyImageTransformations, '1d'); - $totalDailyImageTransformations += $dailyImageTransformations; - }); - $this->createStatsDocuments($region, METRIC_FILES_IMAGES_TRANSFORMED, $totalImageTransformations, 'inf'); - $this->createStatsDocuments($region, METRIC_FILES_IMAGES_TRANSFORMED, $totalDailyImageTransformations, '1d'); - $this->createStatsDocuments($region, METRIC_FILES_IMAGES_TRANSFORMED, $totalHourlyImageTransformations, '1h'); - + $this->createStatsDocuments($region, METRIC_FILES_IMAGES_TRANSFORMED, $totalImageTransformations); } protected function countForDatabase(Database $dbForProject, Database $dbForLogs, string $region) @@ -341,30 +311,19 @@ class StatsResources extends Action }); } - protected function createStatsDocuments(string $region, string $metric, int $value, ?string $period = null) + protected function createStatsDocuments(string $region, string $metric, int $value) { - if ($period === null) { - foreach ($this->periods as $period => $format) { - $time = 'inf' === $period ? null : \date($format, \time()); - $id = \md5("{$time}_{$period}_{$metric}"); - - $this->documents[] = new Document([ - '$id' => $id, - 'metric' => $metric, - 'period' => $period, - 'region' => $region, - 'value' => $value, - ]); - } - } else { - $time = 'inf' === $period ? null : \date($this->periods[$period], \time()); + foreach ($this->periods as $period => $format) { + $time = 'inf' === $period ? null : \date($format, \time()); $id = \md5("{$time}_{$period}_{$metric}"); + $this->documents[] = new Document([ '$id' => $id, 'metric' => $metric, 'period' => $period, 'region' => $region, 'value' => $value, + 'time' => $time, ]); } } diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 3321a786a7..0723f4d5bf 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -463,7 +463,7 @@ trait DatabasesBase ]); $this->assertEquals(400, $attribute['headers']['status-code']); - $this->assertEquals('Index length is longer than the maximum: 768', $attribute['body']['message']); + $this->assertStringContainsString('Index length is longer than the maximum: 76', $attribute['body']['message']); } public function testUpdateAttributeEnum(): void diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index d8d1eb8eb5..3ed4ca727e 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1984,4 +1984,47 @@ class FunctionsCustomServerTest extends Scope $this->cleanupFunction($functionId); } + + public function testFunctionSpecifications() + { + // Check if the function specifications are correctly set in builds + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'runtime' => 'node-18.0', + 'name' => 'Specification Test', + 'entrypoint' => 'index.js', + 'logging' => false, + 'execute' => ['any'], + 'specification' => Specification::S_2VCPU_2GB, + 'commands' => 'echo $APPWRITE_FUNCTION_MEMORY:$APPWRITE_FUNCTION_CPUS', + ]); + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertEquals(Specification::S_2VCPU_2GB, $function['body']['specification']); + $this->assertNotEmpty($function['body']['$id']); + + $functionId = $functionId = $function['body']['$id'] ?? ''; + + $deploymentId = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertTrue(str_contains($deployment['body']['buildLogs'], '2048:2')); + }, 10000, 500); + + // Check if the function specifications are correctly set in executions + $execution = $this->createExecution($functionId); + + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertNotEmpty($execution['body']['$id']); + + $executionResponse = json_decode($execution['body']['responseBody'], true); + $this->assertEquals('2048', $executionResponse['APPWRITE_FUNCTION_MEMORY']); + $this->assertEquals('2', $executionResponse['APPWRITE_FUNCTION_CPUS']); + + $this->cleanupFunction($functionId); + } } diff --git a/tests/resources/functions/node/index.js b/tests/resources/functions/node/index.js index 041e4a8c12..e8eb938a15 100644 --- a/tests/resources/functions/node/index.js +++ b/tests/resources/functions/node/index.js @@ -14,6 +14,8 @@ module.exports = async(context) => { 'APPWRITE_FUNCTION_USER_ID' : context.req.headers['x-appwrite-user-id'] ?? '', 'APPWRITE_FUNCTION_JWT' : context.req.headers['x-appwrite-user-jwt'] ?? '', 'APPWRITE_FUNCTION_PROJECT_ID' : process.env.APPWRITE_FUNCTION_PROJECT_ID, + 'APPWRITE_FUNCTION_MEMORY' : process.env.APPWRITE_FUNCTION_MEMORY, + 'APPWRITE_FUNCTION_CPUS' : process.env.APPWRITE_FUNCTION_CPUS, 'CUSTOM_VARIABLE' : process.env.CUSTOM_VARIABLE }); } \ No newline at end of file