diff --git a/app/config/services.php b/app/config/services.php index 32eac3d324..43e80387f1 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -191,7 +191,7 @@ return [ 'name' => 'Functions', 'subtitle' => 'The Functions Service allows you view, create and manage your Cloud Functions.', 'description' => '/docs/services/functions.md', - 'controller' => 'api/functions.php', + 'controller' => '', // Uses modules 'sdk' => true, 'docs' => true, 'docsUrl' => 'https://appwrite.io/docs/functions', diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php deleted file mode 100644 index c2909eabba..0000000000 --- a/app/controllers/api/functions.php +++ /dev/null @@ -1,1907 +0,0 @@ -groups(['api', 'functions']) - ->desc('List available function runtime specifications') - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'listSpecifications', - description: '/docs/references/functions/list-specifications.md', - auth: [AuthType::KEY, AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_SPECIFICATION_LIST, - ) - ] - )) - ->inject('response') - ->inject('plan') - ->action(function (Response $response, array $plan) { - $allRuntimeSpecs = Config::getParam('runtime-specifications', []); - - $runtimeSpecs = []; - foreach ($allRuntimeSpecs as $spec) { - $spec['enabled'] = true; - - if (array_key_exists('runtimeSpecifications', $plan)) { - $spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']); - } - - // Only add specs that are within the limits set by environment variables - if ($spec['cpus'] <= System::getEnv('_APP_COMPUTE_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_COMPUTE_MEMORY', 512)) { - $runtimeSpecs[] = $spec; - } - } - - $response->dynamic(new Document([ - 'specifications' => $runtimeSpecs, - 'total' => count($runtimeSpecs) - ]), Response::MODEL_SPECIFICATION_LIST); - }); - -App::get('/v1/functions/:functionId') - ->groups(['api', 'functions']) - ->desc('Get function') - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'get', - description: '/docs/references/functions/get-function.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_FUNCTION, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, Response $response, Database $dbForProject) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $response->dynamic($function, Response::MODEL_FUNCTION); - }); - -App::get('/v1/functions/:functionId/usage') - ->desc('Get function usage') - ->groups(['api', 'functions', 'usage']) - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getFunctionUsage', - description: '/docs/references/functions/get-function-usage.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_USAGE_FUNCTION, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, string $range, Response $response, Database $dbForProject) { - - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $periods = Config::getParam('usage', []); - $stats = $usage = []; - $days = $periods[$range]; - $metrics = [ - str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS), - str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_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'], - 'executionsTotal' => $usage[$metrics[5]]['total'], - 'executionsTimeTotal' => $usage[$metrics[6]]['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'], - 'executions' => $usage[$metrics[5]]['data'], - 'executionsTime' => $usage[$metrics[6]]['data'], - 'buildsMbSecondsTotal' => $usage[$metrics[7]]['total'], - 'buildsMbSeconds' => $usage[$metrics[7]]['data'], - 'executionsMbSeconds' => $usage[$metrics[8]]['data'], - 'executionsMbSecondsTotal' => $usage[$metrics[8]]['total'] - ]), Response::MODEL_USAGE_FUNCTION); - }); - -App::get('/v1/functions/usage') - ->desc('Get functions usage') - ->groups(['api', 'functions', 'usage']) - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getUsage', - description: '/docs/references/functions/get-functions-usage.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_USAGE_FUNCTIONS, - ) - ] - )) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) - ->inject('response') - ->inject('dbForProject') - ->action(function (string $range, Response $response, Database $dbForProject) { - - $periods = Config::getParam('usage', []); - $stats = $usage = []; - $days = $periods[$range]; - $metrics = [ - METRIC_FUNCTIONS, - METRIC_DEPLOYMENTS, - METRIC_DEPLOYMENTS_STORAGE, - METRIC_BUILDS, - METRIC_BUILDS_STORAGE, - METRIC_BUILDS_COMPUTE, - METRIC_EXECUTIONS, - METRIC_EXECUTIONS_COMPUTE, - METRIC_BUILDS_MB_SECONDS, - METRIC_EXECUTIONS_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, - 'functionsTotal' => $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'], - 'executionsTotal' => $usage[$metrics[6]]['total'], - 'executionsTimeTotal' => $usage[$metrics[7]]['total'], - 'functions' => $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'], - 'executions' => $usage[$metrics[6]]['data'], - 'executionsTime' => $usage[$metrics[7]]['data'], - 'buildsMbSecondsTotal' => $usage[$metrics[8]]['total'], - 'buildsMbSeconds' => $usage[$metrics[8]]['data'], - 'executionsMbSeconds' => $usage[$metrics[9]]['data'], - 'executionsMbSecondsTotal' => $usage[$metrics[9]]['total'], - ]), Response::MODEL_USAGE_FUNCTIONS); - }); - -App::get('/v1/functions/:functionId/deployments/:deploymentId/download') - ->groups(['api', 'functions']) - ->desc('Download deployment') - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getDeploymentDownload', - description: '/docs/references/functions/get-deployment-download.md', - auth: [AuthType::KEY, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::ANY, - type: MethodType::LOCATION - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('request') - ->inject('dbForProject') - ->inject('deviceForFunctions') - ->action(function (string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions) { - - $function = $dbForProject->getDocument('functions', $functionId); - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - if ($deployment->getAttribute('resourceId') !== $function->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('Cache-Control', 'private, max-age=3888000') // 45 days - ->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)); - } - }); - -App::patch('/v1/functions/:functionId/deployments/:deploymentId') - ->groups(['api', 'functions']) - ->desc('Update deployment') - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].deployments.[deploymentId].update') - ->label('audits.event', 'deployment.update') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'updateDeployment') - ->label('sdk.description', '/docs/references/functions/update-function-deployment.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_FUNCTION) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->inject('dbForPlatform') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform) { - - $function = $dbForProject->getDocument('functions', $functionId); - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - 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); - } - - $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ - 'deploymentInternalId' => $deployment->getInternalId(), - 'deployment' => $deployment->getId(), - ]))); - - // Inform scheduler if function is still active - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $function->getAttribute('schedule')) - ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response->dynamic($function, Response::MODEL_FUNCTION); - }); - -App::delete('/v1/functions/:functionId') - ->groups(['api', 'functions']) - ->desc('Delete function') - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].delete') - ->label('audits.event', 'function.delete') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'delete', - description: '/docs/references/functions/delete-function.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDeletes') - ->inject('queueForEvents') - ->inject('dbForPlatform') - ->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForPlatform) { - - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - if (!$dbForProject->deleteDocument('functions', $function->getId())) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove function from DB'); - } - - // Inform scheduler to no longer run function - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('active', false); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($function); - - $queueForEvents->setParam('functionId', $function->getId()); - - $response->noContent(); - }); - -App::get('/v1/functions/:functionId/deployments') - ->groups(['api', 'functions']) - ->desc('List deployments') - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'listDeployments', - description: '/docs/references/functions/list-deployments.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DEPLOYMENT_LIST, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function 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') - ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject) { - - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_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', [$function->getInternalId()]); - $queries[] = Query::equal('resourceType', ['functions']); - - /** - * 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); - }); - -App::get('/v1/functions/:functionId/deployments/:deploymentId') - ->groups(['api', 'functions']) - ->desc('Get deployment') - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getDeployment', - description: '/docs/references/functions/get-deployment.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DEPLOYMENT, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject) { - - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - - if ($deployment->getAttribute('resourceId') !== $function->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); - }); - -App::delete('/v1/functions/:functionId/deployments/:deploymentId') - ->groups(['api', 'functions']) - ->desc('Delete deployment') - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].deployments.[deploymentId].delete') - ->label('audits.event', 'deployment.delete') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'deleteDeployment', - description: '/docs/references/functions/delete-deployment.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDeletes') - ->inject('queueForEvents') - ->inject('deviceForFunctions') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Device $deviceForFunctions) { - - $function = $dbForProject->getDocument('functions', $functionId); - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - if ($deployment->getAttribute('resourceId') !== $function->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 ($function->getAttribute('deployment') === $deployment->getId()) { // Reset function deployment - $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ - 'deployment' => '', - 'deploymentInternalId' => '', - ]))); - } - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($deployment); - - $response->noContent(); - }); - -App::post('/v1/functions/:functionId/deployments/:deploymentId/build') - ->alias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') - ->groups(['api', 'functions']) - ->desc('Rebuild deployment') - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].deployments.[deploymentId].update') - ->label('audits.event', 'deployment.update') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'createBuild', - description: '/docs/references/functions/create-build.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->param('buildId', '', new UID(), 'Build unique ID.', true) // added as optional param for backward compatibility - ->inject('request') - ->inject('response') - ->inject('dbForProject') - ->inject('project') - ->inject('queueForEvents') - ->inject('queueForBuilds') - ->inject('deviceForFunctions') - ->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_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, - 'entrypoint' => $function->getAttribute('entrypoint'), - 'commands' => $function->getAttribute('commands', ''), - 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]), - ])); - - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($function) - ->setDeployment($deployment); - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response->noContent(); - }); - -App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') - ->groups(['api', 'functions']) - ->desc('Cancel deployment') - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('audits.event', 'deployment.update') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->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('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('project') - ->inject('queueForEvents') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_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' => $function->getAttribute('runtime'), - 'source' => $deployment->getAttribute('path', ''), - 'sourceType' => '', - 'logs' => '', - 'duration' => 0, - 'size' => 0 - ])); - - $deployment->setAttribute('buildId', $build->getId()); - $deployment->setAttribute('buildInternalId', $build->getInternalId()); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - } else { - if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) { - throw new Exception(Exception::BUILD_ALREADY_COMPLETED); - } - - $startTime = new \DateTime($build->getAttribute('startTime')); - $endTime = new \DateTime('now'); - $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); - - $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([ - 'endTime' => DateTime::now(), - 'duration' => $duration, - 'status' => 'canceled' - ])); - } - - $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('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response->dynamic($build, Response::MODEL_BUILD); - }); - -App::post('/v1/functions/:functionId/executions') - ->groups(['api', 'functions']) - ->desc('Create execution') - ->label('scope', 'execution.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].executions.[executionId].create') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'createExecution', - description: '/docs/references/functions/create-execution.md', - auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_EXECUTION, - ) - ], - contentType: ContentType::MULTIPART, - requestType: 'application/json', - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true) - ->param('async', false, new Boolean(true), 'Execute code in the background. Default value is false.', true) - ->param('path', '/', new Text(2048), 'HTTP path of execution. Path can include query params. Default value is /', true) - ->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], true), 'HTTP method of execution. Default value is GET.', true) - ->param('headers', [], new AnyOf([new Assoc(), new Text(65535)], AnyOf::TYPE_MIXED), 'HTTP headers of execution. Defaults to empty.', true) - ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_MINUTES, offset: 60), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true) - ->inject('response') - ->inject('request') - ->inject('project') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('user') - ->inject('queueForEvents') - ->inject('queueForStatsUsage') - ->inject('queueForFunctions') - ->inject('geodb') - ->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForPlatform, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb) { - $async = \strval($async) === 'true' || \strval($async) === '1'; - - if (!$async && !is_null($scheduledAt)) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Scheduled executions must run asynchronously. Set scheduledAt to a future date, or set async to true.'); - } - - /** - * @var array $headers - */ - $assocParams = ['headers']; - foreach ($assocParams as $assocParam) { - if (!empty('headers') && !is_array($$assocParam)) { - $$assocParam = \json_decode($$assocParam, true); - } - } - - $booleanParams = ['async']; - foreach ($booleanParams as $booleamParam) { - if (!empty($$booleamParam) && !is_bool($$booleamParam)) { - $$booleamParam = $$booleamParam === "true" ? true : false; - } - } - - // 'headers' validator - $validator = new Headers(); - if (!$validator->isValid($headers)) { - throw new Exception($validator->getDescription(), 400); - } - - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - - if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $version = $function->getAttribute('version', 'v2'); - $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); - $spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; - - $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; - - if (\is_null($runtime)) { - throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); - } - - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); - - if ($deployment->getAttribute('resourceId') !== $function->getId()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); - } - - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); - } - - /** Check if build has completed */ - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); - if ($build->isEmpty()) { - throw new Exception(Exception::BUILD_NOT_FOUND); - } - - if ($build->getAttribute('status') !== 'ready') { - throw new Exception(Exception::BUILD_NOT_READY); - } - - $validator = new Authorization('execute'); - - if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function - throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); - } - - $jwt = ''; // initialize - if (!$user->isEmpty()) { // If userId exists, generate a JWT for function - $sessions = $user->getAttribute('sessions', []); - $current = new Document(); - - foreach ($sessions as $session) { - /** @var Utopia\Database\Document $session */ - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too - $current = $session; - } - } - - if (!$current->isEmpty()) { - $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); - $jwt = $jwtObj->encode([ - 'userId' => $user->getId(), - 'sessionId' => $current->getId(), - ]); - } - } - - $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['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; - $headers['x-appwrite-trigger'] = 'http'; - $headers['x-appwrite-user-id'] = $user->getId() ?? ''; - $headers['x-appwrite-user-jwt'] = $jwt ?? ''; - $headers['x-appwrite-country-code'] = ''; - $headers['x-appwrite-continent-code'] = ''; - $headers['x-appwrite-continent-eu'] = 'false'; - - $ip = $headers['x-real-ip'] ?? ''; - if (!empty($ip)) { - $record = $geodb->get($ip); - - if ($record) { - $eu = Config::getParam('locale-eu'); - - $headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? ''; - $headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? ''; - $headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false'; - } - } - - $headersFiltered = []; - foreach ($headers as $key => $value) { - if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) { - $headersFiltered[] = ['name' => $key, 'value' => $value]; - } - } - - $executionId = ID::unique(); - - $status = $async ? 'waiting' : 'processing'; - - if (!is_null($scheduledAt)) { - $status = 'scheduled'; - } - - $execution = new Document([ - '$id' => $executionId, - '$permissions' => !$user->isEmpty() ? [Permission::read(Role::user($user->getId()))] : [], - 'resourceInternalId' => $function->getInternalId(), - 'resourceId' => $function->getId(), - 'resourceType' => 'functions', - 'deploymentInternalId' => $deployment->getInternalId(), - 'deploymentId' => $deployment->getId(), - 'trigger' => (!is_null($scheduledAt)) ? 'schedule' : 'http', - 'status' => $status, // waiting / processing / completed / failed / scheduled - 'responseStatusCode' => 0, - 'responseHeaders' => [], - 'requestPath' => $path, - 'requestMethod' => $method, - 'requestHeaders' => $headersFiltered, - 'errors' => '', - 'logs' => '', - 'duration' => 0.0, - 'search' => implode(' ', [$functionId, $executionId]), - ]); - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('executionId', $execution->getId()) - ->setContext('function', $function); - - if ($async) { - if (is_null($scheduledAt)) { - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); - $queueForFunctions - ->setType('http') - ->setExecution($execution) - ->setFunction($function) - ->setBody($body) - ->setHeaders($headers) - ->setPath($path) - ->setMethod($method) - ->setJWT($jwt) - ->setProject($project) - ->setUser($user) - ->setParam('functionId', $function->getId()) - ->setParam('executionId', $execution->getId()) - ->trigger(); - } else { - $data = [ - 'headers' => $headers, - 'path' => $path, - 'method' => $method, - 'body' => $body, - 'userId' => $user->getId() - ]; - - $schedule = $dbForPlatform->createDocument('schedules', new Document([ - 'region' => System::getEnv('_APP_REGION', 'default'), - 'resourceType' => ScheduleExecutions::getSupportedResource(), - 'resourceId' => $execution->getId(), - 'resourceInternalId' => $execution->getInternalId(), - 'resourceUpdatedAt' => DateTime::now(), - 'projectId' => $project->getId(), - 'schedule' => $scheduledAt, - 'data' => $data, - 'active' => true, - ])); - - $execution = $execution - ->setAttribute('scheduleId', $schedule->getId()) - ->setAttribute('scheduleInternalId', $schedule->getInternalId()) - ->setAttribute('scheduledAt', $scheduledAt); - - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); - } - - return $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($execution, Response::MODEL_EXECUTION); - } - - $durationStart = \microtime(true); - - $vars = []; - - // V2 vars - if ($version === 'v2') { - $vars = \array_merge($vars, [ - 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', - 'APPWRITE_FUNCTION_DATA' => $body ?? '', - 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', - 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' - ]); - } - - // Shared vars - foreach ($function->getAttribute('varsProject', []) as $var) { - $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); - } - - // Function vars - foreach ($function->getAttribute('vars', []) as $var) { - $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); - } - - $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; - $hostname = System::getEnv('_APP_DOMAIN'); - $endpoint = $protocol . '://' . $hostname . "/v1"; - - // Appwrite vars - $vars = \array_merge($vars, [ - 'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint, - 'APPWRITE_FUNCTION_ID' => $functionId, - 'APPWRITE_FUNCTION_NAME' => $function->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' => $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - 'APPWRITE_FUNCTION_MEMORY' => $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, - 'APPWRITE_VERSION' => APP_VERSION_STABLE, - 'APPWRITE_REGION' => $project->getAttribute('region'), - 'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''), - 'APPWRITE_VCS_REPOSITORY_ID' => $deployment->getAttribute('providerRepositoryId', ''), - 'APPWRITE_VCS_REPOSITORY_NAME' => $deployment->getAttribute('providerRepositoryName', ''), - 'APPWRITE_VCS_REPOSITORY_OWNER' => $deployment->getAttribute('providerRepositoryOwner', ''), - 'APPWRITE_VCS_REPOSITORY_URL' => $deployment->getAttribute('providerRepositoryUrl', ''), - 'APPWRITE_VCS_REPOSITORY_BRANCH' => $deployment->getAttribute('providerBranch', ''), - 'APPWRITE_VCS_REPOSITORY_BRANCH_URL' => $deployment->getAttribute('providerBranchUrl', ''), - 'APPWRITE_VCS_COMMIT_HASH' => $deployment->getAttribute('providerCommitHash', ''), - 'APPWRITE_VCS_COMMIT_MESSAGE' => $deployment->getAttribute('providerCommitMessage', ''), - 'APPWRITE_VCS_COMMIT_URL' => $deployment->getAttribute('providerCommitUrl', ''), - 'APPWRITE_VCS_COMMIT_AUTHOR_NAME' => $deployment->getAttribute('providerCommitAuthor', ''), - 'APPWRITE_VCS_COMMIT_AUTHOR_URL' => $deployment->getAttribute('providerCommitAuthorUrl', ''), - 'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''), - ]); - - /** 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 . '"'; - $executionResponse = $executor->createExecution( - projectId: $project->getId(), - deploymentId: $deployment->getId(), - body: \strlen($body) > 0 ? $body : null, - variables: $vars, - timeout: $function->getAttribute('timeout', 0), - image: $runtime['image'], - source: $build->getAttribute('path', ''), - entrypoint: $deployment->getAttribute('entrypoint', ''), - version: $version, - path: $path, - method: $method, - headers: $headers, - runtimeEntrypoint: $command, - cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, - logging: $function->getAttribute('logging', true), - requestTimeout: 30 - ); - - $headersFiltered = []; - foreach ($executionResponse['headers'] as $key => $value) { - if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { - $headersFiltered[] = ['name' => $key, 'value' => $value]; - } - } - - /** Update execution status */ - $status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed'; - $execution->setAttribute('status', $status); - $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); - $execution->setAttribute('responseHeaders', $headersFiltered); - $execution->setAttribute('logs', $executionResponse['logs']); - $execution->setAttribute('errors', $executionResponse['errors']); - $execution->setAttribute('duration', $executionResponse['duration']); - } catch (\Throwable $th) { - $durationEnd = \microtime(true); - - $execution - ->setAttribute('duration', $durationEnd - $durationStart) - ->setAttribute('status', 'failed') - ->setAttribute('responseStatusCode', 500) - ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); - Console::error($th->getMessage()); - - if ($th instanceof AppwriteException) { - throw $th; - } - } finally { - $queueForStatsUsage - ->addMetric(METRIC_EXECUTIONS, 1) - ->addMetric(str_replace('{functionInternalId}', $function->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(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) - ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) - ; - - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); - } - - $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); - - if (!$isPrivilegedUser && !$isAppUser) { - $execution->setAttribute('logs', ''); - $execution->setAttribute('errors', ''); - } - - $headers = []; - foreach (($executionResponse['headers'] ?? []) as $key => $value) { - $headers[] = ['name' => $key, 'value' => $value]; - } - - $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); - $execution->setAttribute('responseHeaders', $headers); - - $acceptTypes = \explode(', ', $request->getHeader('accept')); - foreach ($acceptTypes as $acceptType) { - if (\str_starts_with($acceptType, 'application/json') || \str_starts_with($acceptType, 'application/*')) { - $response->setContentType(Response::CONTENT_TYPE_JSON); - break; - } elseif (\str_starts_with($acceptType, 'multipart/form-data') || \str_starts_with($acceptType, 'multipart/*')) { - $response->setContentType(Response::CONTENT_TYPE_MULTIPART); - break; - } - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($execution, Response::MODEL_EXECUTION); - }); - -App::get('/v1/functions/:functionId/executions') - ->groups(['api', 'functions']) - ->desc('List executions') - ->label('scope', 'execution.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'listExecutions', - description: '/docs/references/functions/list-executions.md', - auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_EXECUTION_LIST, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('queries', [], new Executions(), '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(', ', Executions::ALLOWED_ATTRIBUTES), true) - ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('mode') - ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - - if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { - throw new Exception(Exception::FUNCTION_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 internal queries - $queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]); - $queries[] = Query::equal('resourceType', ['functions']); - - /** - * 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()); - } - - $executionId = $cursor->getValue(); - $cursorDocument = $dbForProject->getDocument('executions', $executionId); - - if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Execution '{$executionId}' for the 'cursor' value not found."); - } - - $cursor->setValue($cursorDocument); - } - - $filterQueries = Query::groupByType($queries)['filters']; - - $results = $dbForProject->find('executions', $queries); - $total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT); - - $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); - if (!$isPrivilegedUser && !$isAppUser) { - $results = array_map(function ($execution) { - $execution->setAttribute('logs', ''); - $execution->setAttribute('errors', ''); - return $execution; - }, $results); - } - - $response->dynamic(new Document([ - 'executions' => $results, - 'total' => $total, - ]), Response::MODEL_EXECUTION_LIST); - }); - -App::get('/v1/functions/:functionId/executions/:executionId') - ->groups(['api', 'functions']) - ->desc('Get execution') - ->label('scope', 'execution.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getExecution', - description: '/docs/references/functions/get-execution.md', - auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_EXECUTION, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('executionId', '', new UID(), 'Execution ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('mode') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - - if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $execution = $dbForProject->getDocument('executions', $executionId); - - if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) { - throw new Exception(Exception::EXECUTION_NOT_FOUND); - } - - if ($execution->isEmpty()) { - throw new Exception(Exception::EXECUTION_NOT_FOUND); - } - - $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); - if (!$isPrivilegedUser && !$isAppUser) { - $execution->setAttribute('logs', ''); - $execution->setAttribute('errors', ''); - } - - $response->dynamic($execution, Response::MODEL_EXECUTION); - }); - -App::delete('/v1/functions/:functionId/executions/:executionId') - ->groups(['api', 'functions']) - ->desc('Delete execution') - ->label('scope', 'execution.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].executions.[executionId].delete') - ->label('audits.event', 'executions.delete') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'deleteExecution', - description: '/docs/references/functions/delete-execution.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('executionId', '', new UID(), 'Execution ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('queueForEvents') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $execution = $dbForProject->getDocument('executions', $executionId); - if ($execution->isEmpty()) { - throw new Exception(Exception::EXECUTION_NOT_FOUND); - } - - if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) { - throw new Exception(Exception::EXECUTION_NOT_FOUND); - } - $status = $execution->getAttribute('status'); - - if (!in_array($status, ['completed', 'failed', 'scheduled'])) { - throw new Exception(Exception::EXECUTION_IN_PROGRESS); - } - - if (!$dbForProject->deleteDocument('executions', $execution->getId())) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove execution from DB'); - } - - if ($status === 'scheduled') { - $schedule = $dbForPlatform->findOne('schedules', [ - Query::equal('resourceId', [$execution->getId()]), - Query::equal('resourceType', [ScheduleExecutions::getSupportedResource()]), - Query::equal('active', [true]), - ]); - - if (!$schedule->isEmpty()) { - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('active', false); - - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - } - } - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('executionId', $execution->getId()) - ->setPayload($response->output($execution, Response::MODEL_EXECUTION)); - - $response->noContent(); - }); - -// Variables - -App::post('/v1/functions/:functionId/variables') - ->desc('Create variable') - ->groups(['api', 'functions']) - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('audits.event', 'variable.create') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'createVariable', - description: '/docs/references/functions/create-variable.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_VARIABLE, - ) - ] - )) - ->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', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->action(function (string $functionId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $variableId = ID::unique(); - - $variable = new Document([ - '$id' => $variableId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceInternalId' => $function->getInternalId(), - 'resourceId' => $function->getId(), - 'resourceType' => 'function', - 'key' => $key, - 'value' => $value, - 'secret' => $secret, - 'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']), - ]); - - try { - $variable = $dbForProject->createDocument('variables', $variable); - } catch (DuplicateException $th) { - throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); - } - - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); - - // Inform scheduler to pull the latest changes - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $function->getAttribute('schedule')) - ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($variable, Response::MODEL_VARIABLE); - }); - -App::get('/v1/functions/:functionId/variables') - ->desc('List variables') - ->groups(['api', 'functions']) - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label( - 'sdk', - new Method( - namespace: 'functions', - name: 'listVariables', - description: '/docs/references/functions/list-variables.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_VARIABLE_LIST, - ) - ], - ) - ) - ->param('functionId', '', new UID(), 'Function unique ID.', false) - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, Response $response, Database $dbForProject) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $response->dynamic(new Document([ - 'variables' => $function->getAttribute('vars', []), - 'total' => \count($function->getAttribute('vars', [])), - ]), Response::MODEL_VARIABLE_LIST); - }); - -App::get('/v1/functions/:functionId/variables/:variableId') - ->desc('Get variable') - ->groups(['api', 'functions']) - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'getVariable') - ->label('sdk.description', '/docs/references/functions/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('functionId', '', new UID(), 'Function unique ID.', false) - ->param('variableId', '', new UID(), 'Variable unique ID.', false) - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $variable = $dbForProject->getDocument('variables', $variableId); - if ( - $variable === false || - $variable->isEmpty() || - $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || - $variable->getAttribute('resourceType') !== 'function' - ) { - 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); - }); - -App::put('/v1/functions/:functionId/variables/:variableId') - ->desc('Update variable') - ->groups(['api', 'functions']) - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('audits.event', 'variable.update') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'updateVariable') - ->label('sdk.description', '/docs/references/functions/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('functionId', '', new UID(), 'Function 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) - ->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->action(function (string $functionId, string $variableId, string $key, ?string $value, ?bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform) { - - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $variable = $dbForProject->getDocument('variables', $variableId); - if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') { - throw new Exception(Exception::VARIABLE_NOT_FOUND); - } - - if ($variable === false || $variable->isEmpty()) { - throw new Exception(Exception::VARIABLE_NOT_FOUND); - } - - if ($variable->getAttribute('secret') === true && $secret === false) { - throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET); - } - - $variable - ->setAttribute('key', $key) - ->setAttribute('value', $value ?? $variable->getAttribute('value')) - ->setAttribute('secret', $secret ?? $variable->getAttribute('secret')) - ->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); - - try { - $dbForProject->updateDocument('variables', $variable->getId(), $variable); - } catch (DuplicateException $th) { - throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); - } - - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); - - // Inform scheduler to pull the latest changes - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $function->getAttribute('schedule')) - ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - - $response->dynamic($variable, Response::MODEL_VARIABLE); - }); - -App::delete('/v1/functions/:functionId/variables/:variableId') - ->desc('Delete variable') - ->groups(['api', 'functions']) - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('audits.event', 'variable.delete') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'deleteVariable') - ->label('sdk.description', '/docs/references/functions/delete-variable.md') - ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) - ->label('sdk.response.model', Response::MODEL_NONE) - ->param('functionId', '', new UID(), 'Function unique ID.', false) - ->param('variableId', '', new UID(), 'Variable unique ID.', false) - ->inject('response') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForPlatform) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $variable = $dbForProject->getDocument('variables', $variableId); - if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') { - 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('functions', $function->getId(), $function->setAttribute('live', false)); - - // Inform scheduler to pull the latest changes - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $function->getAttribute('schedule')) - ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - - $response->noContent(); - }); - -App::get('/v1/functions/templates') - ->groups(['api']) - ->desc('List function templates') - ->label('scope', 'public') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'listTemplates', - description: '/docs/references/functions/list-templates.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_TEMPLATE_FUNCTION_LIST, - ) - ] - )) - ->param('runtimes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('runtimes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of runtimes allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' runtimes 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 function 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') - ->action(function (array $runtimes, array $usecases, int $limit, int $offset, Response $response) { - $templates = Config::getParam('function-templates', []); - - if (!empty($runtimes)) { - $templates = \array_filter($templates, function ($template) use ($runtimes) { - return \count(\array_intersect($runtimes, \array_column($template['runtimes'], '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_FUNCTION_LIST); - }); - -App::get('/v1/functions/templates/:templateId') - ->desc('Get function template') - ->label('scope', 'public') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getTemplate', - description: '/docs/references/functions/get-template.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_TEMPLATE_FUNCTION, - ) - ] - )) - ->param('templateId', '', new Text(128), 'Template ID.') - ->inject('response') - ->action(function (string $templateId, Response $response) { - $templates = Config::getParam('function-templates', []); - - $filtered = \array_filter($templates, function ($template) use ($templateId) { - return $template['id'] === $templateId; - }); - - $template = array_shift($filtered); - - if (empty($template)) { - throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND); - } - - $response->dynamic(new Document($template), Response::MODEL_TEMPLATE_FUNCTION); - }); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Create.php new file mode 100644 index 0000000000..1d85bcb11d --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Create.php @@ -0,0 +1,112 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/build') + ->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') + ->desc('Rebuild deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].deployments.[deploymentId].update') + ->label('audits.event', 'deployment.update') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'createBuild', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->param('buildId', '', new UID(), 'Build unique ID.', true) // added as optional param for backward compatibility + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('deviceForFunctions') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, string $buildId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_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, + 'entrypoint' => $function->getAttribute('entrypoint'), + 'commands' => $function->getAttribute('commands', ''), + 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]), + ])); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($function) + ->setDeployment($deployment); + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Update.php new file mode 100644 index 0000000000..85edfd42ca --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Update.php @@ -0,0 +1,136 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/build') + ->desc('Cancel deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('audits.event', 'deployment.update') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'updateDeploymentBuild', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_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' => $function->getAttribute('runtime'), + 'source' => $deployment->getAttribute('path', ''), + 'sourceType' => '', + 'logs' => '', + 'duration' => 0, + 'size' => 0 + ])); + + $deployment->setAttribute('buildId', $build->getId()); + $deployment->setAttribute('buildInternalId', $build->getInternalId()); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + } else { + if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) { + throw new Exception(Exception::BUILD_ALREADY_COMPLETED); + } + + $startTime = new \DateTime($build->getAttribute('startTime')); + $endTime = new \DateTime('now'); + $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); + + $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([ + 'endTime' => DateTime::now(), + 'duration' => $duration, + 'status' => 'canceled' + ])); + } + + $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('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response->dynamic($build, Response::MODEL_BUILD); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php new file mode 100644 index 0000000000..5bde7903d9 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php @@ -0,0 +1,109 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId') + ->desc('Delete deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].deployments.[deploymentId].delete') + ->label('audits.event', 'deployment.delete') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'deleteDeployment', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDeletes') + ->inject('queueForEvents') + ->inject('deviceForFunctions') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Device $deviceForFunctions) + { + $function = $dbForProject->getDocument('functions', $functionId); + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($deployment->getAttribute('resourceId') !== $function->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 ($function->getAttribute('deployment') === $deployment->getId()) { // Reset function deployment + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ + 'deployment' => '', + 'deploymentInternalId' => '', + ]))); + } + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($deployment); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php new file mode 100644 index 0000000000..1a1ff9b938 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php @@ -0,0 +1,129 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/download') + ->groups(['api', 'functions']) + ->desc('Download deployment') + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'getDeploymentDownload', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('request') + ->inject('dbForProject') + ->inject('deviceForFunctions') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions) + { + $function = $dbForProject->getDocument('functions', $functionId); + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($deployment->getAttribute('resourceId') !== $function->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('Cache-Control', 'private, max-age=3888000') // 45 days + ->addHeader('X-Peak', \memory_get_peak_usage()) + ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); + + $size = $deviceForFunctions->getFileSize($path); + $rangeHeader = $request->getHeader('range'); + + if (!empty($rangeHeader)) { + $start = $request->getRangeStart(); + $end = $request->getRangeEnd(); + $unit = $request->getRangeUnit(); + + if ($end === null) { + $end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1)); + } + + if ($unit !== 'bytes' || $start >= $end || $end >= $size) { + throw new Exception(Exception::STORAGE_INVALID_RANGE); + } + + $response + ->addHeader('Accept-Ranges', 'bytes') + ->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size) + ->addHeader('Content-Length', $end - $start + 1) + ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); + + $response->send($deviceForFunctions->read($path, $start, ($end - $start + 1))); + } + + if ($size > APP_STORAGE_READ_BUFFER) { + for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { + $response->chunk( + $deviceForFunctions->read( + $path, + ($i * MAX_OUTPUT_CHUNK_SIZE), + min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) + ), + (($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size + ); + } + } else { + $response->send($deviceForFunctions->read($path)); + } + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Get.php new file mode 100644 index 0000000000..1d074e9e2a --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Get.php @@ -0,0 +1,81 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId') + ->desc('Get deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'getDeployment', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($deployment->getAttribute('resourceId') !== $function->getId()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); + $deployment->setAttribute('status', $build->getAttribute('status', 'waiting')); + $deployment->setAttribute('buildLogs', $build->getAttribute('logs', '')); + $deployment->setAttribute('buildTime', $build->getAttribute('duration', 0)); + $deployment->setAttribute('buildSize', $build->getAttribute('size', 0)); + $deployment->setAttribute('size', $deployment->getAttribute('size', 0)); + + $response->dynamic($deployment, Response::MODEL_DEPLOYMENT); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Update.php new file mode 100644 index 0000000000..75144deefe --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Update.php @@ -0,0 +1,103 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId') + ->desc('Update deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('event', 'functions.[functionId].deployments.[deploymentId].update') + ->label('audits.event', 'deployment.update') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'updateDeployment', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform) + { + $function = $dbForProject->getDocument('functions', $functionId); + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + 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); + } + + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ + 'deploymentInternalId' => $deployment->getInternalId(), + 'deployment' => $deployment->getId(), + ]))); + + // Inform scheduler if function is still active + $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response->dynamic($function, Response::MODEL_FUNCTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php new file mode 100644 index 0000000000..9e03a86bc7 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php @@ -0,0 +1,127 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/deployments') + ->desc('List deployments') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'listDeployments', + description: <<param('functionId', '', new UID(), 'Function 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 $functionId, array $queries, string $search, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_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', [$function->getInternalId()]); + $queries[] = Query::equal('resourceType', ['functions']); + + /** + * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries + */ + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); + }); + $cursor = reset($cursor); + if ($cursor) { + /** @var Query $cursor */ + + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $deploymentId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('deployments', $deploymentId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Deployment '{$deploymentId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + $results = $dbForProject->find('deployments', $queries); + $total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT); + + foreach ($results as $result) { + $build = $dbForProject->getDocument('builds', $result->getAttribute('buildId', '')); + $result->setAttribute('status', $build->getAttribute('status', 'processing')); + $result->setAttribute('buildLogs', $build->getAttribute('logs', '')); + $result->setAttribute('buildTime', $build->getAttribute('duration', 0)); + $result->setAttribute('buildSize', $build->getAttribute('size', 0)); + $result->setAttribute('size', $result->getAttribute('size', 0)); + } + + $response->dynamic(new Document([ + 'deployments' => $results, + 'total' => $total, + ]), Response::MODEL_DEPLOYMENT_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php new file mode 100644 index 0000000000..f522e2dc62 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -0,0 +1,474 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/functions/:functionId/executions') + ->desc('Create execution') + ->groups(['api', 'functions']) + ->label('scope', 'execution.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].executions.[executionId].create') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'createExecution', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true) + ->param('async', false, new Boolean(true), 'Execute code in the background. Default value is false.', true) + ->param('path', '/', new Text(2048), 'HTTP path of execution. Path can include query params. Default value is /', true) + ->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], true), 'HTTP method of execution. Default value is GET.', true) + ->param('headers', [], new AnyOf([new Assoc(), new Text(65535)], AnyOf::TYPE_MIXED), 'HTTP headers of execution. Defaults to empty.', true) + ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_MINUTES, offset: 60), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true) + ->inject('response') + ->inject('request') + ->inject('project') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('user') + ->inject('queueForEvents') + ->inject('queueForStatsUsage') + ->inject('queueForFunctions') + ->inject('geodb') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForPlatform, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb) + { + $async = \strval($async) === 'true' || \strval($async) === '1'; + + if (!$async && !is_null($scheduledAt)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Scheduled executions must run asynchronously. Set scheduledAt to a future date, or set async to true.'); + } + + /** + * @var array $headers + */ + $assocParams = ['headers']; + foreach ($assocParams as $assocParam) { + if (!empty('headers') && !is_array($$assocParam)) { + $$assocParam = \json_decode($$assocParam, true); + } + } + + $booleanParams = ['async']; + foreach ($booleanParams as $booleamParam) { + if (!empty($$booleamParam) && !is_bool($$booleamParam)) { + $$booleamParam = $$booleamParam === "true" ? true : false; + } + } + + // 'headers' validator + $validator = new Headers(); + if (!$validator->isValid($headers)) { + throw new Exception($validator->getDescription(), 400); + } + + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $version = $function->getAttribute('version', 'v2'); + $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); + $spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; + + $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; + + if (\is_null($runtime)) { + throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); + } + + $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); + + if ($deployment->getAttribute('resourceId') !== $function->getId()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); + } + + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); + } + + /** Check if build has completed */ + $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + if ($build->isEmpty()) { + throw new Exception(Exception::BUILD_NOT_FOUND); + } + + if ($build->getAttribute('status') !== 'ready') { + throw new Exception(Exception::BUILD_NOT_READY); + } + + $validator = new Authorization('execute'); + + if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function + throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); + } + + $jwt = ''; // initialize + if (!$user->isEmpty()) { // If userId exists, generate a JWT for function + $sessions = $user->getAttribute('sessions', []); + $current = new Document(); + + foreach ($sessions as $session) { + /** @var Utopia\Database\Document $session */ + if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + $current = $session; + } + } + + if (!$current->isEmpty()) { + $jwtExpiry = $function->getAttribute('timeout', 900); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); + $jwt = $jwtObj->encode([ + 'userId' => $user->getId(), + 'sessionId' => $current->getId(), + ]); + } + } + + $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['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; + $headers['x-appwrite-trigger'] = 'http'; + $headers['x-appwrite-user-id'] = $user->getId() ?? ''; + $headers['x-appwrite-user-jwt'] = $jwt ?? ''; + $headers['x-appwrite-country-code'] = ''; + $headers['x-appwrite-continent-code'] = ''; + $headers['x-appwrite-continent-eu'] = 'false'; + + $ip = $headers['x-real-ip'] ?? ''; + if (!empty($ip)) { + $record = $geodb->get($ip); + + if ($record) { + $eu = Config::getParam('locale-eu'); + + $headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? ''; + $headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? ''; + $headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false'; + } + } + + $headersFiltered = []; + foreach ($headers as $key => $value) { + if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) { + $headersFiltered[] = ['name' => $key, 'value' => $value]; + } + } + + $executionId = ID::unique(); + + $status = $async ? 'waiting' : 'processing'; + + if (!is_null($scheduledAt)) { + $status = 'scheduled'; + } + + $execution = new Document([ + '$id' => $executionId, + '$permissions' => !$user->isEmpty() ? [Permission::read(Role::user($user->getId()))] : [], + 'resourceInternalId' => $function->getInternalId(), + 'resourceId' => $function->getId(), + 'resourceType' => 'functions', + 'deploymentInternalId' => $deployment->getInternalId(), + 'deploymentId' => $deployment->getId(), + 'trigger' => (!is_null($scheduledAt)) ? 'schedule' : 'http', + 'status' => $status, // waiting / processing / completed / failed / scheduled + 'responseStatusCode' => 0, + 'responseHeaders' => [], + 'requestPath' => $path, + 'requestMethod' => $method, + 'requestHeaders' => $headersFiltered, + 'errors' => '', + 'logs' => '', + 'duration' => 0.0, + 'search' => implode(' ', [$functionId, $executionId]), + ]); + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('executionId', $execution->getId()) + ->setContext('function', $function); + + if ($async) { + if (is_null($scheduledAt)) { + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + $queueForFunctions + ->setType('http') + ->setExecution($execution) + ->setFunction($function) + ->setBody($body) + ->setHeaders($headers) + ->setPath($path) + ->setMethod($method) + ->setJWT($jwt) + ->setProject($project) + ->setUser($user) + ->setParam('functionId', $function->getId()) + ->setParam('executionId', $execution->getId()) + ->trigger(); + } else { + $data = [ + 'headers' => $headers, + 'path' => $path, + 'method' => $method, + 'body' => $body, + 'userId' => $user->getId() + ]; + + $schedule = $dbForPlatform->createDocument('schedules', new Document([ + 'region' => System::getEnv('_APP_REGION', 'default'), + 'resourceType' => ScheduleExecutions::getSupportedResource(), + 'resourceId' => $execution->getId(), + 'resourceInternalId' => $execution->getInternalId(), + 'resourceUpdatedAt' => DateTime::now(), + 'projectId' => $project->getId(), + 'schedule' => $scheduledAt, + 'data' => $data, + 'active' => true, + ])); + + $execution = $execution + ->setAttribute('scheduleId', $schedule->getId()) + ->setAttribute('scheduleInternalId', $schedule->getInternalId()) + ->setAttribute('scheduledAt', $scheduledAt); + + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + } + + return $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($execution, Response::MODEL_EXECUTION); + } + + $durationStart = \microtime(true); + + $vars = []; + + // V2 vars + if ($version === 'v2') { + $vars = \array_merge($vars, [ + 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', + 'APPWRITE_FUNCTION_DATA' => $body ?? '', + 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', + 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' + ]); + } + + // Shared vars + foreach ($function->getAttribute('varsProject', []) as $var) { + $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); + } + + // Function vars + foreach ($function->getAttribute('vars', []) as $var) { + $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); + } + + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; + $hostname = System::getEnv('_APP_DOMAIN'); + $endpoint = $protocol . '://' . $hostname . "/v1"; + + // Appwrite vars + $vars = \array_merge($vars, [ + 'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint, + 'APPWRITE_FUNCTION_ID' => $functionId, + 'APPWRITE_FUNCTION_NAME' => $function->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' => $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + 'APPWRITE_FUNCTION_MEMORY' => $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + 'APPWRITE_VERSION' => APP_VERSION_STABLE, + 'APPWRITE_REGION' => $project->getAttribute('region'), + 'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''), + 'APPWRITE_VCS_REPOSITORY_ID' => $deployment->getAttribute('providerRepositoryId', ''), + 'APPWRITE_VCS_REPOSITORY_NAME' => $deployment->getAttribute('providerRepositoryName', ''), + 'APPWRITE_VCS_REPOSITORY_OWNER' => $deployment->getAttribute('providerRepositoryOwner', ''), + 'APPWRITE_VCS_REPOSITORY_URL' => $deployment->getAttribute('providerRepositoryUrl', ''), + 'APPWRITE_VCS_REPOSITORY_BRANCH' => $deployment->getAttribute('providerBranch', ''), + 'APPWRITE_VCS_REPOSITORY_BRANCH_URL' => $deployment->getAttribute('providerBranchUrl', ''), + 'APPWRITE_VCS_COMMIT_HASH' => $deployment->getAttribute('providerCommitHash', ''), + 'APPWRITE_VCS_COMMIT_MESSAGE' => $deployment->getAttribute('providerCommitMessage', ''), + 'APPWRITE_VCS_COMMIT_URL' => $deployment->getAttribute('providerCommitUrl', ''), + 'APPWRITE_VCS_COMMIT_AUTHOR_NAME' => $deployment->getAttribute('providerCommitAuthor', ''), + 'APPWRITE_VCS_COMMIT_AUTHOR_URL' => $deployment->getAttribute('providerCommitAuthorUrl', ''), + 'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''), + ]); + + /** 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 . '"'; + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + deploymentId: $deployment->getId(), + body: \strlen($body) > 0 ? $body : null, + variables: $vars, + timeout: $function->getAttribute('timeout', 0), + image: $runtime['image'], + source: $build->getAttribute('path', ''), + entrypoint: $deployment->getAttribute('entrypoint', ''), + version: $version, + path: $path, + method: $method, + headers: $headers, + runtimeEntrypoint: $command, + cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + logging: $function->getAttribute('logging', true), + requestTimeout: 30 + ); + + $headersFiltered = []; + foreach ($executionResponse['headers'] as $key => $value) { + if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { + $headersFiltered[] = ['name' => $key, 'value' => $value]; + } + } + + /** Update execution status */ + $status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed'; + $execution->setAttribute('status', $status); + $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); + $execution->setAttribute('responseHeaders', $headersFiltered); + $execution->setAttribute('logs', $executionResponse['logs']); + $execution->setAttribute('errors', $executionResponse['errors']); + $execution->setAttribute('duration', $executionResponse['duration']); + } catch (\Throwable $th) { + $durationEnd = \microtime(true); + + $execution + ->setAttribute('duration', $durationEnd - $durationStart) + ->setAttribute('status', 'failed') + ->setAttribute('responseStatusCode', 500) + ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); + Console::error($th->getMessage()); + + if ($th instanceof AppwriteException) { + throw $th; + } + } finally { + $queueForStatsUsage + ->addMetric(METRIC_EXECUTIONS, 1) + ->addMetric(str_replace('{functionInternalId}', $function->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(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) + ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) + ; + + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + } + + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + + if (!$isPrivilegedUser && !$isAppUser) { + $execution->setAttribute('logs', ''); + $execution->setAttribute('errors', ''); + } + + $headers = []; + foreach (($executionResponse['headers'] ?? []) as $key => $value) { + $headers[] = ['name' => $key, 'value' => $value]; + } + + $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); + $execution->setAttribute('responseHeaders', $headers); + + $acceptTypes = \explode(', ', $request->getHeader('accept')); + foreach ($acceptTypes as $acceptType) { + if (\str_starts_with($acceptType, 'application/json') || \str_starts_with($acceptType, 'application/*')) { + $response->setContentType(Response::CONTENT_TYPE_JSON); + break; + } elseif (\str_starts_with($acceptType, 'multipart/form-data') || \str_starts_with($acceptType, 'multipart/*')) { + $response->setContentType(Response::CONTENT_TYPE_MULTIPART); + break; + } + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($execution, Response::MODEL_EXECUTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php new file mode 100644 index 0000000000..7b9fd77a25 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php @@ -0,0 +1,116 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/functions/:functionId/executions/:executionId') + ->desc('Delete execution') + ->groups(['api', 'functions']) + ->label('scope', 'execution.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].executions.[executionId].delete') + ->label('audits.event', 'executions.delete') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'deleteExecution', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('executionId', '', new UID(), 'Execution ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $execution = $dbForProject->getDocument('executions', $executionId); + if ($execution->isEmpty()) { + throw new Exception(Exception::EXECUTION_NOT_FOUND); + } + + if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) { + throw new Exception(Exception::EXECUTION_NOT_FOUND); + } + $status = $execution->getAttribute('status'); + + if (!in_array($status, ['completed', 'failed', 'scheduled'])) { + throw new Exception(Exception::EXECUTION_IN_PROGRESS); + } + + if (!$dbForProject->deleteDocument('executions', $execution->getId())) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove execution from DB'); + } + + if ($status === 'scheduled') { + $schedule = $dbForPlatform->findOne('schedules', [ + Query::equal('resourceId', [$execution->getId()]), + Query::equal('resourceType', [ScheduleExecutions::getSupportedResource()]), + Query::equal('active', [true]), + ]); + + if (!$schedule->isEmpty()) { + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('active', false); + + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + } + } + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('executionId', $execution->getId()) + ->setPayload($response->output($execution, Response::MODEL_EXECUTION)); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php new file mode 100644 index 0000000000..da58d86414 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php @@ -0,0 +1,88 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/executions/:executionId') + ->desc('Get execution') + ->groups(['api', 'functions']) + ->label('scope', 'execution.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'getExecution', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('executionId', '', new UID(), 'Execution ID.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $executionId, Response $response, Database $dbForProject) + { + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $execution = $dbForProject->getDocument('executions', $executionId); + + if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) { + throw new Exception(Exception::EXECUTION_NOT_FOUND); + } + + if ($execution->isEmpty()) { + throw new Exception(Exception::EXECUTION_NOT_FOUND); + } + + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + if (!$isPrivilegedUser && !$isAppUser) { + $execution->setAttribute('logs', ''); + $execution->setAttribute('errors', ''); + } + + $response->dynamic($execution, Response::MODEL_EXECUTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php new file mode 100644 index 0000000000..597e830dd3 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php @@ -0,0 +1,135 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/executions') + ->desc('List executions') + ->groups(['api', 'functions']) + ->label('scope', 'execution.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'listExecutions', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('queries', [], new Executions(), '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(', ', Executions::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 $functionId, array $queries, string $search, Response $response, Database $dbForProject) + { + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::FUNCTION_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 internal queries + $queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]); + $queries[] = Query::equal('resourceType', ['functions']); + + /** + * 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()); + } + + $executionId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('executions', $executionId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Execution '{$executionId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + $results = $dbForProject->find('executions', $queries); + $total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT); + + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + if (!$isPrivilegedUser && !$isAppUser) { + $results = array_map(function ($execution) { + $execution->setAttribute('logs', ''); + $execution->setAttribute('errors', ''); + return $execution; + }, $results); + } + + $response->dynamic(new Document([ + 'executions' => $results, + 'total' => $total, + ]), Response::MODEL_EXECUTION_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php new file mode 100644 index 0000000000..92c99133b7 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php @@ -0,0 +1,93 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/functions/:functionId') + ->desc('Delete function') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].delete') + ->label('audits.event', 'function.delete') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'delete', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDeletes') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $functionId, Response $response, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Database $dbForPlatform) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + if (!$dbForProject->deleteDocument('functions', $function->getId())) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove function from DB'); + } + + // Inform scheduler to no longer run function + $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('active', false); + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($function); + + $queueForEvents->setParam('functionId', $function->getId()); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Get.php new file mode 100644 index 0000000000..77c673b614 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Get.php @@ -0,0 +1,64 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId') + ->desc('Get function') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'get', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $response->dynamic($function, Response::MODEL_FUNCTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Specifications/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Specifications/XList.php new file mode 100644 index 0000000000..dbaf3daa04 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Specifications/XList.php @@ -0,0 +1,76 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/specifications') + ->groups(['api', 'functions']) + ->desc('List available function runtime specifications') + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'listSpecifications', + description: <<inject('response') + ->inject('plan') + ->callback([$this, 'action']); + } + + public function action(Response $response, array $plan) + { + $allRuntimeSpecs = Config::getParam('runtime-specifications', []); + + $runtimeSpecs = []; + foreach ($allRuntimeSpecs as $spec) { + $spec['enabled'] = true; + + if (array_key_exists('runtimeSpecifications', $plan)) { + $spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']); + } + + // Only add specs that are within the limits set by environment variables + if ($spec['cpus'] <= System::getEnv('_APP_COMPUTE_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_COMPUTE_MEMORY', 512)) { + $runtimeSpecs[] = $spec; + } + } + + $response->dynamic(new Document([ + 'specifications' => $runtimeSpecs, + 'total' => count($runtimeSpecs) + ]), Response::MODEL_SPECIFICATION_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Templates/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Templates/Get.php new file mode 100644 index 0000000000..e624b26de6 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Templates/Get.php @@ -0,0 +1,69 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/templates/:templateId') + ->desc('Get function template') + ->label('scope', 'public') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'getTemplate', + description: <<param('templateId', '', new Text(128), 'Template ID.') + ->inject('response') + ->callback([$this, 'action']); + } + + public function action(string $templateId, Response $response) + { + $templates = Config::getParam('function-templates', []); + + $filtered = \array_filter($templates, function ($template) use ($templateId) { + return $template['id'] === $templateId; + }); + + $template = array_shift($filtered); + + if (empty($template)) { + throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND); + } + + $response->dynamic(new Document($template), Response::MODEL_TEMPLATE_FUNCTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php new file mode 100644 index 0000000000..310ed89fdf --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php @@ -0,0 +1,79 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/templates') + ->desc('List function templates') + ->label('scope', 'public') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'listTemplates', + description: <<param('runtimes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('runtimes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of runtimes allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' runtimes 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 function 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 $runtimes, array $usecases, int $limit, int $offset, Response $response) + { + $templates = Config::getParam('function-templates', []); + + if (!empty($runtimes)) { + $templates = \array_filter($templates, function ($template) use ($runtimes) { + return \count(\array_intersect($runtimes, \array_column($template['runtimes'], '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_FUNCTION_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php new file mode 100644 index 0000000000..88221ccac0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php @@ -0,0 +1,149 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/usage') + ->desc('Get function usage') + ->groups(['api', 'functions', 'usage']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'getUsage', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $range, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $periods = Config::getParam('usage', []); + $stats = $usage = []; + $days = $periods[$range]; + $metrics = [ + str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS), + str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_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'], + 'executionsTotal' => $usage[$metrics[5]]['total'], + 'executionsTimeTotal' => $usage[$metrics[6]]['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'], + 'executions' => $usage[$metrics[5]]['data'], + 'executionsTime' => $usage[$metrics[6]]['data'], + 'buildsMbSecondsTotal' => $usage[$metrics[7]]['total'], + 'buildsMbSeconds' => $usage[$metrics[7]]['data'], + 'executionsMbSeconds' => $usage[$metrics[8]]['data'], + 'executionsMbSecondsTotal' => $usage[$metrics[8]]['total'] + ]), Response::MODEL_USAGE_FUNCTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php new file mode 100644 index 0000000000..ca94b59a96 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php @@ -0,0 +1,142 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/usage') + ->desc('Get functions usage') + ->groups(['api', 'functions', 'usage']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'listUsage', + description: <<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_FUNCTIONS, + METRIC_DEPLOYMENTS, + METRIC_DEPLOYMENTS_STORAGE, + METRIC_BUILDS, + METRIC_BUILDS_STORAGE, + METRIC_BUILDS_COMPUTE, + METRIC_EXECUTIONS, + METRIC_EXECUTIONS_COMPUTE, + METRIC_BUILDS_MB_SECONDS, + METRIC_EXECUTIONS_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, + 'functionsTotal' => $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'], + 'executionsTotal' => $usage[$metrics[6]]['total'], + 'executionsTimeTotal' => $usage[$metrics[7]]['total'], + 'functions' => $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'], + 'executions' => $usage[$metrics[6]]['data'], + 'executionsTime' => $usage[$metrics[7]]['data'], + 'buildsMbSecondsTotal' => $usage[$metrics[8]]['total'], + 'buildsMbSeconds' => $usage[$metrics[8]]['data'], + 'executionsMbSeconds' => $usage[$metrics[9]]['data'], + 'executionsMbSecondsTotal' => $usage[$metrics[9]]['total'], + ]), Response::MODEL_USAGE_FUNCTIONS); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php new file mode 100644 index 0000000000..ba48a5c4bc --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php @@ -0,0 +1,115 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/functions/:functionId/variables') + ->desc('Create variable') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('audits.event', 'variable.create') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'createVariable', + description: <<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', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $variableId = ID::unique(); + + $variable = new Document([ + '$id' => $variableId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceInternalId' => $function->getInternalId(), + 'resourceId' => $function->getId(), + 'resourceType' => 'function', + 'key' => $key, + 'value' => $value, + 'secret' => $secret, + 'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']), + ]); + + try { + $variable = $dbForProject->createDocument('variables', $variable); + } catch (DuplicateException $th) { + throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); + } + + $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + + // Inform scheduler to pull the latest changes + $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($variable, Response::MODEL_VARIABLE); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php new file mode 100644 index 0000000000..650eaf4010 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php @@ -0,0 +1,93 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/functions/:functionId/variables/:variableId') + ->desc('Delete variable') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('audits.event', 'variable.delete') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'deleteVariable', + description: <<param('functionId', '', new UID(), 'Function unique ID.', false) + ->param('variableId', '', new UID(), 'Variable unique ID.', false) + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForPlatform) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $variable = $dbForProject->getDocument('variables', $variableId); + if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') { + 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('functions', $function->getId(), $function->setAttribute('live', false)); + + // Inform scheduler to pull the latest changes + $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Get.php new file mode 100644 index 0000000000..eeb5c51f33 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Get.php @@ -0,0 +1,82 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/variables/:variableId') + ->desc('Get variable') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label( + 'sdk', + new Method( + namespace: 'functions', + name: 'getVariable', + description: <<param('functionId', '', new UID(), 'Function unique ID.', false) + ->param('variableId', '', new UID(), 'Variable unique ID.', false) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $variableId, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $variable = $dbForProject->getDocument('variables', $variableId); + if ( + $variable === false || + $variable->isEmpty() || + $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || + $variable->getAttribute('resourceType') !== 'function' + ) { + 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); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php new file mode 100644 index 0000000000..8911dc8abb --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php @@ -0,0 +1,111 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) + ->setHttpPath('/v1/functions/:functionId/variables/:variableId') + ->desc('Update variable') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('audits.event', 'variable.update') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'updateVariable', + description: <<param('functionId', '', new UID(), 'Function 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) + ->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $variableId, string $key, ?string $value, ?bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $variable = $dbForProject->getDocument('variables', $variableId); + if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') { + throw new Exception(Exception::VARIABLE_NOT_FOUND); + } + + if ($variable === false || $variable->isEmpty()) { + throw new Exception(Exception::VARIABLE_NOT_FOUND); + } + + if ($variable->getAttribute('secret') === true && $secret === false) { + throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET); + } + + $variable + ->setAttribute('key', $key) + ->setAttribute('value', $value ?? $variable->getAttribute('value')) + ->setAttribute('secret', $secret ?? $variable->getAttribute('secret')) + ->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); + + try { + $dbForProject->updateDocument('variables', $variable->getId(), $variable); + } catch (DuplicateException $th) { + throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); + } + + $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + + // Inform scheduler to pull the latest changes + $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + + $response->dynamic($variable, Response::MODEL_VARIABLE); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php new file mode 100644 index 0000000000..000e83a0c9 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php @@ -0,0 +1,71 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/variables') + ->desc('List variables') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label( + 'sdk', + new Method( + namespace: 'functions', + name: 'listVariables', + description: <<param('functionId', '', new UID(), 'Function unique ID.', false) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $response->dynamic(new Document([ + 'variables' => $function->getAttribute('vars', []), + 'total' => \count($function->getAttribute('vars', [])), + ]), Response::MODEL_VARIABLE_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Services/Http.php b/src/Appwrite/Platform/Modules/Functions/Services/Http.php index 629817827c..5ae77c5d6b 100644 --- a/src/Appwrite/Platform/Modules/Functions/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Functions/Services/Http.php @@ -2,13 +2,36 @@ namespace Appwrite\Platform\Modules\Functions\Services; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Create as CreateBuild; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Update as UpdateBuild; use Appwrite\Platform\Modules\Functions\Http\Deployments\Create as CreateDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Delete as DeleteDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Download\Get as DownloadDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Get as GetDeployment; use Appwrite\Platform\Modules\Functions\Http\Deployments\Template\Create as CreateTemplateDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Update as UpdateDeployment; use Appwrite\Platform\Modules\Functions\Http\Deployments\Vcs\Create as CreateVcsDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\XList as ListDeployments; +use Appwrite\Platform\Modules\Functions\Http\Executions\Create as CreateExecution; +use Appwrite\Platform\Modules\Functions\Http\Executions\Delete as DeleteExecution; +use Appwrite\Platform\Modules\Functions\Http\Executions\Get as GetExecution; +use Appwrite\Platform\Modules\Functions\Http\Executions\XList as ListExecutions; use Appwrite\Platform\Modules\Functions\Http\Functions\Create as CreateFunction; +use Appwrite\Platform\Modules\Functions\Http\Functions\Delete as DeleteFunction; +use Appwrite\Platform\Modules\Functions\Http\Functions\Get as GetFunction; use Appwrite\Platform\Modules\Functions\Http\Functions\Update as UpdateFunction; use Appwrite\Platform\Modules\Functions\Http\Functions\XList as ListFunctions; use Appwrite\Platform\Modules\Functions\Http\Runtimes\XList as ListRuntimes; +use Appwrite\Platform\Modules\Functions\Http\Specifications\XList as ListSpecifications; +use Appwrite\Platform\Modules\Functions\Http\Templates\Get as GetTemplate; +use Appwrite\Platform\Modules\Functions\Http\Templates\XList as ListTemplates; +use Appwrite\Platform\Modules\Functions\Http\Usage\Get as GetUsage; +use Appwrite\Platform\Modules\Functions\Http\Usage\XList as ListUsage; +use Appwrite\Platform\Modules\Functions\Http\Variables\Create as CreateVariable; +use Appwrite\Platform\Modules\Functions\Http\Variables\Delete as DeleteVariable; +use Appwrite\Platform\Modules\Functions\Http\Variables\Get as GetVariable; +use Appwrite\Platform\Modules\Functions\Http\Variables\Update as UpdateVariable; +use Appwrite\Platform\Modules\Functions\Http\Variables\XList as ListVariables; use Utopia\Platform\Service; class Http extends Service @@ -16,12 +39,51 @@ class Http extends Service public function __construct() { $this->type = Service::TYPE_HTTP; + + // Functions $this->addAction(CreateFunction::getName(), new CreateFunction()); + $this->addAction(GetFunction::getName(), new GetFunction()); $this->addAction(UpdateFunction::getName(), new UpdateFunction()); $this->addAction(ListFunctions::getName(), new ListFunctions()); + $this->addAction(DeleteFunction::getName(), new DeleteFunction()); + + // Runtimes $this->addAction(ListRuntimes::getName(), new ListRuntimes()); + + // Specifications + $this->addAction(ListSpecifications::getName(), new ListSpecifications()); + + // Deployments $this->addAction(CreateDeployment::getName(), new CreateDeployment()); + $this->addAction(GetDeployment::getName(), new GetDeployment()); + $this->addAction(UpdateDeployment::getName(), new UpdateDeployment()); + $this->addAction(ListDeployments::getName(), new ListDeployments()); + $this->addAction(DeleteDeployment::getName(), new DeleteDeployment()); $this->addAction(CreateTemplateDeployment::getName(), new CreateTemplateDeployment()); $this->addAction(CreateVcsDeployment::getName(), new CreateVcsDeployment()); + $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); + $this->addAction(CreateBuild::getName(), new CreateBuild()); + $this->addAction(UpdateBuild::getName(), new UpdateBuild()); + + // Executions + $this->addAction(CreateExecution::getName(), new CreateExecution()); + $this->addAction(GetExecution::getName(), new GetExecution()); + $this->addAction(ListExecutions::getName(), new ListExecutions()); + $this->addAction(DeleteExecution::getName(), new DeleteExecution()); + + // Usage + $this->addAction(GetUsage::getName(), new GetUsage()); + $this->addAction(ListUsage::getName(), new ListUsage()); + + // 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(GetTemplate::getName(), new GetTemplate()); + $this->addAction(ListTemplates::getName(), new ListTemplates()); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php index 423cf0cb32..4f83cb7ff3 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php @@ -8,7 +8,6 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; 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; @@ -47,13 +46,11 @@ class Get extends Action ->param('siteId', '', new UID(), 'Site ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') - ->inject('project') ->inject('dbForProject') - ->inject('dbForPlatform') ->callback([$this, 'action']); } - public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) + public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject) { $site = $dbForProject->getDocument('sites', $siteId); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Update.php index 9e1c516956..846b552eb0 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Update.php @@ -44,7 +44,7 @@ class Update extends Action responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, - model: Response::MODEL_FUNCTION, + model: Response::MODEL_SITE, ) ] )) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php index d96e8701ba..64f7c7c91c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php @@ -53,13 +53,11 @@ class XList extends Action ->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('project') ->inject('dbForProject') - ->inject('dbForPlatform') ->callback([$this, 'action']); } - public function action(string $siteId, array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) + public function action(string $siteId, array $queries, string $search, Response $response, Database $dbForProject) { $site = $dbForProject->getDocument('sites', $siteId); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Templates/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Templates/Get.php index 977bfe7fd1..9d2991bce9 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Templates/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Templates/Get.php @@ -29,7 +29,6 @@ class Get extends Base ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/sites/templates/:templateId') ->desc('Get site template') - ->groups(['api']) ->label('scope', 'public') ->label('sdk', new Method( namespace: 'sites', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php index 277cc2c615..3c27b75578 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php @@ -30,7 +30,6 @@ class XList extends Base ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/sites/templates') ->desc('List templates') - ->groups(['api']) ->label('scope', 'public') ->label('sdk', new Method( namespace: 'sites', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php index 461bcdfdd9..3689343831 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php @@ -31,6 +31,7 @@ class Delete extends Base ->desc('Delete variable') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('resourceType', 'sites') ->label('audits.event', 'variable.delete') ->label('audits.resource', 'site/{request.siteId}') ->label('sdk', new Method( diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Get.php index ca5ad09663..1e56cc5b15 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Get.php @@ -30,6 +30,7 @@ class Get extends Base ->desc('Get variable') ->groups(['api', 'sites']) ->label('scope', 'sites.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label( 'sdk', new Method( diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php index 4040a8ae71..d64af650e3 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php @@ -35,6 +35,7 @@ class Update extends Base ->label('scope', 'sites.write') ->label('audits.event', 'variable.update') ->label('audits.resource', 'site/{request.siteId}') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'updateVariable',