diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 597f02f7d6..0612b50566 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -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], + ], ], ], diff --git a/app/config/errors.php b/app/config/errors.php index e41f663168..73cb9d38cf 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -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 => [ diff --git a/app/config/frameworks.php b/app/config/frameworks.php index 332035a79c..8aee8b9e16 100644 --- a/app/config/frameworks.php +++ b/app/config/frameworks.php @@ -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' => '', ], ] ], diff --git a/app/config/runtimes/specifications.php b/app/config/runtimes/specifications.php deleted file mode 100644 index d3625db8a2..0000000000 --- a/app/config/runtimes/specifications.php +++ /dev/null @@ -1,51 +0,0 @@ - [ - '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 - ] -]; diff --git a/app/config/frameworks/specifications.php b/app/config/specifications.php similarity index 95% rename from app/config/frameworks/specifications.php rename to app/config/specifications.php index 63ca3ea423..82f5633a96 100644 --- a/app/config/frameworks/specifications.php +++ b/app/config/specifications.php @@ -1,6 +1,6 @@ [ diff --git a/app/config/function-templates.php b/app/config/templates/function.php similarity index 100% rename from app/config/function-templates.php rename to app/config/templates/function.php diff --git a/app/config/site-templates.php b/app/config/templates/site.php similarity index 99% rename from app/config/site-templates.php rename to app/config/templates/site.php index 9e0b36053e..52d2817c3d 100644 --- a/app/config/site-templates.php +++ b/app/config/templates/site.php @@ -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', diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 4adfb73908..fbc97f9fdb 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -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') diff --git a/app/controllers/general.php b/app/controllers/general.php index d319a1a3cd..2ce8f96dab 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -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') diff --git a/app/init.php b/app/init.php index 3291b160b9..ae3d96cd97 100644 --- a/app/init.php +++ b/app/init.php @@ -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 diff --git a/composer.json b/composer.json index da94b9462d..1623dbc4d7 100644 --- a/composer.json +++ b/composer.json @@ -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.*", diff --git a/composer.lock b/composer.lock index a7f0992f99..442734db0f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "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", diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php index 8555d5cb00..9af5045fa4 100644 --- a/src/Appwrite/Auth/Auth.php +++ b/src/Appwrite/Auth/Auth.php @@ -103,6 +103,11 @@ class Auth */ public static $cookieName = 'a_session'; + /** + * @var string + */ + public static $cookieNamePreview = 'a_jwt_console'; + /** * User Unique ID. * diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php index 83f8dd408d..89c28c4727 100644 --- a/src/Appwrite/Auth/Key.php +++ b/src/Appwrite/Auth/Key.php @@ -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( diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index f1494b5d67..5974d12f00 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -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'; diff --git a/src/Appwrite/Functions/Specification.php b/src/Appwrite/Functions/Specification.php deleted file mode 100644 index 50a3c02b62..0000000000 --- a/src/Appwrite/Functions/Specification.php +++ /dev/null @@ -1,16 +0,0 @@ - $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(), diff --git a/src/Appwrite/Sites/Specification.php b/src/Appwrite/Platform/Modules/Compute/Specification.php similarity index 91% rename from src/Appwrite/Sites/Specification.php rename to src/Appwrite/Platform/Modules/Compute/Specification.php index c6e725ec31..cf48896a7b 100644 --- a/src/Appwrite/Sites/Specification.php +++ b/src/Appwrite/Platform/Modules/Compute/Specification.php @@ -1,6 +1,6 @@ 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; } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php index 6885ecde6c..580959589b 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php @@ -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)); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php index 5bde7903d9..c02c470980 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php @@ -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'); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php index a63ac18354..4fa279a3d7 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php @@ -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; } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php index 624a410dcd..b942ba9ab7 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php @@ -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 diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Get.php index 1d074e9e2a..8e58433e7c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Get.php @@ -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); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php index e349292e4b..6c8cacdcfd 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php @@ -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); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 9a61693e15..1f6fa364f3 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -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, diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php index 9e03a86bc7..a416001e2f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php @@ -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, diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index f522e2dc62..0bb755a1f2 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -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, diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 0507c68068..b8959ab5bc 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -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']) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php index 96fced8b3c..28bdd1d3df 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php @@ -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); } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index 9244b10c8c..550cc2aa51 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -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')); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Specifications/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Specifications/XList.php index dbaf3daa04..375c99966d 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Specifications/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Specifications/XList.php @@ -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); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index d2d01409c3..608c001b70 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -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)); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index 73f3372de2..2da5e00ca2 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -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)); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php index fe16dafae1..7065ac1917 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php @@ -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'); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php index f4b6b73784..6d969f883f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php @@ -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; } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php index af1d6bee14..3b8cf81e25 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php @@ -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 diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php index c7929b2e7a..8c3beaf2c0 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php @@ -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); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php index 0a15053df1..632ad51c9b 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php @@ -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); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index 7900f09075..75b67004b2 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -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, diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php index c29b14b840..e041554132 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php @@ -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, diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index c39f618691..b3c0c9fb47 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -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] ?? []; diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php index 27376f6a42..d02762c11b 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php @@ -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); } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index f9dbdf604c..35e4fb07e3 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -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')); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Specifications/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Specifications/XList.php new file mode 100644 index 0000000000..63ee7bec10 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Specifications/XList.php @@ -0,0 +1,76 @@ +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: <<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); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index 813a857b7d..6bd151f97e 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -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()); } } diff --git a/src/Appwrite/Platform/Tasks/Screenshot.php b/src/Appwrite/Platform/Tasks/Screenshot.php index 43cf434408..fb0cce3d38 100644 --- a/src/Appwrite/Platform/Tasks/Screenshot.php +++ b/src/Appwrite/Platform/Tasks/Screenshot.php @@ -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']); diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index de385bb646..63a69d2ad7 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -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 diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index f6d441ef3c..febaec54d7 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -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, diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index a6101522fb..fa8d7490ea 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -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); diff --git a/src/Appwrite/Sites/Validator/FrameworkSpecification.php b/src/Appwrite/Sites/Validator/FrameworkSpecification.php deleted file mode 100644 index b8dc3b5cfd..0000000000 --- a/src/Appwrite/Sites/Validator/FrameworkSpecification.php +++ /dev/null @@ -1,112 +0,0 @@ -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; - } -} diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Deployments.php b/src/Appwrite/Utopia/Database/Validator/Queries/Deployments.php index 42aed88ef6..75035ad501 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Deployments.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Deployments.php @@ -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' ]; /** diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index da222822e0..472c957271 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -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()) diff --git a/src/Appwrite/Utopia/Response/Model/Build.php b/src/Appwrite/Utopia/Response/Model/Build.php deleted file mode 100644 index d80c17645a..0000000000 --- a/src/Appwrite/Utopia/Response/Model/Build.php +++ /dev/null @@ -1,94 +0,0 @@ -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; - } -} diff --git a/src/Appwrite/Utopia/Response/Model/Deployment.php b/src/Appwrite/Utopia/Response/Model/Deployment.php index f42ce6d334..2b14977a7e 100644 --- a/src/Appwrite/Utopia/Response/Model/Deployment.php +++ b/src/Appwrite/Utopia/Response/Model/Deployment.php @@ -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, diff --git a/src/Appwrite/Utopia/Response/Model/DetectionFramework.php b/src/Appwrite/Utopia/Response/Model/DetectionFramework.php new file mode 100644 index 0000000000..9aeb885f76 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/DetectionFramework.php @@ -0,0 +1,58 @@ +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; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Detection.php b/src/Appwrite/Utopia/Response/Model/DetectionRuntime.php similarity index 50% rename from src/Appwrite/Utopia/Response/Model/Detection.php rename to src/Appwrite/Utopia/Response/Model/DetectionRuntime.php index c71baa0b0c..1c7c0be16b 100644 --- a/src/Appwrite/Utopia/Response/Model/Detection.php +++ b/src/Appwrite/Utopia/Response/Model/DetectionRuntime.php @@ -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; } } diff --git a/src/Appwrite/Utopia/Response/Model/ProviderRepository.php b/src/Appwrite/Utopia/Response/Model/ProviderRepository.php index c133ae3164..518b5de9ee 100644 --- a/src/Appwrite/Utopia/Response/Model/ProviderRepository.php +++ b/src/Appwrite/Utopia/Response/Model/ProviderRepository.php @@ -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.', diff --git a/src/Appwrite/Utopia/Response/Model/ProviderRepositoryFramework.php b/src/Appwrite/Utopia/Response/Model/ProviderRepositoryFramework.php new file mode 100644 index 0000000000..24556f4453 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ProviderRepositoryFramework.php @@ -0,0 +1,40 @@ +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; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/ProviderRepositoryRuntime.php b/src/Appwrite/Utopia/Response/Model/ProviderRepositoryRuntime.php new file mode 100644 index 0000000000..ea20eeea47 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ProviderRepositoryRuntime.php @@ -0,0 +1,40 @@ +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; + } +} diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 0c6b5a14ff..9702db43de 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -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 * diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 00dc790869..a74101f50d 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -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; diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 0f831b5e38..58e811c8b0 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -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; + } } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 2056d1f689..c3adc25799 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -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); diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 5c38a10472..121d40156e 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -1568,7 +1568,7 @@ trait Base _id buildId entrypoint - size + buildSize status buildLogs } diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 303b70393d..ebb8e7fd2c 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -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; + } } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index bf3287f309..07a9fcf46b 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -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); + } } diff --git a/tests/e2e/Services/VCS/VCSConsoleClientTest.php b/tests/e2e/Services/VCS/VCSConsoleClientTest.php index f04667a0f5..13c3ddb251 100644 --- a/tests/e2e/Services/VCS/VCSConsoleClientTest.php +++ b/tests/e2e/Services/VCS/VCSConsoleClientTest.php @@ -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); } /** diff --git a/tests/resources/sites/astro-static/astro.config.mjs b/tests/resources/sites/astro-static/astro.config.mjs new file mode 100644 index 0000000000..20b0b8e2ba --- /dev/null +++ b/tests/resources/sites/astro-static/astro.config.mjs @@ -0,0 +1,4 @@ +import { defineConfig } from 'astro/config'; + +export default defineConfig({ +}); diff --git a/tests/resources/sites/astro-static/package.json b/tests/resources/sites/astro-static/package.json new file mode 100644 index 0000000000..147dcf1f07 --- /dev/null +++ b/tests/resources/sites/astro-static/package.json @@ -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" + } +} diff --git a/tests/resources/sites/astro-static/src/pages/index.astro b/tests/resources/sites/astro-static/src/pages/index.astro new file mode 100644 index 0000000000..0b4f43c3d6 --- /dev/null +++ b/tests/resources/sites/astro-static/src/pages/index.astro @@ -0,0 +1,10 @@ + + + + + + Astro static + + + + diff --git a/tests/resources/sites/static-single-file/main.html b/tests/resources/sites/static-single-file/main.html new file mode 100644 index 0000000000..2615f0c820 --- /dev/null +++ b/tests/resources/sites/static-single-file/main.html @@ -0,0 +1,10 @@ + + + + + + Main page + + + + \ No newline at end of file