Merge pull request #10849 from appwrite/feat-separate-specifications

Separate specifications
This commit is contained in:
Matej Bačo
2025-11-24 15:39:02 +01:00
committed by GitHub
17 changed files with 378 additions and 76 deletions
+24 -2
View File
@@ -767,7 +767,18 @@ return [
],
[
'array' => false,
'$id' => ID::custom('specification'),
'$id' => ID::custom('buildSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('runtimeSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
@@ -1191,7 +1202,18 @@ return [
],
[
'array' => false,
'$id' => ID::custom('specification'),
'$id' => ID::custom('buildSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('runtimeSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
+15 -7
View File
@@ -29,6 +29,8 @@ use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
@@ -305,7 +307,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
};
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$resource->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$runtime = match ($type) {
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
@@ -1036,17 +1038,23 @@ App::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.4.0', '<')) {
$response->addFilter(new ResponseV16());
if (version_compare($responseFormat, '1.9.0', '<')) {
$response->addFilter(new ResponseV21());
}
if (version_compare($responseFormat, '1.5.0', '<')) {
$response->addFilter(new ResponseV17());
if (version_compare($responseFormat, '1.8.0', '<')) {
$response->addFilter(new ResponseV20());
}
if (version_compare($responseFormat, '1.7.0', '<')) {
$response->addFilter(new ResponseV19());
}
if (version_compare($responseFormat, '1.6.0', '<')) {
$response->addFilter(new ResponseV18());
}
if (version_compare($responseFormat, '1.7.0', '<')) {
$response->addFilter(new ResponseV19());
if (version_compare($responseFormat, '1.5.0', '<')) {
$response->addFilter(new ResponseV17());
}
if (version_compare($responseFormat, '1.4.0', '<')) {
$response->addFilter(new ResponseV16());
}
if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) {
$warnings[] = "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks";
@@ -164,7 +164,7 @@ class Create extends Base
$version = $function->getAttribute('version', 'v2');
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$function->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
@@ -93,12 +93,18 @@ class Create extends Base
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true)
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function and builds.', true, ['plan'])
), 'Build specification for the function deployments.', true, ['plan'])
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function executions.', true, ['plan'])
->param('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true, deprecated: true)
->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true, deprecated: true)
->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true, deprecated: true)
@@ -136,7 +142,8 @@ class Create extends Base
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
string $buildSpecification,
string $runtimeSpecification,
string $templateRepository,
string $templateOwner,
string $templateRootDirectory,
@@ -231,7 +238,8 @@ class Create extends Base
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification
'buildSpecification' => $buildSpecification,
'runtimeSpecification' => $runtimeSpecification,
]));
} catch (DuplicateException) {
throw new Exception(Exception::FUNCTION_ALREADY_EXISTS);
@@ -89,12 +89,18 @@ class Update extends Base
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true)
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function and builds.', true, ['plan'])
), 'Build specification for the function deployments.', true, ['plan'])
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function executions.', true, ['plan'])
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -125,7 +131,8 @@ class Update extends Base
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
string $buildSpecification,
string $runtimeSpecification,
Request $request,
Response $response,
Database $dbForProject,
@@ -235,13 +242,22 @@ class Update extends Base
}
// Enforce Cold Start if spec limits change.
if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deploymentId'))) {
try {
$executor->deleteRuntime($project->getId(), $function->getAttribute('deploymentId'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
if (!empty($function->getAttribute('deploymentId'))) {
$specsChanged = false;
if ($function->getAttribute('runtimeSpecification') !== $runtimeSpecification) {
$specsChanged = true;
} elseif ($function->getAttribute('buildSpecification') !== $buildSpecification) {
$specsChanged = true;
}
if ($specsChanged) {
try {
$executor->deleteRuntime($project->getId(), $function->getAttribute('deploymentId'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
}
}
@@ -267,7 +283,8 @@ class Update extends Base
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildSpecification' => $buildSpecification,
'runtimeSpecification' => $runtimeSpecification,
'search' => implode(' ', [$functionId, $name, $runtime]),
])));
@@ -252,7 +252,7 @@ class Builds extends Action
$version = $this->getVersion($resource);
$runtime = $this->getRuntime($resource, $version);
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
if ($resource->getCollection() === 'functions' && \is_null($runtime)) {
throw new \Exception('Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
@@ -1354,7 +1354,7 @@ class Builds extends Action
protected function sendUsage(Document $resource, Document $deployment, Document $project, StatsUsage $queue): void
{
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
switch ($deployment->getAttribute('status')) {
case 'ready':
@@ -78,12 +78,18 @@ class Create extends Base
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true)
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Framework specification for the site and builds.', true, ['plan'])
), 'Build specification for the site deployments.', true, ['plan'])
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function SSR executions.', true, ['plan'])
->inject('response')
->inject('dbForProject')
->inject('project')
@@ -110,7 +116,8 @@ class Create extends Base
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
string $buildSpecification,
string $runtimeSpecification,
Response $response,
Database $dbForProject,
Document $project,
@@ -161,7 +168,8 @@ class Create extends Base
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildSpecification' => $buildSpecification,
'runtimeSpecification' => $runtimeSpecification,
'buildRuntime' => $buildRuntime,
'adapter' => $adapter,
]));
@@ -82,12 +82,18 @@ class Update extends Base
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true)
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Framework specification for the site and builds.', true, ['plan'])
), 'Build specification for the site deployments.', true, ['plan'])
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function SSR executions.', true, ['plan'])
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -118,7 +124,8 @@ class Update extends Base
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
string $buildSpecification,
string $runtimeSpecification,
Request $request,
Response $response,
Database $dbForProject,
@@ -233,14 +240,22 @@ class Update extends Base
$live = false;
}
// Enforce Cold Start if spec limits change.
if ($site->getAttribute('specification') !== $specification && !empty($site->getAttribute('deploymentId'))) {
try {
$executor->deleteRuntime($project->getId(), $site->getAttribute('deploymentId'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
if (!empty($site->getAttribute('deploymentId'))) {
$specsChanged = false;
if ($site->getAttribute('runtimeSpecification') !== $runtimeSpecification) {
$specsChanged = true;
} elseif ($site->getAttribute('buildSpecification') !== $buildSpecification) {
$specsChanged = true;
}
if ($specsChanged) {
try {
$executor->deleteRuntime($project->getId(), $site->getAttribute('deploymentId'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
}
}
@@ -263,7 +278,8 @@ class Update extends Base
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildSpecification' => $buildSpecification,
'runtimeSpecification' => $runtimeSpecification,
'search' => implode(' ', [$siteId, $name, $framework]),
'buildRuntime' => $buildRuntime,
'adapter' => $adapter,
+1 -1
View File
@@ -356,7 +356,7 @@ class Functions extends Action
$user ??= new Document();
$functionId = $function->getId();
$deploymentId = $function->getAttribute('deploymentId', '');
$spec = Config::getParam('specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$function->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$log->addTag('deploymentId', $deploymentId);
+18 -1
View File
@@ -6,7 +6,7 @@ use Appwrite\Utopia\Request\Filter;
class V21 extends Filter
{
// Convert 1.8.0 params to 1.8.1
// Convert 1.8.0 params to 1.9.0
public function parse(array $content, string $model): array
{
switch ($model) {
@@ -14,6 +14,12 @@ class V21 extends Filter
case 'sites.createTemplateDeployment':
$content = $this->convertVersionToTypeAndReference($content);
break;
case 'functions.create':
case 'sites.create':
case 'functions.update':
case 'sites.update':
$content = $this->convertSpecs($content);
break;
}
return $content;
}
@@ -31,4 +37,15 @@ class V21 extends Filter
}
return $content;
}
protected function convertSpecs(array $content): array
{
if (!empty($content['specification'])) {
$content['buildSpecification'] = $content['specification'];
$content['runtimeSpecification'] = $content['specification'];
unset($content['specification']);
}
return $content;
}
}
@@ -0,0 +1,51 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
// Convert 1.9 Data format to 1.8 format
class V21 extends Filter
{
public function parse(array $content, string $model): array
{
$parsedResponse = $content;
return match ($model) {
Response::MODEL_SITE => $this->parseSite($content),
Response::MODEL_SITE_LIST => $this->handleList(
$content,
"sites",
fn ($item) => $this->parseSite($item),
),
Response::MODEL_FUNCTION => $this->parseFunction($content),
Response::MODEL_FUNCTION_LIST => $this->handleList(
$content,
"functions",
fn ($item) => $this->parseFunction($item),
),
default => $parsedResponse,
};
}
protected function parseSite(array $content): array
{
$content = $this->parseSpecs($content);
return $content;
}
protected function parseFunction(array $content): array
{
$content = $this->parseSpecs($content);
return $content;
}
protected function parseSpecs(array $content): array
{
$content['specification'] = $content['buildSpecification'] ?? null;
unset($content['buildSpecification']);
unset($content['runtimeSpecification']);
return $content;
}
}
+8 -2
View File
@@ -176,9 +176,15 @@ class Func extends Model
'default' => false,
'example' => false,
])
->addRule('specification', [
->addRule('buildSpecification', [
'type' => self::TYPE_STRING,
'description' => 'Machine specification for builds and executions.',
'description' => 'Machine specification for deployment builds.',
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'example' => APP_COMPUTE_SPECIFICATION_DEFAULT,
])
->addRule('runtimeSpecification', [
'type' => self::TYPE_STRING,
'description' => 'Machine specification for executions.',
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'example' => APP_COMPUTE_SPECIFICATION_DEFAULT,
])
+8 -2
View File
@@ -161,9 +161,15 @@ class Site extends Model
'default' => false,
'example' => false,
])
->addRule('specification', [
->addRule('buildSpecification', [
'type' => self::TYPE_STRING,
'description' => 'Machine specification for builds and executions.',
'description' => 'Machine specification for deployment builds.',
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'example' => APP_COMPUTE_SPECIFICATION_DEFAULT,
])
->addRule('runtimeSpecification', [
'type' => self::TYPE_STRING,
'description' => 'Machine specification for SSR executions.',
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'example' => APP_COMPUTE_SPECIFICATION_DEFAULT,
])
+2 -1
View File
@@ -939,7 +939,8 @@ class UsageTest extends Scope
],
'schedule' => '0 0 1 1 *',
'timeout' => 10,
'specification' => Specification::S_8VCPU_8GB
'buildSpecification' => Specification::S_8VCPU_8GB,
'runtimeSpecification' => Specification::S_4VCPU_4GB,
]
);
@@ -36,14 +36,17 @@ class FunctionsCustomServerTest extends Scope
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'node-22',
'specification' => $specifications['body']['specifications'][0]['slug']
'buildSpecification' => $specifications['body']['specifications'][0]['slug'],
'runtimeSpecification' => $specifications['body']['specifications'][1]['slug'],
]);
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $function['body']['specification']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $function['body']['buildSpecification']);
$this->assertEquals($specifications['body']['specifications'][1]['slug'], $function['body']['runtimeSpecification']);
$function = $this->getFunction($function['body']['$id']);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $function['body']['specification']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $function['body']['buildSpecification']);
$this->assertEquals($specifications['body']['specifications'][1]['slug'], $function['body']['runtimeSpecification']);
$this->cleanupFunction($function['body']['$id']);
@@ -51,7 +54,15 @@ class FunctionsCustomServerTest extends Scope
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'node-22',
'specification' => 'cheap-please'
'buildSpecification' => 'cheap-please'
]);
$this->assertEquals(400, $function['headers']['status-code']);
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'node-22',
'runtimeSpecification' => 'cheap-please'
]);
$this->assertEquals(400, $function['headers']['status-code']);
}
@@ -1364,12 +1375,12 @@ class FunctionsCustomServerTest extends Scope
'timeout' => 15,
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'specification' => Specification::S_1VCPU_1GB,
'runtimeSpecification' => Specification::S_1VCPU_1GB,
]);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$this->assertEquals(Specification::S_1VCPU_1GB, $function['body']['specification']);
$this->assertEquals(Specification::S_1VCPU_1GB, $function['body']['runtimeSpecification']);
// Verify the updated specs
$execution = $this->createExecution($data['functionId']);
@@ -1398,12 +1409,12 @@ class FunctionsCustomServerTest extends Scope
'timeout' => 15,
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'specification' => Specification::S_1VCPU_512MB,
'runtimeSpecification' => Specification::S_1VCPU_512MB,
]);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$this->assertEquals(Specification::S_1VCPU_512MB, $function['body']['specification']);
$this->assertEquals(Specification::S_1VCPU_512MB, $function['body']['runtimeSpecification']);
// Verify the updated specs
$execution = $this->createExecution($data['functionId']);
@@ -1428,11 +1439,29 @@ class FunctionsCustomServerTest extends Scope
'timeout' => 15,
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'specification' => 's-2vcpu-512mb', // Invalid specification
'buildSpecification' => 's-2vcpu-512mb', // Invalid specification
]);
$this->assertEquals(400, $function['headers']['status-code']);
$this->assertStringStartsWith('Invalid `specification` param: Specification must be one of:', $function['body']['message']);
$this->assertStringStartsWith('Invalid `buildSpecification` param: Specification must be one of:', $function['body']['message']);
$function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test1',
'events' => [
'users.*.update.name',
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'runtimeSpecification' => 's-2vcpu-512mb', // Invalid specification
]);
$this->assertEquals(400, $function['headers']['status-code']);
$this->assertStringStartsWith('Invalid `runtimeSpecification` param: Specification must be one of:', $function['body']['message']);
return $data;
}
@@ -2015,6 +2044,8 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertArrayNotHasKey('scopes', $response['body']);
$this->assertArrayNotHasKey('specification', $response['body']);
$this->assertArrayNotHasKey('buildSpecification', $response['body']);
$this->assertArrayNotHasKey('runtimeSpecification', $response['body']);
// get function with 1.5.0 response format header
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $response['body']['$id'], array_merge([
@@ -2025,13 +2056,31 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertArrayNotHasKey('scopes', $function['body']);
$this->assertArrayNotHasKey('buildSpecification', $function['body']);
$this->assertArrayNotHasKey('runtimeSpecification', $function['body']);
$this->assertArrayNotHasKey('specification', $function['body']);
$function = $this->getFunction($function['body']['$id']);
// get function with 1.8.0 response format header
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $response['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0', // add response format header
], $this->getHeaders()));
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertArrayHasKey('scopes', $function['body']);
$this->assertArrayHasKey('specification', $function['body']);
$this->assertArrayNotHasKey('buildSpecification', $function['body']);
$this->assertArrayNotHasKey('runtimeSpecification', $function['body']);
// get function with latest version
$function = $this->getFunction($function['body']['$id']);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertArrayHasKey('scopes', $function['body']);
$this->assertArrayNotHasKey('specification', $function['body']);
$this->assertArrayHasKey('buildSpecification', $function['body']);
$this->assertArrayHasKey('runtimeSpecification', $function['body']);
$functionId = $function['body']['$id'] ?? '';
$this->cleanupFunction($functionId);
@@ -2181,15 +2230,17 @@ class FunctionsCustomServerTest extends Scope
'entrypoint' => 'index.js',
'logging' => false,
'execute' => ['any'],
'specification' => Specification::S_2VCPU_2GB,
'buildSpecification' => Specification::S_2VCPU_2GB,
'runtimeSpecification' => Specification::S_1VCPU_1GB,
'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->assertEquals(Specification::S_2VCPU_2GB, $function['body']['buildSpecification']);
$this->assertEquals(Specification::S_1VCPU_1GB, $function['body']['runtimeSpecification']);
$this->assertNotEmpty($function['body']['$id']);
$functionId = $functionId = $function['body']['$id'] ?? '';
$functionId = $function['body']['$id'] ?? '';
$deploymentId = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('basic'),
@@ -2208,8 +2259,8 @@ class FunctionsCustomServerTest extends Scope
$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->assertEquals('1024', $executionResponse['APPWRITE_FUNCTION_MEMORY']);
$this->assertEquals('1', $executionResponse['APPWRITE_FUNCTION_CPUS']);
$this->cleanupFunction($functionId);
}
@@ -38,14 +38,17 @@ class SitesCustomServerTest extends Scope
'framework' => 'other',
'name' => 'Specs site',
'siteId' => ID::unique(),
'specification' => $specifications['body']['specifications'][0]['slug']
'buildSpecification' => $specifications['body']['specifications'][0]['slug'],
'runtimeSpecification' => $specifications['body']['specifications'][1]['slug'],
]);
$this->assertEquals(201, $site['headers']['status-code']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $site['body']['specification']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $site['body']['buildSpecification']);
$this->assertEquals($specifications['body']['specifications'][1]['slug'], $site['body']['runtimeSpecification']);
$site = $this->getSite($site['body']['$id']);
$this->assertEquals(200, $site['headers']['status-code']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $site['body']['specification']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $site['body']['buildSpecification']);
$this->assertEquals($specifications['body']['specifications'][1]['slug'], $site['body']['runtimeSpecification']);
$this->cleanupSite($site['body']['$id']);
@@ -54,7 +57,16 @@ class SitesCustomServerTest extends Scope
'framework' => 'other',
'name' => 'Specs site',
'siteId' => ID::unique(),
'specification' => 'cheap-please'
'buildSpecification' => 'cheap-please'
]);
$this->assertEquals(400, $site['headers']['status-code']);
$site = $this->createSite([
'buildRuntime' => 'node-22',
'framework' => 'other',
'name' => 'Specs site',
'siteId' => ID::unique(),
'runtimeSpecification' => 'cheap-please'
]);
$this->assertEquals(400, $site['headers']['status-code']);
}
@@ -1287,12 +1299,12 @@ class SitesCustomServerTest extends Scope
'providerBranch' => 'main',
'providerRootDirectory' => './',
'$id' => $siteId,
'specification' => Specification::S_1VCPU_1GB,
'runtimeSpecification' => Specification::S_1VCPU_1GB,
]);
$this->assertEquals(200, $site['headers']['status-code']);
$this->assertNotEmpty($site['body']['$id']);
$this->assertEquals(Specification::S_1VCPU_1GB, $site['body']['specification']);
$this->assertEquals(Specification::S_1VCPU_1GB, $site['body']['runtimeSpecification']);
// Change the specs to 1vcpu 512mb
$site = $this->updateSite([
@@ -1304,12 +1316,12 @@ class SitesCustomServerTest extends Scope
'providerBranch' => 'main',
'providerRootDirectory' => './',
'$id' => $siteId,
'specification' => Specification::S_1VCPU_512MB,
'runtimeSpecification' => Specification::S_1VCPU_512MB,
]);
$this->assertEquals(200, $site['headers']['status-code']);
$this->assertNotEmpty($site['body']['$id']);
$this->assertEquals(Specification::S_1VCPU_512MB, $site['body']['specification']);
$this->assertEquals(Specification::S_1VCPU_512MB, $site['body']['runtimeSpecification']);
/**
* Test for FAILURE
@@ -1324,11 +1336,26 @@ class SitesCustomServerTest extends Scope
'providerBranch' => 'main',
'providerRootDirectory' => './',
'$id' => $siteId,
'specification' => 's-2vcpu-512mb', // Invalid specification
'buildSpecification' => 's-2vcpu-512mb', // Invalid specification
]);
$this->assertEquals(400, $site['headers']['status-code']);
$this->assertStringStartsWith('Invalid `specification` param: Specification must be one of:', $site['body']['message']);
$this->assertStringStartsWith('Invalid `buildSpecification` param: Specification must be one of:', $site['body']['message']);
$site = $this->updateSite([
'buildRuntime' => 'node-22',
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
'providerBranch' => 'main',
'providerRootDirectory' => './',
'$id' => $siteId,
'runtimeSpecification' => 's-2vcpu-512mb', // Invalid specification
]);
$this->assertEquals(400, $site['headers']['status-code']);
$this->assertStringStartsWith('Invalid `runtimeSpecification` param: Specification must be one of:', $site['body']['message']);
$this->cleanupSite($siteId);
}
@@ -2939,4 +2966,55 @@ class SitesCustomServerTest extends Scope
$this->cleanupSite($siteId);
}
public function testSiteSpecifications()
{
// Check if the site specifications are correctly set in builds
$site = $this->createSite([
'siteId' => ID::unique(),
'name' => 'Astro site',
'framework' => 'astro',
'adapter' => 'ssr',
'buildRuntime' => 'node-22',
'outputDirectory' => './dist',
'buildCommand' => 'npm run build && echo $APPWRITE_SITE_MEMORY:$APPWRITE_SITE_CPUS',
'installCommand' => 'npm install',
'fallbackFile' => '',
'buildSpecification' => Specification::S_2VCPU_2GB,
'runtimeSpecification' => Specification::S_1VCPU_1GB,
]);
$this->assertEquals(201, $site['headers']['status-code']);
$this->assertEquals(Specification::S_2VCPU_2GB, $site['body']['buildSpecification']);
$this->assertEquals(Specification::S_1VCPU_1GB, $site['body']['runtimeSpecification']);
$this->assertNotEmpty($site['body']['$id']);
$siteId = $site['body']['$id'] ?? '';
$domain = $this->setupSiteDomain($siteId);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('astro'),
'activate' => true
]);
$this->assertEventually(function () use ($siteId, $deploymentId) {
$deployment = $this->getDeployment($siteId, $deploymentId);
\var_dump($deployment['body']['buildLogs']);
$this->assertStringContainsString('2048:2', $deployment['body']['buildLogs']);
}, 10000, 500);
// Check if the sites specifications are correctly set in executions
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/specs');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertEquals('1024', $response['body']['APPWRITE_SITE_MEMORY']);
$this->assertEquals('1', $response['body']['APPWRITE_SITE_CPUS']);
$this->cleanupSite($siteId);
}
}
@@ -0,0 +1,13 @@
export async function GET(_context) {
return new Response(
JSON.stringify({
APPWRITE_SITE_MEMORY: process.env.APPWRITE_SITE_MEMORY,
APPWRITE_SITE_CPUS: process.env.APPWRITE_SITE_CPUS,
}),
{
headers: {
"Content-Type": "application/json",
},
},
);
}