Merge branch 'feat-sites' into chore-rules-rehaul

This commit is contained in:
Matej Bačo
2025-03-08 21:33:13 +01:00
72 changed files with 1806 additions and 1135 deletions
+106 -203
View File
@@ -1154,7 +1154,6 @@ return [
]
],
],
'deployments' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('deployments'),
@@ -1193,28 +1192,6 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('buildInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('buildId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('entrypoint'),
@@ -1228,18 +1205,7 @@ return [
],
[
'array' => false,
'$id' => ID::custom('commands'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildCommand'),
'$id' => ID::custom('buildCommands'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
@@ -1250,7 +1216,7 @@ return [
],
[
'array' => false,
'$id' => ID::custom('installCommand'),
'$id' => ID::custom('buildOutput'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
@@ -1260,18 +1226,7 @@ return [
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('outputDirectory'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'$id' => ID::custom('path'),
'$id' => ID::custom('sourcePath'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
@@ -1285,7 +1240,7 @@ return [
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'size' => 32,
'signed' => true,
'required' => true,
'default' => null,
@@ -1464,7 +1419,7 @@ return [
'array' => false,
],
[
'$id' => ID::custom('size'),
'$id' => ID::custom('sourceSize'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
@@ -1475,7 +1430,7 @@ return [
'filters' => [],
],
[
'$id' => ID::custom('metadata'),
'$id' => ID::custom('sourceMetadata'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384, // https://tools.ietf.org/html/rfc4288#section-4.2
@@ -1486,7 +1441,7 @@ return [
'filters' => ['json'],
],
[
'$id' => ID::custom('chunksTotal'),
'$id' => ID::custom('sourceChunksTotal'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
@@ -1497,7 +1452,7 @@ return [
'filters' => [],
],
[
'$id' => ID::custom('chunksUploaded'),
'$id' => ID::custom('sourceChunksUploaded'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
@@ -1551,6 +1506,83 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('buildStartAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('buildEndAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('buildDuration'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('buildSize'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => true,
'default' => 'processing',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('buildPath'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('buildLogs'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 1000000,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
]
],
'indexes' => [
[
@@ -1575,16 +1607,23 @@ return [
'orders' => [],
],
[
'$id' => ID::custom('_key_size'),
'$id' => ID::custom('_key_sourceSize'),
'type' => Database::INDEX_KEY,
'attributes' => ['size'],
'attributes' => ['sourceSize'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_buildId'),
'$id' => ID::custom('_key_buildSize'),
'type' => Database::INDEX_KEY,
'attributes' => ['buildId'],
'attributes' => ['buildSize'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_buildDuration'),
'type' => Database::INDEX_KEY,
'attributes' => ['buildDuration'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
@@ -1595,155 +1634,19 @@ return [
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
],
'builds' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('builds'),
'name' => 'Builds',
'attributes' => [
[
'$id' => ID::custom('startTime'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('endTime'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('duration'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('size'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('runtime'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => true,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => true,
'default' => 'processing',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('path'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('logs'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 1000000,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('sourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => true,
'default' => 'local',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('source'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => true,
'default' => '',
'array' => false,
'filters' => [],
]
],
'indexes' => [
[
'$id' => ID::custom('_key_deployment'),
'$id' => ID::custom('_key_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['deploymentId'],
'lengths' => [Database::LENGTH_KEY],
'attributes' => ['type'],
'lengths' => [32],
'orders' => [Database::ORDER_ASC],
]
], [
'$id' => ID::custom('_key_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['status'],
'lengths' => [16],
'orders' => [Database::ORDER_ASC],
],
],
],
+6 -1
View File
@@ -527,7 +527,7 @@ return [
'code' => 404,
],
Exception::FUNCTION_ENTRYPOINT_MISSING => [
'name' => Exception::FUNCTION_RUNTIME_UNSUPPORTED,
'name' => Exception::FUNCTION_ENTRYPOINT_MISSING,
'description' => 'Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".',
'code' => 404,
],
@@ -541,6 +541,11 @@ return [
'description' => 'Function Template with the requested ID could not be found.',
'code' => 404,
],
Exception::FUNCTION_RUNTIME_NOT_DETECTED => [
'name' => Exception::FUNCTION_RUNTIME_NOT_DETECTED,
'description' => 'Function runtime could not be detected.',
'code' => 400,
],
/** Sites */
Exception::SITE_NOT_FOUND => [
+10 -28
View File
@@ -23,6 +23,8 @@ return [
'name' => 'Next.js',
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'bundleCommand' => 'sh /usr/local/server/helpers/next-js/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/next-js/env.sh',
'adapters' => [
'ssr' => [
'key' => 'ssr',
@@ -30,8 +32,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './.next',
'startCommand' => 'sh helpers/next-js/server.sh',
'bundleCommand' => 'sh /usr/local/server/helpers/next-js/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/next-js/env.sh',
],
'static' => [
'key' => 'static',
@@ -39,8 +39,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './out',
'startCommand' => 'sh helpers/server.sh',
'bundleCommand' => '',
'envCommand' => '',
]
]
],
@@ -56,8 +54,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './dist',
'startCommand' => 'sh helpers/server.sh',
'bundleCommand' => '',
'envCommand' => '',
'fallbackFile' => 'index.html'
]
]
@@ -67,6 +63,8 @@ return [
'name' => 'Nuxt',
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'bundleCommand' => 'sh /usr/local/server/helpers/nuxt/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/nuxt/env.sh',
'adapters' => [
'ssr' => [
'key' => 'ssr',
@@ -74,8 +72,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './.output',
'startCommand' => 'sh helpers/nuxt/server.sh',
'bundleCommand' => 'sh /usr/local/server/helpers/nuxt/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/nuxt/env.sh',
],
'static' => [
'key' => 'static',
@@ -83,8 +79,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './dist',
'startCommand' => 'sh helpers/server.sh',
'bundleCommand' => '',
'envCommand' => '',
]
]
],
@@ -100,8 +94,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './dist',
'startCommand' => 'sh helpers/server.sh',
'bundleCommand' => '',
'envCommand' => '',
'fallbackFile' => 'index.html'
]
]
@@ -111,6 +103,8 @@ return [
'name' => 'SvelteKit',
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'bundleCommand' => 'sh /usr/local/server/helpers/sveltekit/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/sveltekit/env.sh',
'adapters' => [
'ssr' => [
'key' => 'ssr',
@@ -118,8 +112,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './build',
'startCommand' => 'sh helpers/sveltekit/server.sh',
'bundleCommand' => 'sh /usr/local/server/helpers/sveltekit/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/sveltekit/env.sh',
],
'static' => [
'key' => 'static',
@@ -127,8 +119,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './build',
'startCommand' => 'sh helpers/server.sh',
'bundleCommand' => '',
'envCommand' => '',
]
]
],
@@ -137,6 +127,8 @@ return [
'name' => 'Astro',
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'bundleCommand' => 'sh /usr/local/server/helpers/astro/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/astro/env.sh',
'adapters' => [
'ssr' => [
'key' => 'ssr',
@@ -144,8 +136,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './dist',
'startCommand' => 'sh helpers/astro/server.sh',
'bundleCommand' => 'sh /usr/local/server/helpers/astro/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/astro/env.sh',
],
'static' => [
'key' => 'static',
@@ -153,8 +143,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './dist',
'startCommand' => 'sh helpers/server.sh',
'bundleCommand' => '',
'envCommand' => '',
]
]
],
@@ -163,6 +151,8 @@ return [
'name' => 'Remix',
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'bundleCommand' => 'sh /usr/local/server/helpers/remix/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/remix/env.sh',
'adapters' => [
'ssr' => [
'key' => 'ssr',
@@ -170,8 +160,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './build',
'startCommand' => 'sh helpers/remix/server.sh',
'bundleCommand' => 'sh /usr/local/server/helpers/remix/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/remix/env.sh',
],
'static' => [
'key' => 'static',
@@ -179,8 +167,6 @@ return [
'installCommand' => 'npm install',
'outputDirectory' => './build/client',
'startCommand' => 'sh helpers/server.sh',
'bundleCommand' => '',
'envCommand' => '',
]
]
],
@@ -196,8 +182,6 @@ return [
'installCommand' => '',
'outputDirectory' => './build/web',
'startCommand' => 'sh helpers/server.sh',
'bundleCommand' => '',
'envCommand' => '',
],
],
],
@@ -213,8 +197,6 @@ return [
'installCommand' => '',
'outputDirectory' => './',
'startCommand' => 'sh helpers/server.sh',
'bundleCommand' => '',
'envCommand' => '',
],
]
],
-51
View File
@@ -1,51 +0,0 @@
<?php
use Appwrite\Functions\Specification;
return [
Specification::S_05VCPU_512MB => [
'slug' => Specification::S_05VCPU_512MB,
'memory' => 512,
'cpus' => 0.5
],
Specification::S_1VCPU_512MB => [
'slug' => Specification::S_1VCPU_512MB,
'memory' => 512,
'cpus' => 1
],
Specification::S_1VCPU_1GB => [
'slug' => Specification::S_1VCPU_1GB,
'memory' => 1024,
'cpus' => 1
],
Specification::S_2VCPU_2GB => [
'slug' => Specification::S_2VCPU_2GB,
'memory' => 2048,
'cpus' => 2
],
Specification::S_2VCPU_4GB => [
'slug' => Specification::S_2VCPU_4GB,
'memory' => 4096,
'cpus' => 2
],
Specification::S_4VCPU_4GB => [
'slug' => Specification::S_4VCPU_4GB,
'memory' => 4096,
'cpus' => 4
],
Specification::S_4VCPU_8GB => [
'slug' => Specification::S_4VCPU_8GB,
'memory' => 8192,
'cpus' => 4
],
Specification::S_8VCPU_4GB => [
'slug' => Specification::S_8VCPU_4GB,
'memory' => 4096,
'cpus' => 8
],
Specification::S_8VCPU_8GB => [
'slug' => Specification::S_8VCPU_8GB,
'memory' => 8192,
'cpus' => 8
]
];
@@ -1,6 +1,6 @@
<?php
use Appwrite\Sites\Specification;
use Appwrite\Platform\Modules\Compute\Specification;
return [
Specification::S_05VCPU_512MB => [
@@ -27,7 +27,7 @@ const TEMPLATE_FRAMEWORKS = [
'outputDirectory' => './build',
'buildRuntime' => 'node-22',
'adapter' => 'ssr',
'fallbackFile' => null,
'fallbackFile' => '',
],
'NEXTJS' => [
'key' => 'nextjs',
@@ -37,7 +37,7 @@ const TEMPLATE_FRAMEWORKS = [
'outputDirectory' => './.next',
'buildRuntime' => 'node-22',
'adapter' => 'ssr',
'fallbackFile' => null,
'fallbackFile' => '',
],
'NUXT' => [
'key' => 'nuxt',
@@ -47,7 +47,7 @@ const TEMPLATE_FRAMEWORKS = [
'outputDirectory' => './.output',
'buildRuntime' => 'node-22',
'adapter' => 'ssr',
'fallbackFile' => null,
'fallbackFile' => '',
],
'REMIX' => [
'key' => 'remix',
@@ -57,7 +57,7 @@ const TEMPLATE_FRAMEWORKS = [
'outputDirectory' => './build',
'buildRuntime' => 'node-22',
'adapter' => 'ssr',
'fallbackFile' => null,
'fallbackFile' => '',
],
'ASTRO' => [
'key' => 'astro',
@@ -67,7 +67,7 @@ const TEMPLATE_FRAMEWORKS = [
'outputDirectory' => './dist',
'buildRuntime' => 'node-22',
'adapter' => 'ssr',
'fallbackFile' => null,
'fallbackFile' => '',
],
'FLUTTER' => [
'key' => 'flutter',
@@ -77,7 +77,7 @@ const TEMPLATE_FRAMEWORKS = [
'outputDirectory' => './build/web',
'buildRuntime' => 'flutter-3.24',
'adapter' => 'static',
'fallbackFile' => null,
'fallbackFile' => '',
],
'OTHER' => [
'key' => 'other',
+228 -75
View File
@@ -14,7 +14,6 @@ use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\Comment;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -27,22 +26,35 @@ use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Detector\Adapter\Bun;
use Utopia\Detector\Adapter\CPP;
use Utopia\Detector\Adapter\Dart;
use Utopia\Detector\Adapter\Deno;
use Utopia\Detector\Adapter\Dotnet;
use Utopia\Detector\Adapter\Java;
use Utopia\Detector\Adapter\JavaScript;
use Utopia\Detector\Adapter\PHP;
use Utopia\Detector\Adapter\Python;
use Utopia\Detector\Adapter\Ruby;
use Utopia\Detector\Adapter\Swift;
use Utopia\Detector\Detector;
use Utopia\Detector\Detection\Framework\Astro;
use Utopia\Detector\Detection\Framework\Flutter;
use Utopia\Detector\Detection\Framework\NextJs;
use Utopia\Detector\Detection\Framework\Nuxt;
use Utopia\Detector\Detection\Framework\Remix;
use Utopia\Detector\Detection\Framework\SvelteKit;
use Utopia\Detector\Detection\Packager\NPM;
use Utopia\Detector\Detection\Packager\PNPM;
use Utopia\Detector\Detection\Packager\Yarn;
use Utopia\Detector\Detection\Runtime\Bun;
use Utopia\Detector\Detection\Runtime\CPP;
use Utopia\Detector\Detection\Runtime\Dart;
use Utopia\Detector\Detection\Runtime\Deno;
use Utopia\Detector\Detection\Runtime\Dotnet;
use Utopia\Detector\Detection\Runtime\Java;
use Utopia\Detector\Detection\Runtime\Node;
use Utopia\Detector\Detection\Runtime\PHP;
use Utopia\Detector\Detection\Runtime\Python;
use Utopia\Detector\Detection\Runtime\Ruby;
use Utopia\Detector\Detection\Runtime\Swift;
use Utopia\Detector\Detector\Framework;
use Utopia\Detector\Detector\Packager;
use Utopia\Detector\Detector\Runtime;
use Utopia\Detector\Detector\Strategy;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Host;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
@@ -196,6 +208,17 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['name'];
}
$commands = [];
if (!empty($resource->getAttribute('buildCommand', ''))) {
$commands[] = $resource->getAttribute('buildCommand', '');
}
if (!empty($resource->getAttribute('installCommand', ''))) {
$commands[] = $resource->getAttribute('installCommand', '');
}
if (!empty($resource->getAttribute('commands', ''))) {
$commands[] = $resource->getAttribute('commands', '');
}
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
@@ -207,10 +230,8 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
'resourceInternalId' => $resourceInternalId,
'resourceType' => $resourceCollection,
'entrypoint' => $resource->getAttribute('entrypoint', ''),
'commands' => $resource->getAttribute('commands', ''),
'installCommand' => $resource->getAttribute('installCommand', ''),
'buildCommand' => $resource->getAttribute('buildCommand', ''),
'outputDirectory' => $resource->getAttribute('outputDirectory', ''),
'buildCommands' => \implode(' && ', $commands),
'buildOutput' => $resource->getAttribute('outputDirectory', ''),
'type' => 'vcs',
'installationId' => $installationId,
'installationInternalId' => $installationInternalId,
@@ -608,8 +629,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
]), Response::MODEL_VCS_CONTENT_LIST);
});
App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection')
->desc('Detect runtime settings from source code')
App::post('/v1/vcs/github/installations/:installationId/detections')
->alias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection')
->desc('Create repository detection')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
->label('sdk', new Method(
@@ -620,18 +642,22 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DETECTION,
model: Response::MODEL_DETECTION_RUNTIME,
),
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DETECTION_FRAMEWORK,
)
]
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('providerRepositoryId', '', new Text(256), 'Repository Id')
->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework')
->param('providerRootDirectory', '', new Text(256, 0), 'Path to Root Directory', true)
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
->action(function (string $installationId, string $providerRepositoryId, string $type, string $providerRootDirectory, GitHub $github, Response $response, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
@@ -657,32 +683,108 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr
$files = \array_column($files, 'name');
$languages = $github->listRepositoryLanguages($owner, $repositoryName);
$detectorFactory = new Detector($files, $languages);
$detector = new Packager($files);
$detector
->addOption(new Yarn())
->addOption(new PNPM())
->addOption(new NPM());
$detection = $detector->detect();
$detectorFactory
->addDetector(new JavaScript())
->addDetector(new Bun())
->addDetector(new PHP())
->addDetector(new Python())
->addDetector(new Dart())
->addDetector(new Swift())
->addDetector(new Ruby())
->addDetector(new Java())
->addDetector(new CPP())
->addDetector(new Deno())
->addDetector(new Dotnet());
$packager = !\is_null($detection) ? $detection->getName() : 'npm';
$runtime = $detectorFactory->detect();
if ($type === 'framework') {
$output = new Document([
'framework' => '',
'installCommand' => '',
'buildCommand' => '',
'outputDirectory' => '',
]);
$runtimes = Config::getParam('runtimes');
$runtimeDetail = \array_reverse(\array_filter(\array_keys($runtimes), function ($key) use ($runtime, $runtimes) {
return $runtimes[$key]['key'] === $runtime;
}))[0] ?? '';
$detector = new Framework($files, $packager);
$detector
->addOption(new Flutter())
->addOption(new Nuxt())
->addOption(new Astro())
->addOption(new Remix())
->addOption(new SvelteKit())
->addOption(new NextJs());
$detection = [];
$detection['runtime'] = $runtimeDetail;
$framework = $detector->detect();
$response->dynamic(new Document($detection), Response::MODEL_DETECTION);
if (!\is_null($framework)) {
$framework = $framework->getName();
$output->setAttribute('installCommand', $framework->getInstallCommand());
$output->setAttribute('buildCommand', $framework->getBuildCommand());
$output->setAttribute('outputDirectory', $framework->getOutputDirectory());
} else {
$framework = 'other';
$output->setAttribute('installCommand', '');
$output->setAttribute('buildCommand', '');
$output->setAttribute('outputDirectory', '');
}
$frameworks = Config::getParam('frameworks');
if (!\in_array($framework, array_keys($frameworks), true)) {
$framework = 'other';
}
$output->setAttribute('framework', $framework);
} else {
$output = new Document([
'runtime' => '',
'commands' => '',
'entrypoint' => '',
]);
$strategies = [
new Strategy(Strategy::FILEMATCH),
new Strategy(Strategy::LANGUAGES),
new Strategy(Strategy::EXTENSION),
];
foreach ($strategies as $strategy) {
$detector = new Runtime($strategy === Strategy::LANGUAGES ? $languages : $files, $strategy, $packager);
$detector
->addOption(new Node())
->addOption(new Bun())
->addOption(new Deno())
->addOption(new PHP())
->addOption(new Python())
->addOption(new Dart())
->addOption(new Swift())
->addOption(new Ruby())
->addOption(new Java())
->addOption(new CPP())
->addOption(new Dotnet());
$runtime = $detector->detect();
if (!\is_null($runtime)) {
$output->setAttribute('commands', $runtime->getCommands());
$output->setAttribute('entrypoint', $runtime->getEntrypoint());
$runtime = $runtime->getName();
break;
}
}
if (!empty($runtime)) {
$runtimes = Config::getParam('runtimes');
$runtimeWithVersion = '';
foreach ($runtimes as $runtimeKey => $runtimeConfig) {
if ($runtimeConfig['key'] === $runtime) {
$runtimeWithVersion = $runtimeKey;
}
}
if (empty($runtimeWithVersion)) {
throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED);
}
$output->setAttribute('runtime', $runtimeWithVersion);
} else {
throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED);
}
}
$response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME);
});
App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
@@ -697,17 +799,21 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROVIDER_REPOSITORY_LIST,
model: Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST,
),
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST,
)
]
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForPlatform')
->action(function (string $installationId, string $search, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
->action(function (string $installationId, string $type, string $search, GitHub $github, Response $response, Database $dbForPlatform) {
if (empty($search)) {
$search = "";
}
@@ -737,39 +843,86 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
return $repo;
}, $repos);
$repos = batch(\array_map(function ($repo) use ($github) {
return function () use ($repo, $github) {
try {
$files = $github->listRepositoryContents($repo['organization'], $repo['name'], '');
$files = \array_column($files, 'name');
$repos = batch(\array_map(function ($repo) use ($type, $github) {
return function () use ($repo, $type, $github) {
$files = $github->listRepositoryContents($repo['organization'], $repo['name'], '');
$files = \array_column($files, 'name');
$detector = new Packager($files);
$detector
->addOption(new Yarn())
->addOption(new PNPM())
->addOption(new NPM());
$detection = $detector->detect();
$packager = !\is_null($detection) ? $detection->getName() : 'npm';
if ($type === 'framework') {
$frameworkDetector = new Framework($files, $packager);
$frameworkDetector
->addOption(new Flutter())
->addOption(new Nuxt())
->addOption(new Astro())
->addOption(new Remix())
->addOption(new SvelteKit())
->addOption(new NextJs());
$detectedFramework = $frameworkDetector->detect();
if (!\is_null($detectedFramework)) {
$framework = $detectedFramework->getName();
} else {
$framework = 'other';
}
$frameworks = Config::getParam('frameworks');
if (!\in_array($framework, array_keys($frameworks), true)) {
$framework = 'other';
}
$repo['framework'] = $framework;
} else {
$languages = $github->listRepositoryLanguages($repo['organization'], $repo['name']);
$detectorFactory = new Detector($files, $languages);
$strategies = [
new Strategy(Strategy::FILEMATCH),
new Strategy(Strategy::LANGUAGES),
new Strategy(Strategy::EXTENSION),
];
$detectorFactory
->addDetector(new JavaScript())
->addDetector(new Bun())
->addDetector(new PHP())
->addDetector(new Python())
->addDetector(new Dart())
->addDetector(new Swift())
->addDetector(new Ruby())
->addDetector(new Java())
->addDetector(new CPP())
->addDetector(new Deno())
->addDetector(new Dotnet());
foreach ($strategies as $strategy) {
$detector = new Runtime($strategy === Strategy::LANGUAGES ? $languages : $files, $strategy, $packager);
$detector
->addOption(new Node())
->addOption(new Bun())
->addOption(new Deno())
->addOption(new PHP())
->addOption(new Python())
->addOption(new Dart())
->addOption(new Swift())
->addOption(new Ruby())
->addOption(new Java())
->addOption(new CPP())
->addOption(new Dotnet());
$runtime = $detectorFactory->detect();
$runtime = $detector->detect();
$runtimes = Config::getParam('runtimes');
$runtimeDetail = \array_reverse(\array_filter(\array_keys($runtimes), function ($key) use ($runtime, $runtimes) {
return $runtimes[$key]['key'] === $runtime;
}))[0] ?? '';
if (!\is_null($runtime)) {
$runtime = $runtime->getName();
break;
}
}
$repo['runtime'] = $runtimeDetail;
} catch (Throwable $error) {
$repo['runtime'] = "";
Console::warning("Runtime not detected for " . $repo['organization'] . "/" . $repo['name']);
if (!empty($runtime)) {
$runtimes = Config::getParam('runtimes');
$runtimeWithVersion = '';
foreach ($runtimes as $runtimeKey => $runtimeConfig) {
if ($runtimeConfig['key'] === $runtime) {
$runtimeWithVersion = $runtimeKey;
}
}
$repo['runtime'] = $runtimeWithVersion ?? '';
}
}
return $repo;
};
@@ -780,9 +933,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
}, $repos);
$response->dynamic(new Document([
'providerRepositories' => $repos,
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
'total' => \count($repos),
]), Response::MODEL_PROVIDER_REPOSITORY_LIST);
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);
});
App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
+106 -9
View File
@@ -3,6 +3,7 @@
require_once __DIR__ . '/../init.php';
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Event\Certificate;
@@ -155,6 +156,80 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$path .= '?' . $query;
}
$protocol = $request->getProtocol();
/**
Ensure preview authorization
- Authorization is skippable for tests, and build screenshot
- If cookie is not sent by client -> not authorized
- If JWT in cookie is invalid or expired -> not authorized
- If user is blocked or removed -> not authorized
- If user's session is removed or expired -> not authorized
- If user is not member of team of this deployment -> not authorized
- If not authorized, redirect to Console redirect UI
- If authorized, continue as if auth was not required
*/
$requirePreview = \is_null($apiKey) || !$apiKey->isPreviewAuthDisabled();
if ($isPreview && $requirePreview) {
$cookie = $request->getCookie(Auth::$cookieNamePreview, '');
$authorized = false;
// Security checks to mark authorized true
if (!empty($cookie)) {
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
$payload = [];
try {
$payload = $jwt->decode($cookie);
} catch (JWTException $error) {
// Authorized remains false
}
$userExists = false;
$userId = $payload['userId'] ?? '';
if (!empty($userId)) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if (!$user->isEmpty() && $user->getAttribute('status', false)) {
$userExists = true;
}
}
$sessionExists = false;
$jwtSessionId = $payload['sessionId'] ?? '';
if (!empty($jwtSessionId) && !empty($user->find('$id', $jwtSessionId, 'sessions'))) {
$sessionExists = true;
}
$membershipExists = false;
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if (!$project->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
$membership = $user->find('teamId', $teamId, 'memberships');
if (!empty($membership)) {
$membershipExists = true;
}
}
if ($userExists && $sessionExists && $membershipExists) {
$authorized = true;
}
}
if (!$authorized) {
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . "://" . System::getEnv('_APP_DOMAIN');
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($url . '/console/auth/preview?'
. \http_build_query([
'projectId' => $projectId,
'origin' => $protocol . '://' . $host,
'path' => $path
]));
return true;
}
}
$body = $swooleRequest->getContent() ?? '';
$method = $swooleRequest->server['request_method'];
@@ -174,7 +249,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
};
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('runtime-specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$runtime = match ($type) {
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
@@ -191,13 +266,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
}
/** Check if build has completed */
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
throw new AppwriteException(AppwriteException::BUILD_NOT_FOUND);
}
if ($build->getAttribute('status') !== 'ready') {
if ($deployment->getAttribute('status') !== 'ready') {
throw new AppwriteException(AppwriteException::BUILD_NOT_READY);
}
@@ -388,7 +457,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
variables: $vars,
timeout: $resource->getAttribute('timeout', 30),
image: $runtime['image'],
source: $build->getAttribute('path', ''),
source: $deployment->getAttribute('buildPath', ''),
entrypoint: $entrypoint,
version: $version,
path: $path,
@@ -1277,6 +1346,34 @@ App::get('/v1/ping')
$response->text('Pong!');
});
// Preview authorization
App::get('/_appwrite/authorize')
->inject('request')
->inject('response')
->inject('previewHostname')
->action(function (Request $request, Response $response, string $previewHostname) {
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
$host = $previewHostname;
}
$referrer = $request->getReferer();
$protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME);
$jwt = $request->getParam('jwt', '');
$path = $request->getParam('path', '');
$duration = 60 * 60 * 24; // 1 day in seconds
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addCookie(Auth::$cookieNamePreview, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null)
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($protocol . '://' . $host . $path);
});
App::wildcard()
->groups(['api'])
->label('scope', 'global')
+4 -5
View File
@@ -36,13 +36,13 @@ use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Functions\Specification;
use Appwrite\GraphQL\Promises\Adapter\Swoole;
use Appwrite\GraphQL\Schema;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Origin;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Platform\Modules\Compute\Specification;
use Appwrite\PubSub\Adapter\Redis as PubSub;
use Appwrite\URL\URL as AppwriteURL;
use Appwrite\Utopia\Request;
@@ -389,10 +389,9 @@ Config::load('storage-logos', __DIR__ . '/config/storage/logos.php');
Config::load('storage-mimes', __DIR__ . '/config/storage/mimes.php');
Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php');
Config::load('storage-outputs', __DIR__ . '/config/storage/outputs.php');
Config::load('runtime-specifications', __DIR__ . '/config/runtimes/specifications.php');
Config::load('framework-specifications', __DIR__ . '/config/frameworks/specifications.php');
Config::load('function-templates', __DIR__ . '/config/function-templates.php');
Config::load('site-templates', __DIR__ . '/config/site-templates.php');
Config::load('specifications', __DIR__ . '/config/specifications.php');
Config::load('function-templates', __DIR__ . '/config/templates/function.php');
Config::load('site-templates', __DIR__ . '/config/templates/site.php');
/**
* New DB Filters
+1
View File
@@ -53,6 +53,7 @@
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.59.0",
"utopia-php/detector": "0.1.*",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
Generated
+46 -1
View File
@@ -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": "3b6171de8c624cfbcd723f1cc76a9560",
"content-hash": "093587189a16826d640cab3e6ca5f251",
"packages": [
{
"name": "adhocore/jwt",
@@ -3758,6 +3758,51 @@
},
"time": "2025-02-12T08:08:29+00:00"
},
{
"name": "utopia-php/detector",
"version": "0.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/detector.git",
"reference": "ddeee9c3e702ae10b3eb53cafe5210a0c4896c94"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/detector/zipball/ddeee9c3e702ae10b3eb53cafe5210a0c4896c94",
"reference": "ddeee9c3e702ae10b3eb53cafe5210a0c4896c94",
"shasum": ""
},
"require": {
"php": ">=8.0"
},
"require-dev": {
"laravel/pint": "1.2.*",
"phpstan/phpstan": "1.8.*",
"phpunit/phpunit": "^9.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Utopia\\Detector\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A simple library for fast and reliable environment identification.",
"keywords": [
"detector",
"framework",
"php",
"utopia"
],
"support": {
"issues": "https://github.com/utopia-php/detector/issues",
"source": "https://github.com/utopia-php/detector/tree/0.1.0"
},
"time": "2025-03-08T16:04:33+00:00"
},
{
"name": "utopia-php/domains",
"version": "0.5.0",
+5
View File
@@ -103,6 +103,11 @@ class Auth
*/
public static $cookieName = 'a_session';
/**
* @var string
*/
public static $cookieNamePreview = 'a_jwt_console';
/**
* User Unique ID.
*
+9 -1
View File
@@ -23,6 +23,7 @@ class Key
protected bool $hostnameOverride = false,
protected bool $bannerDisabled = false,
protected bool $projectCheckDisabled = false,
protected bool $previewAuthDisabled = false,
) {
}
@@ -73,6 +74,11 @@ class Key
return $this->bannerDisabled;
}
public function isPreviewAuthDisabled(): bool
{
return $this->previewAuthDisabled;
}
public function isProjectCheckDisabled(): bool
{
return $this->projectCheckDisabled;
@@ -132,6 +138,7 @@ class Key
$hostnameOverride = $payload['hostnameOverride'] ?? false;
$bannerDisabled = $payload['bannerDisabled'] ?? false;
$projectCheckDisabled = $payload['projectCheckDisabled'] ?? false;
$previewAuthDisabled = $payload['previewAuthDisabled'] ?? false;
$scopes = \array_merge($payload['scopes'] ?? [], $scopes);
if (!$projectCheckDisabled && $projectId !== $project->getId()) {
@@ -148,7 +155,8 @@ class Key
$disabledMetrics,
$hostnameOverride,
$bannerDisabled,
$projectCheckDisabled
$projectCheckDisabled,
$previewAuthDisabled
);
case API_KEY_STANDARD:
$key = $project->find(
+1
View File
@@ -165,6 +165,7 @@ class Exception extends \Exception
public const FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing';
public const FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout';
public const FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found';
public const FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected';
/** Deployments */
public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found';
-16
View File
@@ -1,16 +0,0 @@
<?php
namespace Appwrite\Functions;
class Specification
{
public const S_05VCPU_512MB = 's-0.5vcpu-512mb';
public const S_1VCPU_512MB = 's-1vcpu-512mb';
public const S_1VCPU_1GB = 's-1vcpu-1gb';
public const S_2VCPU_2GB = 's-2vcpu-2gb';
public const S_2VCPU_4GB = 's-2vcpu-4gb';
public const S_4VCPU_4GB = 's-4vcpu-4gb';
public const S_4VCPU_8GB = 's-4vcpu-8gb';
public const S_8VCPU_4GB = 's-8vcpu-4gb';
public const S_8VCPU_8GB = 's-8vcpu-8gb';
}
+11 -4
View File
@@ -70,7 +70,7 @@ class Base extends Action
'resourceInternalId' => $function->getInternalId(),
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'commands' => $function->getAttribute('commands', ''),
'buildCommands' => $function->getAttribute('commands', ''),
'type' => 'vcs',
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
@@ -139,6 +139,14 @@ class Base extends Action
}
}
$commands = [];
if (!empty($site->getAttribute('buildCommand', ''))) {
$commands[] = $site->getAttribute('buildCommand', '');
}
if (!empty($site->getAttribute('installCommand', ''))) {
$commands[] = $site->getAttribute('installCommand', '');
}
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
@@ -149,9 +157,8 @@ class Base extends Action
'resourceId' => $site->getId(),
'resourceInternalId' => $site->getInternalId(),
'resourceType' => 'sites',
'buildCommand' => $site->getAttribute('buildCommand', ''),
'installCommand' => $site->getAttribute('installCommand', ''),
'outputDirectory' => $site->getAttribute('outputDirectory', ''),
'buildCommands' => implode(' && ', $commands),
'buildOutput' => $site->getAttribute('outputDirectory', ''),
'type' => 'vcs',
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Sites;
namespace Appwrite\Platform\Modules\Compute;
class Specification
{
@@ -1,10 +1,10 @@
<?php
namespace Appwrite\Functions\Validator;
namespace Appwrite\Platform\Modules\Compute\Validator;
use Utopia\Validator;
class RuntimeSpecification extends Validator
class Specification extends Validator
{
private array $plan;
@@ -35,8 +35,8 @@ class RuntimeSpecification extends Validator
foreach ($this->specifications as $size => $values) {
if ($values['cpus'] <= $this->maxCpus && $values['memory'] <= $this->maxMemory) {
if (!empty($this->plan) && array_key_exists('runtimeSpecifications', $this->plan)) {
if (!\in_array($size, $this->plan['runtimeSpecifications'])) {
if (!empty($this->plan) && array_key_exists('specifications', $this->plan)) {
if (!\in_array($size, $this->plan['specifications'])) {
continue;
}
}
@@ -174,8 +174,8 @@ class Create extends Action
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('chunksTotal', 1);
$metadata = $deployment->getAttribute('metadata', []);
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
$metadata = $deployment->getAttribute('sourceMetadata', []);
if ($chunk === -1) {
$chunk = $chunks;
}
@@ -217,18 +217,17 @@ class Create extends Action
'resourceInternalId' => $function->getInternalId(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'buildInternalId' => '',
'entrypoint' => $entrypoint,
'commands' => $commands,
'path' => $path,
'size' => $fileSize,
'buildCommands' => $commands,
'sourcePath' => $path,
'sourceSize' => $fileSize,
'search' => implode(' ', [$deploymentId, $entrypoint]),
'activate' => $activate,
'metadata' => $metadata,
'sourceMetadata' => $metadata,
'type' => $type
]));
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('size', $fileSize)->setAttribute('metadata', $metadata));
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata));
}
// Start the build
@@ -248,20 +247,19 @@ class Create extends Action
'resourceInternalId' => $function->getInternalId(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'buildInternalId' => '',
'entrypoint' => $entrypoint,
'commands' => $commands,
'path' => $path,
'size' => $fileSize,
'chunksTotal' => $chunks,
'chunksUploaded' => $chunksUploaded,
'buildCommands' => $commands,
'sourcePath' => $path,
'sourceSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'search' => implode(' ', [$deploymentId, $entrypoint]),
'activate' => $activate,
'metadata' => $metadata,
'sourceMetadata' => $metadata,
'type' => $type
]));
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('chunksUploaded', $chunksUploaded)->setAttribute('metadata', $metadata));
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata));
}
}
@@ -83,8 +83,8 @@ class Delete extends Action
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from DB');
}
if (!empty($deployment->getAttribute('path', ''))) {
if (!($deviceForFunctions->delete($deployment->getAttribute('path', '')))) {
if (!empty($deployment->getAttribute('sourcePath', ''))) {
if (!($deviceForFunctions->delete($deployment->getAttribute('sourcePath', '')))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage');
}
}
@@ -81,16 +81,11 @@ class Get extends Action
switch ($type) {
case 'output':
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
$path = $build->getAttribute('path', '');
$path = $deployment->getAttribute('buildPath', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('path', '');
$path = $deployment->getAttribute('sourcePath', '');
$device = $deviceForFunctions;
break;
}
@@ -77,7 +77,7 @@ class Create extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$path = $deployment->getAttribute('path');
$path = $deployment->getAttribute('sourcePath');
if (empty($path) || !$deviceForFunctions->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
@@ -91,12 +91,17 @@ class Create extends Action
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
'$internalId' => '',
'$id' => $deploymentId,
'buildId' => '',
'buildInternalId' => '',
'path' => $destination,
'sourcePath' => $destination,
'entrypoint' => $function->getAttribute('entrypoint'),
'commands' => $function->getAttribute('commands', ''),
'buildCommands' => $function->getAttribute('commands', ''),
'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]),
'buildStartAt' => null,
'buildEndAt' => null,
'buildDuration' => null,
'buildSize' => null,
'status' => 'processing',
'buildPath' => '',
'buildLogs' => '',
]));
$queueForBuilds
@@ -69,13 +69,6 @@ class Get extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
$deployment->setAttribute('status', $build->getAttribute('status', 'waiting'));
$deployment->setAttribute('buildLogs', $build->getAttribute('logs', ''));
$deployment->setAttribute('buildTime', $build->getAttribute('duration', 0));
$deployment->setAttribute('buildSize', $build->getAttribute('size', 0));
$deployment->setAttribute('size', $deployment->getAttribute('size', 0));
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -13,8 +13,6 @@ use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -50,7 +48,7 @@ class Update extends Action
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_BUILD,
model: Response::MODEL_DEPLOYMENT,
)
]
))
@@ -77,46 +75,19 @@ class Update extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
$buildId = ID::unique();
$build = $dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$permissions' => [],
'startTime' => DateTime::now(),
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'status' => 'canceled',
'path' => '',
'runtime' => $function->getAttribute('runtime'),
'source' => $deployment->getAttribute('path', ''),
'sourceType' => '',
'logs' => '',
'duration' => 0,
'size' => 0
]));
$deployment->setAttribute('buildId', $build->getId());
$deployment->setAttribute('buildInternalId', $build->getInternalId());
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
} else {
if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) {
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
}
$startTime = new \DateTime($build->getAttribute('startTime'));
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([
'endTime' => DateTime::now(),
'duration' => $duration,
'status' => 'canceled'
]));
if (\in_array($deployment->getAttribute('status'), ['ready', 'failed'])) {
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
}
$dbForProject->purgeCachedDocument('deployments', $deployment->getId());
$startTime = new \DateTime($deployment->getAttribute('buildStartAt'));
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([
'buildEndAt' => DateTime::now(),
'buildDuration' => $duration,
'status' => 'canceled'
]));
try {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
@@ -132,6 +103,6 @@ class Update extends Action
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($build, Response::MODEL_BUILD);
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -130,7 +130,7 @@ class Create extends Base
'resourceInternalId' => $function->getInternalId(),
'resourceType' => 'functions',
'entrypoint' => $function->getAttribute('entrypoint', ''),
'commands' => $function->getAttribute('commands', ''),
'buildCommands' => $function->getAttribute('commands', ''),
'type' => 'manual',
'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint', '')]),
'activate' => $activate,
@@ -110,15 +110,6 @@ class XList extends Action
$results = $dbForProject->find('deployments', $queries);
$total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT);
foreach ($results as $result) {
$build = $dbForProject->getDocument('builds', $result->getAttribute('buildId', ''));
$result->setAttribute('status', $build->getAttribute('status', 'processing'));
$result->setAttribute('buildLogs', $build->getAttribute('logs', ''));
$result->setAttribute('buildTime', $build->getAttribute('duration', 0));
$result->setAttribute('buildSize', $build->getAttribute('size', 0));
$result->setAttribute('size', $result->getAttribute('size', 0));
}
$response->dynamic(new Document([
'deployments' => $results,
'total' => $total,
@@ -4,8 +4,6 @@ namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Event\Build;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\StatsUsage;
@@ -140,7 +138,7 @@ class Create extends Base
$version = $function->getAttribute('version', 'v2');
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
@@ -158,13 +156,7 @@ class Create extends Base
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
}
/** Check if build has completed */
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
if ($build->getAttribute('status') !== 'ready') {
if ($deployment->getAttribute('status') !== 'ready') {
throw new Exception(Exception::BUILD_NOT_READY);
}
@@ -385,7 +377,7 @@ class Create extends Base
variables: $vars,
timeout: $function->getAttribute('timeout', 0),
image: $runtime['image'],
source: $build->getAttribute('path', ''),
source: $deployment->getAttribute('buildPath', ''),
entrypoint: $deployment->getAttribute('entrypoint', ''),
version: $version,
path: $path,
@@ -5,8 +5,8 @@ namespace Appwrite\Platform\Modules\Functions\Http\Functions;
use Appwrite\Event\Event;
use Appwrite\Event\Validator\FunctionEvent;
use Appwrite\Extend\Exception;
use Appwrite\Functions\Validator\RuntimeSpecification;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Modules\Compute\Validator\Specification;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -85,9 +85,9 @@ 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', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification(
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('runtime-specifications', []),
Config::getParam('specifications', []),
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
), 'Runtime specification for the function and builds.', true, ['plan'])
@@ -68,7 +68,6 @@ class Update extends Base
{
$function = $dbForProject->getDocument('functions', $functionId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
@@ -78,11 +77,7 @@ class Update extends Base
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
if ($build->getAttribute('status') !== 'ready') {
if ($deployment->getAttribute('status') !== 'ready') {
throw new Exception(Exception::BUILD_NOT_READY);
}
@@ -6,8 +6,8 @@ use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Event\Validator\FunctionEvent;
use Appwrite\Extend\Exception;
use Appwrite\Functions\Validator\RuntimeSpecification;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Modules\Compute\Validator\Specification;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -89,9 +89,9 @@ 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', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification(
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('runtime-specifications', []),
Config::getParam('specifications', []),
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
), 'Runtime specification for the function and builds.', true, ['plan'])
@@ -206,8 +206,6 @@ class Update extends Base
$live = false;
}
$spec = Config::getParam('runtime-specifications')[$specification] ?? [];
// Enforce Cold Start if spec limits change.
if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
@@ -19,7 +19,7 @@ class XList extends Base
public static function getName()
{
return 'listFunctionsSpecifications';
return 'listSpecifications';
}
public function __construct()
@@ -28,7 +28,7 @@ class XList extends Base
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/specifications')
->groups(['api', 'functions'])
->desc('List available function runtime specifications')
->desc('List specifications')
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
@@ -52,25 +52,25 @@ class XList extends Base
public function action(Response $response, array $plan)
{
$allRuntimeSpecs = Config::getParam('runtime-specifications', []);
$allSpecs = Config::getParam('specifications', []);
$runtimeSpecs = [];
foreach ($allRuntimeSpecs as $spec) {
$specs = [];
foreach ($allSpecs as $spec) {
$spec['enabled'] = true;
if (array_key_exists('runtimeSpecifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']);
if (array_key_exists('specifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['specifications']);
}
// Only add specs that are within the limits set by environment variables
if ($spec['cpus'] <= System::getEnv('_APP_COMPUTE_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_COMPUTE_MEMORY', 512)) {
$runtimeSpecs[] = $spec;
$specs[] = $spec;
}
}
$response->dynamic(new Document([
'specifications' => $runtimeSpecs,
'total' => count($runtimeSpecs)
'specifications' => $specs,
'total' => count($specs)
]), Response::MODEL_SPECIFICATION_LIST);
}
}
@@ -27,6 +27,9 @@ use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Detector\Detection\Rendering\SSR;
use Utopia\Detector\Detection\Rendering\XStatic;
use Utopia\Detector\Detector\Rendering;
use Utopia\Fetch\Client as FetchClient;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
@@ -173,7 +176,7 @@ class Builds extends Action
$version = $this->getVersion($resource);
$runtime = $this->getRuntime($resource, $version);
$spec = Config::getParam('runtime-specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
if ($resource->getCollection() === 'functions' && \is_null($runtime)) {
throw new \Exception('Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
@@ -187,39 +190,19 @@ class Builds extends Action
$startTime = DateTime::now();
$durationStart = \microtime(true);
$buildId = $deployment->getAttribute('buildId', '');
$build = $dbForProject->getDocument('builds', $buildId);
$isNewBuild = empty($buildId);
if ($build->isEmpty()) {
$buildId = ID::unique();
$build = $dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$permissions' => [],
'startTime' => $startTime,
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'status' => 'processing',
'path' => '',
'runtime' => $resource->getAttribute('runtime'),
'source' => $deployment->getAttribute('path', ''),
'sourceType' => strtolower($deviceForFunctions->getType()),
'logs' => '',
'endTime' => null,
'duration' => 0,
'size' => 0
]));
$deployment->setAttribute('buildId', $build->getId());
$deployment->setAttribute('buildInternalId', $build->getInternalId());
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
} elseif ($build->getAttribute('status') === 'canceled') {
if ($deployment->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
return;
} else {
$build = $dbForProject->getDocument('builds', $buildId);
}
$source = $deployment->getAttribute('path', '');
$deploymentId = $deployment->getId();
$deployment->setAttribute('buildStartAt', $startTime);
$deployment->setAttribute('status', 'processing');
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$source = $deployment->getAttribute('sourcePath', '');
$installationId = $deployment->getAttribute('installationId', '');
$providerRepositoryId = $deployment->getAttribute('providerRepositoryId', '');
$providerCommitHash = $deployment->getAttribute('providerCommitHash', '');
@@ -237,7 +220,7 @@ class Builds extends Action
}
try {
if ($isNewBuild && !$isVcsEnabled) {
if (!$isVcsEnabled) {
// Non-VCS + Template
$templateRepositoryName = $template->getAttribute('repositoryName', '');
$templateOwnerName = $template->getAttribute('ownerName', '');
@@ -253,7 +236,7 @@ class Builds extends Action
$stderr = '';
// Clone template repo
$tmpTemplateDirectory = '/tmp/builds/' . $buildId . '-template';
$tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '-template';
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory);
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
@@ -287,12 +270,14 @@ class Builds extends Action
Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $stdout, $stderr);
$directorySize = $device->getFileSize($source);
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize));
$deployment
->setAttribute('sourcePath', $source)
->setAttribute('sourceSize', $directorySize);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
}
} elseif ($isNewBuild && $isVcsEnabled) {
} elseif ($isVcsEnabled) {
// VCS and VCS+Temaplte
$tmpDirectory = '/tmp/builds/' . $buildId . '/code';
$tmpDirectory = '/tmp/builds/' . $deploymentId . '/code';
$rootDirectory = $resource->getAttribute('providerRootDirectory', '');
$rootDirectory = \rtrim($rootDirectory, '/');
$rootDirectory = \ltrim($rootDirectory, '.');
@@ -318,9 +303,9 @@ class Builds extends Action
$stdout = '';
$stderr = '';
Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $stdout, $stderr);
Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $deploymentId), '', $stdout, $stderr);
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
return;
}
@@ -357,7 +342,7 @@ class Builds extends Action
if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) {
// Clone template repo
$tmpTemplateDirectory = '/tmp/builds/' . $buildId . '/template';
$tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '/template';
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory);
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
@@ -402,19 +387,19 @@ class Builds extends Action
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $build,
payload: $deployment,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
payload: $deployment->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
}
$tmpPath = '/tmp/builds/' . $buildId;
$tmpPath = '/tmp/builds/' . $deployment;
$tmpPathFile = $tmpPath . '/code.tar.gz';
$localDevice = new Local();
@@ -431,7 +416,7 @@ class Builds extends Action
Console::execute('find ' . \escapeshellarg($tmpDirectory) . ' -type d -name ".git" -exec rm -rf {} +', '', $stdout, $stderr);
$tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory);
$tarParamDirectory = '/tmp/builds/' . $deploymentId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory);
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
$source = $device->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
@@ -443,17 +428,19 @@ class Builds extends Action
Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $stdout, $stderr);
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
$directorySize = $device->getFileSize($source);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize));
$deployment
->setAttribute('sourcePath', $source)
->setAttribute('sourceSize', $directorySize);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
}
/** Request the executor to build the code... */
$build->setAttribute('status', 'building');
$build = $dbForProject->updateDocument('builds', $buildId, $build);
$deployment->setAttribute('status', 'building');
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
if ($isVcsEnabled) {
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
@@ -482,13 +469,13 @@ class Builds extends Action
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $build,
payload: $deployment,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
payload: $deployment->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
@@ -583,7 +570,7 @@ class Builds extends Action
$response = null;
$err = null;
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
return;
}
@@ -605,7 +592,7 @@ class Builds extends Action
cpus: $cpus,
memory: $memory,
timeout: $timeout,
remove: true,
remove: false,
entrypoint: $deployment->getAttribute('entrypoint', ''),
destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}",
variables: $vars,
@@ -616,26 +603,22 @@ class Builds extends Action
$err = $error;
}
}),
Co\go(function () use ($executor, $project, $deployment, &$response, &$build, $dbForProject, $allEvents, $timeout, &$err, &$isCanceled) {
Co\go(function () use ($executor, $project, &$deployment, &$response, $dbForProject, $allEvents, $timeout, &$err, &$isCanceled) {
try {
$executor->getLogs(
deploymentId: $deployment->getId(),
projectId: $project->getId(),
timeout: $timeout,
callback: function ($logs) use (&$response, &$err, &$build, $dbForProject, $allEvents, $project, &$isCanceled) {
callback: function ($logs) use (&$response, &$err, $dbForProject, $allEvents, $project, &$isCanceled, &$deployment) {
if ($isCanceled) {
return;
}
// If we have response or error from concurrent coroutine, we already have latest logs
if ($response === null && $err === null) {
$build = $dbForProject->getDocument('builds', $build->getId());
$deployment = $dbForProject->getDocument('deployments', $deployment->getId());
if ($build->isEmpty()) {
throw new \Exception('Build not found');
}
if ($build->getAttribute('status') === 'canceled') {
if ($deployment->getAttribute('status') === 'canceled') {
$isCanceled = true;
Console::info('Ignoring realtime logs because build has been canceled');
return;
@@ -644,7 +627,7 @@ class Builds extends Action
// Get only valid UTF8 part - removes leftover half-multibytes causing SQL errors
$logs = \mb_substr($logs, 0, null, 'UTF-8');
$currentLogs = $build->getAttribute('logs', '');
$currentLogs = $deployment->getAttribute('buildLogs', '');
$streamLogs = \str_replace("\\n", "{APPWRITE_LINEBREAK_PLACEHOLDER}", $logs);
foreach (\explode("\n", $streamLogs) as $streamLog) {
@@ -659,8 +642,8 @@ class Builds extends Action
$currentLogs .= $streamParts[1];
}
$build = $build->setAttribute('logs', $currentLogs);
$build = $dbForProject->updateDocument('builds', $build->getId(), $build);
$deployment = $deployment->setAttribute('buildLogs', $currentLogs);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
/**
* Send realtime Event
@@ -668,12 +651,12 @@ class Builds extends Action
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $build,
payload: $deployment,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
payload: $deployment->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
@@ -689,7 +672,7 @@ class Builds extends Action
}),
]);
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
return;
}
@@ -706,27 +689,55 @@ class Builds extends Action
throw new \Exception('Build size should be less than ' . number_format($buildSizeLimit / 1048576, 2) . ' MBs.');
}
if ($resource->getCollection() === 'sites' && empty($resource->getAttribute('adapter'))) {
// TODO: Refactor with structured command in future, using utopia library (CLI)
$listFilesCommand = "cd /usr/local/build && cd " . \escapeshellarg($resource->getAttribute('outputDirectory')) . " && find . -name 'node_modules' -prune -o -type f -print";
$command = $executor->createCommand(
deploymentId: $deployment->getId(),
projectId: $project->getId(),
command: $listFilesCommand,
timeout: 15
);
$files = \explode("\n", $command['output']); // Parse output
$files = \array_filter($files); // Remove empty
$files = \array_map(fn ($file) => \trim($file), $files); // Remove whitepsaces
$files = \array_map(fn ($file) => \str_starts_with($file, './') ? \substr($file, 2) : $file, $files); // Remove beginning ./
$detector = new Rendering($files, $resource->getAttribute('framework', ''));
$detector
->addOption(new SSR())
->addOption(new XStatic());
$detection = $detector->detect();
$resource->setAttribute('adapter', $detection->getName());
$resource->setAttribute('fallbackFile', $detection->getFallbackFile() ?? '');
$resource = $dbForProject->updateDocument('sites', $resource->getId(), $resource);
}
$executor->deleteRuntime($project->getId(), $deployment->getId(), '-build');
/** Update the build document */
$build->setAttribute('startTime', DateTime::format((new \DateTime())->setTimestamp(floor($response['startTime']))));
$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']);
$deployment->setAttribute('buildStartAt', DateTime::format((new \DateTime())->setTimestamp(floor($response['startTime']))));
$deployment->setAttribute('buildEndAt', $endTime);
$deployment->setAttribute('buildDuration', \intval(\ceil($durationEnd - $durationStart)));
$deployment->setAttribute('status', 'ready');
$deployment->setAttribute('buildPath', $response['path']);
$deployment->setAttribute('buildSize', $response['size']);
$logs = '';
foreach ($response['output'] as $log) {
$logs .= $log['content'];
}
$build->setAttribute('logs', $logs);
$deployment->setAttribute('buildLogs', $logs);
$build = $dbForProject->updateDocument('builds', $buildId, $build);
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
if ($isVcsEnabled) {
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
}
Console::success("Build id: $buildId created");
Console::success("Build id: $deploymentId created");
if ($resource->getCollection() === 'sites') {
try {
@@ -763,7 +774,8 @@ class Builds extends Action
$apiKey = $jwtObj->encode([
'hostnameOverride' => true,
'bannerDisabled' => true,
'projectCheckDisabled' => true
'projectCheckDisabled' => true,
'previewAuthDisabled' => true,
]);
// TODO: @Meldiron if becomes too slow, do concurrently
@@ -946,7 +958,7 @@ class Builds extends Action
}
}
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
return;
}
@@ -963,20 +975,19 @@ class Builds extends Action
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
}
} catch (\Throwable $th) {
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
return;
}
$endTime = DateTime::now();
$durationEnd = \microtime(true);
$build->setAttribute('endTime', $endTime);
$build->setAttribute('duration', \intval(\ceil($durationEnd - $durationStart)));
$build->setAttribute('status', 'failed');
$deployment->setAttribute('buildEndAt', $endTime);
$deployment->setAttribute('buildDuration', \intval(\ceil($durationEnd - $durationStart)));
$deployment->setAttribute('status', 'failed');
$deployment->setAttribute('buildLogs', "" . $th->getMessage());
$build->setAttribute('logs', "[31m" . $th->getMessage() . "[0m");
$build = $dbForProject->updateDocument('builds', $buildId, $build);
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
if ($isVcsEnabled) {
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
@@ -988,26 +999,26 @@ class Builds extends Action
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $build,
payload: $deployment,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
payload: $deployment->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
$this->sendUsage(
resource:$resource,
build: $build,
deployment: $deployment,
project: $project,
queue: $queueForStatsUsage
);
}
}
protected function sendUsage(Document $resource, Document $build, Document $project, StatsUsage $queue): void
protected function sendUsage(Document $resource, Document $deployment, Document $project, StatsUsage $queue): void
{
$key = match($resource->getCollection()) {
'functions' => 'functionInternalId',
@@ -1038,32 +1049,32 @@ class Builds extends Action
]
};
switch ($build->getAttribute('status')) {
switch ($deployment->getAttribute('status')) {
case 'ready':
$queue
->addMetric(METRIC_BUILDS_SUCCESS, 1) // per project
->addMetric(METRIC_BUILDS_COMPUTE_SUCCESS, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(METRIC_BUILDS_COMPUTE_SUCCESS, (int)$deployment->getAttribute('buildDuration', 0) * 1000)
->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_SUCCESS), 1) // per function
->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS), (int)$build->getAttribute('duration', 0) * 1000);
->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS), (int)$deployment->getAttribute('buildDuration', 0) * 1000);
break;
case 'failed':
$queue
->addMetric(METRIC_BUILDS_FAILED, 1) // per project
->addMetric(METRIC_BUILDS_COMPUTE_FAILED, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(METRIC_BUILDS_COMPUTE_FAILED, (int)$deployment->getAttribute('buildDuration', 0) * 1000)
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsFailed']), 1) // per function
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsComputeFailed']), (int)$build->getAttribute('duration', 0) * 1000);
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsComputeFailed']), (int)$deployment->getAttribute('buildDuration', 0) * 1000);
break;
}
$queue
->addMetric(METRIC_BUILDS, 1) // per project
->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0))
->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(METRIC_BUILDS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
->addMetric(METRIC_BUILDS_STORAGE, $deployment->getAttribute('buildSize', 0))
->addMetric(METRIC_BUILDS_COMPUTE, (int)$deployment->getAttribute('buildDuration', 0) * 1000)
->addMetric(METRIC_BUILDS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $deployment->getAttribute('buildDuration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS), 1) // per function
->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), $build->getAttribute('size', 0))
->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), $deployment->getAttribute('buildSize', 0))
->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$deployment->getAttribute('buildDuration', 0) * 1000)
->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $deployment->getAttribute('buildDuration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
->setProject($project)
->trigger();
}
@@ -1095,7 +1106,7 @@ class Builds extends Action
protected function getCommand(Document $resource, Document $deployment): string
{
if ($resource->getCollection() === 'functions') {
return $deployment->getAttribute('commands', '');
return $deployment->getAttribute('buildCommands', '');
} elseif ($resource->getCollection() === 'sites') {
$commands = [];
@@ -1104,20 +1115,13 @@ class Builds extends Action
$envCommand = '';
$bundleCommand = '';
if (!is_null($framework)) {
$adapter = ($framework['adapters'] ?? [])[$resource->getAttribute('adapter', '')] ?? null;
if (!is_null($adapter) && isset($adapter['envCommand'])) {
$envCommand = $adapter['envCommand'];
}
if (!is_null($adapter) && isset($adapter['bundleCommand'])) {
$bundleCommand = $adapter['bundleCommand'];
}
$envCommand = $framework['envCommand'] ?? '';
$bundleCommand = $framework['bundleCommand'] ?? '';
}
$commands[] = $envCommand;
$commands[] = $deployment->getAttribute('installCommand', '');
$commands[] = $deployment->getAttribute('buildCommand', '');
$commands[] = $deployment->getAttribute('buildCommands', '');
$commands[] = $bundleCommand;
$commands = array_filter($commands, fn ($command) => !empty($command));
@@ -173,8 +173,8 @@ class Create extends Action
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('chunksTotal', 1);
$metadata = $deployment->getAttribute('metadata', []);
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
$metadata = $deployment->getAttribute('sourceMetadata', []);
if ($chunk === -1) {
$chunk = $chunks;
}
@@ -188,6 +188,14 @@ class Create extends Action
$type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual';
$commands = [];
if (!empty($installCommand)) {
$commands[] = $installCommand;
}
if (!empty($buildCommand)) {
$commands[] = $buildCommand;
}
if ($chunksUploaded === $chunks) {
if ($activate) {
// Remove deploy for all other deployments.
@@ -216,15 +224,13 @@ class Create extends Action
'resourceInternalId' => $site->getInternalId(),
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildInternalId' => '',
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'outputDirectory' => $outputDirectory,
'path' => $path,
'size' => $fileSize,
'buildCommands' => \implode(' && ', $commands),
'buildOutput' => $outputDirectory,
'sourcePath' => $path,
'sourceSize' => $fileSize,
'search' => implode(' ', [$deploymentId]),
'activate' => $activate,
'metadata' => $metadata,
'sourceMetadata' => $metadata,
'type' => $type
]));
@@ -253,7 +259,7 @@ class Create extends Action
]))
);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('size', $fileSize)->setAttribute('metadata', $metadata));
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata));
}
// Start the build
@@ -273,17 +279,15 @@ class Create extends Action
'resourceInternalId' => $site->getInternalId(),
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildInternalId' => '',
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'outputDirectory' => $outputDirectory,
'path' => $path,
'size' => $fileSize,
'chunksTotal' => $chunks,
'chunksUploaded' => $chunksUploaded,
'buildCommands' => \implode(' && ', $commands),
'buildOutput' => $outputDirectory,
'sourcePath' => $path,
'sourceSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'search' => implode(' ', [$deploymentId]),
'activate' => $activate,
'metadata' => $metadata,
'sourceMetadata' => $metadata,
'type' => $type
]));
@@ -305,7 +309,7 @@ class Create extends Action
]))
);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('chunksUploaded', $chunksUploaded)->setAttribute('metadata', $metadata));
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata));
}
}
@@ -83,8 +83,8 @@ class Delete extends Action
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from DB');
}
if (!empty($deployment->getAttribute('path', ''))) {
if (!($deviceForSites->delete($deployment->getAttribute('path', '')))) {
if (!empty($deployment->getAttribute('sourcePath', ''))) {
if (!($deviceForSites->delete($deployment->getAttribute('sourcePath', '')))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage');
}
}
@@ -80,16 +80,11 @@ class Get extends Action
switch ($type) {
case 'output':
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
$path = $build->getAttribute('path', '');
$path = $deployment->getAttribute('buildPath', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('path', '');
$path = $deployment->getAttribute('sourcePath', '');
$device = $deviceForSites;
break;
}
@@ -78,7 +78,7 @@ class Create extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$path = $deployment->getAttribute('path');
$path = $deployment->getAttribute('sourcePath');
if (empty($path) || !$deviceForSites->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
@@ -88,19 +88,31 @@ class Create extends Action
$destination = $deviceForSites->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$deviceForSites->transfer($path, $destination, $deviceForSites);
$commands = [];
if (!empty($site->getAttribute('buildCommand', ''))) {
$commands[] = $site->getAttribute('buildCommand', '');
}
if (!empty($site->getAttribute('installCommand', ''))) {
$commands[] = $site->getAttribute('installCommand', '');
}
$deployment->removeAttribute('$internalId');
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
'$internalId' => '',
'$id' => $deploymentId,
'buildId' => '',
'buildInternalId' => '',
'path' => $destination,
'buildCommand' => $site->getAttribute('buildCommand', ''),
'installCommand' => $site->getAttribute('installCommand', ''),
'outputDirectory' => $site->getAttribute('outputDirectory', ''),
'sourcePath' => $destination,
'buildCommands' => \implode(' && ', $commands),
'buildOutput' => $site->getAttribute('outputDirectory', ''),
'search' => implode(' ', [$deploymentId]),
'screenshotLight' => '',
'screenshotDark' => ''
'screenshotDark' => '',
'buildStartAt' => null,
'buildEndAt' => null,
'buildDuration' => null,
'buildSize' => null,
'status' => 'processing',
'buildPath' => '',
'buildLogs' => '',
]));
// Preview deployments for sites
@@ -69,13 +69,6 @@ class Get extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
$deployment->setAttribute('status', $build->getAttribute('status', 'waiting'));
$deployment->setAttribute('buildLogs', $build->getAttribute('logs', ''));
$deployment->setAttribute('buildTime', $build->getAttribute('duration', 0));
$deployment->setAttribute('buildSize', $build->getAttribute('size', 0));
$deployment->setAttribute('size', $deployment->getAttribute('size', 0));
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -13,8 +13,6 @@ use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -48,7 +46,7 @@ class Update extends Action
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_BUILD,
model: Response::MODEL_DEPLOYMENT,
)
]
))
@@ -75,46 +73,19 @@ class Update extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
$buildId = ID::unique();
$build = $dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$permissions' => [],
'startTime' => DateTime::now(),
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'status' => 'canceled',
'path' => '',
'runtime' => $site->getAttribute('framework'),
'source' => $deployment->getAttribute('path', ''),
'sourceType' => '',
'logs' => '',
'duration' => 0,
'size' => 0
]));
$deployment->setAttribute('buildId', $build->getId());
$deployment->setAttribute('buildInternalId', $build->getInternalId());
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
} else {
if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) {
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
}
$startTime = new \DateTime($build->getAttribute('startTime'));
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([
'endTime' => DateTime::now(),
'duration' => $duration,
'status' => 'canceled'
]));
if (\in_array($deployment->getAttribute('status'), ['ready', 'failed'])) {
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
}
$dbForProject->purgeCachedDocument('deployments', $deployment->getId());
$startTime = new \DateTime($deployment->getAttribute('buildStartAt'));
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([
'buildEndAt' => DateTime::now(),
'buildDuration' => $duration,
'status' => 'canceled'
]));
try {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
@@ -130,6 +101,6 @@ class Update extends Action
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($build, Response::MODEL_BUILD);
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -121,6 +121,14 @@ class Create extends Base
return;
}
$commands = [];
if (!empty($site->getAttribute('installCommand', ''))) {
$commands[] = $site->getAttribute('installCommand', '');
}
if (!empty($site->getAttribute('buildCommand', ''))) {
$commands[] = $site->getAttribute('buildCommand', '');
}
$deploymentId = ID::unique();
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
@@ -132,9 +140,8 @@ class Create extends Base
'resourceId' => $site->getId(),
'resourceInternalId' => $site->getInternalId(),
'resourceType' => 'sites',
'installCommand' => $site->getAttribute('installCommand', ''),
'buildCommand' => $site->getAttribute('buildCommand', ''),
'outputDirectory' => $site->getAttribute('outputDirectory', ''),
'buildCommands' => \implode(' && ', $commands),
'buildOutput' => $site->getAttribute('outputDirectory', ''),
'type' => 'manual',
'search' => implode(' ', [$deploymentId]),
'activate' => $activate,
@@ -110,15 +110,6 @@ class XList extends Action
$results = $dbForProject->find('deployments', $queries);
$total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT);
foreach ($results as $result) {
$build = $dbForProject->getDocument('builds', $result->getAttribute('buildId', ''));
$result->setAttribute('status', $build->getAttribute('status', 'processing'));
$result->setAttribute('buildLogs', $build->getAttribute('logs', ''));
$result->setAttribute('buildTime', $build->getAttribute('duration', 0));
$result->setAttribute('buildSize', $build->getAttribute('size', 0));
$result->setAttribute('size', $result->getAttribute('size', 0));
}
$response->dynamic(new Document([
'deployments' => $results,
'total' => $total,
@@ -5,10 +5,10 @@ namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Modules\Compute\Validator\Specification;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Sites\Validator\FrameworkSpecification;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
@@ -77,9 +77,9 @@ 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', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new FrameworkSpecification(
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('framework-specifications', []),
Config::getParam('specifications', []),
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
), 'Framework specification for the site and builds.', true, ['plan'])
@@ -91,7 +91,7 @@ class Create extends Base
->callback([$this, 'action']);
}
public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $buildRuntime, string $adapter, string $installationId, ?string $fallbackFile, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Database $dbForPlatform)
public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $buildRuntime, string $adapter, string $installationId, string $fallbackFile, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Database $dbForPlatform)
{
if (!empty($adapter)) {
$configFramework = Config::getParam('frameworks')[$framework] ?? [];
@@ -65,7 +65,6 @@ class Update extends Base
{
$site = $dbForProject->getDocument('sites', $siteId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
@@ -75,11 +74,7 @@ class Update extends Base
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
if ($build->getAttribute('status') !== 'ready') {
if ($deployment->getAttribute('status') !== 'ready') {
throw new Exception(Exception::BUILD_NOT_READY);
}
@@ -6,10 +6,10 @@ use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Modules\Compute\Validator\Specification;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Sites\Validator\FrameworkSpecification;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\App;
@@ -81,9 +81,9 @@ 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', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new FrameworkSpecification(
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('framework-specifications', []),
Config::getParam('specifications', []),
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
), 'Framework specification for the site and builds.', true, ['plan'])
@@ -98,7 +98,7 @@ class Update extends Base
->callback([$this, 'action']);
}
public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $buildRuntime, string $adapter, ?string $fallbackFile, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github)
public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $buildRuntime, string $adapter, string $fallbackFile, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github)
{
if (!empty($adapter)) {
$configFramework = Config::getParam('frameworks')[$framework] ?? [];
@@ -204,8 +204,6 @@ class Update extends Base
$live = false;
}
$spec = Config::getParam('framework-specifications')[$specification] ?? [];
// Enforce Cold Start if spec limits change.
if ($site->getAttribute('specification') !== $specification && !empty($site->getAttribute('deploymentId'))) {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
@@ -0,0 +1,76 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Specifications;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listSpecifications';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/specifications')
->groups(['api', 'sites'])
->desc('List specifications')
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
name: 'listSpecifications',
description: <<<EOT
List allowed site specifications for this instance.
EOT,
auth: [AuthType::KEY, AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SPECIFICATION_LIST,
)
]
))
->inject('response')
->inject('plan')
->callback([$this, 'action']);
}
public function action(Response $response, array $plan)
{
$allSpecs = Config::getParam('specifications', []);
$specs = [];
foreach ($allSpecs as $spec) {
$spec['enabled'] = true;
if (array_key_exists('specifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['specifications']);
}
// Only add specs that are within the limits set by environment variables
if ($spec['cpus'] <= System::getEnv('_APP_COMPUTE_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_COMPUTE_MEMORY', 512)) {
$specs[] = $spec;
}
}
$response->dynamic(new Document([
'specifications' => $specs,
'total' => count($specs)
]), Response::MODEL_SPECIFICATION_LIST);
}
}
@@ -21,6 +21,7 @@ use Appwrite\Platform\Modules\Sites\Http\Sites\Deployment\Update as UpdateSiteDe
use Appwrite\Platform\Modules\Sites\Http\Sites\Get as GetSite;
use Appwrite\Platform\Modules\Sites\Http\Sites\Update as UpdateSite;
use Appwrite\Platform\Modules\Sites\Http\Sites\XList as ListSites;
use Appwrite\Platform\Modules\Sites\Http\Specifications\XList as ListSpecifications;
use Appwrite\Platform\Modules\Sites\Http\Templates\Get as GetTemplate;
use Appwrite\Platform\Modules\Sites\Http\Templates\XList as ListTemplates;
use Appwrite\Platform\Modules\Sites\Http\Usage\Get as GetUsage;
@@ -79,5 +80,7 @@ class Http extends Service
// Usage
$this->addAction(ListUsage::getName(), new ListUsage());
$this->addAction(GetUsage::getName(), new GetUsage());
$this->addAction(ListSpecifications::getName(), new ListSpecifications());
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ class Screenshot extends Action
$template = \array_shift($allowedTemplates);
if (empty($template)) {
throw new \Exception("Template {$templateId} not found. Find correct ID in app/config/site-templates.php");
throw new \Exception("Template {$templateId} not found. Find correct ID in app/config/templates/site.php");
}
Console::info("Found: " . $template['name']);
+31 -34
View File
@@ -785,9 +785,10 @@ class Deletes extends Action
$deploymentIds = [];
$this->deleteByGroup('deployments', [
Query::equal('resourceInternalId', [$siteInternalId])
], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForSites, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) {
], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) {
$deploymentInternalIds[] = $document->getInternalId();
$deploymentIds[] = $document->getId();
$this->deleteBuildFiles($deviceForBuilds, $document);
$this->deleteDeploymentFiles($deviceForSites, $document);
$this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document);
$this->deleteDeploymentRules($dbForPlatform, $document, $project, $certificates);
@@ -796,12 +797,26 @@ class Deletes extends Action
/**
* Delete builds
*/
Console::info("Deleting builds for site " . $siteId);
foreach ($deploymentInternalIds as $deploymentInternalId) {
$this->deleteByGroup('builds', [
Query::equal('deploymentInternalId', [$deploymentInternalId])
], $dbForProject, function (Document $document) use ($deviceForBuilds) {
$this->deleteBuildFiles($deviceForBuilds, $document);
Console::info("Deleting builds for site " . $siteId);
foreach ($deploymentInternalIds as $deploymentInternalId) {
$this->deleteByGroup('builds', [
Query::equal('deploymentInternalId', [$deploymentInternalId])
], $dbForProject, function (Document $document) use ($deviceForBuilds) {
$this->deleteBuildFiles($deviceForBuilds, $document);
});
}
/**
* Delete rules for all deployments of the site
*/
foreach ($deploymentIds as $deploymentId) {
Console::info("Deleting rules for site " . $siteId . "'s deployment " . $deploymentId);
$this->deleteByGroup('rules', [
Query::equal('type', ['deployment']),
Query::equal('value', [$deploymentId]),
Query::equal('projectInternalId', [$project->getInternalId()])
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
});
}
@@ -869,25 +884,13 @@ class Deletes extends Action
$deploymentInternalIds = [];
$this->deleteByGroup('deployments', [
Query::equal('resourceInternalId', [$functionInternalId])
], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, &$deploymentInternalIds) {
], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
$deploymentInternalIds[] = $document->getInternalId();
$this->deleteDeploymentFiles($deviceForFunctions, $document);
$this->deleteBuildFiles($deviceForBuilds, $document);
$this->deleteDeploymentRules($dbForPlatform, $document, $project, $certificates);
});
/**
* Delete builds
*/
Console::info("Deleting builds for function " . $functionId);
foreach ($deploymentInternalIds as $deploymentInternalId) {
$this->deleteByGroup('builds', [
Query::equal('deploymentInternalId', [$deploymentInternalId])
], $dbForProject, function (Document $document) use ($deviceForBuilds) {
$this->deleteBuildFiles($deviceForBuilds, $document);
});
}
/**
* Delete Executions
*/
@@ -987,7 +990,7 @@ class Deletes extends Action
private function deleteDeploymentFiles(Device $device, Document $deployment): void
{
$deploymentId = $deployment->getId();
$deploymentPath = $deployment->getAttribute('path', '');
$deploymentPath = $deployment->getAttribute('sourcePath', '');
if (empty($deploymentPath)) {
Console::info("No deployment files for deployment " . $deploymentId);
@@ -1016,13 +1019,13 @@ class Deletes extends Action
* @param Document $build
* @return void
*/
private function deleteBuildFiles(Device $device, Document $build): void
private function deleteBuildFiles(Device $device, Document $deployment): void
{
$buildId = $build->getId();
$buildPath = $build->getAttribute('path', '');
$deploymentId = $deployment->getId();
$buildPath = $deployment->getAttribute('buildPath', '');
if (empty($buildPath)) {
Console::info("No build files for build " . $buildId);
Console::info("No build files for deployment " . $deploymentId);
return;
}
@@ -1073,15 +1076,9 @@ class Deletes extends Action
$this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document);
/**
* Delete builds
* Delete deployment build
*/
Console::info("Deleting builds for deployment " . $deploymentId);
$this->deleteByGroup('builds', [
Query::equal('deploymentInternalId', [$deploymentInternalId])
], $dbForProject, function (Document $document) use ($deviceForBuilds) {
$this->deleteBuildFiles($deviceForBuilds, $document);
});
$this->deleteBuildFiles($deviceForBuilds, $document);
/**
* Delete rules associated with the deployment
+3 -15
View File
@@ -327,7 +327,7 @@ class Functions extends Action
$user ??= new Document();
$functionId = $function->getId();
$deploymentId = $function->getAttribute('deployment', '');
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$log->addTag('deploymentId', $deploymentId);
@@ -346,19 +346,7 @@ class Functions extends Action
return;
}
$buildId = $deployment->getAttribute('buildId', '');
$log->addTag('buildId', $buildId);
/** Check if build has exists */
$build = $dbForProject->getDocument('builds', $buildId);
if ($build->isEmpty()) {
$errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.';
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
if ($build->getAttribute('status') !== 'ready') {
if ($deployment->getAttribute('status') !== 'ready') {
$errorMessage = 'The execution could not be completed because the build is not ready. Please wait for the build to complete and try again.';
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
@@ -513,7 +501,7 @@ class Functions extends Action
variables: $vars,
timeout: $function->getAttribute('timeout', 0),
image: $runtime['image'],
source: $build->getAttribute('path', ''),
source: $deployment->getAttribute('buildPath', ''),
entrypoint: $deployment->getAttribute('entrypoint', ''),
version: $version,
path: $path,
@@ -268,15 +268,13 @@ class StatsResources extends Action
protected function countForFunctions(Database $dbForProject, Database $dbForLogs, string $region)
{
$deploymentsStorage = $dbForProject->sum('deployments', 'size');
$buildsStorage = $dbForProject->sum('builds', 'size');
$buildsStorage = $dbForProject->sum('deployments', 'buildSize');
$this->createStatsDocuments($region, METRIC_DEPLOYMENTS_STORAGE, $deploymentsStorage);
$this->createStatsDocuments($region, METRIC_BUILDS_STORAGE, $buildsStorage);
$deployments = $dbForProject->count('deployments');
$builds = $dbForProject->count('builds');
$this->createStatsDocuments($region, METRIC_DEPLOYMENTS, $deployments);
$this->createStatsDocuments($region, METRIC_BUILDS, $builds);
$this->createStatsDocuments($region, METRIC_BUILDS, $deployments);
$this->foreachDocument($dbForProject, 'functions', [], function (Document $function) use ($dbForProject, $dbForLogs, $region) {
$functionDeploymentsStorage = $dbForProject->sum('deployments', 'size', [
@@ -302,9 +300,8 @@ class StatsResources extends Action
$this->foreachDocument($dbForProject, 'deployments', [
Query::equal('resourceInternalId', [$function->getInternalId()]),
Query::equal('resourceType', [RESOURCE_TYPE_FUNCTIONS]),
], function (Document $deployment) use ($dbForProject, &$functionBuildsStorage): void {
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
$functionBuildsStorage += $build->getAttribute('size', 0);
], function (Document $deployment) use (&$functionBuildsStorage): void {
$functionBuildsStorage += $deployment->getAttribute('buildSize', 0);
});
$this->createStatsDocuments($region, str_replace(['{resourceType}','{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS,$function->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE), $functionBuildsStorage);
@@ -1,112 +0,0 @@
<?php
namespace Appwrite\Sites\Validator;
use Utopia\Validator;
class FrameworkSpecification extends Validator
{
private array $plan;
private array $specifications;
private float $maxCpus;
private int $maxMemory;
public function __construct(array $plan, array $specifications, float $maxCpus, int $maxMemory)
{
$this->plan = $plan;
$this->specifications = $specifications;
$this->maxCpus = $maxCpus;
$this->maxMemory = $maxMemory;
}
/**
* Get Allowed Specifications.
*
* Get allowed specifications taking into account the limits set by the environment variables and the plan.
*
* @return array
*/
public function getAllowedSpecifications(): array
{
$allowedSpecifications = [];
foreach ($this->specifications as $size => $values) {
if ($values['cpus'] <= $this->maxCpus && $values['memory'] <= $this->maxMemory) {
if (!empty($this->plan) && array_key_exists('frameworkSpecifications', $this->plan)) {
if (!\in_array($size, $this->plan['frameworkSpecifications'])) {
continue;
}
}
$allowedSpecifications[] = $size;
}
}
return $allowedSpecifications;
}
/**
* Get Description.
*
* Returns validator description.
*
* @return string
*/
public function getDescription(): string
{
return 'Specification must be one of: ' . implode(', ', $this->getAllowedSpecifications());
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
if (empty($value)) {
return false;
}
if (!\is_string($value)) {
return false;
}
if (!\in_array($value, $this->getAllowedSpecifications())) {
return false;
}
return true;
}
/**
* Is array.
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type.
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}
@@ -5,13 +5,12 @@ namespace Appwrite\Utopia\Database\Validator\Queries;
class Deployments extends Base
{
public const ALLOWED_ATTRIBUTES = [
'size',
'buildId',
'buildSize',
'sourceSize',
'buildDuration',
'status',
'activate',
'entrypoint',
'commands',
'type',
'size'
];
/**
+16 -9
View File
@@ -30,7 +30,6 @@ use Appwrite\Utopia\Response\Model\AuthProvider;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Branch;
use Appwrite\Utopia\Response\Model\Bucket;
use Appwrite\Utopia\Response\Model\Build;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\ConsoleVariables;
use Appwrite\Utopia\Response\Model\Continent;
@@ -38,7 +37,8 @@ use Appwrite\Utopia\Response\Model\Country;
use Appwrite\Utopia\Response\Model\Currency;
use Appwrite\Utopia\Response\Model\Database;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Utopia\Response\Model\Detection;
use Appwrite\Utopia\Response\Model\DetectionFramework;
use Appwrite\Utopia\Response\Model\DetectionRuntime;
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
use Appwrite\Utopia\Response\Model\Error;
use Appwrite\Utopia\Response\Model\ErrorDev;
@@ -83,6 +83,8 @@ use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\Rule;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\Session;
@@ -247,9 +249,14 @@ class Response extends SwooleResponse
public const MODEL_INSTALLATION_LIST = 'installationList';
public const MODEL_PROVIDER_REPOSITORY = 'providerRepository';
public const MODEL_PROVIDER_REPOSITORY_LIST = 'providerRepositoryList';
public const MODEL_PROVIDER_REPOSITORY_FRAMEWORK = 'providerRepositoryFramework';
public const MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST = 'providerRepositoryFrameworkList';
public const MODEL_PROVIDER_REPOSITORY_RUNTIME = 'providerRepositoryRuntime';
public const MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST = 'providerRepositoryRuntimeList';
public const MODEL_BRANCH = 'branch';
public const MODEL_BRANCH_LIST = 'branchList';
public const MODEL_DETECTION = 'detection';
public const MODEL_DETECTION_FRAMEWORK = 'detectionFramework';
public const MODEL_DETECTION_RUNTIME = 'detectionRuntime';
public const MODEL_VCS_CONTENT = 'vcsContent';
public const MODEL_VCS_CONTENT_LIST = 'vcsContentList';
@@ -272,8 +279,6 @@ class Response extends SwooleResponse
public const MODEL_DEPLOYMENT_LIST = 'deploymentList';
public const MODEL_EXECUTION = 'execution';
public const MODEL_EXECUTION_LIST = 'executionList';
public const MODEL_BUILD = 'build';
public const MODEL_BUILD_LIST = 'buildList'; // Not used anywhere yet
public const MODEL_FUNC_PERMISSIONS = 'funcPermissions';
public const MODEL_HEADERS = 'headers';
public const MODEL_SPECIFICATION = 'specification';
@@ -375,13 +380,13 @@ class Response extends SwooleResponse
->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION))
->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION))
->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION))
->setModel(new BaseList('Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_LIST, 'providerRepositories', self::MODEL_PROVIDER_REPOSITORY))
->setModel(new BaseList('Framework Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', self::MODEL_PROVIDER_REPOSITORY_FRAMEWORK))
->setModel(new BaseList('Runtime Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', self::MODEL_PROVIDER_REPOSITORY_RUNTIME))
->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH))
->setModel(new BaseList('Frameworks List', self::MODEL_FRAMEWORK_LIST, 'frameworks', self::MODEL_FRAMEWORK))
->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME))
->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT))
->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION))
->setModel(new BaseList('Builds List', self::MODEL_BUILD_LIST, 'builds', self::MODEL_BUILD)) // Not used anywhere yet
->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false))
->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false))
->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false))
@@ -453,7 +458,10 @@ class Response extends SwooleResponse
->setModel(new TemplateVariable())
->setModel(new Installation())
->setModel(new ProviderRepository())
->setModel(new Detection())
->setModel(new ProviderRepositoryFramework())
->setModel(new ProviderRepositoryRuntime())
->setModel(new DetectionFramework())
->setModel(new DetectionRuntime())
->setModel(new VcsContent())
->setModel(new Branch())
->setModel(new Runtime())
@@ -461,7 +469,6 @@ class Response extends SwooleResponse
->setModel(new FrameworkAdapter())
->setModel(new Deployment())
->setModel(new Execution())
->setModel(new Build())
->setModel(new Project())
->setModel(new Webhook())
->setModel(new Key())
@@ -1,94 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Build extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Build ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('deploymentId', [
'type' => self::TYPE_STRING,
'description' => 'The deployment that created this build.',
'default' => '',
'example' => '5e5ea5c16897e',
])
// Build Status
// Failed - The deployment build has failed. More details can usually be found in buildStderr
// Ready - The deployment build was successful and the deployment is ready to be deployed
// Processing - The deployment is currently waiting to have a build triggered
// Building - The deployment is currently being built
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'The build status. There are a few different types and each one means something different. \nFailed - The deployment build has failed. More details can usually be found in buildStderr\nReady - The deployment build was successful and the deployment is ready to be deployed\nProcessing - The deployment is currently waiting to have a build triggered\nBuilding - The deployment is currently being built',
'default' => '',
'example' => 'ready',
])
->addRule('stdout', [
'type' => self::TYPE_STRING,
'description' => 'The stdout of the build.',
'default' => '',
'example' => '',
])
->addRule('stderr', [
'type' => self::TYPE_STRING,
'description' => 'The stderr of the build.',
'default' => '',
'example' => '',
])
->addRule('startTime', [
'type' => self::TYPE_DATETIME,
'description' => 'The deployment creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('endTime', [
'type' => self::TYPE_DATETIME,
'description' => 'The time the build was finished in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('duration', [
'type' => self::TYPE_INTEGER,
'description' => 'The build duration in seconds.',
'default' => 0,
'example' => 0,
])
->addRule('size', [
'type' => self::TYPE_INTEGER,
'description' => 'The code size in bytes.',
'default' => 0,
'example' => 128,
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Build';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_BUILD;
}
}
@@ -52,7 +52,7 @@ class Deployment extends Model
'default' => '',
'example' => 'index.js',
])
->addRule('size', [
->addRule('sourceSize', [
'type' => self::TYPE_INTEGER,
'description' => 'The code size in bytes.',
'default' => 0,
@@ -100,7 +100,7 @@ class Deployment extends Model
'default' => '',
'example' => 'Compiling source files...',
])
->addRule('buildTime', [
->addRule('buildDuration', [
'type' => self::TYPE_INTEGER,
'description' => 'The current build time in seconds.',
'default' => 0,
@@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class DetectionFramework extends Model
{
public function __construct()
{
$this
->addRule('framework', [
'type' => self::TYPE_STRING,
'description' => 'Framework',
'default' => '',
'example' => 'nuxt',
])
->addRule('installCommand', [
'type' => self::TYPE_STRING,
'description' => 'Site Install Command',
'default' => '',
'example' => 'npm install',
])
->addRule('buildCommand', [
'type' => self::TYPE_STRING,
'description' => 'Site Build Command',
'default' => '',
'example' => 'npm run build',
])
->addRule('outputDirectory', [
'type' => self::TYPE_STRING,
'description' => 'Site Output Directory',
'default' => '',
'example' => 'dist',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'DetectionFramework';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_DETECTION_FRAMEWORK;
}
}
@@ -5,7 +5,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Detection extends Model
class DetectionRuntime extends Model
{
public function __construct()
{
@@ -15,6 +15,18 @@ class Detection extends Model
'description' => 'Runtime',
'default' => '',
'example' => 'node',
])
->addRule('entrypoint', [
'type' => self::TYPE_STRING,
'description' => 'Function Entrypoint',
'default' => '',
'example' => 'index.js',
])
->addRule('commands', [
'type' => self::TYPE_STRING,
'description' => 'Function install and build commands',
'default' => '',
'example' => 'npm install && npm run build',
]);
}
@@ -25,7 +37,7 @@ class Detection extends Model
*/
public function getName(): string
{
return 'Detection';
return 'DetectionRuntime';
}
/**
@@ -35,6 +47,6 @@ class Detection extends Model
*/
public function getType(): string
{
return Response::MODEL_DETECTION;
return Response::MODEL_DETECTION_RUNTIME;
}
}
@@ -41,12 +41,6 @@ class ProviderRepository extends Model
'default' => false,
'example' => true,
])
->addRule('runtime', [
'type' => self::TYPE_STRING,
'description' => 'Auto-detected runtime suggestion. Empty if getting response of getRuntime().',
'default' => '',
'example' => 'node',
])
->addRule('pushedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Last commit date in ISO 8601 format.',
@@ -0,0 +1,40 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class ProviderRepositoryFramework extends ProviderRepository
{
public function __construct()
{
parent::__construct();
$this->addRule('framework', [
'type' => self::TYPE_STRING,
'description' => 'Auto-detected framework. Empty if type is not "framework".',
'default' => '',
'example' => 'nextjs',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'ProviderRepositoryFramework';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK;
}
}
@@ -0,0 +1,40 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class ProviderRepositoryRuntime extends ProviderRepository
{
public function __construct()
{
parent::__construct();
$this->addRule('runtime', [
'type' => self::TYPE_STRING,
'description' => 'Auto-detected runtime. Empty if type is not "runtime".',
'default' => '',
'example' => 'node-22',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'ProviderRepositoryRuntime';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PROVIDER_REPOSITORY_RUNTIME;
}
}
+27 -2
View File
@@ -135,9 +135,9 @@ class Executor
* @param string $projectId
* @param string $deploymentId
*/
public function deleteRuntime(string $projectId, string $deploymentId)
public function deleteRuntime(string $projectId, string $deploymentId, string $suffix = '')
{
$runtimeId = "$projectId-$deploymentId";
$runtimeId = "$projectId-$deploymentId" . $suffix;
$route = "/runtimes/$runtimeId";
$response = $this->call(self::METHOD_DELETE, $route, [
@@ -249,6 +249,31 @@ class Executor
return $response['body'];
}
public function createCommand(
string $deploymentId,
string $projectId,
string $command,
int $timeout
) {
$runtimeId = "$projectId-$deploymentId-build";
$route = "/runtimes/$runtimeId/commands";
$params = [
'command' => $command,
'timeout' => $timeout
];
$response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
$status = $response['headers']['status-code'];
if ($status >= 400) {
$message = \is_string($response['body']) ? $response['body'] : $response['body']['message'];
throw new \Exception($message, $status);
}
return $response['body'];
}
/**
* Call
*
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Tests\E2E\General;
use Appwrite\Functions\Specification;
use Appwrite\Platform\Modules\Compute\Specification;
use Appwrite\Tests\Retry;
use CURLFile;
use DateTime;
@@ -390,4 +390,13 @@ trait FunctionsBase
return $deployment;
}
protected function listSpecifications(): mixed
{
$specifications = $this->client->call(Client::METHOD_GET, '/functions/specifications', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
return $specifications;
}
}
@@ -2,7 +2,7 @@
namespace Tests\E2E\Services\Functions;
use Appwrite\Functions\Specification;
use Appwrite\Platform\Modules\Compute\Specification;
use Appwrite\Tests\Retry;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -22,6 +22,41 @@ class FunctionsCustomServerTest extends Scope
use ProjectCustom;
use SideServer;
public function testListSpecs(): void
{
$specifications = $this->listSpecifications();
$this->assertEquals(200, $specifications['headers']['status-code']);
$this->assertGreaterThan(0, $specifications['body']['total']);
$this->assertArrayHasKey(0, $specifications['body']['specifications']);
$this->assertArrayHasKey('memory', $specifications['body']['specifications'][0]);
$this->assertArrayHasKey('cpus', $specifications['body']['specifications'][0]);
$this->assertArrayHasKey('enabled', $specifications['body']['specifications'][0]);
$this->assertArrayHasKey('slug', $specifications['body']['specifications'][0]);
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'php-8.0',
'specification' => $specifications['body']['specifications'][0]['slug']
]);
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $function['body']['specification']);
$function = $this->getFunction($function['body']['$id']);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $function['body']['specification']);
$this->cleanupFunction($function['body']['$id']);
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'php-8.0',
'specification' => 'cheap-please'
]);
$this->assertEquals(400, $function['headers']['status-code']);
}
public function testCreateFunction(): array
{
/**
@@ -382,7 +417,7 @@ class FunctionsCustomServerTest extends Scope
$lastDeployment = $deployments['body']['deployments'][0];
$this->assertNotEmpty($lastDeployment['$id']);
$this->assertEquals(0, $lastDeployment['size']);
$this->assertEquals(0, $lastDeployment['sourceSize']);
$deploymentId = $lastDeployment['$id'];
@@ -598,10 +633,10 @@ class FunctionsCustomServerTest extends Scope
$this->assertNotEmpty($largeTag['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($largeTag['body']['$createdAt']));
$this->assertEquals('index.php', $largeTag['body']['entrypoint']);
$this->assertGreaterThan(1024 * 1024 * 5, $largeTag['body']['size']); // ~7MB video file
$this->assertLessThan(1024 * 1024 * 10, $largeTag['body']['size']); // ~7MB video file
$this->assertGreaterThan(1024 * 1024 * 5, $largeTag['body']['sourceSize']); // ~7MB video file
$this->assertLessThan(1024 * 1024 * 10, $largeTag['body']['sourceSize']); // ~7MB video file
$deploymentSize = $largeTag['body']['size'];
$deploymentSize = $largeTag['body']['sourceSize'];
$deploymentId = $largeTag['body']['$id'];
$this->assertEventually(function () use ($functionId, $deploymentId, $deploymentSize) {
@@ -609,7 +644,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('ready', $deployment['body']['status']);
$this->assertEquals($deploymentSize, $deployment['body']['size']);
$this->assertEquals($deploymentSize, $deployment['body']['sourceSize']);
$this->assertGreaterThan(1024 * 1024 * 10, $deployment['body']['buildSize']); // ~7MB video file + 10MB sample file
}, 500000, 1000);
@@ -655,7 +690,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals($deployments['body']['total'], 3);
$this->assertIsArray($deployments['body']['deployments']);
$this->assertCount(3, $deployments['body']['deployments']);
$this->assertArrayHasKey('size', $deployments['body']['deployments'][0]);
$this->assertArrayHasKey('sourceSize', $deployments['body']['deployments'][0]);
$this->assertArrayHasKey('buildSize', $deployments['body']['deployments'][0]);
$deployments = $this->listDeployments($functionId, [
@@ -676,24 +711,6 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertCount(2, $deployments['body']['deployments']);
$deployments = $this->listDeployments($functionId, [
'queries' => [
Query::equal('entrypoint', ['index.php'])->toString(),
],
]);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertCount(3, $deployments['body']['deployments']);
$deployments = $this->listDeployments($functionId, [
'queries' => [
Query::equal('entrypoint', ['index.js'])->toString(),
],
]);
$this->assertEquals($deployments['headers']['status-code'], 200);
$this->assertCount(0, $deployments['body']['deployments']);
$deployments = $this->listDeployments($functionId, [
'search' => 'php-8.0'
]);
@@ -744,7 +761,7 @@ class FunctionsCustomServerTest extends Scope
$functionId,
[
'queries' => [
Query::greaterThan('size', 10000)->toString(),
Query::greaterThan('sourceSize', 10000)->toString(),
],
]
);
@@ -756,7 +773,7 @@ class FunctionsCustomServerTest extends Scope
$functionId,
[
'queries' => [
Query::greaterThan('size', 0)->toString(),
Query::greaterThan('sourceSize', 0)->toString(),
],
]
);
@@ -768,7 +785,7 @@ class FunctionsCustomServerTest extends Scope
$functionId,
[
'queries' => [
Query::greaterThan('size', -100)->toString(),
Query::greaterThan('sourceSize', -100)->toString(),
],
]
);
@@ -789,16 +806,16 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(200, $deployments['headers']['status-code']);
$this->assertGreaterThanOrEqual(1, $deployments['body']['total']);
$this->assertNotEmpty($deployments['body']['deployments'][0]['$id']);
$this->assertNotEmpty($deployments['body']['deployments'][0]['size']);
$this->assertNotEmpty($deployments['body']['deployments'][0]['sourceSize']);
$deploymentId = $deployments['body']['deployments'][0]['$id'];
$deploymentSize = $deployments['body']['deployments'][0]['size'];
$deploymentSize = $deployments['body']['deployments'][0]['sourceSize'];
$deployments = $this->listDeployments(
$functionId,
[
'queries' => [
Query::equal('size', [$deploymentSize])->toString(),
Query::equal('sourceSize', [$deploymentSize])->toString(),
],
]
);
@@ -815,7 +832,7 @@ class FunctionsCustomServerTest extends Scope
if (!empty($matchingDeployment)) {
$deployment = reset($matchingDeployment);
$this->assertEquals($deploymentSize, $deployment['size']);
$this->assertEquals($deploymentSize, $deployment['sourceSize']);
}
return $data;
@@ -832,10 +849,10 @@ class FunctionsCustomServerTest extends Scope
$deployment = $this->getDeployment($data['functionId'], $data['deploymentId']);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertGreaterThan(0, $deployment['body']['buildTime']);
$this->assertGreaterThan(0, $deployment['body']['buildDuration']);
$this->assertNotEmpty($deployment['body']['status']);
$this->assertNotEmpty($deployment['body']['buildLogs']);
$this->assertArrayHasKey('size', $deployment['body']);
$this->assertArrayHasKey('sourceSize', $deployment['body']);
$this->assertArrayHasKey('buildSize', $deployment['body']);
/**
@@ -2006,14 +2023,14 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertStringContainsString('APPWRITE_FUNCTION_ID', $execution['body']['responseBody']);
$site = $this->updateFunction($functionId, [
$function = $this->updateFunction($functionId, [
'runtime' => 'node-18.0',
'name' => 'Duplicate Deployment Test',
'entrypoint' => 'index.js',
'commands' => 'rm index.js && mv maintenance.js index.js'
]);
$this->assertEquals(200, $site['headers']['status-code']);
$this->assertStringContainsString('maintenance.js', $site['body']['commands']);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertStringContainsString('maintenance.js', $function['body']['commands']);
$deploymentId2 = $this->setupDuplicateDeployment($functionId, $deploymentId1);
$this->assertNotEmpty($deploymentId2);
+1 -1
View File
@@ -1568,7 +1568,7 @@ trait Base
_id
buildId
entrypoint
size
buildSize
status
buildLogs
}
+9
View File
@@ -415,4 +415,13 @@ trait SitesBase
return $deployment;
}
protected function listSpecifications(): mixed
{
$specifications = $this->client->call(Client::METHOD_GET, '/sites/specifications', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
return $specifications;
}
}
@@ -2,7 +2,8 @@
namespace Tests\E2E\Services\Sites;
use Appwrite\Sites\Specification;
use Ahc\Jwt\JWT;
use Appwrite\Platform\Modules\Compute\Specification;
use Appwrite\Tests\Retry;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -20,6 +21,43 @@ class SitesCustomServerTest extends Scope
use ProjectCustom;
use SideServer;
public function testListSpecs(): void
{
$specifications = $this->listSpecifications();
$this->assertEquals(200, $specifications['headers']['status-code']);
$this->assertGreaterThan(0, $specifications['body']['total']);
$this->assertArrayHasKey(0, $specifications['body']['specifications']);
$this->assertArrayHasKey('memory', $specifications['body']['specifications'][0]);
$this->assertArrayHasKey('cpus', $specifications['body']['specifications'][0]);
$this->assertArrayHasKey('enabled', $specifications['body']['specifications'][0]);
$this->assertArrayHasKey('slug', $specifications['body']['specifications'][0]);
$site = $this->createSite([
'buildRuntime' => 'node-22',
'framework' => 'other',
'name' => 'Specs site',
'siteId' => ID::unique(),
'specification' => $specifications['body']['specifications'][0]['slug']
]);
$this->assertEquals(201, $site['headers']['status-code']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $site['body']['specification']);
$site = $this->getSite($site['body']['$id']);
$this->assertEquals(200, $site['headers']['status-code']);
$this->assertEquals($specifications['body']['specifications'][0]['slug'], $site['body']['specification']);
$this->cleanupSite($site['body']['$id']);
$site = $this->createSite([
'buildRuntime' => 'node-22',
'framework' => 'other',
'name' => 'Specs site',
'siteId' => ID::unique(),
'specification' => 'cheap-please'
]);
$this->assertEquals(400, $site['headers']['status-code']);
}
public function testCreateSite(): void
{
/**
@@ -27,7 +65,7 @@ class SitesCustomServerTest extends Scope
*/
$site = $this->createSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -75,7 +113,7 @@ class SitesCustomServerTest extends Scope
'framework' => 'other',
'buildRuntime' => 'node-22',
'outputDirectory' => './',
'fallbackFile' => null,
'fallbackFile' => '',
]);
$this->assertNotEmpty($siteId);
@@ -141,7 +179,7 @@ class SitesCustomServerTest extends Scope
{
$site = $this->createSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -325,6 +363,168 @@ class SitesCustomServerTest extends Scope
$this->cleanupSite($siteId);
}
public function testAdapterDetectionAstroSSR(): void
{
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'Astro SSR site',
'framework' => 'astro',
'buildRuntime' => 'node-22',
'outputDirectory' => './dist',
'buildCommand' => 'npm run build',
'installCommand' => 'npm install',
]);
$this->assertNotEmpty($siteId);
$site = $this->getSite($siteId);
$this->assertEquals('200', $site['headers']['status-code']);
$this->assertArrayHasKey('adapter', $site['body']);
$this->assertEmpty($site['body']['adapter']);
$domain = $this->setupSiteDomain($siteId);
$this->assertNotEmpty($domain);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('astro'),
'activate' => 'true'
]);
$this->assertNotEmpty($deploymentId);
$site = $this->getSite($siteId);
$this->assertEquals('ssr', $site['body']['adapter']);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(200, $response['headers']['status-code']);
$this->cleanupSite($siteId);
}
public function testAdapterDetectionAstroStatic(): void
{
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'Astro static site',
'framework' => 'astro',
'buildRuntime' => 'node-22',
'outputDirectory' => './dist',
'buildCommand' => 'npm run build',
'installCommand' => 'npm install',
]);
$this->assertNotEmpty($siteId);
$site = $this->getSite($siteId);
$this->assertEquals('200', $site['headers']['status-code']);
$this->assertArrayHasKey('adapter', $site['body']);
$this->assertEmpty($site['body']['adapter']);
$domain = $this->setupSiteDomain($siteId);
$this->assertNotEmpty($domain);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('astro-static'),
'activate' => 'true'
]);
$this->assertNotEmpty($deploymentId);
$site = $this->getSite($siteId);
$this->assertEquals('200', $site['headers']['status-code']);
$this->assertEquals('static', $site['body']['adapter']);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(200, $response['headers']['status-code']);
$this->cleanupSite($siteId);
}
public function testAdapterDetectionStatic(): void
{
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'Static site',
'framework' => 'other',
'buildRuntime' => 'node-22',
'outputDirectory' => '',
'buildCommand' => '',
'installCommand' => '',
]);
$this->assertNotEmpty($siteId);
$site = $this->getSite($siteId);
$this->assertEquals('200', $site['headers']['status-code']);
$this->assertArrayHasKey('adapter', $site['body']);
$this->assertEmpty($site['body']['adapter']);
$domain = $this->setupSiteDomain($siteId);
$this->assertNotEmpty($domain);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static'),
'activate' => 'true'
]);
$this->assertNotEmpty($deploymentId);
$site = $this->getSite($siteId);
$this->assertEquals('200', $site['headers']['status-code']);
$this->assertEquals('static', $site['body']['adapter']);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(200, $response['headers']['status-code']);
$this->cleanupSite($siteId);
}
public function testAdapterDetectionStaticSPA(): void
{
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'Static site',
'framework' => 'other',
'buildRuntime' => 'node-22',
'outputDirectory' => '',
'buildCommand' => '',
'installCommand' => '',
]);
$this->assertNotEmpty($siteId);
$site = $this->getSite($siteId);
$this->assertEquals('200', $site['headers']['status-code']);
$this->assertArrayHasKey('adapter', $site['body']);
$this->assertArrayHasKey('fallbackFile', $site['body']);
$this->assertEmpty($site['body']['adapter']);
$this->assertEmpty($site['body']['fallbackFile']);
$domain = $this->setupSiteDomain($siteId);
$this->assertNotEmpty($domain);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static-single-file'),
'activate' => 'true'
]);
$this->assertNotEmpty($deploymentId);
$site = $this->getSite($siteId);
$this->assertEquals('200', $site['headers']['status-code']);
$this->assertEquals('static', $site['body']['adapter']);
$this->assertEquals('main.html', $site['body']['fallbackFile']);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString('Main page', $response['body']);
$response = $proxyClient->call(Client::METHOD_GET, '/something');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString('Main page', $response['body']);
$this->cleanupSite($siteId);
}
public function testListSites(): void
{
/**
@@ -332,7 +532,7 @@ class SitesCustomServerTest extends Scope
*/
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -412,7 +612,7 @@ class SitesCustomServerTest extends Scope
*/
$siteId2 = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site 2',
'outputDirectory' => './',
@@ -468,7 +668,7 @@ class SitesCustomServerTest extends Scope
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -501,7 +701,7 @@ class SitesCustomServerTest extends Scope
{
$site = $this->createSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -518,7 +718,7 @@ class SitesCustomServerTest extends Scope
$site = $this->updateSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site Updated',
'outputDirectory' => './',
@@ -598,7 +798,7 @@ class SitesCustomServerTest extends Scope
$lastDeployment = $deployments['body']['deployments'][0];
$this->assertNotEmpty($lastDeployment['$id']);
$this->assertEquals(0, $lastDeployment['size']);
$this->assertEquals(0, $lastDeployment['sourceSize']);
$deploymentId = $lastDeployment['$id'];
@@ -619,7 +819,7 @@ class SitesCustomServerTest extends Scope
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -680,7 +880,7 @@ class SitesCustomServerTest extends Scope
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -732,7 +932,7 @@ class SitesCustomServerTest extends Scope
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -777,7 +977,7 @@ class SitesCustomServerTest extends Scope
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -812,7 +1012,7 @@ class SitesCustomServerTest extends Scope
$this->assertEquals($deployments['body']['total'], 2);
$this->assertIsArray($deployments['body']['deployments']);
$this->assertCount(2, $deployments['body']['deployments']);
$this->assertArrayHasKey('size', $deployments['body']['deployments'][0]);
$this->assertArrayHasKey('sourceSize', $deployments['body']['deployments'][0]);
$this->assertArrayHasKey('buildSize', $deployments['body']['deployments'][0]);
$deployments = $this->listDeployments($siteId, [
@@ -873,7 +1073,7 @@ class SitesCustomServerTest extends Scope
$siteId,
[
'queries' => [
Query::greaterThan('size', 10000)->toString(),
Query::greaterThan('sourceSize', 10000)->toString(),
],
]
);
@@ -885,7 +1085,7 @@ class SitesCustomServerTest extends Scope
$siteId,
[
'queries' => [
Query::greaterThan('size', 0)->toString(),
Query::greaterThan('sourceSize', 0)->toString(),
],
]
);
@@ -897,7 +1097,7 @@ class SitesCustomServerTest extends Scope
$siteId,
[
'queries' => [
Query::greaterThan('size', -100)->toString(),
Query::greaterThan('sourceSize', -100)->toString(),
],
]
);
@@ -918,16 +1118,16 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(200, $deployments['headers']['status-code']);
$this->assertGreaterThanOrEqual(1, $deployments['body']['total']);
$this->assertNotEmpty($deployments['body']['deployments'][0]['$id']);
$this->assertNotEmpty($deployments['body']['deployments'][0]['size']);
$this->assertNotEmpty($deployments['body']['deployments'][0]['sourceSize']);
$deploymentId = $deployments['body']['deployments'][0]['$id'];
$deploymentSize = $deployments['body']['deployments'][0]['size'];
$deploymentSize = $deployments['body']['deployments'][0]['sourceSize'];
$deployments = $this->listDeployments(
$siteId,
[
'queries' => [
Query::equal('size', [$deploymentSize])->toString(),
Query::equal('sourceSize', [$deploymentSize])->toString(),
],
]
);
@@ -944,7 +1144,7 @@ class SitesCustomServerTest extends Scope
if (!empty($matchingDeployment)) {
$deployment = reset($matchingDeployment);
$this->assertEquals($deploymentSize, $deployment['size']);
$this->assertEquals($deploymentSize, $deployment['sourceSize']);
}
$this->cleanupDeployment($siteId, $deploymentIdActive);
@@ -956,7 +1156,7 @@ class SitesCustomServerTest extends Scope
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -986,10 +1186,10 @@ class SitesCustomServerTest extends Scope
$deployment = $this->getDeployment($siteId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertGreaterThan(0, $deployment['body']['buildTime']);
$this->assertGreaterThan(0, $deployment['body']['buildDuration']);
$this->assertNotEmpty($deployment['body']['status']);
$this->assertNotEmpty($deployment['body']['buildLogs']);
$this->assertArrayHasKey('size', $deployment['body']);
$this->assertArrayHasKey('sourceSize', $deployment['body']);
$this->assertArrayHasKey('buildSize', $deployment['body']);
/**
@@ -1007,7 +1207,7 @@ class SitesCustomServerTest extends Scope
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -1024,7 +1224,7 @@ class SitesCustomServerTest extends Scope
// Change the function specs
$site = $this->updateSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -1041,7 +1241,7 @@ class SitesCustomServerTest extends Scope
// Change the specs to 1vcpu 512mb
$site = $this->updateSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -1061,7 +1261,7 @@ class SitesCustomServerTest extends Scope
$site = $this->updateSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -1081,7 +1281,7 @@ class SitesCustomServerTest extends Scope
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -1125,7 +1325,7 @@ class SitesCustomServerTest extends Scope
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'outputDirectory' => './',
@@ -1481,7 +1681,18 @@ class SitesCustomServerTest extends Scope
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $oldDeploymentDomain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringContainsString('/console/auth/preview', $response['headers']['location']);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0);
$apiKey = $jwtObj->encode([
'projectCheckDisabled' => true,
'previewAuthDisabled' => true,
]);
$response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [
'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Hello Appwrite", $response['body']);
$this->assertStringContainsString("Preview by", $response['body']);
@@ -1629,7 +1840,7 @@ class SitesCustomServerTest extends Scope
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => null,
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'adapter' => 'static',
@@ -1837,4 +2048,165 @@ class SitesCustomServerTest extends Scope
$this->cleanupSite($siteId);
}
public function testPreviewDomain(): void
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'framework' => 'other',
'name' => 'Authorized preview site',
'siteId' => ID::unique(),
'adapter' => 'static',
]);
$this->assertNotEmpty($siteId);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId);
$domain = $this->getDeploymentDomain($deploymentId);
$this->assertNotEmpty($domain);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/contact', followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringContainsString('/console/auth/preview', $response['headers']['location']);
$this->assertStringContainsString('projectId=' . $this->getProject()['$id'], $response['headers']['location']);
$this->assertStringContainsString('origin=', $response['headers']['location']);
$this->assertStringContainsString('path=%2Fcontact', $response['headers']['location']);
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
]), [
'email' => $this->getRoot()['email'],
'password' => 'password'
]);
$this->assertEquals(201, $session['headers']['status-code']);
$this->assertNotEmpty($session['cookies']['a_session_console']);
$this->assertNotEmpty($session['body']['$id']);
$cookie = 'a_session_console=' . $session['cookies']['a_session_console'];
$jwt = $this->client->call(Client::METHOD_POST, '/account/jwts', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'cookie' => $cookie,
'x-appwrite-project' => 'console',
]), []);
$this->assertEquals(201, $jwt['headers']['status-code']);
$this->assertNotEmpty($jwt['body']['jwt']);
$response = $proxyClient->call(Client::METHOD_GET, '/_appwrite/authorize', params: [
'jwt' => $jwt['body']['jwt'],
'path' => '/contact'
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertArrayHasKey('set-cookie', $response['headers']);
$this->assertStringContainsString('a_jwt_console=', $response['headers']['set-cookie']);
$this->assertStringContainsString('httponly', $response['headers']['set-cookie']);
$this->assertStringContainsString('domain=' . $domain, $response['headers']['set-cookie']);
$this->assertStringContainsString('path=/', $response['headers']['set-cookie']);
$this->assertNotEmpty($response['cookies']['a_jwt_console']);
$this->assertEquals($jwt['body']['jwt'], $response['cookies']['a_jwt_console']);
$response = $proxyClient->call(Client::METHOD_GET, '/contact', headers: [
'cookie' => 'a_jwt_console=' . $jwt['body']['jwt']
], followRedirects: false);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Contact page", $response['body']);
$this->assertStringContainsString("Preview by", $response['body']);
// Failure: Session missing (old bad, new ok)
$session = $this->client->call(Client::METHOD_DELETE, '/account/sessions/current', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'cookie' => $cookie,
'x-appwrite-project' => 'console',
]), []);
$this->assertEquals(204, $session['headers']['status-code']);
$response = $proxyClient->call(Client::METHOD_GET, '/contact', headers: [
'cookie' => 'a_jwt_console=' . $jwt['body']['jwt']
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringContainsString('/console/auth/preview', $response['headers']['location']);
// Failure: User missing
$cookie = 'a_session_console=' .$this->getRoot()['session'];
$jwt = $this->client->call(Client::METHOD_POST, '/account/jwts', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'cookie' => $cookie,
'x-appwrite-project' => 'console',
]), []);
$this->assertEquals(201, $jwt['headers']['status-code']);
$this->assertNotEmpty($jwt['body']['jwt']);
$response = $proxyClient->call(Client::METHOD_GET, '/contact', headers: [
'cookie' => 'a_jwt_console=' . $jwt['body']['jwt']
], followRedirects: false);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Contact page", $response['body']);
$this->assertStringContainsString("Preview by", $response['body']);
$user = $this->client->call(Client::METHOD_PATCH, '/account/status', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'cookie' => $cookie,
'x-appwrite-project' => 'console',
]), []);
$this->assertEquals(200, $user['headers']['status-code']);
$this->assertFalse($user['body']['status']);
$response = $proxyClient->call(Client::METHOD_GET, '/contact', headers: [
'cookie' => 'a_jwt_console=' . $jwt['body']['jwt']
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringContainsString('/console/auth/preview', $response['headers']['location']);
// Failure: Membership missing
$user = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], [
'userId' => ID::unique(),
'email' => 'newuser@appwrite.io',
'password' => 'password'
]);
$this->assertEquals(201, $user['headers']['status-code']);
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], [
'email' => 'newuser@appwrite.io',
'password' => 'password',
]);
$this->assertEquals(201, $session['headers']['status-code']);
$this->assertNotEmpty($session['cookies']['a_session_console']);
$cookie = 'a_session_console=' . $session['cookies']['a_session_console'];
$jwt = $this->client->call(Client::METHOD_POST, '/account/jwts', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'cookie' => $cookie,
'x-appwrite-project' => 'console',
]), []);
$this->assertEquals(201, $jwt['headers']['status-code']);
$this->assertNotEmpty($jwt['body']['jwt']);
$response = $proxyClient->call(Client::METHOD_GET, '/contact', headers: [
'cookie' => 'a_jwt_console=' . $jwt['body']['jwt']
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringContainsString('/console/auth/preview', $response['headers']['location']);
$this->cleanupSite($siteId);
}
}
+169 -20
View File
@@ -22,6 +22,8 @@ class VCSConsoleClientTest extends Scope
public string $providerInstallationId = '42954928'; // appwrite-test
public string $providerRepositoryId = '705764267'; // ruby-starter (public)
public string $providerRepositoryId2 = '708688544'; // function1.4 (private)
public string $providerRepositoryId3 = '943139433'; // svelte-starter (public)
public string $providerRepositoryId4 = '943245292'; // templates-for-sites (public)
public function testGitHubAuthorize(): string
{
@@ -67,24 +69,102 @@ class VCSConsoleClientTest extends Scope
* Test for SUCCESS
*/
$runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/detection', array_merge([
$runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/detections', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
'content-type' => 'application/json',
], $this->getHeaders()), [
'providerRepositoryId' => $this->providerRepositoryId,
'type' => 'runtime',
]);
$this->assertEquals(200, $runtime['headers']['status-code']);
$this->assertEquals($runtime['body']['runtime'], 'ruby-3.1');
$this->assertEquals($runtime['body']['runtime'], 'ruby-3.3');
$this->assertEquals($runtime['body']['commands'], 'bundle install && bundle exec rake build');
$this->assertEquals($runtime['body']['entrypoint'], 'main.rb');
/**
* Test for FAILURE
*/
$runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories/randomRepositoryId/detection', array_merge([
$runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/detections', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
'content-type' => 'application/json',
], $this->getHeaders()), [
'providerRepositoryId' => 'randomRepositoryId', // Invalid repository ID
'type' => 'runtime',
]);
$this->assertEquals(404, $runtime['headers']['status-code']);
}
/**
* @depends testGitHubAuthorize
*/
public function testDetectFramework(string $installationId)
{
/**
* Test for SUCCESS
*/
$framework = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/detections', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
'content-type' => 'application/json',
], $this->getHeaders()), [
'providerRepositoryId' => $this->providerRepositoryId3,
'type' => 'framework',
]);
$this->assertEquals(200, $framework['headers']['status-code']);
$this->assertEquals($framework['body']['framework'], 'sveltekit');
$this->assertEquals($framework['body']['installCommand'], 'npm install');
$this->assertEquals($framework['body']['buildCommand'], 'npm run build');
$this->assertEquals($framework['body']['outputDirectory'], './build');
$framework = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/detections', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
'content-type' => 'application/json',
], $this->getHeaders()), [
'providerRepositoryId' => $this->providerRepositoryId4,
'type' => 'framework',
'providerRootDirectory' => 'astro/starter'
]);
$this->assertEquals(200, $framework['headers']['status-code']);
$this->assertEquals($framework['body']['framework'], 'astro');
$this->assertEquals($framework['body']['installCommand'], 'npm install');
$this->assertEquals($framework['body']['buildCommand'], 'npm run build');
$this->assertEquals($framework['body']['outputDirectory'], './dist');
$framework = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/detections', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
'content-type' => 'application/json',
], $this->getHeaders()), [
'providerRepositoryId' => $this->providerRepositoryId4,
'type' => 'framework',
'providerRootDirectory' => 'remix/starter'
]);
$this->assertEquals(200, $framework['headers']['status-code']);
$this->assertEquals($framework['body']['framework'], 'remix');
$this->assertEquals($framework['body']['installCommand'], 'npm install');
$this->assertEquals($framework['body']['buildCommand'], 'npm run build');
$this->assertEquals($framework['body']['outputDirectory'], './build');
/**
* Test for FAILURE
*/
$framework = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/detections', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
'content-type' => 'application/json',
], $this->getHeaders()), [
'providerRepositoryId' => 'randomRepositoryId', // Invalid repository ID
'type' => 'framework',
]);
$this->assertEquals(404, $framework['headers']['status-code']);
}
/**
* @depends testGitHubAuthorize
*/
@@ -94,7 +174,7 @@ class VCSConsoleClientTest extends Scope
* Test for SUCCESS
*/
$runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/contents', array_merge([
$runtime = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/contents', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
@@ -126,7 +206,7 @@ class VCSConsoleClientTest extends Scope
$this->assertTrue($libContent['isDirectory']);
$this->assertEquals(0, $gemfileContent['size']);
$runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/contents?providerRootDirectory=lib', array_merge([
$runtime = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/contents?providerRootDirectory=lib', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
@@ -150,7 +230,7 @@ class VCSConsoleClientTest extends Scope
* Test for FAILURE
*/
$runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories/randomRepositoryId/contents', array_merge([
$runtime = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/randomRepositoryId/contents', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
@@ -168,26 +248,75 @@ class VCSConsoleClientTest extends Scope
$repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
], $this->getHeaders()), [
'type' => 'runtime'
]);
$this->assertEquals(200, $repositories['headers']['status-code']);
$this->assertEquals($repositories['body']['total'], 3);
$this->assertEquals($repositories['body']['providerRepositories'][0]['name'], 'function1.4');
$this->assertEquals($repositories['body']['providerRepositories'][0]['organization'], 'appwrite-test');
$this->assertEquals($repositories['body']['providerRepositories'][0]['provider'], 'github');
$this->assertEquals($repositories['body']['providerRepositories'][1]['name'], 'appwrite');
$this->assertEquals($repositories['body']['providerRepositories'][2]['name'], 'ruby-starter');
$this->assertEquals($repositories['body']['total'], 4);
$this->assertEquals($repositories['body']['runtimeProviderRepositories'][0]['name'], 'starter-for-svelte');
$this->assertEquals($repositories['body']['runtimeProviderRepositories'][0]['organization'], 'appwrite-test');
$this->assertEquals($repositories['body']['runtimeProviderRepositories'][0]['provider'], 'github');
$this->assertEquals($repositories['body']['runtimeProviderRepositories'][0]['runtime'], 'node-22');
$searchedRepositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'func'
'search' => 'function1.4',
'type' => 'runtime'
]);
$this->assertEquals(200, $searchedRepositories['headers']['status-code']);
$this->assertEquals($searchedRepositories['body']['total'], 1);
$this->assertEquals($searchedRepositories['body']['providerRepositories'][0]['name'], 'function1.4');
$this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['name'], 'function1.4');
$this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['runtime'], 'node-2');
$searchedRepositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'appwrite',
'type' => 'runtime'
]);
$this->assertEquals(200, $searchedRepositories['headers']['status-code']);
$this->assertEquals($searchedRepositories['body']['total'], 1);
$this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['name'], 'appwrite');
$this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['runtime'], 'php-8.3');
$searchedRepositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'ruby-starter',
'type' => 'runtime'
]);
$this->assertEquals(200, $searchedRepositories['headers']['status-code']);
$this->assertEquals($searchedRepositories['body']['total'], 1);
$this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['name'], 'ruby-starter');
$this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['runtime'], 'ruby-3.3');
$repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'type' => 'framework'
]);
$this->assertEquals(200, $repositories['headers']['status-code']);
$this->assertEquals($repositories['body']['total'], 4);
$this->assertEquals($repositories['body']['frameworkProviderRepositories'][0]['name'], 'starter-for-svelte');
$this->assertEquals($repositories['body']['frameworkProviderRepositories'][0]['organization'], 'appwrite-test');
$this->assertEquals($repositories['body']['frameworkProviderRepositories'][0]['provider'], 'github');
$this->assertEquals($repositories['body']['frameworkProviderRepositories'][0]['framework'], 'sveltekit');
$searchedRepositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'appwrite',
'type' => 'runtime'
]);
$this->assertEquals(200, $searchedRepositories['headers']['status-code']);
$this->assertEquals($searchedRepositories['body']['total'], 1);
$this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['name'], 'appwrite');
$this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['runtime'], 'other');
// TODO: If you are about to add another check, rewrite this to @provideScenarios
/**
* Test for FAILURE
@@ -195,9 +324,29 @@ class VCSConsoleClientTest extends Scope
$repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/randomInstallationId/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
], $this->getHeaders()), [
'type' => 'runtime'
]);
$this->assertEquals(404, $repositories['headers']['status-code']);
$repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'type' => 'randomType'
]);
$this->assertEquals(400, $repositories['headers']['status-code']);
$repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'randomSearch',
'type' => 'framework'
]);
$this->assertEquals(200, $repositories['headers']['status-code']);
$this->assertEquals($repositories['body']['total'], 0);
}
/**
@@ -0,0 +1,4 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
});
@@ -0,0 +1,14 @@
{
"name": "my-astro-app",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^5.2.5"
}
}
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Astro static</title>
</head>
<body>
</body>
</html>
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Main page</title>
</head>
<body>
</body>
</html>