From 081cf5c6aa07f2ef24ba2c6fd2cc0d590a5ac2d1 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 6 Nov 2024 23:35:30 +0530 Subject: [PATCH 1/5] Delete rules when site or deployment is deleted --- app/init.php | 1 + src/Appwrite/Platform/Workers/Deletes.php | 115 +++++++++++++++++++++- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/app/init.php b/app/init.php index 882d96f57e..055f68d10d 100644 --- a/app/init.php +++ b/app/init.php @@ -179,6 +179,7 @@ const DELETE_TYPE_DATABASES = 'databases'; const DELETE_TYPE_DOCUMENT = 'document'; const DELETE_TYPE_COLLECTIONS = 'collections'; const DELETE_TYPE_PROJECTS = 'projects'; +const DELETE_TYPE_SITES = 'sites'; const DELETE_TYPE_FUNCTIONS = 'functions'; const DELETE_TYPE_DEPLOYMENTS = 'deployments'; const DELETE_TYPE_USERS = 'users'; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 676120b741..fcb8ef5e12 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -87,11 +87,14 @@ class Deletes extends Action case DELETE_TYPE_PROJECTS: $this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document); break; + case DELETE_TYPE_SITES: + $this->deleteSite($dbForConsole, $getProjectDB, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $document, $project); + break; case DELETE_TYPE_FUNCTIONS: $this->deleteFunction($dbForConsole, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project); break; case DELETE_TYPE_DEPLOYMENTS: - $this->deleteDeployment($getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project); + $this->deleteDeployment($dbForConsole, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project); break; case DELETE_TYPE_USERS: $this->deleteUser($getProjectDB, $document, $project); @@ -732,6 +735,100 @@ class Deletes extends Action } } + /** + * @param callable $getProjectDB + * @param Device $deviceForSites + * @param Device $deviceForFunctions + * @param Device $deviceForBuilds + * @param Document $document function document + * @param Document $project + * @return void + * @throws Exception + */ + private function deleteSite(Database $dbForConsole, callable $getProjectDB, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, Document $project): void + { + $dbForProject = $getProjectDB($project); + $siteId = $document->getId(); + $siteInternalId = $document->getInternalId(); + + /** + * Delete rules for site + */ + Console::info("Deleting rules for site " . $siteId); + $this->deleteByGroup('rules', [ + Query::equal('resourceType', ['site']), + Query::equal('resourceInternalId', [$siteInternalId]), + Query::equal('projectInternalId', [$project->getInternalId()]) + ], $dbForConsole, function (Document $document) use ($dbForConsole) { + $this->deleteRule($dbForConsole, $document); + }); + + /** + * Delete Variables + */ + Console::info("Deleting variables for site " . $siteId); + $this->deleteByGroup('variables', [ + Query::equal('resourceType', ['site']), + Query::equal('resourceInternalId', [$siteInternalId]) + ], $dbForProject); + + /** + * Delete Deployments + */ + Console::info("Deleting deployments for site " . $siteId); + $deploymentInternalIds = []; + $this->deleteByGroup('deployments', [ + Query::equal('resourceInternalId', [$siteInternalId]) + ], $dbForProject, function (Document $document) use ($deviceForFunctions, &$deploymentInternalIds) { + $deploymentInternalIds[] = $document->getInternalId(); + $this->deleteDeploymentFiles($deviceForFunctions, $document); + }); + + /** + * Delete rules for all deployments of the site + */ + //TODO: If functions also have previews in the future, change the logic here to use unique identifier for sites and functions + foreach ($deploymentInternalIds as $deploymentInternalId) { + Console::info("Deleting rules for site " . $siteId . "'s deployment " . $deploymentInternalId); + $this->deleteByGroup('rules', [ + Query::equal('resourceType', ['deployment']), + Query::equal('resourceInternalId', [$deploymentInternalId]), + Query::equal('projectInternalId', [$project->getInternalId()]) + ], $dbForConsole, function (Document $document) use ($dbForConsole) { + $this->deleteRule($dbForConsole, $document); + }); + } + + /** + * 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); + }); + } + + /** + * Delete VCS Repositories and VCS Comments + */ + Console::info("Deleting VCS repositories and comments linked to site " . $siteId); + $this->deleteByGroup('repositories', [ + Query::equal('projectInternalId', [$project->getInternalId()]), + Query::equal('resourceInternalId', [$siteInternalId]), + Query::equal('resourceType', ['site']), + ], $dbForConsole, function (Document $document) use ($dbForConsole) { + $providerRepositoryId = $document->getAttribute('providerRepositoryId', ''); + $projectInternalId = $document->getAttribute('projectInternalId', ''); + $this->deleteByGroup('vcsComments', [ + Query::equal('providerRepositoryId', [$providerRepositoryId]), + Query::equal('projectInternalId', [$projectInternalId]), + ], $dbForConsole); + }); + } + /** * @param callable $getProjectDB * @param Device $deviceForFunctions @@ -898,7 +995,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteDeployment(callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, Document $project): void + private function deleteDeployment(Database $dbForConsole, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, Document $project): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -908,7 +1005,7 @@ class Deletes extends Action /** * Delete deployment files */ - $this->deleteDeploymentFiles($deviceForFunctions, $document); + $this->deleteDeploymentFiles($deviceForFunctions, $document); //TODO: For sites, this should be deviceForSites /** * Delete builds @@ -921,6 +1018,18 @@ class Deletes extends Action $this->deleteBuildFiles($deviceForBuilds, $document); }); + /** + * Delete rules associated with the deployment + */ + Console::info("Deleting rules for deployment " . $deploymentId); + $this->deleteByGroup('rules', [ + Query::equal('resourceType', ['deployment']), + Query::equal('resourceInternalId', [$deploymentInternalId]), + Query::equal('projectInternalId', [$project->getInternalId()]) + ], $dbForConsole, function (Document $document) use ($dbForConsole) { + $this->deleteRule($dbForConsole, $document); + }); + /** * Request executor to delete all deployment containers */ From a031b54feb2e179a9e783acf7bfb55987d8e4324 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:04:26 +0530 Subject: [PATCH 2/5] Add sitesBase test file --- tests/e2e/Services/Sites/SitesBase.php | 178 +++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 tests/e2e/Services/Sites/SitesBase.php diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php new file mode 100644 index 0000000000..8b1444ea1a --- /dev/null +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -0,0 +1,178 @@ +client->call(Client::METHOD_POST, '/sites', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $params); + + $this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + + $siteId = $site['body']['$id']; + + return $siteId; + } + + protected function setupDeployment(string $siteId, mixed $params): string + { + $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $params); + $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + $deploymentId = $deployment['body']['$id'] ?? ''; + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + }, 50000, 500); + + return $deploymentId; + } + + protected function cleanupSite(string $siteId): void + { + $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->assertEquals($site['headers']['status-code'], 204); + } + + protected function createSite(mixed $params): mixed + { + $site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $site; + } + + protected function createVariable(string $siteId, mixed $params): mixed + { + $variable = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $variable; + } + + protected function getSite(string $siteId): mixed + { + $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $site; + } + + protected function getDeployment(string $siteId, string $deploymentId): mixed + { + $deployment = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $deployment; + } + + protected function listSites(mixed $params = []): mixed + { + $sites = $this->client->call(Client::METHOD_GET, '/sites', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $sites; + } + + protected function listDeployments(string $siteId, $params = []): mixed + { + $deployments = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $deployments; + } + + protected function packageSite(string $site): CURLFile + { + $folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site"; + $tarPath = "$folderPath/code.tar.gz"; + + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); + + if (filesize($tarPath) > 1024 * 1024 * 5) { + throw new \Exception('Code package is too large. Use the chunked upload method instead.'); + } + + return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); + } + + protected function createDeployment(string $siteId, mixed $params = []): mixed + { + $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $deployment; + } + + protected function getSiteUsage(string $siteId, mixed $params): mixed + { + $usage = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/usage', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $usage; + } + + protected function getTemplate(string $templateId) + { + $template = $this->client->call(Client::METHOD_GET, '/sites/templates/' . $templateId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $template; + } + + protected function deleteSite(string $siteId): mixed + { + $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $site; + } +} From b88cd8e5444a4f29c791e069d5ba767ea03e0556 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:55:41 +0530 Subject: [PATCH 3/5] Add subdomain param in functions and tests --- app/config/services.php | 13 ++ .../Http/Functions/CreateFunction.php | 28 +++- tests/e2e/Scopes/ProjectCustom.php | 2 + .../Functions/FunctionsCustomServerTest.php | 22 +++ .../Services/Sites/SitesCustomServerTest.php | 155 ++++++++++++++++++ 5 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/Services/Sites/SitesCustomServerTest.php diff --git a/app/config/services.php b/app/config/services.php index c4fb98c59a..e8789fcbec 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -173,6 +173,19 @@ return [ 'optional' => false, 'icon' => '', ], + 'sites' => [ + 'key' => 'sites', + 'name' => 'Sites', + 'subtitle' => 'The Sites Service allows you view, create and manage your Cloud Sites.', + 'description' => '/docs/services/sites.md', + 'controller' => 'api/sites.php', + 'sdk' => true, + 'docs' => true, + 'docsUrl' => 'https://appwrite.io/docs/sites', + 'tests' => false, + 'optional' => true, + 'icon' => '', // TODO: Update icon later + ], 'functions' => [ 'key' => 'functions', 'name' => 'Functions', diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php index 4074eca1c0..14da817a3f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php @@ -21,6 +21,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; +use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Roles; use Utopia\Platform\Action; @@ -88,6 +89,7 @@ class CreateFunction extends Base 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']) + ->param('subdomain', '', new CustomId(), 'Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true) ->inject('request') ->inject('response') ->inject('dbForProject') @@ -100,8 +102,27 @@ class CreateFunction extends Base ->callback([$this, 'action']); } - public function action(string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) + public function action(string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, string $subdomain, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) { + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + $ruleId = ''; + $routeSubdomain = ''; + $domain = ''; + + if (!empty($functionsDomain)) { + $ruleId = ID::unique(); + $routeSubdomain = $subdomain ?: ID::unique(); + $domain = "{$routeSubdomain}.{$functionsDomain}"; + + $subdomain = Authorization::skip(fn () => $dbForConsole->findOne('rules', [ + Query::equal('domain', [$domain]) + ])); + + if (!empty($subdomain)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Subdomain already exists. Please choose a different subdomain.'); + } + } + $functionId = ($functionId == 'unique()') ? ID::unique() : $functionId; $allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', ''))); @@ -241,12 +262,7 @@ class CreateFunction extends Base ->setTemplate($template); } - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); if (!empty($functionsDomain)) { - $ruleId = ID::unique(); - $routeSubdomain = ID::unique(); - $domain = "{$routeSubdomain}.{$functionsDomain}"; - $rule = Authorization::skip( fn () => $dbForConsole->createDocument('rules', new Document([ '$id' => $ruleId, diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index ad2c93790c..69bf680f5e 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -74,6 +74,8 @@ trait ProjectCustom 'files.write', 'buckets.read', 'buckets.write', + 'sites.read', + 'sites.write', 'functions.read', 'functions.write', 'execution.read', diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 41b890caf6..f7a856a76f 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -36,6 +36,7 @@ class FunctionsCustomServerTest extends Scope 'buckets.*.delete', ], 'timeout' => 10, + 'subdomain' => 'test' ]); $functionId = $functionId = $function['body']['$id'] ?? ''; @@ -1984,4 +1985,25 @@ class FunctionsCustomServerTest extends Scope $this->cleanupFunction($functionId); } + + /** + * @depends testCreateFunction + */ + public function testUniqueSubdomain(array $data): void + { + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'events' => [ + 'buckets.*.create', + 'buckets.*.delete', + ], + 'timeout' => 10, + 'subdomain' => 'test' + ]); + + $this->assertEquals(400, $function['headers']['status-code']); + } } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php new file mode 100644 index 0000000000..25ebfc9283 --- /dev/null +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -0,0 +1,155 @@ +createSite([ + 'siteId' => ID::unique(), + 'name' => 'Test', + 'framework' => 'sveltekit', + 'installCommand' => 'npm install --force', + 'buildCommand' => 'npm run build', + 'outputDirectory' => './build', + 'buildRuntime' => 'node-22', + 'serveRuntime' => 'static-1', + 'subdomain' => 'test' + ]); + + $siteId = $site['body']['$id'] ?? ''; + + $dateValidator = new DatetimeValidator(); + $this->assertEquals(201, $site['headers']['status-code']); + $this->assertNotEmpty($site['body']['$id']); + $this->assertEquals('Test', $site['body']['name']); + $this->assertEquals('sveltekit', $site['body']['framework']); + $this->assertEquals(true, $dateValidator->isValid($site['body']['$createdAt'])); + $this->assertEquals(true, $dateValidator->isValid($site['body']['$updatedAt'])); + $this->assertEquals('npm install --force', $site['body']['installCommand']); + $this->assertEquals('npm run build', $site['body']['buildCommand']); + $this->assertEquals('./build', $site['body']['outputDirectory']); + $this->assertEquals('node-22', $site['body']['buildRuntime']); + $this->assertEquals('static-1', $site['body']['serveRuntime']); + + $variable = $this->createVariable($siteId, [ + 'key' => 'siteKey1', + 'value' => 'siteValue1', + ]); + $variable2 = $this->createVariable($siteId, [ + 'key' => 'siteKey2', + 'value' => 'siteValue2', + ]); + $variable3 = $this->createVariable($siteId, [ + 'key' => 'siteKey3', + 'value' => 'siteValue3', + ]); + + $this->assertEquals(201, $variable['headers']['status-code']); + $this->assertEquals(201, $variable2['headers']['status-code']); + $this->assertEquals(201, $variable3['headers']['status-code']); + + return [ + 'siteId' => $siteId, + ]; + } + + /** + * @depends testCreateSite + */ + public function testGetSite(array $data): array + { + /** + * Test for SUCCESS + */ + $site = $this->getSite($data['siteId']); + + $this->assertEquals($site['headers']['status-code'], 200); + $this->assertEquals($site['body']['name'], 'Test'); + + /** + * Test for FAILURE + */ + $site = $this->getSite('x'); + + $this->assertEquals($site['headers']['status-code'], 404); + + return $data; + } + + /** + * @depends testGetSite + */ + public function testDeleteSite(array $data): array + { + /** + * Test for SUCCESS + */ + $site = $this->deleteSite($data['siteId']); + + $this->assertEquals(204, $site['headers']['status-code']); + $this->assertEmpty($site['body']); + + $site = $this->getSite($data['siteId']); + + $this->assertEquals(404, $site['headers']['status-code']); + + return $data; + } + + /** + * @depends testGetSite + */ + public function testUniqueSubdomain(array $data): void + { + /** + * Test for SUCCESS + */ + $site = $this->createSite([ + 'siteId' => ID::unique(), + 'name' => 'Test', + 'framework' => 'sveltekit', + 'installCommand' => 'npm install --force', + 'buildCommand' => 'npm run build', + 'outputDirectory' => './build', + 'buildRuntime' => 'node-22', + 'serveRuntime' => 'static-1', + 'subdomain' => 'test' + ]); + + $this->assertEquals(201, $site['headers']['status-code']); + + /** + * Test for FAILURE + */ + $site = $this->createSite([ + 'siteId' => ID::unique(), + 'name' => 'Test2', + 'framework' => 'sveltekit', + 'installCommand' => 'npm install --force', + 'buildCommand' => 'npm run build', + 'outputDirectory' => './build', + 'buildRuntime' => 'node-22', + 'serveRuntime' => 'static-1', + 'subdomain' => 'test' + ]); + + $this->assertEquals(400, $site['headers']['status-code']); + + return; + } +} From ba74ffd4bf95d348f12f105f69f4bbdc5f4b700f Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:48:26 +0530 Subject: [PATCH 4/5] Address PR comments --- app/config/services.php | 2 +- .../Functions/FunctionsCustomServerTest.php | 39 +++++++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/app/config/services.php b/app/config/services.php index e8789fcbec..d3e221e2fa 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -176,7 +176,7 @@ return [ 'sites' => [ 'key' => 'sites', 'name' => 'Sites', - 'subtitle' => 'The Sites Service allows you view, create and manage your Cloud Sites.', + 'subtitle' => 'The Sites Service allows you view, create and manage your web applications.', 'description' => '/docs/services/sites.md', 'controller' => 'api/sites.php', 'sdk' => true, diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index f7a856a76f..67f2f17c86 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -73,6 +73,24 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(201, $variable2['headers']['status-code']); $this->assertEquals(201, $variable3['headers']['status-code']); + /** + * Test for FAILURE + */ + $function2 = $this->createFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'events' => [ + 'buckets.*.create', + 'buckets.*.delete', + ], + 'timeout' => 10, + 'subdomain' => 'test' + ]); + + $this->assertEquals(400, $function2['headers']['status-code']); + return [ 'functionId' => $functionId, ]; @@ -1985,25 +2003,4 @@ class FunctionsCustomServerTest extends Scope $this->cleanupFunction($functionId); } - - /** - * @depends testCreateFunction - */ - public function testUniqueSubdomain(array $data): void - { - $function = $this->createFunction([ - 'functionId' => ID::unique(), - 'name' => 'Test', - 'runtime' => 'php-8.0', - 'entrypoint' => 'index.php', - 'events' => [ - 'buckets.*.create', - 'buckets.*.delete', - ], - 'timeout' => 10, - 'subdomain' => 'test' - ]); - - $this->assertEquals(400, $function['headers']['status-code']); - } } From 8a260ceece3c4f63a09c0c7c91019e8a1194dc41 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:41:45 +0530 Subject: [PATCH 5/5] Remove unrelated changes to site deletion --- .../Http/Functions/CreateFunction.php | 24 +-- .../Functions/FunctionsCustomServerTest.php | 19 -- tests/e2e/Services/Sites/SitesBase.php | 178 ------------------ .../Services/Sites/SitesCustomServerTest.php | 155 --------------- 4 files changed, 2 insertions(+), 374 deletions(-) delete mode 100644 tests/e2e/Services/Sites/SitesBase.php delete mode 100644 tests/e2e/Services/Sites/SitesCustomServerTest.php diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php index b44b615d48..16df486c8c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php @@ -21,7 +21,6 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Roles; use Utopia\Platform\Action; @@ -90,7 +89,6 @@ class CreateFunction extends Base 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']) - ->param('subdomain', '', new CustomId(), 'Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true) ->inject('request') ->inject('response') ->inject('dbForProject') @@ -103,27 +101,8 @@ class CreateFunction extends Base ->callback([$this, 'action']); } - public function action(string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, string $subdomain, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) + public function action(string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) { - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $ruleId = ''; - $routeSubdomain = ''; - $domain = ''; - - if (!empty($functionsDomain)) { - $ruleId = ID::unique(); - $routeSubdomain = $subdomain ?: ID::unique(); - $domain = "{$routeSubdomain}.{$functionsDomain}"; - - $subdomain = Authorization::skip(fn () => $dbForConsole->findOne('rules', [ - Query::equal('domain', [$domain]) - ])); - - if (!empty($subdomain)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Subdomain already exists. Please choose a different subdomain.'); - } - } - $functionId = ($functionId == 'unique()') ? ID::unique() : $functionId; $allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', ''))); @@ -263,6 +242,7 @@ class CreateFunction extends Base ->setTemplate($template); } + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); if (!empty($functionsDomain)) { $routeSubdomain = ID::unique(); $domain = "{$routeSubdomain}.{$functionsDomain}"; diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 67f2f17c86..41b890caf6 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -36,7 +36,6 @@ class FunctionsCustomServerTest extends Scope 'buckets.*.delete', ], 'timeout' => 10, - 'subdomain' => 'test' ]); $functionId = $functionId = $function['body']['$id'] ?? ''; @@ -73,24 +72,6 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(201, $variable2['headers']['status-code']); $this->assertEquals(201, $variable3['headers']['status-code']); - /** - * Test for FAILURE - */ - $function2 = $this->createFunction([ - 'functionId' => ID::unique(), - 'name' => 'Test', - 'runtime' => 'php-8.0', - 'entrypoint' => 'index.php', - 'events' => [ - 'buckets.*.create', - 'buckets.*.delete', - ], - 'timeout' => 10, - 'subdomain' => 'test' - ]); - - $this->assertEquals(400, $function2['headers']['status-code']); - return [ 'functionId' => $functionId, ]; diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php deleted file mode 100644 index 8b1444ea1a..0000000000 --- a/tests/e2e/Services/Sites/SitesBase.php +++ /dev/null @@ -1,178 +0,0 @@ -client->call(Client::METHOD_POST, '/sites', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $params); - - $this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); - - $siteId = $site['body']['$id']; - - return $siteId; - } - - protected function setupDeployment(string $siteId, mixed $params): string - { - $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $params); - $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); - $deploymentId = $deployment['body']['$id'] ?? ''; - - $this->assertEventually(function () use ($siteId, $deploymentId) { - $deployment = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ])); - $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); - }, 50000, 500); - - return $deploymentId; - } - - protected function cleanupSite(string $siteId): void - { - $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ])); - - $this->assertEquals($site['headers']['status-code'], 204); - } - - protected function createSite(mixed $params): mixed - { - $site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $params); - - return $site; - } - - protected function createVariable(string $siteId, mixed $params): mixed - { - $variable = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $params); - - return $variable; - } - - protected function getSite(string $siteId): mixed - { - $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - return $site; - } - - protected function getDeployment(string $siteId, string $deploymentId): mixed - { - $deployment = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - return $deployment; - } - - protected function listSites(mixed $params = []): mixed - { - $sites = $this->client->call(Client::METHOD_GET, '/sites', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $params); - - return $sites; - } - - protected function listDeployments(string $siteId, $params = []): mixed - { - $deployments = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $params); - - return $deployments; - } - - protected function packageSite(string $site): CURLFile - { - $folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site"; - $tarPath = "$folderPath/code.tar.gz"; - - Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); - - if (filesize($tarPath) > 1024 * 1024 * 5) { - throw new \Exception('Code package is too large. Use the chunked upload method instead.'); - } - - return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); - } - - protected function createDeployment(string $siteId, mixed $params = []): mixed - { - $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $params); - - return $deployment; - } - - protected function getSiteUsage(string $siteId, mixed $params): mixed - { - $usage = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/usage', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $params); - - return $usage; - } - - protected function getTemplate(string $templateId) - { - $template = $this->client->call(Client::METHOD_GET, '/sites/templates/' . $templateId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - return $template; - } - - protected function deleteSite(string $siteId): mixed - { - $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - return $site; - } -} diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php deleted file mode 100644 index 25ebfc9283..0000000000 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ /dev/null @@ -1,155 +0,0 @@ -createSite([ - 'siteId' => ID::unique(), - 'name' => 'Test', - 'framework' => 'sveltekit', - 'installCommand' => 'npm install --force', - 'buildCommand' => 'npm run build', - 'outputDirectory' => './build', - 'buildRuntime' => 'node-22', - 'serveRuntime' => 'static-1', - 'subdomain' => 'test' - ]); - - $siteId = $site['body']['$id'] ?? ''; - - $dateValidator = new DatetimeValidator(); - $this->assertEquals(201, $site['headers']['status-code']); - $this->assertNotEmpty($site['body']['$id']); - $this->assertEquals('Test', $site['body']['name']); - $this->assertEquals('sveltekit', $site['body']['framework']); - $this->assertEquals(true, $dateValidator->isValid($site['body']['$createdAt'])); - $this->assertEquals(true, $dateValidator->isValid($site['body']['$updatedAt'])); - $this->assertEquals('npm install --force', $site['body']['installCommand']); - $this->assertEquals('npm run build', $site['body']['buildCommand']); - $this->assertEquals('./build', $site['body']['outputDirectory']); - $this->assertEquals('node-22', $site['body']['buildRuntime']); - $this->assertEquals('static-1', $site['body']['serveRuntime']); - - $variable = $this->createVariable($siteId, [ - 'key' => 'siteKey1', - 'value' => 'siteValue1', - ]); - $variable2 = $this->createVariable($siteId, [ - 'key' => 'siteKey2', - 'value' => 'siteValue2', - ]); - $variable3 = $this->createVariable($siteId, [ - 'key' => 'siteKey3', - 'value' => 'siteValue3', - ]); - - $this->assertEquals(201, $variable['headers']['status-code']); - $this->assertEquals(201, $variable2['headers']['status-code']); - $this->assertEquals(201, $variable3['headers']['status-code']); - - return [ - 'siteId' => $siteId, - ]; - } - - /** - * @depends testCreateSite - */ - public function testGetSite(array $data): array - { - /** - * Test for SUCCESS - */ - $site = $this->getSite($data['siteId']); - - $this->assertEquals($site['headers']['status-code'], 200); - $this->assertEquals($site['body']['name'], 'Test'); - - /** - * Test for FAILURE - */ - $site = $this->getSite('x'); - - $this->assertEquals($site['headers']['status-code'], 404); - - return $data; - } - - /** - * @depends testGetSite - */ - public function testDeleteSite(array $data): array - { - /** - * Test for SUCCESS - */ - $site = $this->deleteSite($data['siteId']); - - $this->assertEquals(204, $site['headers']['status-code']); - $this->assertEmpty($site['body']); - - $site = $this->getSite($data['siteId']); - - $this->assertEquals(404, $site['headers']['status-code']); - - return $data; - } - - /** - * @depends testGetSite - */ - public function testUniqueSubdomain(array $data): void - { - /** - * Test for SUCCESS - */ - $site = $this->createSite([ - 'siteId' => ID::unique(), - 'name' => 'Test', - 'framework' => 'sveltekit', - 'installCommand' => 'npm install --force', - 'buildCommand' => 'npm run build', - 'outputDirectory' => './build', - 'buildRuntime' => 'node-22', - 'serveRuntime' => 'static-1', - 'subdomain' => 'test' - ]); - - $this->assertEquals(201, $site['headers']['status-code']); - - /** - * Test for FAILURE - */ - $site = $this->createSite([ - 'siteId' => ID::unique(), - 'name' => 'Test2', - 'framework' => 'sveltekit', - 'installCommand' => 'npm install --force', - 'buildCommand' => 'npm run build', - 'outputDirectory' => './build', - 'buildRuntime' => 'node-22', - 'serveRuntime' => 'static-1', - 'subdomain' => 'test' - ]); - - $this->assertEquals(400, $site['headers']['status-code']); - - return; - } -}