From a4bdc23aef1a5df0850c40961ac082986667e898 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:19:07 +0200 Subject: [PATCH 1/2] Add more endpoints and models for sites --- .../Modules/Sites/Http/Sites/CreateSite.php | 2 +- .../Modules/Sites/Http/Sites/GetSite.php | 53 ++++ .../Sites/Http/Sites/ListFrameworks.php | 63 +++++ .../Modules/Sites/Http/Sites/ListSites.php | 93 +++++++ .../Modules/Sites/Http/Sites/UpdateSite.php | 231 ++++++++++++++++++ .../Platform/Modules/Sites/Services/Http.php | 8 + .../Database/Validator/Queries/Sites.php | 26 ++ src/Appwrite/Utopia/Response.php | 5 + .../Utopia/Response/Model/Framework.php | 66 +++++ 9 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Sites/GetSite.php create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Sites/ListFrameworks.php create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Sites/ListSites.php create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Sites/UpdateSite.php create mode 100644 src/Appwrite/Utopia/Database/Validator/Queries/Sites.php create mode 100644 src/Appwrite/Utopia/Response/Model/Framework.php diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/CreateSite.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/CreateSite.php index 086aeddc35..476aad070e 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/CreateSite.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/CreateSite.php @@ -79,7 +79,7 @@ class CreateSite extends Base Config::getParam('framework-specifications', []), App::getEnv('_APP_SITES_CPUS', APP_SITE_CPUS_DEFAULT), App::getEnv('_APP_SITES_MEMORY', APP_SITE_MEMORY_DEFAULT) - ), 'Runtime specification for the site and builds.', true, ['plan']) + ), 'Framework specification for the site and builds.', true, ['plan']) ->inject('request') ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/GetSite.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/GetSite.php new file mode 100644 index 0000000000..03c6e390e4 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/GetSite.php @@ -0,0 +1,53 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/sites/:siteId') + ->desc('Get site') + ->groups(['api', 'sites']) + ->label('scope', 'functions.read') // TODO: Update scope to sites.read + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'get') + ->label('sdk.description', '/docs/references/sites/get-site.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SITE) + ->param('siteId', '', new UID(), 'Site ID.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $siteId, Response $response, Database $dbForProject) + { + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $response->dynamic($site, Response::MODEL_SITE); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/ListFrameworks.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/ListFrameworks.php new file mode 100644 index 0000000000..e3e5aaa3e2 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/ListFrameworks.php @@ -0,0 +1,63 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/sites/frameworks') + ->desc('List frameworks') + ->groups(['api', 'sites']) + ->label('scope', 'functions.read') // TODO: Update scope to sites.read + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'listFrameworks') + ->label('sdk.description', '/docs/references/sites/list-frameworks.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_FRAMEWORK_LIST) + ->inject('response') + ->callback([$this, 'action']); + } + + public function action(Response $response) + { + $frameworks = Config::getParam('frameworks'); + + $allowList = \array_filter(\explode(',', System::getEnv('_APP_SITES_FRAMEWORKS', ''))); + + $allowed = []; + foreach ($frameworks as $id => $framework) { + if (!empty($allowList) && !\in_array($id, $allowList)) { + continue; + } + + $framework['$id'] = $id; + $allowed[] = $framework; + } + + $response->dynamic(new Document([ + 'total' => count($allowed), + 'frameworks' => $allowed + ]), Response::MODEL_FRAMEWORK_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/ListSites.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/ListSites.php new file mode 100644 index 0000000000..df3715f3bd --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/ListSites.php @@ -0,0 +1,93 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/sites') + ->desc('List sites') + ->groups(['api', 'sites']) + ->label('scope', 'functions.write') // TODO: Update scope to sites.write + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'list') + ->label('sdk.description', '/docs/references/sites/list-sites.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SITE_LIST) + ->param('queries', [], new Sites(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Sites::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(array $queries, string $search, Response $response, Database $dbForProject) + { + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + /** + * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries + */ + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); + }); + $cursor = reset($cursor); + if ($cursor) { + /** @var Query $cursor */ + + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $siteId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('sites', $siteId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Site '{$siteId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + $response->dynamic(new Document([ + 'sites' => $dbForProject->find('sites', $queries), + 'total' => $dbForProject->count('sites', $filterQueries, APP_LIMIT_COUNT), + ]), Response::MODEL_SITE_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/UpdateSite.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/UpdateSite.php new file mode 100644 index 0000000000..67b47f00fc --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/UpdateSite.php @@ -0,0 +1,231 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) + ->setHttpPath('/v1/sites/:siteId') + ->desc('Update site') + ->groups(['api', 'sites']) + ->label('scope', 'functions.write') // TODO: update it to sites.write later + ->label('event', 'sites.[siteId].update') + ->label('audits.event', 'sites.update') + ->label('audits.resource', 'site/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'update') + ->label('sdk.description', '/docs/references/sites/update-site.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SITE) + ->param('siteId', '', new UID(), 'Site ID.') + ->param('name', '', new Text(128), 'Site name. Max length: 128 chars.') + ->param('framework', '', new WhiteList(Config::getParam('frameworks'), true), 'Sites framework.') + ->param('enabled', true, new Boolean(), 'Is site enabled? When set to \'disabled\', users cannot access the site but Server SDKs with and API key can still access the site. No data is lost when this is toggled.', true) // TODO: Add logging param later + ->param('installCommand', '', new Text(8192, 0), 'Install Command.', true) + ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) + ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) + ->param('fallbackRedirect', '', new Text(8192, 0), 'Fallback Redirect URL for site in case a route is not found.', true) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true) //TODO: Update description of scopes + ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) + ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true) + ->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_SITE_SPECIFICATION_DEFAULT, fn (array $plan) => new FrameworkSpecification( + $plan, + Config::getParam('framework-specifications', []), + App::getEnv('_APP_SITES_CPUS', APP_SITE_CPUS_DEFAULT), + App::getEnv('_APP_SITES_MEMORY', APP_SITE_MEMORY_DEFAULT) + ), 'Framework specification for the site and builds.', true, ['plan']) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('dbForConsole') + ->inject('gitHub') + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $name, string $framework, bool $enabled, string $installCommand, string $buildCommand, string $outputDirectory, string $fallbackRedirect, array $scopes, 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 $dbForConsole, GitHub $github) + { + // TODO: If only branch changes, re-deploy + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $installation = $dbForConsole->getDocument('installations', $installationId); + + if (!empty($installationId) && $installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".'); + } + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + if (empty($framework)) { + $framework = $site->getAttribute('framework'); + } + + $enabled ??= $site->getAttribute('enabled', true); + + $repositoryId = $site->getAttribute('repositoryId', ''); + $repositoryInternalId = $site->getAttribute('repositoryInternalId', ''); + + $isConnected = !empty($site->getAttribute('providerRepositoryId', '')); + + // Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git + if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) { + $repositories = $dbForConsole->find('repositories', [ + Query::equal('projectInternalId', [$project->getInternalId()]), + Query::equal('resourceInternalId', [$site->getInternalId()]), + Query::equal('resourceType', ['site']), + Query::limit(100), + ]); + + foreach ($repositories as $repository) { + $dbForConsole->deleteDocument('repositories', $repository->getId()); + } + + $providerRepositoryId = ''; + $installationId = ''; + $providerBranch = ''; + $providerRootDirectory = ''; + $providerSilentMode = true; + $repositoryId = ''; + $repositoryInternalId = ''; + } + + // Git connect logic + if (!$isConnected && !empty($providerRepositoryId)) { + $teamId = $project->getAttribute('teamId', ''); + + $repository = $dbForConsole->createDocument('repositories', new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + Permission::update(Role::team(ID::custom($teamId), 'owner')), + Permission::update(Role::team(ID::custom($teamId), 'developer')), + Permission::delete(Role::team(ID::custom($teamId), 'owner')), + Permission::delete(Role::team(ID::custom($teamId), 'developer')), + ], + 'installationId' => $installation->getId(), + 'installationInternalId' => $installation->getInternalId(), + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'providerRepositoryId' => $providerRepositoryId, + 'resourceId' => $site->getId(), + 'resourceInternalId' => $site->getInternalId(), + 'resourceType' => 'site', + 'providerPullRequestIds' => [] + ])); + + $repositoryId = $repository->getId(); + $repositoryInternalId = $repository->getInternalId(); + } + + $live = true; + + if ( + $site->getAttribute('name') !== $name || + $site->getAttribute('buildCommand') !== $buildCommand || + $site->getAttribute('installCommand') !== $installCommand || + $site->getAttribute('outputDirectory') !== $outputDirectory || + $site->getAttribute('fallbackRedirect') !== $fallbackRedirect || + $site->getAttribute('providerRootDirectory') !== $providerRootDirectory || + $site->getAttribute('framework') !== $framework + ) { + $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')); + try { + $executor->deleteRuntime($project->getId(), $site->getAttribute('deploymentId')); + } catch (\Throwable $th) { + // Don't throw if the deployment doesn't exist + if ($th->getCode() !== 404) { + throw $th; + } + } + } + + $site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [ + 'name' => $name, + 'framework' => $framework, + 'enabled' => $enabled, + 'live' => $live, + 'buildCommand' => $buildCommand, + 'installCommand' => $installCommand, + 'outputDirectory' => $outputDirectory, + 'fallbackRedirect' => $fallbackRedirect, + 'scopes' => $scopes, + 'installationId' => $installation->getId(), + 'installationInternalId' => $installation->getInternalId(), + 'providerRepositoryId' => $providerRepositoryId, + 'repositoryId' => $repositoryId, + 'repositoryInternalId' => $repositoryInternalId, + 'providerBranch' => $providerBranch, + 'providerRootDirectory' => $providerRootDirectory, + 'providerSilentMode' => $providerSilentMode, + 'specification' => $specification, + 'search' => implode(' ', [$siteId, $name, $framework]), + ]))); + + // Redeploy logic + if (!$isConnected && !empty($providerRepositoryId)) { + $this->redeployVcsFunction($request, $site, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github); + } + + $queueForEvents->setParam('siteId', $site->getId()); + + $response->dynamic($site, Response::MODEL_SITE); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index d2d2641cb5..fcfd122c02 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -4,6 +4,10 @@ namespace Appwrite\Platform\Modules\Sites\Services; use Appwrite\Platform\Modules\Sites\Http\Deployments\CreateDeployment; use Appwrite\Platform\Modules\Sites\Http\Sites\CreateSite; +use Appwrite\Platform\Modules\Sites\Http\Sites\GetSite; +use Appwrite\Platform\Modules\Sites\Http\Sites\ListFrameworks; +use Appwrite\Platform\Modules\Sites\Http\Sites\ListSites; +use Appwrite\Platform\Modules\Sites\Http\Sites\UpdateSite; use Utopia\Platform\Service; class Http extends Service @@ -12,6 +16,10 @@ class Http extends Service { $this->type = Service::TYPE_HTTP; $this->addAction(CreateSite::getName(), new CreateSite()); + $this->addAction(GetSite::getName(), new GetSite()); + $this->addAction(ListSites::getName(), new ListSites()); + $this->addAction(UpdateSite::getName(), new UpdateSite()); + $this->addAction(ListFrameworks::getName(), new ListFrameworks()); $this->addAction(CreateDeployment::getName(), new CreateDeployment()); } } diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Sites.php b/src/Appwrite/Utopia/Database/Validator/Queries/Sites.php new file mode 100644 index 0000000000..35d4bdb5ef --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Sites.php @@ -0,0 +1,26 @@ +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('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)) @@ -439,6 +443,7 @@ class Response extends SwooleResponse ->setModel(new VcsContent()) ->setModel(new Branch()) ->setModel(new Runtime()) + ->setModel(new Framework()) ->setModel(new Deployment()) ->setModel(new Execution()) ->setModel(new Build()) diff --git a/src/Appwrite/Utopia/Response/Model/Framework.php b/src/Appwrite/Utopia/Response/Model/Framework.php new file mode 100644 index 0000000000..dcdf31ad1f --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Framework.php @@ -0,0 +1,66 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Framework ID.', + 'default' => '', + 'example' => 'sveltekit', + ]) + ->addRule('key', [ + 'type' => self::TYPE_STRING, + 'description' => 'Parent framework key.', + 'default' => '', + 'example' => 'sveltekit', + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'Framework Name.', + 'default' => '', + 'example' => 'SvelteKit' + ]) + ->addRule('logo', [ + 'type' => self::TYPE_STRING, + 'description' => 'Name of the logo image.', + 'default' => '', + 'example' => 'sveltekit.png', + ]) + ->addRule('supports', [ + 'type' => self::TYPE_STRING, + 'description' => 'List of supported architectures.', + 'default' => '', + 'example' => 'amd64', + 'array' => true, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Framework'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_FRAMEWORK; + } +} From 9793cf4ece402c98ec36341d79f3166b3d3841fa Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:07:28 +0200 Subject: [PATCH 2/2] Add all deployment endpoints for sites --- .../Http/Deployments/CancelDeployment.php | 124 ++++++++++++++++++ .../Http/Deployments/CreateDeployment.php | 3 +- .../Http/Deployments/DeleteDeployment.php | 96 ++++++++++++++ .../Http/Deployments/DownloadDeployment.php | 115 ++++++++++++++++ .../Sites/Http/Deployments/GetDeployment.php | 70 ++++++++++ .../Http/Deployments/ListDeployments.php | 116 ++++++++++++++++ .../Http/Deployments/RebuildDeployment.php | 99 ++++++++++++++ .../Http/Deployments/UpdateDeployment.php | 83 ++++++++++++ .../Platform/Modules/Sites/Services/Http.php | 14 ++ 9 files changed, 718 insertions(+), 2 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/CancelDeployment.php create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/DeleteDeployment.php create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/DownloadDeployment.php create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/GetDeployment.php create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/ListDeployments.php create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/RebuildDeployment.php create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/UpdateDeployment.php diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/CancelDeployment.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/CancelDeployment.php new file mode 100644 index 0000000000..ef6acefdc5 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/CancelDeployment.php @@ -0,0 +1,124 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build') + ->desc('Cancel deployment') + ->groups(['api', 'sites']) + ->label('scope', 'functions.write') //TODO: Update the scope to sites later + ->label('audits.event', 'deployment.update') + ->label('audits.resource', 'site/{request.siteId}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'updateDeploymentBuild') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_BUILD) + ->param('siteId', '', new UID(), 'Site ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) + { + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($deployment->isEmpty()) { + 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' + ])); + } + + $dbForProject->purgeCachedDocument('deployments', $deployment->getId()); + + try { + $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); + } catch (\Throwable $th) { + // Don't throw if the deployment doesn't exist + if ($th->getCode() !== 404) { + throw $th; + } + } + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response->dynamic($build, Response::MODEL_BUILD); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/CreateDeployment.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/CreateDeployment.php index b710305dc0..b35708fb50 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/CreateDeployment.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/CreateDeployment.php @@ -65,7 +65,6 @@ class CreateDeployment extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('project') ->inject('deviceForSites') ->inject('deviceForFunctions') // TODO: Remove this later once volume is added to executor ->inject('deviceForLocal') @@ -73,7 +72,7 @@ class CreateDeployment extends Action ->callback([$this, 'action']); } - public function action(string $siteId, ?string $installCommand, ?string $buildCommand, ?string $outputDirectory, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Event $queueForEvents, Document $project, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds) + public function action(string $siteId, ?string $installCommand, ?string $buildCommand, ?string $outputDirectory, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Event $queueForEvents, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds) { $activate = \strval($activate) === 'true' || \strval($activate) === '1'; diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/DeleteDeployment.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/DeleteDeployment.php new file mode 100644 index 0000000000..313870bb97 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/DeleteDeployment.php @@ -0,0 +1,96 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId') + ->desc('Delete deployment') + ->groups(['api', 'sites']) + ->label('scope', 'functions.write') //TODO: Update the scope to sites later + ->label('event', 'sites.[siteId].deployments.[deploymentId].delete') + ->label('audits.event', 'deployment.delete') + ->label('audits.resource', 'site/{request.siteId}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'deleteDeployment') + ->label('sdk.description', '/docs/references/sites/delete-deployment.md') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.model', Response::MODEL_NONE) + ->param('siteId', '', new UID(), 'Site ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDeletes') + ->inject('queueForEvents') + ->inject('deviceForSites') + ->inject('deviceForFunctions') //TODO: remove it later + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Device $deviceForSites, Device $deviceForFunctions) + { + $site = $dbForProject->getDocument('sites', $siteId); + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($deployment->getAttribute('resourceId') !== $site->getId()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if (!$dbForProject->deleteDocument('deployments', $deployment->getId())) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from DB'); + } + + if (!empty($deployment->getAttribute('path', ''))) { + if (!($deviceForFunctions->delete($deployment->getAttribute('path', '')))) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage'); + } + } + + if ($site->getAttribute('deployment') === $deployment->getId()) { // Reset site deployment + $site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [ + 'deployment' => '', + 'deploymentInternalId' => '', + ]))); + } + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($deployment); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/DownloadDeployment.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/DownloadDeployment.php new file mode 100644 index 0000000000..0d748514b1 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/DownloadDeployment.php @@ -0,0 +1,115 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/download') + ->desc('Download deployment') + ->groups(['api', 'sites']) + ->label('scope', 'functions.read') //TODO: Update the scope to sites later + ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'getDeploymentDownload') + ->label('sdk.description', '/docs/references/sites/get-deployment-download.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', '*/*') + ->label('sdk.methodType', 'location') + ->param('siteId', '', new UID(), 'Site ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('request') + ->inject('dbForProject') + ->inject('deviceForSites') + ->inject('deviceForFunctions') //TODO: Remove this later + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForSites, Device $deviceForFunctions) + { + $site = $dbForProject->getDocument('sites', $siteId); + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($deployment->getAttribute('resourceId') !== $site->getId()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + $path = $deployment->getAttribute('path', ''); + if (!$deviceForFunctions->exists($path)) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + $response + ->setContentType('application/gzip') + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache + ->addHeader('X-Peak', \memory_get_peak_usage()) + ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); + + $size = $deviceForFunctions->getFileSize($path); + $rangeHeader = $request->getHeader('range'); + + if (!empty($rangeHeader)) { + $start = $request->getRangeStart(); + $end = $request->getRangeEnd(); + $unit = $request->getRangeUnit(); + + if ($end === null) { + $end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1)); + } + + if ($unit !== 'bytes' || $start >= $end || $end >= $size) { + throw new Exception(Exception::STORAGE_INVALID_RANGE); + } + + $response + ->addHeader('Accept-Ranges', 'bytes') + ->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size) + ->addHeader('Content-Length', $end - $start + 1) + ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); + + $response->send($deviceForFunctions->read($path, $start, ($end - $start + 1))); + } + + if ($size > APP_STORAGE_READ_BUFFER) { + for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { + $response->chunk( + $deviceForFunctions->read( + $path, + ($i * MAX_OUTPUT_CHUNK_SIZE), + min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) + ), + (($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size + ); + } + } else { + $response->send($deviceForFunctions->read($path)); + } + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/GetDeployment.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/GetDeployment.php new file mode 100644 index 0000000000..b83aa75c6e --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/GetDeployment.php @@ -0,0 +1,70 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId') + ->desc('Get deployment') + ->groups(['api', 'sites']) + ->label('scope', 'functions.read') //TODO: Update the scope to sites later + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'getDeployment') + ->label('sdk.description', '/docs/references/sites/get-deployment.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DEPLOYMENT) + ->param('siteId', '', new UID(), 'Site ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject) + { + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($deployment->getAttribute('resourceId') !== $site->getId()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($deployment->isEmpty()) { + 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/ListDeployments.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/ListDeployments.php new file mode 100644 index 0000000000..2d2adb9572 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/ListDeployments.php @@ -0,0 +1,116 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/sites/:siteId/deployments') + ->desc('List deployments') + ->groups(['api', 'sites']) + ->label('scope', 'functions.read') //TODO: Update the scope to sites later + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'listDeployments') + ->label('sdk.description', '/docs/references/sites/list-deployments.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DEPLOYMENT_LIST) + ->param('siteId', '', new UID(), 'Site ID.') + ->param('queries', [], new Deployments(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $siteId, array $queries, string $search, Response $response, Database $dbForProject) + { + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + // Set resource queries + $queries[] = Query::equal('resourceInternalId', [$site->getInternalId()]); + $queries[] = Query::equal('resourceType', ['sites']); + + /** + * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries + */ + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); + }); + $cursor = reset($cursor); + if ($cursor) { + /** @var Query $cursor */ + + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $deploymentId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('deployments', $deploymentId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Deployment '{$deploymentId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + $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, + ]), Response::MODEL_DEPLOYMENT_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/RebuildDeployment.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/RebuildDeployment.php new file mode 100644 index 0000000000..ee09cf9fd0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/RebuildDeployment.php @@ -0,0 +1,99 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build') + ->desc('Rebuild deployment') + ->groups(['api', 'sites']) + ->label('scope', 'functions.write') //TODO: Update the scope to sites later + ->label('event', 'sites.[siteId].deployments.[deploymentId].update') + ->label('audits.event', 'deployment.update') + ->label('audits.resource', 'site/{request.siteId}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'createBuild') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.model', Response::MODEL_NONE) + ->param('siteId', '', new UID(), 'Site ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('deviceForSites') + ->inject('deviceForFunctions') //TODO: remove it later + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites, Device $deviceForFunctions) + { + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + $path = $deployment->getAttribute('path'); + if (empty($path) || !$deviceForFunctions->exists($path)) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + $deploymentId = ID::unique(); + + $destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); + $deviceForFunctions->transfer($path, $destination, $deviceForFunctions); + + $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', ''), + 'search' => implode(' ', [$deploymentId]), + ])); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($site) + ->setDeployment($deployment); + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/UpdateDeployment.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/UpdateDeployment.php new file mode 100644 index 0000000000..ea798a1513 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/UpdateDeployment.php @@ -0,0 +1,83 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId') + ->desc('Update deployment') + ->groups(['api', 'sites']) + ->label('scope', 'functions.write') //TODO: Update the scope to sites later + ->label('event', 'sites.[siteId].deployments.[deploymentId].update') + ->label('audits.event', 'deployment.update') + ->label('audits.resource', 'site/{request.siteId}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'updateDeployment') + ->label('sdk.description', '/docs/references/sites/update-site-deployment.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SITE) + ->param('siteId', '', new UID(), 'Site ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('dbForConsole') + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole) + { + $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); + } + + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($build->isEmpty()) { + throw new Exception(Exception::BUILD_NOT_FOUND); + } + + if ($build->getAttribute('status') !== 'ready') { + throw new Exception(Exception::BUILD_NOT_READY); + } + + $site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [ + 'deploymentInternalId' => $deployment->getInternalId(), + 'deploymentId' => $deployment->getId(), + ]))); + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response->dynamic($site, Response::MODEL_SITE); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index fcfd122c02..7a66646ab3 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -2,7 +2,14 @@ namespace Appwrite\Platform\Modules\Sites\Services; +use Appwrite\Platform\Modules\Sites\Http\Deployments\CancelDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\CreateDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\DeleteDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\DownloadDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\GetDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\ListDeployments; +use Appwrite\Platform\Modules\Sites\Http\Deployments\RebuildDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\UpdateDeployment; use Appwrite\Platform\Modules\Sites\Http\Sites\CreateSite; use Appwrite\Platform\Modules\Sites\Http\Sites\GetSite; use Appwrite\Platform\Modules\Sites\Http\Sites\ListFrameworks; @@ -21,5 +28,12 @@ class Http extends Service $this->addAction(UpdateSite::getName(), new UpdateSite()); $this->addAction(ListFrameworks::getName(), new ListFrameworks()); $this->addAction(CreateDeployment::getName(), new CreateDeployment()); + $this->addAction(GetDeployment::getName(), new GetDeployment()); + $this->addAction(ListDeployments::getName(), new ListDeployments()); + $this->addAction(UpdateDeployment::getName(), new UpdateDeployment()); + $this->addAction(DeleteDeployment::getName(), new DeleteDeployment()); + $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); + $this->addAction(RebuildDeployment::getName(), new RebuildDeployment()); + $this->addAction(CancelDeployment::getName(), new CancelDeployment()); } }