Merge pull request #8844 from appwrite/feat-site-endpointsa

Add new endpoints in HTTP structure
This commit is contained in:
Matej Bačo 2024-10-25 18:46:11 +02:00 committed by GitHub
commit 826a1a02a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 3593 additions and 236 deletions

1
.env
View file

@ -67,6 +67,7 @@ _APP_SMS_FROM=+123456789
_APP_SMS_PROJECTS_DENY_LIST=
_APP_STORAGE_LIMIT=30000000
_APP_STORAGE_PREVIEW_LIMIT=20000000
_APP_SITES_SIZE_LIMIT=30000000
_APP_FUNCTIONS_SIZE_LIMIT=30000000
_APP_FUNCTIONS_TIMEOUT=900
_APP_FUNCTIONS_BUILD_TIMEOUT=900

View file

@ -4400,6 +4400,17 @@ $projectCollections = array_merge([
'array' => false,
'filters' => ['encrypt']
],
[
'$id' => ID::custom('secret'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => false,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,

View file

@ -536,11 +536,21 @@ return [
],
/** Sites */
Exception::SITE_NOT_FOUND => [
'name' => Exception::SITE_NOT_FOUND,
'description' => 'Site with the requested ID could not be found.',
'code' => 404,
],
Exception::SITE_FRAMEWORK_UNSUPPORTED => [
'name' => Exception::SITE_FRAMEWORK_UNSUPPORTED,
'description' => 'The requested framework is either inactive or unsupported. Please check the value of the _APP_SITES_FRAMEWORKS environment variable.',
'code' => 404,
],
Exception::SITE_TEMPLATE_NOT_FOUND => [
'name' => Exception::SITE_TEMPLATE_NOT_FOUND,
'description' => 'Site Template with the requested ID could not be found.',
'code' => 404,
],
/** Builds */
Exception::BUILD_NOT_FOUND => [

View file

@ -4,4 +4,27 @@
* List of Appwrite Sites supported frameworks
*/
return ['sveltekit', 'nextjs'];
return [
"sveltekit" => [
'key' => 'sveltekit',
'name' => 'SvelteKit',
'logo' => 'sveltekit.png',
'defaultRuntime' => 'node-20.0',
'runtimes' => [
'node-16.0',
'node-18.0',
'node-20.0'
],
],
"nextjs" => [
'key' => 'nextjs',
'name' => 'Next.js',
'logo' => 'nextjs.png',
'defaultRuntime' => 'node-20.0',
'runtimes' => [
'node-16.0',
'node-18.0',
'node-20.0'
],
]
];

View file

@ -0,0 +1,61 @@
<?php
const TEMPLATE_FRAMEWORKS = [
'SVELTEKIT' => [
'name' => 'sveltekit'
],
'NEXTJS' => [
'name' => 'nextjs'
],
];
function getFramework($framework, $installCommand, $buildCommand, $outputDirectory, $fallbackRedirect, $providerRootDirectory)
{
return [
'name' => $framework['name'],
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'outputDirectory' => $outputDirectory,
'fallbackRedirect' => $fallbackRedirect,
'providerRootDirectory' => $providerRootDirectory
];
}
return [
[
'icon' => 'icon-lightning-bolt',
'id' => 'starter',
'name' => 'Starter site',
'tagline' =>
'A simple site to get started. Edit this site to explore endless possibilities with Appwrite Sites.',
'useCases' => ['starter'],
'frameworks' => [
...getFramework(TEMPLATE_FRAMEWORKS['SVELTEKIT'], 'npm install', 'npm run build', 'build', 'index.html', 'node/starter')
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/starter">file</a>.',
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates',
'providerOwner' => 'appwrite',
'providerVersion' => '0.2.*',
'variables' => [],
'scopes' => ['users.read']
],
[
'icon' => 'icon-lightning-bolt',
'id' => 'starter1',
'name' => 'Starter1 site',
'tagline' =>
'A simple site to get started. Edit this site to explore endless possibilities with Appwrite Sites.',
'useCases' => ['messaging'],
'frameworks' => [
...getFramework(TEMPLATE_FRAMEWORKS['SVELTEKIT'], 'npm install', 'npm run build', 'build', 'index.html', 'node/starter1')
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/starter">file</a>.',
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates',
'providerOwner' => 'appwrite',
'providerVersion' => '0.2.*',
'variables' => [],
'scopes' => ['users.read']
]
];

View file

@ -655,7 +655,7 @@ App::get('/v1/functions/:functionId/usage')
App::get('/v1/functions/usage')
->desc('Get functions usage')
->groups(['api', 'functions'])
->groups(['api', 'functions', 'usage'])
->label('scope', 'functions.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'functions')
@ -2337,10 +2337,11 @@ App::post('/v1/functions/:functionId/variables')
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', false, new Boolean(), 'Is secret? Secret variables can only be updated or deleted, they cannot be read.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole) {
->action(function (string $functionId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Database $dbForConsole) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -2361,6 +2362,7 @@ App::post('/v1/functions/:functionId/variables')
'resourceType' => 'function',
'key' => $key,
'value' => $value,
'secret' => $secret,
'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']),
]);
@ -2613,8 +2615,8 @@ App::get('/v1/functions/templates/:templateId')
->action(function (string $templateId, Response $response) {
$templates = Config::getParam('function-templates', []);
$template = array_shift(\array_filter($templates, function ($template) use ($templateId) {
return $template['id'] === $templateId;
$template = array_shift(array_filter($templates, function ($item) use ($templateId) {
return $item['id'] === $templateId;
}));
if (empty($template)) {

View file

@ -13,6 +13,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DateTimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@ -322,11 +323,12 @@ App::post('/v1/project/variables')
->label('sdk.response.model', Response::MODEL_VARIABLE)
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', false, new Boolean(), 'Is secret? Secret variables can only be updated or deleted, they cannot be read.', true)
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $key, string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
->action(function (string $key, string $value, bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
$variableId = ID::unique();
$variable = new Document([
@ -341,6 +343,7 @@ App::post('/v1/project/variables')
'resourceType' => 'project',
'key' => $key,
'value' => $value,
'secret' => $secret,
'search' => implode(' ', [$variableId, $key, 'project']),
]);

View file

@ -44,29 +44,30 @@ use function Swoole\Coroutine\batch;
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$errors = [];
foreach ($repositories as $resource) {
foreach ($repositories as $repository) {
try {
$resourceType = $resource->getAttribute('resourceType');
$resourceType = $repository->getAttribute('resourceType');
if ($resourceType !== "function") {
if ($resourceType !== "function" && $resourceType !== "site") {
continue;
}
$projectId = $resource->getAttribute('projectId');
$projectId = $repository->getAttribute('projectId');
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
$functionId = $resource->getAttribute('resourceId');
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
$functionInternalId = $function->getInternalId();
$resourceCollection = $resourceType === "function" ? 'functions' : 'sites';
$resourceId = $repository->getAttribute('resourceId');
$resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resourceInternalId = $resource->getInternalId();
$deploymentId = ID::unique();
$repositoryId = $resource->getId();
$repositoryInternalId = $resource->getInternalId();
$providerRepositoryId = $resource->getAttribute('providerRepositoryId');
$installationId = $resource->getAttribute('installationId');
$installationInternalId = $resource->getAttribute('installationInternalId');
$productionBranch = $function->getAttribute('providerBranch');
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getInternalId();
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
$installationId = $repository->getAttribute('installationId');
$installationInternalId = $repository->getAttribute('installationInternalId');
$productionBranch = $resource->getAttribute('providerBranch');
$activate = false;
if ($providerBranch == $productionBranch && $external === false) {
@ -90,7 +91,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$isAuthorized = !$external;
if (!$isAuthorized && !empty($providerPullRequestId)) {
if (\in_array($providerPullRequestId, $resource->getAttribute('providerPullRequestIds', []))) {
if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) {
$isAuthorized = true;
}
}
@ -103,7 +104,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = '';
if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false) === false) {
if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) {
$latestComment = Authorization::skip(fn () => $dbForConsole->findOne('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]),
@ -114,12 +115,12 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = $latestComment->getAttribute('providerCommentId', '');
$comment = new Comment();
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $function, $commentStatus, $deploymentId, $action);
$comment->addBuild($project, $resource, $commentStatus, $deploymentId, $action);
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} else {
$comment = new Comment();
$comment->addBuild($project, $function, $commentStatus, $deploymentId, $action);
$comment->addBuild($project, $resource, $commentStatus, $deploymentId, $action);
$latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment()));
if (!empty($latestCommentId)) {
@ -156,19 +157,19 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = $comment->getAttribute('providerCommentId', '');
$comment = new Comment();
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $function, $commentStatus, $deploymentId, $action);
$comment->addBuild($project, $resource, $commentStatus, $deploymentId, $action);
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
}
}
if (!$isAuthorized) {
$functionName = $function->getAttribute('name');
$resourceName = $resource->getAttribute('name');
$projectName = $project->getAttribute('name');
$name = "{$functionName} ({$projectName})";
$name = "{$resourceName} ({$projectName})";
$message = 'Authorization required for external contributor.';
$providerRepositoryId = $resource->getAttribute('providerRepositoryId');
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
@ -195,11 +196,15 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $functionId,
'resourceInternalId' => $functionInternalId,
'resourceType' => 'functions',
'entrypoint' => $function->getAttribute('entrypoint'),
'commands' => $function->getAttribute('commands'),
'resourceId' => $resourceId,
'resourceInternalId' => $resourceInternalId,
'resourceType' => $resourceCollection,
'entrypoint' => $resource->getAttribute('entrypoint', ''),
'commands' => $resource->getAttribute('commands', []),
'installCommand' => $resource->getAttribute('installCommand', ''),
'buildCommand' => $resource->getAttribute('buildCommand', ''),
'outputDirectory' => $resource->getAttribute('outputDirectory', ''),
'fallbackRedirect' => $resource->getAttribute('fallbackRedirect', ''),
'type' => 'vcs',
'installationId' => $installationId,
'installationInternalId' => $installationInternalId,
@ -217,17 +222,17 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
'providerCommitUrl' => $providerCommitUrl,
'providerCommentId' => \strval($latestCommentId),
'providerBranch' => $providerBranch,
'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]),
'search' => implode(' ', [$deploymentId, $resource->getAttribute('entrypoint', '')]),
'activate' => $activate,
]));
if (!empty($providerCommitHash) && $function->getAttribute('providerSilentMode', false) === false) {
$functionName = $function->getAttribute('name');
if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) {
$resourceName = $resource->getAttribute('name');
$projectName = $project->getAttribute('name');
$name = "{$functionName} ({$projectName})";
$name = "{$resourceName} ({$projectName})";
$message = 'Starting...';
$providerRepositoryId = $resource->getAttribute('providerRepositoryId');
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
@ -238,17 +243,17 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
$owner = $github->getOwnerName($providerInstallationId);
$providerTargetUrl = $request->getProtocol() . '://' . $request->getHostname() . "/console/project-$projectId/functions/function-$functionId";
$providerTargetUrl = $request->getProtocol() . '://' . $request->getHostname() . "/console/project-$projectId/$resourceCollection/$resourceType-$resourceId";
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $providerTargetUrl, $name);
}
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($function)
->setResource($resource)
->setDeployment($deployment)
->setProject($project); // set the project because it won't be set for git deployments
$queueForBuilds->trigger(); // must trigger here so that we create a build for each function
$queueForBuilds->trigger(); // must trigger here so that we create a build for each function/site
//TODO: Add event?
} catch (Throwable $e) {
@ -936,7 +941,7 @@ App::post('/v1/vcs/github/events')
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find functionId from functions table
//find resourceId from relevant resources table
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
@ -948,7 +953,7 @@ App::post('/v1/vcs/github/events')
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
// TODO: Use worker for this job instead (update function as well)
// TODO: Use worker for this job instead (update function/site as well)
$providerInstallationId = $parsedPayload["installationId"];
$installations = $dbForConsole->find('installations', [

View file

@ -54,19 +54,19 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$host = $request->getHostname() ?? '';
$route = Authorization::skip(
$rule = Authorization::skip(
fn () => $dbForConsole->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? null;
if ($route === null) {
if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) {
if ($rule === null) {
if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '') || $host === System::getEnv('_APP_DOMAIN_SITES', '')) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.');
}
if (\str_ends_with($host, System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))) {
if (\str_ends_with($host, System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) || \str_ends_with($host, System::getEnv('_APP_DOMAIN_SITES', ''))) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain is not connected to any Appwrite resource yet. Please configure custom domain or function domain to allow this request.');
}
@ -78,13 +78,12 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
// Act as API - no Proxy logic
$utopia->getRoute()?->label('error', '');
return false;
}
$projectId = $route->getAttribute('projectId');
$project = Authorization::skip(
fn () => $dbForConsole->getDocument('projects', $projectId)
);
$projectId = $rule->getAttribute('projectId');
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
if (array_key_exists('proxy', $project->getAttribute('services', []))) {
$status = $project->getAttribute('services', [])['proxy'];
if (!$status) {
@ -98,11 +97,13 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
return false;
}
$type = $route->getAttribute('resourceType');
$type = $rule->getAttribute('resourceType');
if ($type === 'function' || $type === 'sites') {
$isFunction = $type === 'function' ;
$isSite = $type === 'sites';
if ($type === 'function' || $type === 'site') {
$resourceCollection = match($type) {
'function' => 'functions',
'site' => 'sites'
};
$utopia->getRoute()?->label('sdk.namespace', 'functions');
$utopia->getRoute()?->label('sdk.method', 'createExecution');
@ -116,8 +117,8 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
}
}
$resourceId = $route->getAttribute('resourceId');
$projectId = $route->getAttribute('projectId');
$resourceId = $rule->getAttribute('resourceId');
$projectId = $rule->getAttribute('projectId');
$path = ($swooleRequest->server['request_uri'] ?? '/');
$query = ($swooleRequest->server['query_string'] ?? '');
@ -132,27 +133,47 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
$function = Authorization::skip(fn () => $dbForProject->getDocument($isSite ? 'sites' : 'functions', $resourceId));
$resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
if ($function->isEmpty() || !$function->getAttribute('enabled')) {
if ($resource->isEmpty() || !$resource->getAttribute('enabled')) {
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND);
}
$version = $function->getAttribute('version', 'v2');
$version = $resource->getAttribute('version', 'v2');
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('runtime-specifications')[$resource->getAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT)];
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
//todo: have runtime configs for sites
$runtime = match($type) {
'function' => (isset($runtimes[$resource->getAttribute('runtime', '')])) ? $runtimes[$resource->getAttribute('runtime', '')] : null,
'site' => [
'key' => 'static-for-now',
'name' => 'Static',
'logo' => 'node.png',
'startCommand' => null,
'version' => 'v1',
'base' => 'static:1.0',
'image' => 'static:1.0',
'supports' => [System::X86, System::ARM64, System::ARMV7, System::ARMV8]
],
default => null
};
if (\is_null($runtime)) {
throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
}
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
$deploymentId = match($type) {
'function' => $resource->getAttribute('deployment', ''),
'site' => $resource->getAttribute('deploymentId', '')
};
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $deploymentId));
if ($deployment->getAttribute('resourceId') !== $resource->getId()) {
throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
}
@ -170,28 +191,34 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
throw new AppwriteException(AppwriteException::BUILD_NOT_READY);
}
$permissions = $function->getAttribute('execute');
//todo: figure out for sites/functions
if ($type === 'function') {
$permissions = $resource->getAttribute('execute');
if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) {
throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"');
if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) {
throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"');
}
}
$jwtExpiry = $function->getAttribute('timeout', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$apiKey = $jwtObj->encode([
'projectId' => $project->getId(),
'scopes' => $function->getAttribute('scopes', [])
]);
$headers = \array_merge([], $requestHeaders);
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-id'] = '';
$headers['x-appwrite-user-jwt'] = '';
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
//todo: check if this would work for sites
if ($type === 'function') {
$jwtExpiry = $resource->getAttribute('timeout', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$apiKey = $jwtObj->encode([
'projectId' => $project->getId(),
'scopes' => $resource->getAttribute('scopes', [])
]);
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-jwt'] = '';
}
$ip = $headers['x-real-ip'] ?? '';
if (!empty($ip)) {
$record = $geodb->get($ip);
@ -217,8 +244,8 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$execution = new Document([
'$id' => $executionId,
'$permissions' => [],
'functionInternalId' => $function->getInternalId(),
'functionId' => $function->getId(),
'functionInternalId' => $resource->getInternalId(),
'functionId' => $resource->getId(),
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'trigger' => 'http', // http / schedule / event
@ -235,9 +262,9 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
]);
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('functionId', $resource->getId())
->setParam('executionId', $execution->getId())
->setContext('function', $function);
->setContext('function', $resource);
$durationStart = \microtime(true);
@ -254,12 +281,12 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
}
// Shared vars
foreach ($function->getAttribute('varsProject', []) as $var) {
foreach ($resource->getAttribute('varsProject', []) as $var) {
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
// Function vars
foreach ($function->getAttribute('vars', []) as $var) {
foreach ($resource->getAttribute('vars', []) as $var) {
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
@ -271,7 +298,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_ID' => $resourceId,
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
@ -298,26 +325,38 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
/** Execute function */
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
try {
$version = $function->getAttribute('version', 'v2');
$command = $runtime['startCommand'];
$command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"';
$version = match($type) {
'function' => $resource->getAttribute('version', 'v2'),
'site' => 'v4'
};
$entrypoint = match($type) {
'function' => $deployment->getAttribute('entrypoint', ''),
//todo: check if null works
'site' => 'placeholder' // entrypoint is required in api, but not needed with site
};
$runtimeEntrypoint = match ($version) {
'v2' => '',
default => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $runtime['startCommand'] . '"'
};
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deployment->getId(),
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $function->getAttribute('timeout', 0),
// todo: figure out timeouts for sites
timeout: $resource->getAttribute('timeout', 30),
image: $runtime['image'],
source: $build->getAttribute('path', ''),
entrypoint: $deployment->getAttribute('entrypoint', ''),
entrypoint: $entrypoint,
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $command,
runtimeEntrypoint: $runtimeEntrypoint,
cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
logging: $resource->getAttribute('logging', true),
requestTimeout: 30
);
@ -336,7 +375,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$execution->setAttribute('logs', $executionResponse['logs']);
$execution->setAttribute('errors', $executionResponse['errors']);
$execution->setAttribute('duration', $executionResponse['duration']);
} catch (\Throwable $th) {
$durationEnd = \microtime(true);
@ -361,21 +399,22 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
if ($isFunction) {
//todo: add metrics for sites
if ($type === 'function') {
$queueForUsage
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)));
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)));
}
$queueForUsage
->setProject($project)
->trigger();
if ($isFunction) {
if ($type === 'function') {
$queueForFunctions
->setType(Func::TYPE_ASYNC_WRITE)
->setExecution($execution)

View file

@ -133,6 +133,15 @@ $databaseListener = function (string $event, Document $document, Document $proje
$queueForUsage
->addMetric(METRIC_FUNCTIONS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'sites':
$queueForUsage
->addMetric(METRIC_SITES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForUsage
->addReduce($document);

View file

@ -132,6 +132,7 @@ const APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH = 1_073_741_824; // 2^32 bits / 4
const APP_DATABASE_TIMEOUT_MILLISECONDS = 15_000;
const APP_DATABASE_QUERY_MAX_VALUES = 500;
const APP_STORAGE_UPLOADS = '/storage/uploads';
const APP_STORAGE_SITES = '/storage/sites';
const APP_STORAGE_FUNCTIONS = '/storage/functions';
const APP_STORAGE_BUILDS = '/storage/builds';
const APP_STORAGE_CACHE = '/storage/cache';
@ -258,6 +259,7 @@ const METRIC_FILES = 'files';
const METRIC_FILES_STORAGE = 'files.storage';
const METRIC_BUCKET_ID_FILES = '{bucketInternalId}.files';
const METRIC_BUCKET_ID_FILES_STORAGE = '{bucketInternalId}.files.storage';
const METRIC_SITES = 'sites';
const METRIC_FUNCTIONS = 'functions';
const METRIC_DEPLOYMENTS = 'deployments';
const METRIC_DEPLOYMENTS_STORAGE = 'deployments.storage';
@ -276,15 +278,29 @@ const METRIC_FUNCTION_ID_BUILDS_STORAGE = '{functionInternalId}.builds.storage';
const METRIC_FUNCTION_ID_BUILDS_COMPUTE = '{functionInternalId}.builds.compute';
const METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS = '{functionInternalId}.builds.compute.success';
const METRIC_FUNCTION_ID_BUILDS_COMPUTE_FAILED = '{functionInternalId}.builds.compute.failed';
const METRIC_FUNCTION_ID_BUILDS_MB_SECONDS = '{functionInternalId}.builds.mbSeconds';
const METRIC_SITES_ID_BUILDS = 'sites.{siteInternalId}.builds';
const METRIC_SITES_ID_BUILDS_SUCCESS = 'sites.{siteInternalId}.builds.success';
const METRIC_SITES_ID_BUILDS_FAILED = 'sites.{siteInternalId}.builds.failed';
const METRIC_SITES_ID_BUILDS_STORAGE = 'sites.{siteInternalId}.builds.storage';
const METRIC_SITES_ID_BUILDS_COMPUTE = 'sites.{siteInternalId}.builds.compute';
const METRIC_SITES_ID_BUILDS_COMPUTE_SUCCESS = 'sites.{siteInternalId}.builds.compute.success';
const METRIC_SITES_ID_BUILDS_COMPUTE_FAILED = 'sites.{siteInternalId}.builds.compute.failed';
const METRIC_SITES_ID_BUILDS_MB_SECONDS = 'sites.{siteInternalId}.builds.mbSeconds';
const METRIC_FUNCTION_ID_DEPLOYMENTS = '{resourceType}.{resourceInternalId}.deployments';
const METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE = '{resourceType}.{resourceInternalId}.deployments.storage';
const METRIC_FUNCTION_ID_BUILDS_MB_SECONDS = '{functionInternalId}.builds.mbSeconds';
const METRIC_EXECUTIONS = 'executions';
const METRIC_EXECUTIONS_COMPUTE = 'executions.compute';
const METRIC_EXECUTIONS_MB_SECONDS = 'executions.mbSeconds';
const METRIC_FUNCTION_ID_EXECUTIONS = '{functionInternalId}.executions';
const METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE = '{functionInternalId}.executions.compute';
const METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS = '{functionInternalId}.executions.mbSeconds';
const METRIC_SITE_ID_DEPLOYMENTS = '{resourceType}.{resourceInternalId}.deployments';
const METRIC_SITE_ID_DEPLOYMENTS_STORAGE = '{resourceType}.{resourceInternalId}.deployments.storage';
const METRIC_SITE_ID_BUILDS = '{siteInternalId}.builds';
const METRIC_SITE_ID_BUILDS_STORAGE = '{siteInternalId}.builds.storage';
const METRIC_SITE_ID_BUILDS_COMPUTE = '{siteInternalId}.builds.compute';
const METRIC_SITE_ID_BUILDS_MB_SECONDS = '{siteInternalId}.builds.mbSeconds';
const METRIC_NETWORK_REQUESTS = 'network.requests';
const METRIC_NETWORK_INBOUND = 'network.inbound';
const METRIC_NETWORK_OUTBOUND = 'network.outbound';
@ -335,6 +351,7 @@ 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('function-templates', __DIR__ . '/config/function-templates.php');
Config::load('site-templates', __DIR__ . '/config/site-templates.php');
/**
* New DB Filters
@ -557,7 +574,7 @@ Database::addFilter(
return $database
->find('variables', [
Query::equal('resourceInternalId', [$document->getInternalId()]),
Query::equal('resourceType', ['function']),
Query::equal('resourceType', ['function', 'site']),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
@ -1523,6 +1540,10 @@ App::setResource('deviceForFiles', function ($project) {
return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId());
}, ['project']);
App::setResource('deviceForSites', function ($project) {
return getDevice(APP_STORAGE_SITES . '/app-' . $project->getId());
}, ['project']);
App::setResource('deviceForFunctions', function ($project) {
return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId());
}, ['project']);

View file

@ -256,6 +256,10 @@ Server::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
Server::setResource('deviceForSites', function (Document $project) {
return getDevice(APP_STORAGE_SITES . '/app-' . $project->getId());
}, ['project']);
Server::setResource('deviceForFunctions', function (Document $project) {
return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId());
}, ['project']);

View file

@ -76,6 +76,7 @@ services:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
- appwrite-functions:/storage/functions:rw
- appwrite-builds:/storage/builds:rw
- ./phpunit.xml:/usr/src/code/phpunit.xml
- ./tests:/usr/src/code/tests
- ./app:/usr/src/code/app
@ -161,6 +162,11 @@ services:
- _APP_FUNCTIONS_CPUS
- _APP_FUNCTIONS_MEMORY
- _APP_FUNCTIONS_RUNTIMES
- _APP_SITES_FRAMEWORKS
- _APP_SITES_CPUS
- _APP_SITES_MEMORY
- _APP_SITES_SIZE_LIMIT
- _APP_DOMAIN_SITES
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_LOGGING_CONFIG

View file

@ -152,7 +152,9 @@ class Exception extends \Exception
public const GENERAL_PROVIDER_FAILURE = 'general_provider_failure';
/** Sites */
public const SITE_NOT_FOUND = 'site_not_found';
public const SITE_FRAMEWORK_UNSUPPORTED = 'site_framework_unsupported';
public const SITE_TEMPLATE_NOT_FOUND = 'site_template_not_found';
/** Functions */
public const FUNCTION_NOT_FOUND = 'function_not_found';

View file

@ -119,14 +119,15 @@ class Builds extends Action
*/
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, Log $log): void
{
// todo: refactor
$isFunction = $resource->getCollection() === 'functions';
$isSite = $resource->getCollection() === 'sites';
$foreignKey = $isFunction ? 'functionId' : 'siteId';
$resourceKey = match($resource->getCollection()) {
'functions' => 'functionId',
'sites' => 'siteId',
default => throw new \Exception('Invalid resource type')
};
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
$log->addTag($foreignKey, $resource->getId());
$log->addTag($resourceKey, $resource->getId());
$resource = $dbForProject->getDocument($resource->getCollection(), $resource->getId());
if ($resource->isEmpty()) {
@ -140,55 +141,18 @@ class Builds extends Action
throw new \Exception('Deployment not found', 404);
}
if ($isFunction && empty($deployment->getAttribute('entrypoint', ''))) {
// todo: figure out a better way, entrypoint is not required for sites
if ($resource->getCollection() === 'functions' && empty($deployment->getAttribute('entrypoint', ''))) {
throw new \Exception('Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".', 500);
}
$version = $resource->getAttribute('version', 'v2');
if ($isSite) {
$version = 'v4';
}
$version = $this->getVersion($resource);
$runtime = $this->getRuntime($resource, $version);
$spec = Config::getParam('runtime-specifications')[$resource->getAttribute('specifications', APP_FUNCTION_SPECIFICATION_DEFAULT)];
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
// todo: fix for sites using frameworks
$key = $resource->getAttribute('runtime');
$runtime = $runtimes[$key] ?? null;
if ($isSite) {
// $key = "{$this->key}-{$version->version}";
// $list[$key] = array_merge(
// [
// 'key' => $this->key,
// 'name' => $this->name,
// 'logo' => "{$this->key}.png",
// 'startCommand' => $this->startCommand,
// ],
// [
// 'version' => $this->version,
// 'base' => $this->base,
// 'image' => $this->image,
// 'supports' => $this->supports,
// ]
// );
$runtime = [
'key' => 'static-for-now',
'name' => 'Static',
'logo' => 'node.png',
'startCommand' => null,
'version' => 'v1',
'base' => 'rtsp/lighttpd',
'image' => 'rtsp/lighttpd',
'supports' => [System::X86, System::ARM64, System::ARMV7, System::ARMV8]
];
}
if (\is_null($runtime)) {
throw new \Exception('Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
}
// Realtime preparation
$allEvents = Event::generateEvents("{$resource->getCollection()}.[{$foreignKey}].deployments.[deploymentId].update", [
$foreignKey => $resource->getId(),
$allEvents = Event::generateEvents("{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update", [
$resourceKey => $resource->getId(),
'deploymentId' => $deployment->getId()
]);
@ -469,8 +433,8 @@ class Builds extends Action
->setQueue(Event::WEBHOOK_QUEUE_NAME)
->setClass(Event::WEBHOOK_CLASS_NAME)
->setProject($project)
->setEvent("{$resource->getCollection()}.[{$foreignKey}].deployments.[deploymentId].update")
->setParam($foreignKey, $resource->getId())
->setEvent("{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update")
->setParam($resourceKey, $resource->getId())
->setParam('deploymentId', $deployment->getId())
->setPayload($deployment->getArrayCopy(array_keys($deploymentModel->getRules())));
@ -524,36 +488,6 @@ class Builds extends Action
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
//todo: ugly, but works
if ($isFunction) {
$vars = [
...$vars,
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_API_KEY' => API_KEY_DYNAMIC . '_' . $apiKey,
'APPWRITE_FUNCTION_ID' => $resource->getId(),
'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_FUNCTION_CPUS' => $cpus,
'APPWRITE_FUNCTION_MEMORY' => $memory
];
}
if ($isSite) {
$vars = [
...$vars,
'APPWRITE_SITE_ID' => $resource->getId(),
'APPWRITE_SITE_NAME' => $resource->getAttribute('name'),
'APPWRITE_SITE_DEPLOYMENT' => $deployment->getId(),
'APPWRITE_SITE_PROJECT_ID' => $project->getId(),
'APPWRITE_SITE_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_SITE_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_SITE_CPUS' => $cpus,
'APPWRITE_SITE_MEMORY' => $memory
];
}
// Appwrite vars
$vars = \array_merge($vars, [
'APPWRITE_VERSION' => APP_VERSION_STABLE,
@ -573,13 +507,42 @@ class Builds extends Action
'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''),
]);
$command = $deployment->getAttribute('commands', '');
//todo: for sites use isntall and build command
if ($isSite) {
$command = 'npm ci && npm run build';
switch ($resource->getCollection()) {
case 'functions':
$vars = [
...$vars,
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_API_KEY' => API_KEY_DYNAMIC . '_' . $apiKey,
'APPWRITE_FUNCTION_ID' => $resource->getId(),
'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_FUNCTION_CPUS' => $cpus,
'APPWRITE_FUNCTION_MEMORY' => $memory
];
break;
case 'sites':
$vars = [
...$vars,
'APPWRITE_SITE_ID' => $resource->getId(),
'APPWRITE_SITE_NAME' => $resource->getAttribute('name'),
'APPWRITE_SITE_DEPLOYMENT' => $deployment->getId(),
'APPWRITE_SITE_PROJECT_ID' => $project->getId(),
'APPWRITE_SITE_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_SITE_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_SITE_CPUS' => $cpus,
'APPWRITE_SITE_MEMORY' => $memory
];
break;
}
$command = $this->getCommand(
resource: $resource,
deployment: $deployment
);
$response = null;
$err = null;
@ -591,9 +554,8 @@ class Builds extends Action
$isCanceled = false;
Co::join([
Co\go(function () use ($executor, &$response, $project, $deployment, $source, $resource, $runtime, $vars, $command, $cpus, $memory, &$err) {
Co\go(function () use ($executor, &$response, $project, $deployment, $source, $resource, $runtime, $vars, $command, $cpus, $memory, &$err, $version) {
try {
$version = $resource->getAttribute('version', 'v2');
$command = $version === 'v2' ? 'tar -zxf /tmp/code.tar.gz -C /usr/code && cd /usr/local/src/ && ./build.sh' : 'tar -zxf /tmp/code.tar.gz -C /mnt/code && helpers/build.sh "' . \trim(\escapeshellarg($command), "\'") . '"';
$response = $executor->createRuntime(
@ -605,7 +567,7 @@ class Builds extends Action
cpus: $cpus,
memory: $memory,
remove: true,
entrypoint: $deployment->getAttribute('entrypoint'),
entrypoint: $deployment->getAttribute('entrypoint', 'package.json'), // TODO: change this later so that sites don't need to have an entrypoint
destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}",
variables: $vars,
command: $command
@ -707,9 +669,17 @@ class Builds extends Action
/** Set auto deploy */
if ($deployment->getAttribute('activate') === true) {
$resource->setAttribute('deploymentInternalId', $deployment->getInternalId());
$resource->setAttribute('deployment', $deployment->getId());
$resource->setAttribute('live', true);
$resource = $dbForProject->updateDocument('functions', $resource->getId(), $resource);
switch ($resource->getCollection()) {
case 'functions':
$resource->setAttribute('deployment', $deployment->getId());
$resource = $dbForProject->updateDocument('functions', $resource->getId(), $resource);
break;
case 'sites':
$resource->setAttribute('deploymentId', $deployment->getId());
$resource = $dbForProject->updateDocument('sites', $resource->getId(), $resource);
break;
}
}
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
@ -720,7 +690,7 @@ class Builds extends Action
/** Update function schedule */
// Inform scheduler if function is still active
if ($isFunction) {
if ($resource->getCollection() === 'functions') {
$schedule = $dbForConsole->getDocument('schedules', $resource->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
@ -763,41 +733,111 @@ class Builds extends Action
channels: $target['channels'],
roles: $target['roles']
);
/** Trigger usage queue */
if ($build->getAttribute('status') === 'ready') {
if ($isFunction) {
$queueForUsage
->addMetric(METRIC_BUILDS_SUCCESS, 1) // per project
->addMetric(METRIC_BUILDS_COMPUTE_SUCCESS, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_SUCCESS), 1) // per function
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS), (int)$build->getAttribute('duration', 0) * 1000);
}
} elseif ($build->getAttribute('status') === 'failed') {
if ($isFunction) {
$queueForUsage
->addMetric(METRIC_BUILDS_FAILED, 1) // per project
->addMetric(METRIC_BUILDS_COMPUTE_FAILED, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_FAILED), 1) // per function
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_FAILED), (int)$build->getAttribute('duration', 0) * 1000);
}
}
if ($isFunction) {
$queueForUsage
->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_FUNCTION_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS), 1) // per function
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), $build->getAttribute('size', 0))
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->setProject($project)
->trigger();
}
$this->sendUsage(
resource:$resource,
build: $build,
project: $project,
queue: $queueForUsage
);
}
}
protected function sendUsage(Document $resource, Document $build, Document $project, Usage $queue): void
{
$key = match($resource->getCollection()) {
'functions' => 'functionInternalId',
'sites' => 'siteInternalId',
default => throw new \Exception('Invalid resource type')
};
$metrics = match($resource->getCollection()) {
'functions' => [
'builds' => METRIC_FUNCTION_ID_BUILDS,
'buildsSuccess' => METRIC_FUNCTION_ID_BUILDS_SUCCESS,
'buildsFailed' => METRIC_FUNCTION_ID_BUILDS_FAILED,
'buildsComputeSuccess' => METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS,
'buildsComputeFailed' => METRIC_FUNCTION_ID_BUILDS_COMPUTE_FAILED,
'buildsStorage' => METRIC_FUNCTION_ID_BUILDS_STORAGE,
'buildsCompute' => METRIC_FUNCTION_ID_BUILDS_COMPUTE,
'buildsMbSeconds' => METRIC_FUNCTION_ID_BUILDS_MB_SECONDS
],
'sites' => [
'builds' => METRIC_SITES_ID_BUILDS,
'buildsSuccess' => METRIC_SITES_ID_BUILDS_SUCCESS,
'buildsFailed' => METRIC_SITES_ID_BUILDS_FAILED,
'buildsComputeSuccess' => METRIC_SITES_ID_BUILDS_COMPUTE_SUCCESS,
'buildsComputeFailed' => METRIC_SITES_ID_BUILDS_COMPUTE_FAILED,
'buildsStorage' => METRIC_SITES_ID_BUILDS_STORAGE,
'buildsCompute' => METRIC_SITES_ID_BUILDS_COMPUTE,
'buildsMbSeconds' => METRIC_SITES_ID_BUILDS_MB_SECONDS
]
};
switch ($build->getAttribute('status')) {
case 'ready':
$queue
->addMetric(METRIC_BUILDS_SUCCESS, 1) // per project
->addMetric(METRIC_BUILDS_COMPUTE_SUCCESS, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsSuccess']), 1) // per function
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsComputeSuccess']), (int)$build->getAttribute('duration', 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(str_replace($key, $resource->getInternalId(), $metrics['buildsFailed']), 1) // per function
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsComputeFailed']), (int)$build->getAttribute('duration', 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_FUNCTION_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['builds']), 1) // per function
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsStorage']), $build->getAttribute('size', 0))
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsCompute']), (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsMbSeconds']), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->setProject($project)
->trigger();
}
protected function getRuntime(Document $resource, string $version): array
{
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$key = $resource->getAttribute('runtime');
$runtime = match ($resource->getCollection()) {
'functions' => $runtimes[$key] ?? null,
'sites' => $runtimes['node-18.0'] ?? null, //todo: fix hardcode
default => null
};
if (\is_null($runtime)) {
throw new \Exception('Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
}
return $runtime;
}
protected function getVersion(Document $resource): string
{
return match ($resource->getCollection()) {
'functions' => $resource->getAttribute('version', 'v2'),
'sites' => 'v4',
};
}
protected function getCommand(Document $resource, Document $deployment): string
{
return match($resource->getCollection()) {
'functions' => $deployment->getAttribute('commands', ''),
'sites' => implode(' && ', array_filter([
$deployment->getAttribute('installCommand'),
$deployment->getAttribute('buildCommand')
]))
};
}
/**
* @param string $status
* @param GitHub $github

View file

@ -0,0 +1,124 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Executor\Executor;
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;
class CancelDeployment extends Action
{
use HTTP;
public static function getName()
{
return 'cancelDeployment';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,265 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
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\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Storage\Validator\File;
use Utopia\Storage\Validator\FileExt;
use Utopia\Storage\Validator\FileSize;
use Utopia\Storage\Validator\Upload;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class CreateDeployment extends Action
{
use HTTP;
public static function getName()
{
return 'createDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/sites/:siteId/deployments')
->desc('Create deployment')
->groups(['api', 'sites'])
->label('scope', 'functions.write') //TODO: Update the scope to sites later
->label('event', 'sites.[siteId].deployments.[deploymentId].create')
->label('audits.event', 'deployment.create')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'sites')
->label('sdk.method', 'createDeployment')
->label('sdk.methodType', 'upload')
->label('sdk.description', '/docs/references/sites/create-deployment.md') //TODO: Create new docs
->label('sdk.packaging', true)
->label('sdk.request.type', 'multipart/form-data')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DEPLOYMENT)
->param('siteId', '', new UID(), 'Site ID.')
->param('installCommand', null, new Text(8192, 0), 'Install Commands.', true)
->param('buildCommand', null, new Text(8192, 0), 'Build Commands.', true)
->param('outputDirectory', null, new Text(8192, 0), 'Output Directory.', true)
->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', skipValidation: true)
->param('activate', false, new Boolean(true), 'Automatically activate the deployment when it is finished building.')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('deviceForSites')
->inject('deviceForFunctions') // TODO: Remove this later once volume is added to executor
->inject('deviceForLocal')
->inject('queueForBuilds')
->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, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds)
{
$activate = \strval($activate) === 'true' || \strval($activate) === '1';
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
if ($installCommand === null) {
$installCommand = $site->getAttribute('installCommand', '');
}
if ($buildCommand === null) {
$buildCommand = $site->getAttribute('buildCommand', '');
}
if ($outputDirectory === null) {
$outputDirectory = $site->getAttribute('outputDirectory', '');
}
$file = $request->getFiles('code');
// GraphQL multipart spec adds files with index keys
if (empty($file)) {
$file = $request->getFiles(0);
}
if (empty($file)) {
throw new Exception(Exception::STORAGE_FILE_EMPTY, 'No file sent');
}
$fileExt = new FileExt([FileExt::TYPE_GZIP]);
$fileSizeValidator = new FileSize(System::getEnv('_APP_SITES_SIZE_LIMIT', '30000000'));
$upload = new Upload();
// Make sure we handle a single file and multiple files the same way
$fileName = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name'];
$fileTmpName = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name'];
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
if (!$fileExt->isValid($file['name'])) { // Check if file type is allowed
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
}
$contentRange = $request->getHeader('content-range');
$deploymentId = ID::unique();
$chunk = 1;
$chunks = 1;
if (!empty($contentRange)) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$fileSize = $request->getContentRangeSize();
$deploymentId = $request->getHeader('x-appwrite-id', $deploymentId);
// TODO make `end >= $fileSize` in next breaking version
if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) {
throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE);
}
// TODO remove the condition that checks `$end === $fileSize` in next breaking version
if ($end === $fileSize - 1 || $end === $fileSize) {
//if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk
$chunks = $chunk = -1;
} else {
// Calculate total number of chunks based on the chunk size i.e ($rangeEnd - $rangeStart)
$chunks = (int) ceil($fileSize / ($end + 1 - $start));
$chunk = (int) ($start / ($end + 1 - $start)) + 1;
}
}
if (!$fileSizeValidator->isValid($fileSize)) { // Check if file size is exceeding allowed limit
throw new Exception(Exception::STORAGE_INVALID_FILE_SIZE);
}
if (!$upload->isValid($fileTmpName)) {
throw new Exception(Exception::STORAGE_INVALID_FILE);
}
// Save to storage
$fileSize ??= $deviceForLocal->getFileSize($fileTmpName);
$path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('chunksTotal', 1);
$metadata = $deployment->getAttribute('metadata', []);
if ($chunk === -1) {
$chunk = $chunks;
}
}
$chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
if (empty($chunksUploaded)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file');
}
$type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual';
if ($chunksUploaded === $chunks) {
if ($activate) {
// Remove deploy for all other deployments.
$activeDeployments = $dbForProject->find('deployments', [
Query::equal('activate', [true]),
Query::equal('resourceId', [$siteId]),
Query::equal('resourceType', ['sites'])
]);
foreach ($activeDeployments as $activeDeployment) {
$activeDeployment->setAttribute('activate', false);
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment);
}
}
$fileSize = $deviceForFunctions->getFileSize($path);
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $site->getInternalId(),
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildInternalId' => '',
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'outputDirectory' => $outputDirectory,
'path' => $path,
'size' => $fileSize,
'search' => implode(' ', [$deploymentId]),
'activate' => $activate,
'metadata' => $metadata,
'type' => $type
]));
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('size', $fileSize)->setAttribute('metadata', $metadata));
}
// Start the build
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($site)
->setDeployment($deployment);
} else {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $site->getInternalId(),
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildInternalId' => '',
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'outputDirectory' => $outputDirectory,
'path' => $path,
'size' => $fileSize,
'chunksTotal' => $chunks,
'chunksUploaded' => $chunksUploaded,
'search' => implode(' ', [$deploymentId]),
'activate' => $activate,
'metadata' => $metadata,
'type' => $type
]));
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('chunksUploaded', $chunksUploaded)->setAttribute('metadata', $metadata));
}
}
$metadata = null;
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
class DeleteDeployment extends Action
{
use HTTP;
public static function getName()
{
return 'deleteDeployment';
}
public function __construct()
{
$this
->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();
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Swoole\Request;
class DownloadBuild extends Action
{
use HTTP;
public static function getName()
{
return 'downloadBuild';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build/download')
->desc('Download build')
->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', 'getBuildDownload')
->label('sdk.description', '/docs/references/sites/get-build-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('deviceForBuilds')
->callback([$this, 'action']);
}
public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForBuilds)
{
$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);
}
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
$path = $build->getAttribute('path', '');
if (!$deviceForBuilds->exists($path)) {
throw new Exception(Exception::BUILD_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 = $deviceForBuilds->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($deviceForBuilds->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(
$deviceForBuilds->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($deviceForBuilds->read($path));
}
}
}

View file

@ -0,0 +1,115 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Swoole\Request;
class DownloadDeployment extends Action
{
use HTTP;
public static function getName()
{
return 'downloadDeployment';
}
public function __construct()
{
$this
->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));
}
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class GetDeployment extends Action
{
use HTTP;
public static function getName()
{
return 'getDeployment';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Database\Validator\Queries\Deployments;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class ListDeployments extends Action
{
use HTTP;
public static function getName()
{
return 'listDeployments';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
class RebuildDeployment extends Action
{
use HTTP;
public static function getName()
{
return 'rebuildDeployment';
}
public function __construct()
{
$this
->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();
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class UpdateDeployment extends Action
{
use HTTP;
public static function getName()
{
return 'updateDeployment';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -58,7 +58,7 @@ class CreateSite extends Base
->label('sdk.response.model', Response::MODEL_SITE)
->param('siteId', '', new CustomId(), 'Site ID. Choose a custom ID or generate a random ID with `ID.unique()`. 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.')
->param('name', '', new Text(128), 'Site name. Max length: 128 chars.')
->param('framework', '', new WhiteList(Config::getParam('frameworks'), true), 'Sites framework.')
->param('framework', '', new WhiteList(array_keys(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)
@ -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')

View file

@ -0,0 +1,69 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class DeleteSite extends Base
{
use HTTP;
public static function getName()
{
return 'deleteSite';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/sites/:siteId')
->desc('Delete site')
->groups(['api', 'sites'])
->label('scope', 'functions.write') // TODO: Update scope to sites.write
->label('event', 'sites.[siteId].delete')
->label('audits.event', 'site.delete')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'sites')
->label('sdk.method', 'delete')
->label('sdk.description', '/docs/references/sites/delete-site.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('siteId', '', new UID(), 'Site ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('queueForEvents')
->callback([$this, 'action']);
}
public function action(string $siteId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('sites', $site->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove site from DB');
}
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($site);
$queueForEvents->setParam('siteId', $site->getId());
$response->noContent();
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class GetSite extends Base
{
use HTTP;
public static function getName()
{
return 'getSite';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,129 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\WhiteList;
class GetSiteUsage extends Base
{
use HTTP;
public static function getName()
{
return 'getSiteUsage';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/usage')
->desc('Get site usage')
->groups(['api', 'sites', 'usage'])
->label('scope', 'functions.read') // TODO: Update scope to sites.read
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'sites')
->label('sdk.method', 'getSiteUsage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_SITE)
->param('siteId', '', new UID(), 'Site ID.')
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, string $range, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
str_replace(['{resourceType}', '{resourceInternalId}'], ['sites', $site->getInternalId()], METRIC_SITE_ID_DEPLOYMENTS),
str_replace(['{resourceType}', '{resourceInternalId}'], ['sites', $site->getInternalId()], METRIC_SITE_ID_DEPLOYMENTS_STORAGE),
str_replace('{siteInternalId}', $site->getInternalId(), METRIC_SITE_ID_BUILDS),
str_replace('{siteInternalId}', $site->getInternalId(), METRIC_SITE_ID_BUILDS_STORAGE),
str_replace('{siteInternalId}', $site->getInternalId(), METRIC_SITE_ID_BUILDS_COMPUTE),
str_replace('{siteInternalId}', $site->getInternalId(), METRIC_SITE_ID_BUILDS_MB_SECONDS)
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric]['data'] = [];
foreach ($results as $result) {
$stats[$metric]['data'][$result->getAttribute('time')] = [
'value' => $result->getAttribute('value'),
];
}
}
});
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
};
foreach ($metrics as $metric) {
$usage[$metric]['total'] = $stats[$metric]['total'];
$usage[$metric]['data'] = [];
$leap = time() - ($days['limit'] * $days['factor']);
while ($leap < time()) {
$leap += $days['factor'];
$formatDate = date($format, $leap);
$usage[$metric]['data'][] = [
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
'date' => $formatDate,
];
}
}
$response->dynamic(new Document([
'range' => $range,
'deploymentsTotal' => $usage[$metrics[0]]['total'],
'deploymentsStorageTotal' => $usage[$metrics[1]]['total'],
'buildsTotal' => $usage[$metrics[2]]['total'],
'buildsStorageTotal' => $usage[$metrics[3]]['total'],
'buildsTimeTotal' => $usage[$metrics[4]]['total'],
'deployments' => $usage[$metrics[0]]['data'],
'deploymentsStorage' => $usage[$metrics[1]]['data'],
'builds' => $usage[$metrics[2]]['data'],
'buildsStorage' => $usage[$metrics[3]]['data'],
'buildsTime' => $usage[$metrics[4]]['data'],
'buildsMbSecondsTotal' => $usage[$metrics[7]]['total'],
'buildsMbSeconds' => $usage[$metrics[7]]['data']
// TODO: Add more metrics for requests, bandwidth, etc.
]), Response::MODEL_USAGE_SITE);
}
}

View file

@ -0,0 +1,121 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\WhiteList;
class GetSitesUsage extends Base
{
use HTTP;
public static function getName()
{
return 'getSitesUsage';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/usage')
->desc('Get sites usage')
->groups(['api', 'sites', 'usage'])
->label('scope', 'functions.read') // TODO: Update scope to sites.read
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'sites')
->label('sdk.method', 'getUsage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_SITES)
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $range, Response $response, Database $dbForProject)
{
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
METRIC_SITES,
METRIC_DEPLOYMENTS,
METRIC_DEPLOYMENTS_STORAGE,
METRIC_BUILDS,
METRIC_BUILDS_STORAGE,
METRIC_BUILDS_COMPUTE,
METRIC_BUILDS_MB_SECONDS,
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric]['data'] = [];
foreach ($results as $result) {
$stats[$metric]['data'][$result->getAttribute('time')] = [
'value' => $result->getAttribute('value'),
];
}
}
});
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
};
foreach ($metrics as $metric) {
$usage[$metric]['total'] = $stats[$metric]['total'];
$usage[$metric]['data'] = [];
$leap = time() - ($days['limit'] * $days['factor']);
while ($leap < time()) {
$leap += $days['factor'];
$formatDate = date($format, $leap);
$usage[$metric]['data'][] = [
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
'date' => $formatDate,
];
}
}
$response->dynamic(new Document([
'range' => $range,
'sitesTotal' => $usage[$metrics[0]]['total'],
'deploymentsTotal' => $usage[$metrics[1]]['total'],
'deploymentsStorageTotal' => $usage[$metrics[2]]['total'],
'buildsTotal' => $usage[$metrics[3]]['total'],
'buildsStorageTotal' => $usage[$metrics[4]]['total'],
'buildsTimeTotal' => $usage[$metrics[5]]['total'],
'sites' => $usage[$metrics[0]]['data'],
'deployments' => $usage[$metrics[1]]['data'],
'deploymentsStorage' => $usage[$metrics[2]]['data'],
'builds' => $usage[$metrics[3]]['data'],
'buildsStorage' => $usage[$metrics[4]]['data'],
'buildsTime' => $usage[$metrics[5]]['data'],
'buildsMbSecondsTotal' => $usage[$metrics[8]]['total'],
'buildsMbSeconds' => $usage[$metrics[8]]['data']
]), Response::MODEL_USAGE_SITES);
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class GetTemplate extends Base
{
use HTTP;
public static function getName()
{
return 'getTemplate';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/templates/:templateId')
->desc('Get site template')
->groups(['api'])
->label('scope', 'public')
->label('sdk.namespace', 'sites')
->label('sdk.method', 'getTemplate')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.description', '/docs/references/sites/get-template.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEMPLATE_SITE)
->param('templateId', '', new Text(128), 'Template ID.')
->inject('response')
->callback([$this, 'action']);
}
public function action(string $templateId, Response $response)
{
$templates = Config::getParam('site-templates', []);
$template = array_shift(\array_filter($templates, function ($item) use ($templateId) {
return $item['id'] === $templateId;
}));
if (empty($template)) {
throw new Exception(Exception::SITE_TEMPLATE_NOT_FOUND);
}
$response->dynamic(new Document($template), Response::MODEL_TEMPLATE_SITE);
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class ListFrameworks extends Base
{
use HTTP;
public static function getName()
{
return 'listFrameworks';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Database\Validator\Queries\Sites;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class ListSites extends Base
{
use HTTP;
public static function getName()
{
return 'listSites';
}
public function __construct()
{
$this
->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);
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class ListTemplates extends Base
{
use HTTP;
public static function getName()
{
return 'listTemplates';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/templates')
->desc('List templates')
->groups(['api'])
->label('scope', 'public')
->label('sdk.namespace', 'sites')
->label('sdk.method', 'listTemplates')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.description', '/docs/references/sites/list-templates.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEMPLATE_SITE_LIST)
->param('frameworks', [], new ArrayList(new WhiteList(array_keys(Config::getParam('frameworks')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of frameworks allowed for filtering site templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' frameworks are allowed.', true)
->param('useCases', [], new ArrayList(new WhiteList(['dev-tools', 'starter', 'databases', 'ai', 'messaging', 'utilities']), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering site templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true)
->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true)
->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', true)
->inject('response')
->callback([$this, 'action']);
}
public function action(array $frameworks, array $usecases, int $limit, int $offset, Response $response)
{
$templates = Config::getParam('site-templates', []);
if (!empty($frameworks)) {
$templates = \array_filter($templates, function ($template) use ($frameworks) {
return \count(\array_intersect($frameworks, \array_column($template['frameworks'], 'name'))) > 0;
});
}
if (!empty($usecases)) {
$templates = \array_filter($templates, function ($template) use ($usecases) {
return \count(\array_intersect($usecases, $template['useCases'])) > 0;
});
}
$responseTemplates = \array_slice($templates, $offset, $limit);
$response->dynamic(new Document([
'templates' => $responseTemplates,
'total' => \count($responseTemplates),
]), Response::MODEL_TEMPLATE_SITE_LIST);
}
}

View file

@ -0,0 +1,231 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Sites\Validator\FrameworkSpecification;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
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\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Request;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class UpdateSite extends Base
{
use HTTP;
public static function getName()
{
return 'updateSite';
}
public function __construct()
{
$this->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(array_keys(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);
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class CreateVariable extends Base
{
use HTTP;
public static function getName()
{
return 'createVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/sites/:siteId/variables')
->desc('Create variable')
->groups(['api', 'sites'])
->label('scope', 'functions.write') // TODO: Update scope to sites.write
->label('audits.event', 'variable.create')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'sites')
->label('sdk.method', 'createVariable')
->label('sdk.description', '/docs/references/sites/create-variable.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_VARIABLE)
->param('siteId', '', new UID(), 'Site unique ID.', false)
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', false, new Boolean(), 'Is secret? Secret variables can only be updated or deleted, they cannot be read.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->callback([$this, 'action']);
}
public function action(string $siteId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Database $dbForConsole)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$variableId = ID::unique();
$variable = new Document([
'$id' => $variableId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $site->getInternalId(),
'resourceId' => $site->getId(),
'resourceType' => 'site',
'key' => $key,
'value' => $value,
'secret' => $secret,
'search' => implode(' ', [$variableId, $site->getId(), $key, 'site']),
]);
try {
$variable = $dbForProject->createDocument('variables', $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class DeleteVariable extends Base
{
use HTTP;
public static function getName()
{
return 'deleteVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/sites/:siteId/variables/:variableId')
->desc('Delete variable')
->groups(['api', 'sites'])
->label('scope', 'functions.write') // TODO: Update scope to sites
->label('audits.event', 'variable.delete')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'sites')
->label('sdk.method', 'deleteVariable')
->label('sdk.description', '/docs/references/sites/delete-variable.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('siteId', '', new UID(), 'Site unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, string $variableId, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $site->getInternalId() || $variable->getAttribute('resourceType') !== 'site') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable === false || $variable->isEmpty()) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$dbForProject->deleteDocument('variables', $variable->getId());
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
$response->noContent();
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class GetVariable extends Base
{
use HTTP;
public static function getName()
{
return 'getVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/variables/:variableId')
->desc('Get variable')
->groups(['api', 'sites'])
->label('scope', 'functions.read') // TODO: Update scope to sites
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'sites')
->label('sdk.method', 'getVariable')
->label('sdk.description', '/docs/references/sites/get-variable.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_VARIABLE)
->param('siteId', '', new UID(), 'Site unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, string $variableId, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if (
$variable === false ||
$variable->isEmpty() ||
$variable->getAttribute('resourceInternalId') !== $site->getInternalId() ||
$variable->getAttribute('resourceType') !== 'site'
) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable === false || $variable->isEmpty()) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class ListVariables extends Base
{
use HTTP;
public static function getName()
{
return 'listVariables';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/variables')
->desc('List variables')
->groups(['api', 'sites'])
->label('scope', 'functions.read') // TODO: Update scope to sites
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'sites')
->label('sdk.method', 'listVariables')
->label('sdk.description', '/docs/references/sites/list-variables.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_VARIABLE_LIST)
->param('siteId', '', new UID(), 'Site unique ID.', false)
->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(new Document([
'variables' => $site->getAttribute('vars', []),
'total' => \count($site->getAttribute('vars', [])),
]), Response::MODEL_VARIABLE_LIST);
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class UpdateVariable extends Base
{
use HTTP;
public static function getName()
{
return 'updateVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/sites/:siteId/variables/:variableId')
->desc('Update variable')
->groups(['api', 'sites'])
->label('scope', 'functions.write') // TODO: Update scope to sites
->label('audits.event', 'variable.update')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'sites')
->label('sdk.method', 'updateVariable')
->label('sdk.description', '/docs/references/sites/update-variable.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_VARIABLE)
->param('siteId', '', new UID(), 'Site unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $siteId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $site->getInternalId() || $variable->getAttribute('resourceType') !== 'site') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable === false || $variable->isEmpty()) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$variable
->setAttribute('key', $key)
->setAttribute('value', $value ?? $variable->getAttribute('value'))
->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site']));
try {
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}

View file

@ -2,7 +2,30 @@
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\DownloadBuild;
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\DeleteSite;
use Appwrite\Platform\Modules\Sites\Http\Sites\GetSite;
use Appwrite\Platform\Modules\Sites\Http\Sites\GetSitesUsage;
use Appwrite\Platform\Modules\Sites\Http\Sites\GetSiteUsage;
use Appwrite\Platform\Modules\Sites\Http\Sites\GetTemplate;
use Appwrite\Platform\Modules\Sites\Http\Sites\ListFrameworks;
use Appwrite\Platform\Modules\Sites\Http\Sites\ListSites;
use Appwrite\Platform\Modules\Sites\Http\Sites\ListTemplates;
use Appwrite\Platform\Modules\Sites\Http\Sites\UpdateSite;
use Appwrite\Platform\Modules\Sites\Http\Variables\CreateVariable;
use Appwrite\Platform\Modules\Sites\Http\Variables\DeleteVariable;
use Appwrite\Platform\Modules\Sites\Http\Variables\GetVariable;
use Appwrite\Platform\Modules\Sites\Http\Variables\ListVariables;
use Appwrite\Platform\Modules\Sites\Http\Variables\UpdateVariable;
use Utopia\Platform\Service;
class Http extends Service
@ -10,6 +33,41 @@ class Http extends Service
public function __construct()
{
$this->type = Service::TYPE_HTTP;
// Sites
$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(DeleteSite::getName(), new DeleteSite());
// Frameworks
$this->addAction(ListFrameworks::getName(), new ListFrameworks());
// Deployments
$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(DownloadBuild::getName(), new DownloadBuild());
$this->addAction(RebuildDeployment::getName(), new RebuildDeployment());
$this->addAction(CancelDeployment::getName(), new CancelDeployment());
// Variables
$this->addAction(CreateVariable::getName(), new CreateVariable());
$this->addAction(GetVariable::getName(), new GetVariable());
$this->addAction(ListVariables::getName(), new ListVariables());
$this->addAction(UpdateVariable::getName(), new UpdateVariable());
$this->addAction(DeleteVariable::getName(), new DeleteVariable());
// Templates
$this->addAction(ListTemplates::getName(), new ListTemplates());
$this->addAction(GetTemplate::getName(), new GetTemplate());
// Usage
$this->addAction(GetSiteUsage::getName(), new GetSiteUsage());
$this->addAction(GetSitesUsage::getName(), new GetSitesUsage());
}
}

View file

@ -47,6 +47,7 @@ class Deletes extends Action
->inject('dbForConsole')
->inject('getProjectDB')
->inject('deviceForFiles')
->inject('deviceForSites')
->inject('deviceForFunctions')
->inject('deviceForBuilds')
->inject('deviceForCache')
@ -54,14 +55,14 @@ class Deletes extends Action
->inject('executionRetention')
->inject('auditRetention')
->inject('log')
->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log));
->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log));
}
/**
* @throws Exception
* @throws Throwable
*/
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void
{
$payload = $message->getPayload() ?? [];
@ -84,7 +85,7 @@ class Deletes extends Action
case DELETE_TYPE_DOCUMENT:
switch ($document->getCollection()) {
case DELETE_TYPE_PROJECTS:
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document);
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document);
break;
case DELETE_TYPE_FUNCTIONS:
$this->deleteFunction($dbForConsole, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project);
@ -451,11 +452,12 @@ class Deletes extends Action
foreach ($projects as $project) {
$deviceForFiles = getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId());
$deviceForSites = getDevice(APP_STORAGE_SITES . '/app-' . $project->getId());
$deviceForFunctions = getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId());
$deviceForBuilds = getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId());
$deviceForCache = getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId());
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $project);
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $project);
$dbForConsole->deleteDocument('projects', $project->getId());
}
}
@ -473,7 +475,7 @@ class Deletes extends Action
* @throws Authorization
* @throws DatabaseException
*/
private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, Document $document): void
private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, Document $document): void
{
$projectInternalId = $document->getInternalId();
$projectId = $document->getId();
@ -503,7 +505,7 @@ class Deletes extends Action
try {
$dbForProject->deleteCollection($collection->getId());
} catch (Throwable $e) {
Console::error('Error deleting '.$collection->getId().' '.$e->getMessage());
Console::error('Error deleting ' . $collection->getId() . ' ' . $e->getMessage());
/**
* Ignore junction tables;
@ -579,6 +581,7 @@ class Deletes extends Action
// Delete all storage directories
$deviceForFiles->delete($deviceForFiles->getRoot(), true);
$deviceForSites->delete($deviceForSites->getRoot(), true);
$deviceForFunctions->delete($deviceForFunctions->getRoot(), true);
$deviceForBuilds->delete($deviceForBuilds->getRoot(), true);
$deviceForCache->delete($deviceForCache->getRoot(), true);

View file

@ -198,6 +198,17 @@ abstract class Format
break;
}
break;
case 'sites':
switch ($method) {
case 'getUsage':
case 'getSiteUsage':
switch ($param) {
case 'range':
return 'SiteUsageRange';
}
break;
}
break;
case 'messaging':
switch ($method) {
case 'getUsage':
@ -386,6 +397,14 @@ abstract class Format
return ['Twenty Four Hours', 'Thirty Days', 'Ninety Days'];
}
break;
case 'sites':
switch ($method) {
case 'getUsage':
case 'getSiteUsage':
// Range Enum Keys
return ['Twenty Four Hours', 'Thirty Days', 'Ninety Days'];
}
break;
case 'users':
switch ($method) {
case 'getUsage':

View file

@ -0,0 +1,26 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Sites extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'enabled',
'framework',
'deploymentId',
'buildCommand',
'installCommand',
'outputDirectory',
'installationId'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('sites', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -44,6 +44,7 @@ use Appwrite\Utopia\Response\Model\Error;
use Appwrite\Utopia\Response\Model\ErrorDev;
use Appwrite\Utopia\Response\Model\Execution;
use Appwrite\Utopia\Response\Model\File;
use Appwrite\Utopia\Response\Model\Framework;
use Appwrite\Utopia\Response\Model\Func;
use Appwrite\Utopia\Response\Model\Headers;
use Appwrite\Utopia\Response\Model\HealthAntivirus;
@ -90,8 +91,10 @@ use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Target;
use Appwrite\Utopia\Response\Model\Team;
use Appwrite\Utopia\Response\Model\TemplateEmail;
use Appwrite\Utopia\Response\Model\TemplateFramework;
use Appwrite\Utopia\Response\Model\TemplateFunction;
use Appwrite\Utopia\Response\Model\TemplateRuntime;
use Appwrite\Utopia\Response\Model\TemplateSite;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\TemplateVariable;
use Appwrite\Utopia\Response\Model\Token;
@ -103,6 +106,8 @@ use Appwrite\Utopia\Response\Model\UsageDatabases;
use Appwrite\Utopia\Response\Model\UsageFunction;
use Appwrite\Utopia\Response\Model\UsageFunctions;
use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageSite;
use Appwrite\Utopia\Response\Model\UsageSites;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageUsers;
use Appwrite\Utopia\Response\Model\User;
@ -141,6 +146,8 @@ class Response extends SwooleResponse
public const MODEL_USAGE_STORAGE = 'usageStorage';
public const MODEL_USAGE_FUNCTIONS = 'usageFunctions';
public const MODEL_USAGE_FUNCTION = 'usageFunction';
public const MODEL_USAGE_SITES = 'usageSites';
public const MODEL_USAGE_SITE = 'usageSite';
public const MODEL_USAGE_PROJECT = 'usageProject';
// Database
@ -248,6 +255,11 @@ class Response extends SwooleResponse
// Sites
public const MODEL_SITE = 'site';
public const MODEL_SITE_LIST = 'siteList';
public const MODEL_FRAMEWORK = 'framework';
public const MODEL_FRAMEWORK_LIST = 'frameworkList';
public const MODEL_TEMPLATE_SITE = 'templateSite';
public const MODEL_TEMPLATE_SITE_LIST = 'templateSiteList';
public const MODEL_TEMPLATE_FRAMEWORK = 'templateFramework';
// Functions
public const MODEL_FUNCTION = 'function';
@ -357,11 +369,13 @@ class Response extends SwooleResponse
->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM))
->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP))
->setModel(new BaseList('Sites List', self::MODEL_SITE_LIST, 'sites', self::MODEL_SITE))
->setModel(new BaseList('Site Templates List', self::MODEL_TEMPLATE_SITE_LIST, 'templates', self::MODEL_TEMPLATE_SITE))
->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('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))
@ -429,6 +443,8 @@ class Response extends SwooleResponse
->setModel(new Team())
->setModel(new Membership())
->setModel(new Site())
->setModel(new TemplateSite())
->setModel(new TemplateFramework())
->setModel(new Func())
->setModel(new TemplateFunction())
->setModel(new TemplateRuntime())
@ -439,6 +455,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())
@ -470,6 +487,8 @@ class Response extends SwooleResponse
->setModel(new UsageBuckets())
->setModel(new UsageFunctions())
->setModel(new UsageFunction())
->setModel(new UsageSites())
->setModel(new UsageSite())
->setModel(new UsageProject())
->setModel(new Headers())
->setModel(new Specification())

View file

@ -0,0 +1,72 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Framework extends Model
{
public function __construct()
{
$this
->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('defaultRuntime', [
'type' => self::TYPE_STRING,
'description' => 'Default runtime version.',
'default' => '',
'example' => 'node-20.0',
])
->addRule('runtimes', [
'type' => self::TYPE_STRING,
'description' => 'List of supported runtime versions.',
'default' => '',
'example' => 'node-16.0',
'array' => true,
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Framework';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_FRAMEWORK;
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class TemplateFramework extends Model
{
public function __construct()
{
$this
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Framework Name.',
'default' => '',
'example' => 'sveltekit',
])
->addRule('installCommand', [
'type' => self::TYPE_STRING,
'description' => 'The install command used to install the dependencies.',
'default' => '',
'example' => 'npm install',
])
->addRule('buildCommand', [
'type' => self::TYPE_STRING,
'description' => 'The build command used to build the deployment.',
'default' => '',
'example' => 'npm run build',
])
->addRule('outputDirectory', [
'type' => self::TYPE_STRING,
'description' => 'The output directory to store the build output.',
'default' => '',
'example' => 'build',
])
->addRule('fallbackRedirect', [
'type' => self::TYPE_STRING,
'description' => 'The fallback redirect for the site when a route is not found.',
'default' => '',
'example' => 'index.html',
])
->addRule('providerRootDirectory', [
'type' => self::TYPE_STRING,
'description' => 'Path to site in VCS (Version Control System) repository',
'default' => '',
'example' => 'node/starter',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Template Framework';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TEMPLATE_FRAMEWORK;
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class TemplateSite extends Model
{
public function __construct()
{
$this
->addRule('icon', [
'type' => self::TYPE_STRING,
'description' => 'Site Template Icon.',
'default' => '',
'example' => 'icon-lightning-bolt',
])
->addRule('id', [
'type' => self::TYPE_STRING,
'description' => 'Site Template ID.',
'default' => '',
'example' => 'starter',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Site Template Name.',
'default' => '',
'example' => 'Starter site',
])
->addRule('tagline', [
'type' => self::TYPE_STRING,
'description' => 'Site Template Tagline.',
'default' => '',
'example' => 'A simple site to get started.',
])
->addRule('useCases', [
'type' => self::TYPE_STRING,
'description' => 'Site use cases.',
'default' => [],
'example' => 'Starter',
'array' => true,
])
->addRule('frameworks', [
'type' => Response::MODEL_TEMPLATE_FRAMEWORK,
'description' => 'List of frameworks that can be used with this template.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('instructions', [
'type' => self::TYPE_STRING,
'description' => 'Site Template Instructions.',
'default' => '',
'example' => 'For documentation and instructions check out <link>.',
])
->addRule('vcsProvider', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) Provider.',
'default' => '',
'example' => 'github',
])
->addRule('providerRepositoryId', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) Repository ID',
'default' => '',
'example' => 'templates',
])
->addRule('providerOwner', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) Owner.',
'default' => '',
'example' => 'appwrite',
])
->addRule('providerVersion', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) branch version (tag).',
'default' => '',
'example' => 'main',
])
->addRule('variables', [
'type' => Response::MODEL_TEMPLATE_VARIABLE,
'description' => 'Site variables.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('scopes', [
'type' => self::TYPE_STRING,
'description' => 'Site scopes.',
'default' => [],
'example' => 'users.read',
'array' => true,
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Template Site';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TEMPLATE_SITE;
}
}

View file

@ -28,6 +28,12 @@ class TemplateVariable extends Model
'default' => '',
'example' => '512',
])
->addRule('secret', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Variable secret flag. Secret variables can only be updated or deleted, but never read.',
'default' => false,
'example' => false,
])
->addRule('placeholder', [
'type' => self::TYPE_STRING,
'description' => 'Variable Placeholder.',

View file

@ -0,0 +1,119 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class UsageSite extends Model
{
public function __construct()
{
$this
->addRule('range', [
'type' => self::TYPE_STRING,
'description' => 'The time range of the usage stats.',
'default' => '',
'example' => '30d',
])
->addRule('deploymentsTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated number of site deployments.',
'default' => 0,
'example' => 0,
])
->addRule('deploymentsStorageTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated sum of site deployments storage.',
'default' => 0,
'example' => 0,
])
->addRule('buildsTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated number of site builds.',
'default' => 0,
'example' => 0,
])
->addRule('buildsStorageTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'total aggregated sum of site builds storage.',
'default' => 0,
'example' => 0,
])
->addRule('buildsTimeTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated sum of site builds compute time.',
'default' => 0,
'example' => 0,
])
->addRule('buildsMbSecondsTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated sum of site builds mbSeconds.',
'default' => 0,
'example' => 0,
])
->addRule('deployments', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of site deployments per period.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('deploymentsStorage', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of site deployments storage per period.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('builds', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of site builds per period.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('buildsStorage', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated sum of site builds storage per period.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('buildsTime', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated sum of site builds compute time per period.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('buildsMbSeconds', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of site builds mbSeconds per period.',
'default' => [],
'example' => [],
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'UsageSite';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_USAGE_SITE;
}
}

View file

@ -0,0 +1,132 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class UsageSites extends Model
{
public function __construct()
{
$this
->addRule('range', [
'type' => self::TYPE_STRING,
'description' => 'Time range of the usage stats.',
'default' => '',
'example' => '30d',
])
->addRule('sitesTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated number of sites.',
'default' => 0,
'example' => 0,
])
->addRule('deploymentsTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated number of sites deployments.',
'default' => 0,
'example' => 0,
])
->addRule('deploymentsStorageTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated sum of sites deployment storage.',
'default' => 0,
'example' => 0,
])
->addRule('buildsTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated number of sites build.',
'default' => 0,
'example' => 0,
])
->addRule('buildsStorageTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'total aggregated sum of sites build storage.',
'default' => 0,
'example' => 0,
])
->addRule('buildsTimeTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated sum of sites build compute time.',
'default' => 0,
'example' => 0,
])
->addRule('buildsMbSecondsTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated sum of sites build mbSeconds.',
'default' => 0,
'example' => 0,
])
->addRule('sites', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of sites per period.',
'default' => 0,
'example' => 0,
'array' => true
])
->addRule('deployments', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of sites deployment per period.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('deploymentsStorage', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of sites deployment storage per period.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('builds', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of sites build per period.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('buildsStorage', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated sum of sites build storage per period.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('buildsTime', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated sum of sites build compute time per period.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('buildsMbSeconds', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated sum of sites build mbSeconds per period.',
'default' => [],
'example' => [],
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'UsageSites';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_USAGE_SITES;
}
}

View file

@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Document;
class Variable extends Model
{
@ -41,6 +42,12 @@ class Variable extends Model
'default' => '',
'example' => 'myPa$$word1',
])
->addRule('secret', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Variable secret flag. Secret variables can only be updated or deleted, but never read.',
'default' => false,
'example' => false,
])
->addRule('resourceType', [
'type' => self::TYPE_STRING,
'description' => 'Service to which the variable belongs. Possible values are "project", "function"',
@ -56,6 +63,21 @@ class Variable extends Model
;
}
/**
* Filter
*
* @param Document $document
* @return Document
*/
public function filter(Document $document): Document
{
$secret = $document->getAttribute('secret');
if ($secret === true) {
$document->setAttribute('value', null);
}
return $document;
}
/**
* Get Name
*

View file

@ -118,6 +118,22 @@ class FunctionsConsoleClientTest extends Scope
$variableId = $variable['body']['$id'];
// test for secret variable
$variable = $this->createVariable(
$data['functionId'],
[
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
]
);
$this->assertEquals(201, $variable['headers']['status-code']);
$this->assertEquals('APP_TEST_1', $variable['body']['key']);
$this->assertEmpty($variable['body']['value']);
$secretVariableId = $variable['body']['$id'];
/**
* Test for FAILURE
*/
@ -157,7 +173,8 @@ class FunctionsConsoleClientTest extends Scope
return array_merge(
$data,
[
'variableId' => $variableId
'variableId' => $variableId,
'secretVariableId' => $secretVariableId
]
);
}
@ -177,10 +194,12 @@ class FunctionsConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, sizeof($response['body']['variables']));
$this->assertEquals(1, $response['body']['total']);
$this->assertEquals(2, sizeof($response['body']['variables']));
$this->assertEquals(2, $response['body']['total']);
$this->assertEquals("APP_TEST", $response['body']['variables'][0]['key']);
$this->assertEquals("TESTINGVALUE", $response['body']['variables'][0]['value']);
$this->assertEquals("APP_TEST_1", $response['body']['variables'][1]['key']);
$this->assertEmpty($response['body']['variables'][1]['value']);
/**
* Test for FAILURE
@ -207,6 +226,15 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals("APP_TEST", $response['body']['key']);
$this->assertEquals("TESTINGVALUE", $response['body']['value']);
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/variables/' . $data['secretVariableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals("APP_TEST_1", $response['body']['key']);
$this->assertEmpty($response['body']['value']);
/**
* Test for FAILURE
*/
@ -251,6 +279,27 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals("APP_TEST_UPDATE", $variable['body']['key']);
$this->assertEquals("TESTINGVALUEUPDATED", $variable['body']['value']);
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'] . '/variables/' . $data['secretVariableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'key' => 'APP_TEST_UPDATE_1',
'value' => 'TESTINGVALUEUPDATED_1'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals("APP_TEST_UPDATE_1", $response['body']['key']);
$this->assertEmpty($response['body']['value']);
$variable = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/variables/' . $data['secretVariableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $variable['headers']['status-code']);
$this->assertEquals("APP_TEST_UPDATE_1", $variable['body']['key']);
$this->assertEmpty($variable['body']['value']);
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'] . '/variables/' . $data['variableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -332,6 +381,13 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals(204, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $data['functionId'] . '/variables/' . $data['secretVariableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],

View file

@ -3814,6 +3814,23 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(201, $variable['headers']['status-code']);
$variableId = $variable['body']['$id'];
// test for secret variable
$variable = $this->client->call(Client::METHOD_POST, '/project/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
]);
$this->assertEquals(201, $variable['headers']['status-code']);
$this->assertEquals('APP_TEST_1', $variable['body']['key']);
$this->assertEmpty($variable['body']['value']);
$secretVariableId = $variable['body']['$id'];
/**
* Test for FAILURE
*/
@ -3857,6 +3874,7 @@ class ProjectsConsoleClientTest extends Scope
$data,
[
'variableId' => $variableId,
'secretVariableId' => $secretVariableId
]
);
}
@ -3877,10 +3895,12 @@ class ProjectsConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['variables']);
$this->assertEquals(1, $response['body']['total']);
$this->assertCount(2, $response['body']['variables']);
$this->assertEquals(2, $response['body']['total']);
$this->assertEquals("APP_TEST", $response['body']['variables'][0]['key']);
$this->assertEquals("TESTINGVALUE", $response['body']['variables'][0]['value']);
$this->assertEquals("APP_TEST_1", $response['body']['variables'][1]['key']);
$this->assertEmpty($response['body']['variables'][1]['value']);
return $data;
}
@ -3903,6 +3923,16 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals("APP_TEST", $response['body']['key']);
$this->assertEquals("TESTINGVALUE", $response['body']['value']);
$response = $this->client->call(Client::METHOD_GET, '/project/variables/' . $data['secretVariableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals("APP_TEST_1", $response['body']['key']);
$this->assertEmpty($response['body']['value']);
/**
* Test for FAILURE
*/
@ -3950,6 +3980,29 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals("APP_TEST_UPDATE", $variable['body']['key']);
$this->assertEquals("TESTINGVALUEUPDATED", $variable['body']['value']);
$response = $this->client->call(Client::METHOD_PUT, '/project/variables/' . $data['secretVariableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'key' => 'APP_TEST_UPDATE_1',
'value' => 'TESTINGVALUEUPDATED_1'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals("APP_TEST_UPDATE_1", $response['body']['key']);
$this->assertEmpty($response['body']['value']);
$variable = $this->client->call(Client::METHOD_GET, '/project/variables/' . $data['secretVariableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()));
$this->assertEquals(200, $variable['headers']['status-code']);
$this->assertEquals("APP_TEST_UPDATE_1", $variable['body']['key']);
$this->assertEmpty($variable['body']['value']);
$response = $this->client->call(Client::METHOD_GET, '/project/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
@ -3957,8 +4010,9 @@ class ProjectsConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['variables']);
$this->assertCount(2, $response['body']['variables']);
$this->assertEquals("APP_TEST_UPDATE", $response['body']['variables'][0]['key']);
$this->assertEquals("APP_TEST_UPDATE_1", $response['body']['variables'][1]['key']);
/**
* Test for FAILURE
@ -4037,6 +4091,14 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEquals(204, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_DELETE, '/project/variables/' . $data['secretVariableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()));
$response = $this->client->call(Client::METHOD_GET, '/project/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],