diff --git a/.env b/.env index f6a6a7f642..6b6a4cfa47 100644 --- a/.env +++ b/.env @@ -21,6 +21,7 @@ _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled _APP_OPENSSL_KEY_V1=your-secret-key _APP_DOMAIN=traefik _APP_DOMAIN_FUNCTIONS=functions.localhost +_APP_DOMAIN_SITES=sites.localhost _APP_DOMAIN_TARGET=localhost _APP_REDIS_HOST=redis _APP_REDIS_PORT=6379 @@ -77,6 +78,7 @@ _APP_FUNCTIONS_RUNTIMES_NETWORK=runtimes _APP_EXECUTOR_SECRET=your-secret-key _APP_EXECUTOR_HOST=http://proxy/v1 _APP_FUNCTIONS_RUNTIMES=php-8.0,node-18.0,python-3.9,ruby-3.1 +_APP_SITES_FRAMEWORKS=sveltekit,nextjs _APP_MAINTENANCE_INTERVAL=86400 _APP_MAINTENANCE_DELAY= _APP_MAINTENANCE_RETENTION_CACHE=2592000 diff --git a/app/config/collections.php b/app/config/collections.php index 3efcc52a41..83925b07ed 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -3282,16 +3282,16 @@ $projectCollections = array_merge([ 'default' => false, 'array' => false, ], - [ - '$id' => ID::custom('logging'), - 'type' => Database::VAR_BOOLEAN, - 'signed' => true, - 'size' => 0, - 'format' => '', - 'filters' => [], - 'required' => true, - 'array' => false, - ], + // [ + // '$id' => ID::custom('logging'), + // 'type' => Database::VAR_BOOLEAN, + // 'signed' => true, + // 'size' => 0, + // 'format' => '', + // 'filters' => [], + // 'required' => true, + // 'array' => false, + // ], [ '$id' => ID::custom('framework'), 'type' => Database::VAR_STRING, @@ -3304,14 +3304,15 @@ $projectCollections = array_merge([ 'filters' => [], ], [ + 'array' => false, '$id' => ID::custom('outputDirectory'), 'type' => Database::VAR_STRING, - 'signed' => true, - 'size' => Database::LENGTH_KEY, 'format' => '', - 'filters' => [], + 'size' => 16384, + 'signed' => true, 'required' => false, - 'array' => false, + 'default' => null, + 'filters' => [], ], [ 'array' => false, @@ -3489,9 +3490,9 @@ $projectCollections = array_merge([ 'orders' => [Database::ORDER_ASC], ], [ - '$id' => ID::custom('_key_deployment'), + '$id' => ID::custom('_key_deploymentId'), 'type' => Database::INDEX_KEY, - 'attributes' => ['deployment'], + 'attributes' => ['deploymentId'], 'lengths' => [], 'orders' => [Database::ORDER_ASC], ] @@ -3585,7 +3586,7 @@ $projectCollections = array_merge([ '$id' => ID::custom('buildCommand'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 2048, + 'size' => 16384, 'signed' => true, 'required' => false, 'default' => null, @@ -3596,7 +3597,18 @@ $projectCollections = array_merge([ '$id' => ID::custom('installCommand'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 2048, + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'filters' => [], + ], + [ + 'array' => false, + '$id' => ID::custom('outputDirectory'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, 'signed' => true, 'required' => false, 'default' => null, diff --git a/app/config/errors.php b/app/config/errors.php index fc79599b12..6e05d87052 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -535,6 +535,13 @@ return [ 'code' => 404, ], + /** Sites */ + Exception::SITE_FRAMEWORK_UNSUPPORTED => [ + 'name' => Exception::SITE_FRAMEWORK_UNSUPPORTED, + 'description' => 'The requested framework is either inactive or unsupported. Please check the value of the _APP_SITES_FRAMEWORKS environment variable.', + 'code' => 404, + ], + /** Builds */ Exception::BUILD_NOT_FOUND => [ 'name' => Exception::BUILD_NOT_FOUND, diff --git a/app/config/events.php b/app/config/events.php index 5378502faf..0bfddf4f1f 100644 --- a/app/config/events.php +++ b/app/config/events.php @@ -217,6 +217,34 @@ return [ ], ] ], + 'sites' => [ + '$model' => Response::MODEL_SITE, + '$resource' => true, + '$description' => 'This event triggers on any sites event.', + 'deployments' => [ + '$model' => Response::MODEL_DEPLOYMENT, + '$resource' => true, + '$description' => 'This event triggers on any deployments event.', + 'create' => [ + '$description' => 'This event triggers when a deployment is created.', + ], + 'delete' => [ + '$description' => 'This event triggers when a deployment is deleted.' + ], + 'update' => [ + '$description' => 'This event triggers when a deployment is updated.' + ], + ], + 'create' => [ + '$description' => 'This event triggers when a site is created.' + ], + 'delete' => [ + '$description' => 'This event triggers when a site is deleted.', + ], + 'update' => [ + '$description' => 'This event triggers when a site is updated.', + ] + ], 'functions' => [ '$model' => Response::MODEL_FUNCTION, '$resource' => true, diff --git a/app/config/frameworks.php b/app/config/frameworks.php new file mode 100644 index 0000000000..895c0cdf30 --- /dev/null +++ b/app/config/frameworks.php @@ -0,0 +1,7 @@ +getAttribute('entrypoint', ''); - $providerInstallationId = $installation->getAttribute('providerInstallationId', ''); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - $owner = $github->getOwnerName($providerInstallationId); - $providerRepositoryId = $function->getAttribute('providerRepositoryId', ''); - try { - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - if (empty($repositoryName)) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - } catch (RepositoryNotFound $e) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - $providerBranch = $function->getAttribute('providerBranch', 'main'); - $authorUrl = "https://github.com/$owner"; - $repositoryUrl = "https://github.com/$owner/$repositoryName"; - $branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch"; +// $redeployVcs = function (Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, Build $queueForBuilds, Document $template, GitHub $github) { +// $deploymentId = ID::unique(); +// $entrypoint = $function->getAttribute('entrypoint', ''); +// $providerInstallationId = $installation->getAttribute('providerInstallationId', ''); +// $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); +// $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); +// $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); +// $owner = $github->getOwnerName($providerInstallationId); +// $providerRepositoryId = $function->getAttribute('providerRepositoryId', ''); +// try { +// $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; +// if (empty($repositoryName)) { +// throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); +// } +// } catch (RepositoryNotFound $e) { +// throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); +// } +// $providerBranch = $function->getAttribute('providerBranch', 'main'); +// $authorUrl = "https://github.com/$owner"; +// $repositoryUrl = "https://github.com/$owner/$repositoryName"; +// $branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch"; - $commitDetails = []; - if ($template->isEmpty()) { - try { - $commitDetails = $github->getLatestCommit($owner, $repositoryName, $providerBranch); - } catch (\Throwable $error) { - Console::warning('Failed to get latest commit details'); - Console::warning($error->getMessage()); - Console::warning($error->getTraceAsString()); - } - } +// $commitDetails = []; +// if ($template->isEmpty()) { +// try { +// $commitDetails = $github->getLatestCommit($owner, $repositoryName, $providerBranch); +// } catch (\Throwable $error) { +// Console::warning('Failed to get latest commit details'); +// Console::warning($error->getMessage()); +// Console::warning($error->getTraceAsString()); +// } +// } - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceId' => $function->getId(), - 'resourceInternalId' => $function->getInternalId(), - 'resourceType' => 'functions', - 'entrypoint' => $entrypoint, - 'commands' => $function->getAttribute('commands', ''), - 'type' => 'vcs', - 'installationId' => $installation->getId(), - 'installationInternalId' => $installation->getInternalId(), - 'providerRepositoryId' => $providerRepositoryId, - 'repositoryId' => $function->getAttribute('repositoryId', ''), - 'repositoryInternalId' => $function->getAttribute('repositoryInternalId', ''), - 'providerBranchUrl' => $branchUrl, - 'providerRepositoryName' => $repositoryName, - 'providerRepositoryOwner' => $owner, - 'providerRepositoryUrl' => $repositoryUrl, - 'providerCommitHash' => $commitDetails['commitHash'] ?? '', - 'providerCommitAuthorUrl' => $authorUrl, - 'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '', - 'providerCommitMessage' => $commitDetails['commitMessage'] ?? '', - 'providerCommitUrl' => $commitDetails['commitUrl'] ?? '', - 'providerBranch' => $providerBranch, - 'providerRootDirectory' => $function->getAttribute('providerRootDirectory', ''), - 'search' => implode(' ', [$deploymentId, $entrypoint]), - 'activate' => true, - ])); +// $deployment = $dbForProject->createDocument('deployments', new Document([ +// '$id' => $deploymentId, +// '$permissions' => [ +// Permission::read(Role::any()), +// Permission::update(Role::any()), +// Permission::delete(Role::any()), +// ], +// 'resourceId' => $function->getId(), +// 'resourceInternalId' => $function->getInternalId(), +// 'resourceType' => 'functions', +// 'entrypoint' => $entrypoint, +// 'commands' => $function->getAttribute('commands', ''), +// 'type' => 'vcs', +// 'installationId' => $installation->getId(), +// 'installationInternalId' => $installation->getInternalId(), +// 'providerRepositoryId' => $providerRepositoryId, +// 'repositoryId' => $function->getAttribute('repositoryId', ''), +// 'repositoryInternalId' => $function->getAttribute('repositoryInternalId', ''), +// 'providerBranchUrl' => $branchUrl, +// 'providerRepositoryName' => $repositoryName, +// 'providerRepositoryOwner' => $owner, +// 'providerRepositoryUrl' => $repositoryUrl, +// 'providerCommitHash' => $commitDetails['commitHash'] ?? '', +// 'providerCommitAuthorUrl' => $authorUrl, +// 'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '', +// 'providerCommitMessage' => $commitDetails['commitMessage'] ?? '', +// 'providerCommitUrl' => $commitDetails['commitUrl'] ?? '', +// 'providerBranch' => $providerBranch, +// 'providerRootDirectory' => $function->getAttribute('providerRootDirectory', ''), +// 'search' => implode(' ', [$deploymentId, $entrypoint]), +// 'activate' => true, +// ])); - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($function) - ->setDeployment($deployment) - ->setTemplate($template); -}; +// $queueForBuilds +// ->setType(BUILD_TYPE_DEPLOYMENT) +// ->setResource($function) +// ->setDeployment($deployment) +// ->setTemplate($template); +// }; -App::post('/v1/functions') - ->groups(['api', 'functions']) - ->desc('Create function') - ->label('scope', 'functions.write') - ->label('event', 'functions.[functionId].create') - ->label('audits.event', 'function.create') - ->label('audits.resource', 'function/{response.$id}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'create') - ->label('sdk.description', '/docs/references/functions/create-function.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_FUNCTION) - ->param('functionId', '', new CustomId(), 'Function ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') - ->param('name', '', new Text(128), 'Function name. Max length: 128 chars.') - ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.') - ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) - ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) - ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) - ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true) - ->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true) - ->param('logging', true, new Boolean(), 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.', true) - ->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true) - ->param('commands', '', new Text(8192, 0), 'Build Commands.', true) - ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true) - ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) - ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the function.', true) - ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true) - ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) - ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) - ->param('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true) - ->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true) - ->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true) - ->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) - ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( - $plan, - Config::getParam('runtime-specifications', []), - App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), - App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) - ), 'Runtime specification for the function and builds.', true, ['plan']) - ->inject('request') - ->inject('response') - ->inject('dbForProject') - ->inject('project') - ->inject('user') - ->inject('queueForEvents') - ->inject('queueForBuilds') - ->inject('dbForConsole') - ->inject('gitHub') - ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) { - $functionId = ($functionId == 'unique()') ? ID::unique() : $functionId; +// App::post('/v1/functions') +// ->groups(['api', 'functions']) +// ->desc('Create function') +// ->label('scope', 'functions.write') +// ->label('event', 'functions.[functionId].create') +// ->label('audits.event', 'function.create') +// ->label('audits.resource', 'function/{response.$id}') +// ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) +// ->label('sdk.namespace', 'functions') +// ->label('sdk.method', 'create') +// ->label('sdk.description', '/docs/references/functions/create-function.md') +// ->label('sdk.response.code', Response::STATUS_CODE_CREATED) +// ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) +// ->label('sdk.response.model', Response::MODEL_FUNCTION) +// ->param('functionId', '', new CustomId(), 'Function ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') +// ->param('name', '', new Text(128), 'Function name. Max length: 128 chars.') +// ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.') +// ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) +// ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) +// ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) +// ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true) +// ->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true) +// ->param('logging', true, new Boolean(), 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.', true) +// ->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true) +// ->param('commands', '', new Text(8192, 0), 'Build Commands.', true) +// ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true) +// ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) +// ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the function.', true) +// ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true) +// ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) +// ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) +// ->param('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true) +// ->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true) +// ->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true) +// ->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) +// ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( +// $plan, +// Config::getParam('runtime-specifications', []), +// App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), +// App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) +// ), 'Runtime specification for the function and builds.', true, ['plan']) +// ->inject('request') +// ->inject('response') +// ->inject('dbForProject') +// ->inject('project') +// ->inject('user') +// ->inject('queueForEvents') +// ->inject('queueForBuilds') +// ->inject('dbForConsole') +// ->inject('gitHub') +// ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) { +// $functionId = ($functionId == 'unique()') ? ID::unique() : $functionId; - $allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', ''))); +// $allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', ''))); - if (!empty($allowList) && !\in_array($runtime, $allowList)) { - throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $runtime . '" is not supported'); - } +// if (!empty($allowList) && !\in_array($runtime, $allowList)) { +// throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $runtime . '" is not supported'); +// } - // build from template - $template = new Document([]); - if ( - !empty($templateRepository) - && !empty($templateOwner) - && !empty($templateRootDirectory) - && !empty($templateVersion) - ) { - $template->setAttribute('repositoryName', $templateRepository) - ->setAttribute('ownerName', $templateOwner) - ->setAttribute('rootDirectory', $templateRootDirectory) - ->setAttribute('version', $templateVersion); - } +// // build from template +// $template = new Document([]); +// if ( +// !empty($templateRepository) +// && !empty($templateOwner) +// && !empty($templateRootDirectory) +// && !empty($templateVersion) +// ) { +// $template->setAttribute('repositoryName', $templateRepository) +// ->setAttribute('ownerName', $templateOwner) +// ->setAttribute('rootDirectory', $templateRootDirectory) +// ->setAttribute('version', $templateVersion); +// } - $installation = $dbForConsole->getDocument('installations', $installationId); +// $installation = $dbForConsole->getDocument('installations', $installationId); - if (!empty($installationId) && $installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } +// if (!empty($installationId) && $installation->isEmpty()) { +// throw new Exception(Exception::INSTALLATION_NOT_FOUND); +// } - if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".'); - } +// if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) { +// throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".'); +// } - $function = $dbForProject->createDocument('functions', new Document([ - '$id' => $functionId, - 'execute' => $execute, - 'enabled' => $enabled, - 'live' => true, - 'logging' => $logging, - 'name' => $name, - 'runtime' => $runtime, - 'deploymentInternalId' => '', - 'deployment' => '', - 'events' => $events, - 'schedule' => $schedule, - 'scheduleInternalId' => '', - 'scheduleId' => '', - 'timeout' => $timeout, - 'entrypoint' => $entrypoint, - 'commands' => $commands, - 'scopes' => $scopes, - 'search' => implode(' ', [$functionId, $name, $runtime]), - 'version' => 'v4', - 'installationId' => $installation->getId(), - 'installationInternalId' => $installation->getInternalId(), - 'providerRepositoryId' => $providerRepositoryId, - 'repositoryId' => '', - 'repositoryInternalId' => '', - 'providerBranch' => $providerBranch, - 'providerRootDirectory' => $providerRootDirectory, - 'providerSilentMode' => $providerSilentMode, - 'specification' => $specification - ])); +// $function = $dbForProject->createDocument('functions', new Document([ +// '$id' => $functionId, +// 'execute' => $execute, +// 'enabled' => $enabled, +// 'live' => true, +// 'logging' => $logging, +// 'name' => $name, +// 'runtime' => $runtime, +// 'deploymentInternalId' => '', +// 'deployment' => '', +// 'events' => $events, +// 'schedule' => $schedule, +// 'scheduleInternalId' => '', +// 'scheduleId' => '', +// 'timeout' => $timeout, +// 'entrypoint' => $entrypoint, +// 'commands' => $commands, +// 'scopes' => $scopes, +// 'search' => implode(' ', [$functionId, $name, $runtime]), +// 'version' => 'v4', +// 'installationId' => $installation->getId(), +// 'installationInternalId' => $installation->getInternalId(), +// 'providerRepositoryId' => $providerRepositoryId, +// 'repositoryId' => '', +// 'repositoryInternalId' => '', +// 'providerBranch' => $providerBranch, +// 'providerRootDirectory' => $providerRootDirectory, +// 'providerSilentMode' => $providerSilentMode, +// 'specification' => $specification +// ])); - $schedule = Authorization::skip( - fn () => $dbForConsole->createDocument('schedules', new Document([ - 'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region - 'resourceType' => 'function', - 'resourceId' => $function->getId(), - 'resourceInternalId' => $function->getInternalId(), - 'resourceUpdatedAt' => DateTime::now(), - 'projectId' => $project->getId(), - 'schedule' => $function->getAttribute('schedule'), - 'active' => false, - ])) - ); +// $schedule = Authorization::skip( +// fn () => $dbForConsole->createDocument('schedules', new Document([ +// 'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region +// 'resourceType' => 'function', +// 'resourceId' => $function->getId(), +// 'resourceInternalId' => $function->getInternalId(), +// 'resourceUpdatedAt' => DateTime::now(), +// 'projectId' => $project->getId(), +// 'schedule' => $function->getAttribute('schedule'), +// 'active' => false, +// ])) +// ); - $function->setAttribute('scheduleId', $schedule->getId()); - $function->setAttribute('scheduleInternalId', $schedule->getInternalId()); +// $function->setAttribute('scheduleId', $schedule->getId()); +// $function->setAttribute('scheduleInternalId', $schedule->getInternalId()); - // Git connect logic - if (!empty($providerRepositoryId)) { - $teamId = $project->getAttribute('teamId', ''); +// // Git connect logic +// if (!empty($providerRepositoryId)) { +// $teamId = $project->getAttribute('teamId', ''); - $repository = $dbForConsole->createDocument('repositories', new Document([ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::team(ID::custom($teamId))), - Permission::update(Role::team(ID::custom($teamId), 'owner')), - Permission::update(Role::team(ID::custom($teamId), 'developer')), - Permission::delete(Role::team(ID::custom($teamId), 'owner')), - Permission::delete(Role::team(ID::custom($teamId), 'developer')), - ], - 'installationId' => $installation->getId(), - 'installationInternalId' => $installation->getInternalId(), - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'providerRepositoryId' => $providerRepositoryId, - 'resourceId' => $function->getId(), - 'resourceInternalId' => $function->getInternalId(), - 'resourceType' => 'function', - 'providerPullRequestIds' => [] - ])); +// $repository = $dbForConsole->createDocument('repositories', new Document([ +// '$id' => ID::unique(), +// '$permissions' => [ +// Permission::read(Role::team(ID::custom($teamId))), +// Permission::update(Role::team(ID::custom($teamId), 'owner')), +// Permission::update(Role::team(ID::custom($teamId), 'developer')), +// Permission::delete(Role::team(ID::custom($teamId), 'owner')), +// Permission::delete(Role::team(ID::custom($teamId), 'developer')), +// ], +// 'installationId' => $installation->getId(), +// 'installationInternalId' => $installation->getInternalId(), +// 'projectId' => $project->getId(), +// 'projectInternalId' => $project->getInternalId(), +// 'providerRepositoryId' => $providerRepositoryId, +// 'resourceId' => $function->getId(), +// 'resourceInternalId' => $function->getInternalId(), +// 'resourceType' => 'function', +// 'providerPullRequestIds' => [] +// ])); - $function->setAttribute('repositoryId', $repository->getId()); - $function->setAttribute('repositoryInternalId', $repository->getInternalId()); - } +// $function->setAttribute('repositoryId', $repository->getId()); +// $function->setAttribute('repositoryInternalId', $repository->getInternalId()); +// } - $function = $dbForProject->updateDocument('functions', $function->getId(), $function); +// $function = $dbForProject->updateDocument('functions', $function->getId(), $function); - if (!empty($providerRepositoryId)) { - // Deploy VCS - $redeployVcs($request, $function, $project, $installation, $dbForProject, $queueForBuilds, $template, $github); - } elseif (!$template->isEmpty()) { - // Deploy non-VCS from template - $deploymentId = ID::unique(); - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceId' => $function->getId(), - 'resourceInternalId' => $function->getInternalId(), - 'resourceType' => 'functions', - 'entrypoint' => $function->getAttribute('entrypoint', ''), - 'commands' => $function->getAttribute('commands', ''), - 'type' => 'manual', - 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint', '')]), - 'activate' => true, - ])); +// if (!empty($providerRepositoryId)) { +// // Deploy VCS +// $redeployVcs($request, $function, $project, $installation, $dbForProject, $queueForBuilds, $template, $github); +// } elseif (!$template->isEmpty()) { +// // Deploy non-VCS from template +// $deploymentId = ID::unique(); +// $deployment = $dbForProject->createDocument('deployments', new Document([ +// '$id' => $deploymentId, +// '$permissions' => [ +// Permission::read(Role::any()), +// Permission::update(Role::any()), +// Permission::delete(Role::any()), +// ], +// 'resourceId' => $function->getId(), +// 'resourceInternalId' => $function->getInternalId(), +// 'resourceType' => 'functions', +// 'entrypoint' => $function->getAttribute('entrypoint', ''), +// 'commands' => $function->getAttribute('commands', ''), +// 'type' => 'manual', +// 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint', '')]), +// 'activate' => true, +// ])); - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($function) - ->setDeployment($deployment) - ->setTemplate($template); - } +// $queueForBuilds +// ->setType(BUILD_TYPE_DEPLOYMENT) +// ->setResource($function) +// ->setDeployment($deployment) +// ->setTemplate($template); +// } - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - if (!empty($functionsDomain)) { - $ruleId = ID::unique(); - $routeSubdomain = ID::unique(); - $domain = "{$routeSubdomain}.{$functionsDomain}"; +// $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); +// if (!empty($functionsDomain)) { +// $ruleId = ID::unique(); +// $routeSubdomain = ID::unique(); +// $domain = "{$routeSubdomain}.{$functionsDomain}"; - $rule = Authorization::skip( - fn () => $dbForConsole->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $domain, - 'resourceType' => 'function', - 'resourceId' => $function->getId(), - 'resourceInternalId' => $function->getInternalId(), - 'status' => 'verified', - 'certificateId' => '', - ])) - ); +// $rule = Authorization::skip( +// fn () => $dbForConsole->createDocument('rules', new Document([ +// '$id' => $ruleId, +// 'projectId' => $project->getId(), +// 'projectInternalId' => $project->getInternalId(), +// 'domain' => $domain, +// 'resourceType' => 'function', +// 'resourceId' => $function->getId(), +// 'resourceInternalId' => $function->getInternalId(), +// 'status' => 'verified', +// 'certificateId' => '', +// ])) +// ); - /** Trigger Webhook */ - $ruleModel = new Rule(); - $ruleCreate = - $queueForEvents - ->setClass(Event::WEBHOOK_CLASS_NAME) - ->setQueue(Event::WEBHOOK_QUEUE_NAME); +// /** Trigger Webhook */ +// $ruleModel = new Rule(); +// $ruleCreate = +// $queueForEvents +// ->setClass(Event::WEBHOOK_CLASS_NAME) +// ->setQueue(Event::WEBHOOK_QUEUE_NAME); - $ruleCreate - ->setProject($project) - ->setEvent('rules.[ruleId].create') - ->setParam('ruleId', $rule->getId()) - ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) - ->trigger(); +// $ruleCreate +// ->setProject($project) +// ->setEvent('rules.[ruleId].create') +// ->setParam('ruleId', $rule->getId()) +// ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) +// ->trigger(); - /** Trigger Functions */ - $ruleCreate - ->setClass(Event::FUNCTIONS_CLASS_NAME) - ->setQueue(Event::FUNCTIONS_QUEUE_NAME) - ->trigger(); +// /** Trigger Functions */ +// $ruleCreate +// ->setClass(Event::FUNCTIONS_CLASS_NAME) +// ->setQueue(Event::FUNCTIONS_QUEUE_NAME) +// ->trigger(); - /** Trigger realtime event */ - $allEvents = Event::generateEvents('rules.[ruleId].create', [ - 'ruleId' => $rule->getId(), - ]); - $target = Realtime::fromPayload( - // Pass first, most verbose event pattern - event: $allEvents[0], - payload: $rule, - project: $project - ); - Realtime::send( - projectId: 'console', - payload: $rule->getArrayCopy(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'] - ); - Realtime::send( - projectId: $project->getId(), - payload: $rule->getArrayCopy(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'] - ); - } +// /** Trigger realtime event */ +// $allEvents = Event::generateEvents('rules.[ruleId].create', [ +// 'ruleId' => $rule->getId(), +// ]); +// $target = Realtime::fromPayload( +// // Pass first, most verbose event pattern +// event: $allEvents[0], +// payload: $rule, +// project: $project +// ); +// Realtime::send( +// projectId: 'console', +// payload: $rule->getArrayCopy(), +// events: $allEvents, +// channels: $target['channels'], +// roles: $target['roles'] +// ); +// Realtime::send( +// projectId: $project->getId(), +// payload: $rule->getArrayCopy(), +// events: $allEvents, +// channels: $target['channels'], +// roles: $target['roles'] +// ); +// } - $queueForEvents->setParam('functionId', $function->getId()); +// $queueForEvents->setParam('functionId', $function->getId()); - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($function, Response::MODEL_FUNCTION); - }); +// $response +// ->setStatusCode(Response::STATUS_CODE_CREATED) +// ->dynamic($function, Response::MODEL_FUNCTION); +// }); App::get('/v1/functions') ->groups(['api', 'functions']) @@ -752,207 +752,207 @@ App::get('/v1/functions/usage') ]), Response::MODEL_USAGE_FUNCTIONS); }); -App::put('/v1/functions/:functionId') - ->groups(['api', 'functions']) - ->desc('Update function') - ->label('scope', 'functions.write') - ->label('event', 'functions.[functionId].update') - ->label('audits.event', 'function.update') - ->label('audits.resource', 'function/{response.$id}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'update') - ->label('sdk.description', '/docs/references/functions/update-function.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('name', '', new Text(128), 'Function name. Max length: 128 chars.') - ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', true) - ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) - ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) - ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) - ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Maximum execution time in seconds.', true) - ->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true) - ->param('logging', true, new Boolean(), 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.', true) - ->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true) - ->param('commands', '', new Text(8192, 0), 'Build Commands.', true) - ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true) - ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Controle System) deployment.', true) - ->param('providerRepositoryId', null, new Nullable(new Text(128, 0)), 'Repository ID of the repo linked to the function', true) - ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true) - ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) - ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) - ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( - $plan, - Config::getParam('runtime-specifications', []), - App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), - App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) - ), 'Runtime specification for the function and builds.', true, ['plan']) - ->inject('request') - ->inject('response') - ->inject('dbForProject') - ->inject('project') - ->inject('queueForEvents') - ->inject('queueForBuilds') - ->inject('dbForConsole') - ->inject('gitHub') - ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) { - // TODO: If only branch changes, re-deploy - $function = $dbForProject->getDocument('functions', $functionId); +// App::put('/v1/functions/:functionId') +// ->groups(['api', 'functions']) +// ->desc('Update function') +// ->label('scope', 'functions.write') +// ->label('event', 'functions.[functionId].update') +// ->label('audits.event', 'function.update') +// ->label('audits.resource', 'function/{response.$id}') +// ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) +// ->label('sdk.namespace', 'functions') +// ->label('sdk.method', 'update') +// ->label('sdk.description', '/docs/references/functions/update-function.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('name', '', new Text(128), 'Function name. Max length: 128 chars.') +// ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', true) +// ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) +// ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) +// ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) +// ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Maximum execution time in seconds.', true) +// ->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true) +// ->param('logging', true, new Boolean(), 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.', true) +// ->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true) +// ->param('commands', '', new Text(8192, 0), 'Build Commands.', true) +// ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true) +// ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Controle System) deployment.', true) +// ->param('providerRepositoryId', null, new Nullable(new Text(128, 0)), 'Repository ID of the repo linked to the function', true) +// ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true) +// ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) +// ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) +// ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( +// $plan, +// Config::getParam('runtime-specifications', []), +// App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), +// App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) +// ), 'Runtime specification for the function and builds.', true, ['plan']) +// ->inject('request') +// ->inject('response') +// ->inject('dbForProject') +// ->inject('project') +// ->inject('queueForEvents') +// ->inject('queueForBuilds') +// ->inject('dbForConsole') +// ->inject('gitHub') +// ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) { +// // TODO: If only branch changes, re-deploy +// $function = $dbForProject->getDocument('functions', $functionId); - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } +// if ($function->isEmpty()) { +// throw new Exception(Exception::FUNCTION_NOT_FOUND); +// } - $installation = $dbForConsole->getDocument('installations', $installationId); +// $installation = $dbForConsole->getDocument('installations', $installationId); - if (!empty($installationId) && $installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } +// if (!empty($installationId) && $installation->isEmpty()) { +// throw new Exception(Exception::INSTALLATION_NOT_FOUND); +// } - if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".'); - } +// if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) { +// throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".'); +// } - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } +// if ($function->isEmpty()) { +// throw new Exception(Exception::FUNCTION_NOT_FOUND); +// } - if (empty($runtime)) { - $runtime = $function->getAttribute('runtime'); - } +// if (empty($runtime)) { +// $runtime = $function->getAttribute('runtime'); +// } - $enabled ??= $function->getAttribute('enabled', true); +// $enabled ??= $function->getAttribute('enabled', true); - $repositoryId = $function->getAttribute('repositoryId', ''); - $repositoryInternalId = $function->getAttribute('repositoryInternalId', ''); +// $repositoryId = $function->getAttribute('repositoryId', ''); +// $repositoryInternalId = $function->getAttribute('repositoryInternalId', ''); - if (empty($entrypoint)) { - $entrypoint = $function->getAttribute('entrypoint', ''); - } +// if (empty($entrypoint)) { +// $entrypoint = $function->getAttribute('entrypoint', ''); +// } - $isConnected = !empty($function->getAttribute('providerRepositoryId', '')); +// $isConnected = !empty($function->getAttribute('providerRepositoryId', '')); - // Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git - if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) { - $repositories = $dbForConsole->find('repositories', [ - Query::equal('projectInternalId', [$project->getInternalId()]), - Query::equal('resourceInternalId', [$function->getInternalId()]), - Query::equal('resourceType', ['function']), - Query::limit(100), - ]); +// // Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git +// if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) { +// $repositories = $dbForConsole->find('repositories', [ +// Query::equal('projectInternalId', [$project->getInternalId()]), +// Query::equal('resourceInternalId', [$function->getInternalId()]), +// Query::equal('resourceType', ['function']), +// Query::limit(100), +// ]); - foreach ($repositories as $repository) { - $dbForConsole->deleteDocument('repositories', $repository->getId()); - } +// foreach ($repositories as $repository) { +// $dbForConsole->deleteDocument('repositories', $repository->getId()); +// } - $providerRepositoryId = ''; - $installationId = ''; - $providerBranch = ''; - $providerRootDirectory = ''; - $providerSilentMode = true; - $repositoryId = ''; - $repositoryInternalId = ''; - } +// $providerRepositoryId = ''; +// $installationId = ''; +// $providerBranch = ''; +// $providerRootDirectory = ''; +// $providerSilentMode = true; +// $repositoryId = ''; +// $repositoryInternalId = ''; +// } - // Git connect logic - if (!$isConnected && !empty($providerRepositoryId)) { - $teamId = $project->getAttribute('teamId', ''); +// // Git connect logic +// if (!$isConnected && !empty($providerRepositoryId)) { +// $teamId = $project->getAttribute('teamId', ''); - $repository = $dbForConsole->createDocument('repositories', new Document([ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::team(ID::custom($teamId))), - Permission::update(Role::team(ID::custom($teamId), 'owner')), - Permission::update(Role::team(ID::custom($teamId), 'developer')), - Permission::delete(Role::team(ID::custom($teamId), 'owner')), - Permission::delete(Role::team(ID::custom($teamId), 'developer')), - ], - 'installationId' => $installation->getId(), - 'installationInternalId' => $installation->getInternalId(), - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'providerRepositoryId' => $providerRepositoryId, - 'resourceId' => $function->getId(), - 'resourceInternalId' => $function->getInternalId(), - 'resourceType' => 'function', - 'providerPullRequestIds' => [] - ])); +// $repository = $dbForConsole->createDocument('repositories', new Document([ +// '$id' => ID::unique(), +// '$permissions' => [ +// Permission::read(Role::team(ID::custom($teamId))), +// Permission::update(Role::team(ID::custom($teamId), 'owner')), +// Permission::update(Role::team(ID::custom($teamId), 'developer')), +// Permission::delete(Role::team(ID::custom($teamId), 'owner')), +// Permission::delete(Role::team(ID::custom($teamId), 'developer')), +// ], +// 'installationId' => $installation->getId(), +// 'installationInternalId' => $installation->getInternalId(), +// 'projectId' => $project->getId(), +// 'projectInternalId' => $project->getInternalId(), +// 'providerRepositoryId' => $providerRepositoryId, +// 'resourceId' => $function->getId(), +// 'resourceInternalId' => $function->getInternalId(), +// 'resourceType' => 'function', +// 'providerPullRequestIds' => [] +// ])); - $repositoryId = $repository->getId(); - $repositoryInternalId = $repository->getInternalId(); - } +// $repositoryId = $repository->getId(); +// $repositoryInternalId = $repository->getInternalId(); +// } - $live = true; +// $live = true; - if ( - $function->getAttribute('name') !== $name || - $function->getAttribute('entrypoint') !== $entrypoint || - $function->getAttribute('commands') !== $commands || - $function->getAttribute('providerRootDirectory') !== $providerRootDirectory || - $function->getAttribute('runtime') !== $runtime - ) { - $live = false; - } +// if ( +// $function->getAttribute('name') !== $name || +// $function->getAttribute('entrypoint') !== $entrypoint || +// $function->getAttribute('commands') !== $commands || +// $function->getAttribute('providerRootDirectory') !== $providerRootDirectory || +// $function->getAttribute('runtime') !== $runtime +// ) { +// $live = false; +// } - $spec = Config::getParam('runtime-specifications')[$specification] ?? []; +// $spec = Config::getParam('runtime-specifications')[$specification] ?? []; - // Enforce Cold Start if spec limits change. - if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); - try { - $executor->deleteRuntime($project->getId(), $function->getAttribute('deployment')); - } catch (\Throwable $th) { - // Don't throw if the deployment doesn't exist - if ($th->getCode() !== 404) { - throw $th; - } - } - } +// // Enforce Cold Start if spec limits change. +// if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) { +// $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); +// try { +// $executor->deleteRuntime($project->getId(), $function->getAttribute('deployment')); +// } catch (\Throwable $th) { +// // Don't throw if the deployment doesn't exist +// if ($th->getCode() !== 404) { +// throw $th; +// } +// } +// } - $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ - 'execute' => $execute, - 'name' => $name, - 'runtime' => $runtime, - 'events' => $events, - 'schedule' => $schedule, - 'timeout' => $timeout, - 'enabled' => $enabled, - 'live' => $live, - 'logging' => $logging, - 'entrypoint' => $entrypoint, - 'commands' => $commands, - 'scopes' => $scopes, - 'installationId' => $installation->getId(), - 'installationInternalId' => $installation->getInternalId(), - 'providerRepositoryId' => $providerRepositoryId, - 'repositoryId' => $repositoryId, - 'repositoryInternalId' => $repositoryInternalId, - 'providerBranch' => $providerBranch, - 'providerRootDirectory' => $providerRootDirectory, - 'providerSilentMode' => $providerSilentMode, - 'specification' => $specification, - 'search' => implode(' ', [$functionId, $name, $runtime]), - ]))); +// $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ +// 'execute' => $execute, +// 'name' => $name, +// 'runtime' => $runtime, +// 'events' => $events, +// 'schedule' => $schedule, +// 'timeout' => $timeout, +// 'enabled' => $enabled, +// 'live' => $live, +// 'logging' => $logging, +// 'entrypoint' => $entrypoint, +// 'commands' => $commands, +// 'scopes' => $scopes, +// 'installationId' => $installation->getId(), +// 'installationInternalId' => $installation->getInternalId(), +// 'providerRepositoryId' => $providerRepositoryId, +// 'repositoryId' => $repositoryId, +// 'repositoryInternalId' => $repositoryInternalId, +// 'providerBranch' => $providerBranch, +// 'providerRootDirectory' => $providerRootDirectory, +// 'providerSilentMode' => $providerSilentMode, +// 'specification' => $specification, +// 'search' => implode(' ', [$functionId, $name, $runtime]), +// ]))); - // Redeploy logic - if (!$isConnected && !empty($providerRepositoryId)) { - $redeployVcs($request, $function, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github); - } +// // Redeploy logic +// if (!$isConnected && !empty($providerRepositoryId)) { +// $redeployVcs($request, $function, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github); +// } - // Inform scheduler if function is still active - $schedule = $dbForConsole->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 () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); +// // Inform scheduler if function is still active +// $schedule = $dbForConsole->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 () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); - $queueForEvents->setParam('functionId', $function->getId()); +// $queueForEvents->setParam('functionId', $function->getId()); - $response->dynamic($function, Response::MODEL_FUNCTION); - }); +// $response->dynamic($function, Response::MODEL_FUNCTION); +// }); App::get('/v1/functions/:functionId/deployments/:deploymentId/download') ->groups(['api', 'functions']) @@ -1148,224 +1148,224 @@ App::delete('/v1/functions/:functionId') $response->noContent(); }); -App::post('/v1/functions/:functionId/deployments') - ->groups(['api', 'functions']) - ->desc('Create deployment') - ->label('scope', 'functions.write') - ->label('event', 'functions.[functionId].deployments.[deploymentId].create') - ->label('audits.event', 'deployment.create') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'createDeployment') - ->label('sdk.methodType', 'upload') - ->label('sdk.description', '/docs/references/functions/create-deployment.md') - ->label('sdk.packaging', true) - ->label('sdk.request.type', 'multipart/form-data') - ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_DEPLOYMENT) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('entrypoint', null, new Text(1028), 'Entrypoint File.', true) - ->param('commands', null, new Text(8192, 0), 'Build Commands.', true) - ->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', skipValidation: true) - ->param('activate', false, new Boolean(true), 'Automatically activate the deployment when it is finished building.') - ->inject('request') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->inject('project') - ->inject('deviceForFunctions') - ->inject('deviceForLocal') - ->inject('queueForBuilds') - ->action(function (string $functionId, ?string $entrypoint, ?string $commands, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Event $queueForEvents, Document $project, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds) { +// App::post('/v1/functions/:functionId/deployments') +// ->groups(['api', 'functions']) +// ->desc('Create deployment') +// ->label('scope', 'functions.write') +// ->label('event', 'functions.[functionId].deployments.[deploymentId].create') +// ->label('audits.event', 'deployment.create') +// ->label('audits.resource', 'function/{request.functionId}') +// ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) +// ->label('sdk.namespace', 'functions') +// ->label('sdk.method', 'createDeployment') +// ->label('sdk.methodType', 'upload') +// ->label('sdk.description', '/docs/references/functions/create-deployment.md') +// ->label('sdk.packaging', true) +// ->label('sdk.request.type', 'multipart/form-data') +// ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) +// ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) +// ->label('sdk.response.model', Response::MODEL_DEPLOYMENT) +// ->param('functionId', '', new UID(), 'Function ID.') +// ->param('entrypoint', null, new Text(1028), 'Entrypoint File.', true) +// ->param('commands', null, new Text(8192, 0), 'Build Commands.', true) +// ->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', skipValidation: true) +// ->param('activate', false, new Boolean(true), 'Automatically activate the deployment when it is finished building.') +// ->inject('request') +// ->inject('response') +// ->inject('dbForProject') +// ->inject('queueForEvents') +// ->inject('project') +// ->inject('deviceForFunctions') +// ->inject('deviceForLocal') +// ->inject('queueForBuilds') +// ->action(function (string $functionId, ?string $entrypoint, ?string $commands, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Event $queueForEvents, Document $project, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds) { - $activate = \strval($activate) === 'true' || \strval($activate) === '1'; +// $activate = \strval($activate) === 'true' || \strval($activate) === '1'; - $function = $dbForProject->getDocument('functions', $functionId); +// $function = $dbForProject->getDocument('functions', $functionId); - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } +// if ($function->isEmpty()) { +// throw new Exception(Exception::FUNCTION_NOT_FOUND); +// } - if ($entrypoint === null) { - $entrypoint = $function->getAttribute('entrypoint', ''); - } +// if ($entrypoint === null) { +// $entrypoint = $function->getAttribute('entrypoint', ''); +// } - if ($commands === null) { - $commands = $function->getAttribute('commands', ''); - } +// if ($commands === null) { +// $commands = $function->getAttribute('commands', ''); +// } - if (empty($entrypoint)) { - throw new Exception(Exception::FUNCTION_ENTRYPOINT_MISSING); - } +// if (empty($entrypoint)) { +// throw new Exception(Exception::FUNCTION_ENTRYPOINT_MISSING); +// } - $file = $request->getFiles('code'); +// $file = $request->getFiles('code'); - // GraphQL multipart spec adds files with index keys - if (empty($file)) { - $file = $request->getFiles(0); - } +// // GraphQL multipart spec adds files with index keys +// if (empty($file)) { +// $file = $request->getFiles(0); +// } - if (empty($file)) { - throw new Exception(Exception::STORAGE_FILE_EMPTY, 'No file sent'); - } +// if (empty($file)) { +// throw new Exception(Exception::STORAGE_FILE_EMPTY, 'No file sent'); +// } - $fileExt = new FileExt([FileExt::TYPE_GZIP]); - $fileSizeValidator = new FileSize(System::getEnv('_APP_FUNCTIONS_SIZE_LIMIT', '30000000')); - $upload = new Upload(); +// $fileExt = new FileExt([FileExt::TYPE_GZIP]); +// $fileSizeValidator = new FileSize(System::getEnv('_APP_FUNCTIONS_SIZE_LIMIT', '30000000')); +// $upload = new Upload(); - // Make sure we handle a single file and multiple files the same way - $fileName = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name']; - $fileTmpName = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name']; - $fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; +// // Make sure we handle a single file and multiple files the same way +// $fileName = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name']; +// $fileTmpName = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name']; +// $fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; - if (!$fileExt->isValid($file['name'])) { // Check if file type is allowed - throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED); - } +// if (!$fileExt->isValid($file['name'])) { // Check if file type is allowed +// throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED); +// } - $contentRange = $request->getHeader('content-range'); - $deploymentId = ID::unique(); - $chunk = 1; - $chunks = 1; +// $contentRange = $request->getHeader('content-range'); +// $deploymentId = ID::unique(); +// $chunk = 1; +// $chunks = 1; - if (!empty($contentRange)) { - $start = $request->getContentRangeStart(); - $end = $request->getContentRangeEnd(); - $fileSize = $request->getContentRangeSize(); - $deploymentId = $request->getHeader('x-appwrite-id', $deploymentId); - // TODO make `end >= $fileSize` in next breaking version - if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) { - throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE); - } +// if (!empty($contentRange)) { +// $start = $request->getContentRangeStart(); +// $end = $request->getContentRangeEnd(); +// $fileSize = $request->getContentRangeSize(); +// $deploymentId = $request->getHeader('x-appwrite-id', $deploymentId); +// // TODO make `end >= $fileSize` in next breaking version +// if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) { +// throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE); +// } - // TODO remove the condition that checks `$end === $fileSize` in next breaking version - if ($end === $fileSize - 1 || $end === $fileSize) { - //if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk - $chunks = $chunk = -1; - } else { - // Calculate total number of chunks based on the chunk size i.e ($rangeEnd - $rangeStart) - $chunks = (int) ceil($fileSize / ($end + 1 - $start)); - $chunk = (int) ($start / ($end + 1 - $start)) + 1; - } - } +// // TODO remove the condition that checks `$end === $fileSize` in next breaking version +// if ($end === $fileSize - 1 || $end === $fileSize) { +// //if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk +// $chunks = $chunk = -1; +// } else { +// // Calculate total number of chunks based on the chunk size i.e ($rangeEnd - $rangeStart) +// $chunks = (int) ceil($fileSize / ($end + 1 - $start)); +// $chunk = (int) ($start / ($end + 1 - $start)) + 1; +// } +// } - if (!$fileSizeValidator->isValid($fileSize)) { // Check if file size is exceeding allowed limit - throw new Exception(Exception::STORAGE_INVALID_FILE_SIZE); - } +// if (!$fileSizeValidator->isValid($fileSize)) { // Check if file size is exceeding allowed limit +// throw new Exception(Exception::STORAGE_INVALID_FILE_SIZE); +// } - if (!$upload->isValid($fileTmpName)) { - throw new Exception(Exception::STORAGE_INVALID_FILE); - } +// if (!$upload->isValid($fileTmpName)) { +// throw new Exception(Exception::STORAGE_INVALID_FILE); +// } - // Save to storage - $fileSize ??= $deviceForLocal->getFileSize($fileTmpName); - $path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); - $deployment = $dbForProject->getDocument('deployments', $deploymentId); +// // Save to storage +// $fileSize ??= $deviceForLocal->getFileSize($fileTmpName); +// $path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); +// $deployment = $dbForProject->getDocument('deployments', $deploymentId); - $metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)]; - if (!$deployment->isEmpty()) { - $chunks = $deployment->getAttribute('chunksTotal', 1); - $metadata = $deployment->getAttribute('metadata', []); - if ($chunk === -1) { - $chunk = $chunks; - } - } +// $metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)]; +// if (!$deployment->isEmpty()) { +// $chunks = $deployment->getAttribute('chunksTotal', 1); +// $metadata = $deployment->getAttribute('metadata', []); +// if ($chunk === -1) { +// $chunk = $chunks; +// } +// } - $chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata); +// $chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata); - if (empty($chunksUploaded)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file'); - } +// if (empty($chunksUploaded)) { +// throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file'); +// } - $type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual'; +// $type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual'; - if ($chunksUploaded === $chunks) { - if ($activate) { - // Remove deploy for all other deployments. - $activeDeployments = $dbForProject->find('deployments', [ - Query::equal('activate', [true]), - Query::equal('resourceId', [$functionId]), - Query::equal('resourceType', ['functions']) - ]); +// if ($chunksUploaded === $chunks) { +// if ($activate) { +// // Remove deploy for all other deployments. +// $activeDeployments = $dbForProject->find('deployments', [ +// Query::equal('activate', [true]), +// Query::equal('resourceId', [$functionId]), +// Query::equal('resourceType', ['functions']) +// ]); - foreach ($activeDeployments as $activeDeployment) { - $activeDeployment->setAttribute('activate', false); - $dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment); - } - } +// foreach ($activeDeployments as $activeDeployment) { +// $activeDeployment->setAttribute('activate', false); +// $dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment); +// } +// } - $fileSize = $deviceForFunctions->getFileSize($path); +// $fileSize = $deviceForFunctions->getFileSize($path); - if ($deployment->isEmpty()) { - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceInternalId' => $function->getInternalId(), - 'resourceId' => $function->getId(), - 'resourceType' => 'functions', - 'buildInternalId' => '', - 'entrypoint' => $entrypoint, - 'commands' => $commands, - 'path' => $path, - 'size' => $fileSize, - 'search' => implode(' ', [$deploymentId, $entrypoint]), - 'activate' => $activate, - 'metadata' => $metadata, - 'type' => $type - ])); - } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('size', $fileSize)->setAttribute('metadata', $metadata)); - } +// if ($deployment->isEmpty()) { +// $deployment = $dbForProject->createDocument('deployments', new Document([ +// '$id' => $deploymentId, +// '$permissions' => [ +// Permission::read(Role::any()), +// Permission::update(Role::any()), +// Permission::delete(Role::any()), +// ], +// 'resourceInternalId' => $function->getInternalId(), +// 'resourceId' => $function->getId(), +// 'resourceType' => 'functions', +// 'buildInternalId' => '', +// 'entrypoint' => $entrypoint, +// 'commands' => $commands, +// 'path' => $path, +// 'size' => $fileSize, +// 'search' => implode(' ', [$deploymentId, $entrypoint]), +// 'activate' => $activate, +// 'metadata' => $metadata, +// 'type' => $type +// ])); +// } else { +// $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('size', $fileSize)->setAttribute('metadata', $metadata)); +// } - // Start the build - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($function) - ->setDeployment($deployment); - } else { - if ($deployment->isEmpty()) { - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceInternalId' => $function->getInternalId(), - 'resourceId' => $function->getId(), - 'resourceType' => 'functions', - 'buildInternalId' => '', - 'entrypoint' => $entrypoint, - 'commands' => $commands, - 'path' => $path, - 'size' => $fileSize, - 'chunksTotal' => $chunks, - 'chunksUploaded' => $chunksUploaded, - 'search' => implode(' ', [$deploymentId, $entrypoint]), - 'activate' => $activate, - 'metadata' => $metadata, - 'type' => $type - ])); - } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('chunksUploaded', $chunksUploaded)->setAttribute('metadata', $metadata)); - } - } +// // Start the build +// $queueForBuilds +// ->setType(BUILD_TYPE_DEPLOYMENT) +// ->setResource($function) +// ->setDeployment($deployment); +// } else { +// if ($deployment->isEmpty()) { +// $deployment = $dbForProject->createDocument('deployments', new Document([ +// '$id' => $deploymentId, +// '$permissions' => [ +// Permission::read(Role::any()), +// Permission::update(Role::any()), +// Permission::delete(Role::any()), +// ], +// 'resourceInternalId' => $function->getInternalId(), +// 'resourceId' => $function->getId(), +// 'resourceType' => 'functions', +// 'buildInternalId' => '', +// 'entrypoint' => $entrypoint, +// 'commands' => $commands, +// 'path' => $path, +// 'size' => $fileSize, +// 'chunksTotal' => $chunks, +// 'chunksUploaded' => $chunksUploaded, +// 'search' => implode(' ', [$deploymentId, $entrypoint]), +// 'activate' => $activate, +// 'metadata' => $metadata, +// 'type' => $type +// ])); +// } else { +// $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('chunksUploaded', $chunksUploaded)->setAttribute('metadata', $metadata)); +// } +// } - $metadata = null; +// $metadata = null; - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); +// $queueForEvents +// ->setParam('functionId', $function->getId()) +// ->setParam('deploymentId', $deployment->getId()); - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($deployment, Response::MODEL_DEPLOYMENT); - }); +// $response +// ->setStatusCode(Response::STATUS_CODE_ACCEPTED) +// ->dynamic($deployment, Response::MODEL_DEPLOYMENT); +// }); App::get('/v1/functions/:functionId/deployments') ->groups(['api', 'functions']) diff --git a/app/controllers/general.php b/app/controllers/general.php index cb8c0523df..483fa88a12 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -10,6 +10,7 @@ use Appwrite\Event\Func; use Appwrite\Event\Usage; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Network\Validator\Origin; +use Appwrite\Platform\Appwrite; use Appwrite\Utopia\Request; use Appwrite\Utopia\Request\Filters\V16 as RequestV16; use Appwrite\Utopia\Request\Filters\V17 as RequestV17; @@ -38,6 +39,7 @@ use Utopia\Logger\Adapter\Sentry; use Utopia\Logger\Log; use Utopia\Logger\Log\User; use Utopia\Logger\Logger; +use Utopia\Platform\Service; use Utopia\System\System; use Utopia\Validator\Hostname; use Utopia\Validator\Text; @@ -1107,3 +1109,8 @@ App::wildcard() foreach (Config::getParam('services', []) as $service) { include_once $service['controller']; } + +// Modules + +$platform = new Appwrite(); +$platform->init(Service::TYPE_HTTP); diff --git a/app/init.php b/app/init.php index eb149adc04..9867c59e8f 100644 --- a/app/init.php +++ b/app/init.php @@ -153,6 +153,9 @@ const APP_HOSTNAME_INTERNAL = 'appwrite'; const APP_FUNCTION_SPECIFICATION_DEFAULT = Specification::S_05VCPU_512MB; const APP_FUNCTION_CPUS_DEFAULT = 0.5; const APP_FUNCTION_MEMORY_DEFAULT = 512; +const APP_SITE_SPECIFICATION_DEFAULT = Specification::S_05VCPU_512MB; +const APP_SITE_CPUS_DEFAULT = 0.5; +const APP_SITE_MEMORY_DEFAULT = 512; const APP_PLATFORM_SERVER = 'server'; const APP_PLATFORM_CLIENT = 'client'; const APP_PLATFORM_CONSOLE = 'console'; @@ -306,6 +309,7 @@ Config::load('errors', __DIR__ . '/config/errors.php'); Config::load('oAuthProviders', __DIR__ . '/config/oAuthProviders.php'); Config::load('platforms', __DIR__ . '/config/platforms.php'); Config::load('collections', __DIR__ . '/config/collections.php'); +Config::load('frameworks', __DIR__ . '/config/frameworks.php'); Config::load('runtimes', __DIR__ . '/config/runtimes.php'); Config::load('runtimes-v2', __DIR__ . '/config/runtimes-v2.php'); Config::load('usage', __DIR__ . '/config/usage.php'); diff --git a/composer.lock b/composer.lock index 9079512e60..a7e95a95ca 100644 --- a/composer.lock +++ b/composer.lock @@ -2124,16 +2124,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.12.1", + "version": "0.12.2", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "b9dfafb5efc1d12cbee01d03dc98853ef026e35b" + "reference": "f6790fba1fcee12163d51c65d2c226a7856295d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/b9dfafb5efc1d12cbee01d03dc98853ef026e35b", - "reference": "b9dfafb5efc1d12cbee01d03dc98853ef026e35b", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/f6790fba1fcee12163d51c65d2c226a7856295d9", + "reference": "f6790fba1fcee12163d51c65d2c226a7856295d9", "shasum": "" }, "require": { @@ -2169,9 +2169,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.12.1" + "source": "https://github.com/utopia-php/messaging/tree/0.12.2" }, - "time": "2024-10-09T08:17:07+00:00" + "time": "2024-10-22T01:02:20+00:00" }, { "name": "utopia-php/migration", @@ -2341,16 +2341,16 @@ }, { "name": "utopia-php/platform", - "version": "0.7.0", + "version": "0.7.1", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52" + "reference": "3433a0f1a54988f2a59c735f507745cb2c24638a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", - "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/3433a0f1a54988f2a59c735f507745cb2c24638a", + "reference": "3433a0f1a54988f2a59c735f507745cb2c24638a", "shasum": "" }, "require": { @@ -2385,9 +2385,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.7.0" + "source": "https://github.com/utopia-php/platform/tree/0.7.1" }, - "time": "2024-05-08T17:00:55+00:00" + "time": "2024-10-22T10:27:49+00:00" }, { "name": "utopia-php/pools", @@ -7029,5 +7029,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index 43eda511df..e87a2fc8f9 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -24,6 +24,9 @@ class Event public const FUNCTIONS_QUEUE_NAME = 'v1-functions'; public const FUNCTIONS_CLASS_NAME = 'FunctionsV1'; + public const SITES_QUEUE_NAME = 'v1-sites'; + public const SITES_CLASS_NAME = 'SitesV1'; + public const USAGE_QUEUE_NAME = 'v1-usage'; public const USAGE_CLASS_NAME = 'UsageV1'; diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index d25332126c..29d0cf959b 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -151,6 +151,9 @@ class Exception extends \Exception public const PROVIDER_CONTRIBUTION_CONFLICT = 'provider_contribution_conflict'; public const GENERAL_PROVIDER_FAILURE = 'general_provider_failure'; + /** Sites */ + public const SITE_FRAMEWORK_UNSUPPORTED = 'site_framework_unsupported'; + /** Functions */ public const FUNCTION_NOT_FOUND = 'function_not_found'; public const FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported'; diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php index 6b3eb077fa..b77ccce979 100644 --- a/src/Appwrite/Platform/Appwrite.php +++ b/src/Appwrite/Platform/Appwrite.php @@ -3,6 +3,8 @@ namespace Appwrite\Platform; use Appwrite\Platform\Modules\Core; +use Appwrite\Platform\Modules\Functions; +use Appwrite\Platform\Modules\Sites; use Utopia\Platform\Platform; class Appwrite extends Platform @@ -10,5 +12,7 @@ class Appwrite extends Platform public function __construct() { parent::__construct(new Core()); + $this->addModule(new Functions\Module()); + $this->addModule(new Sites\Module()); } } diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php new file mode 100644 index 0000000000..f11b681c3e --- /dev/null +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -0,0 +1,169 @@ +getAttribute('entrypoint', ''); + $providerInstallationId = $installation->getAttribute('providerInstallationId', ''); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + $owner = $github->getOwnerName($providerInstallationId); + $providerRepositoryId = $function->getAttribute('providerRepositoryId', ''); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + $providerBranch = $function->getAttribute('providerBranch', 'main'); + $authorUrl = "https://github.com/$owner"; + $repositoryUrl = "https://github.com/$owner/$repositoryName"; + $branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch"; + + $commitDetails = []; + if ($template->isEmpty()) { + try { + $commitDetails = $github->getLatestCommit($owner, $repositoryName, $providerBranch); + } catch (\Throwable $error) { + Console::warning('Failed to get latest commit details'); + Console::warning($error->getMessage()); + Console::warning($error->getTraceAsString()); + } + } + + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceId' => $function->getId(), + 'resourceInternalId' => $function->getInternalId(), + 'resourceType' => 'functions', + 'entrypoint' => $entrypoint, + 'commands' => $function->getAttribute('commands', ''), + 'type' => 'vcs', + 'installationId' => $installation->getId(), + 'installationInternalId' => $installation->getInternalId(), + 'providerRepositoryId' => $providerRepositoryId, + 'repositoryId' => $function->getAttribute('repositoryId', ''), + 'repositoryInternalId' => $function->getAttribute('repositoryInternalId', ''), + 'providerBranchUrl' => $branchUrl, + 'providerRepositoryName' => $repositoryName, + 'providerRepositoryOwner' => $owner, + 'providerRepositoryUrl' => $repositoryUrl, + 'providerCommitHash' => $commitDetails['commitHash'] ?? '', + 'providerCommitAuthorUrl' => $authorUrl, + 'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '', + 'providerCommitMessage' => $commitDetails['commitMessage'] ?? '', + 'providerCommitUrl' => $commitDetails['commitUrl'] ?? '', + 'providerBranch' => $providerBranch, + 'providerRootDirectory' => $function->getAttribute('providerRootDirectory', ''), + 'search' => implode(' ', [$deploymentId, $entrypoint]), + 'activate' => true, + ])); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($function) + ->setDeployment($deployment) + ->setTemplate($template); + } + + public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Build $queueForBuilds, Document $template, GitHub $github) + { + $deploymentId = ID::unique(); + $providerInstallationId = $installation->getAttribute('providerInstallationId', ''); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + $owner = $github->getOwnerName($providerInstallationId); + $providerRepositoryId = $site->getAttribute('providerRepositoryId', ''); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + + $providerBranch = $site->getAttribute('providerBranch', 'main'); + $authorUrl = "https://github.com/$owner"; + $repositoryUrl = "https://github.com/$owner/$repositoryName"; + $branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch"; + + $commitDetails = []; + if ($template->isEmpty()) { + try { + $commitDetails = $github->getLatestCommit($owner, $repositoryName, $providerBranch); + } catch (\Throwable $error) { + Console::warning('Failed to get latest commit details'); + Console::warning($error->getMessage()); + Console::warning($error->getTraceAsString()); + } + } + + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceId' => $site->getId(), + 'resourceInternalId' => $site->getInternalId(), + 'resourceType' => 'sites', + 'buildCommand' => $site->getAttribute('buildCommand', ''), + 'installCommand' => $site->getAttribute('installCommand', ''), + 'outputDirectory' => $site->getAttribute('outputDirectory', ''), + 'type' => 'vcs', + 'installationId' => $installation->getId(), + 'installationInternalId' => $installation->getInternalId(), + 'providerRepositoryId' => $providerRepositoryId, + 'repositoryId' => $site->getAttribute('repositoryId', ''), + 'repositoryInternalId' => $site->getAttribute('repositoryInternalId', ''), + 'providerBranchUrl' => $branchUrl, + 'providerRepositoryName' => $repositoryName, + 'providerRepositoryOwner' => $owner, + 'providerRepositoryUrl' => $repositoryUrl, + 'providerCommitHash' => $commitDetails['commitHash'] ?? '', + 'providerCommitAuthorUrl' => $authorUrl, + 'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '', + 'providerCommitMessage' => $commitDetails['commitMessage'] ?? '', + 'providerCommitUrl' => $commitDetails['commitUrl'] ?? '', + 'providerBranch' => $providerBranch, + 'providerRootDirectory' => $site->getAttribute('providerRootDirectory', ''), + 'search' => implode(' ', [$deploymentId]), + 'activate' => true, + ])); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($site) + ->setDeployment($deployment) + ->setTemplate($template); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/CreateDeployment.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/CreateDeployment.php new file mode 100644 index 0000000000..c8e466e11a --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/CreateDeployment.php @@ -0,0 +1,262 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/functions/:functionId/deployments') + ->desc('Create deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('event', 'functions.[functionId].deployments.[deploymentId].create') + ->label('audits.event', 'deployment.create') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'createDeployment') + ->label('sdk.methodType', 'upload') + ->label('sdk.description', '/docs/references/functions/create-deployment.md') + ->label('sdk.packaging', true) + ->label('sdk.request.type', 'multipart/form-data') + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DEPLOYMENT) + ->param('functionId', '', new UID(), 'Function ID.') + ->param('entrypoint', null, new Text(1028), 'Entrypoint File.', true) + ->param('commands', null, new Text(8192, 0), 'Build Commands.', true) + ->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', skipValidation: true) + ->param('activate', false, new Boolean(true), 'Automatically activate the deployment when it is finished building.') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('project') + ->inject('deviceForFunctions') + ->inject('deviceForLocal') + ->inject('queueForBuilds') + ->callback([$this, 'action']); + } + + public function action(string $functionId, ?string $entrypoint, ?string $commands, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Event $queueForEvents, Document $project, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds) + { + $activate = \strval($activate) === 'true' || \strval($activate) === '1'; + + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + if ($entrypoint === null) { + $entrypoint = $function->getAttribute('entrypoint', ''); + } + + if ($commands === null) { + $commands = $function->getAttribute('commands', ''); + } + + if (empty($entrypoint)) { + throw new Exception(Exception::FUNCTION_ENTRYPOINT_MISSING); + } + + $file = $request->getFiles('code'); + + // GraphQL multipart spec adds files with index keys + if (empty($file)) { + $file = $request->getFiles(0); + } + + if (empty($file)) { + throw new Exception(Exception::STORAGE_FILE_EMPTY, 'No file sent'); + } + + $fileExt = new FileExt([FileExt::TYPE_GZIP]); + $fileSizeValidator = new FileSize(System::getEnv('_APP_FUNCTIONS_SIZE_LIMIT', '30000000')); + $upload = new Upload(); + + // Make sure we handle a single file and multiple files the same way + $fileName = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name']; + $fileTmpName = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name']; + $fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; + + if (!$fileExt->isValid($file['name'])) { // Check if file type is allowed + throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED); + } + + $contentRange = $request->getHeader('content-range'); + $deploymentId = ID::unique(); + $chunk = 1; + $chunks = 1; + + if (!empty($contentRange)) { + $start = $request->getContentRangeStart(); + $end = $request->getContentRangeEnd(); + $fileSize = $request->getContentRangeSize(); + $deploymentId = $request->getHeader('x-appwrite-id', $deploymentId); + // TODO make `end >= $fileSize` in next breaking version + if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) { + throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE); + } + + // TODO remove the condition that checks `$end === $fileSize` in next breaking version + if ($end === $fileSize - 1 || $end === $fileSize) { + //if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk + $chunks = $chunk = -1; + } else { + // Calculate total number of chunks based on the chunk size i.e ($rangeEnd - $rangeStart) + $chunks = (int) ceil($fileSize / ($end + 1 - $start)); + $chunk = (int) ($start / ($end + 1 - $start)) + 1; + } + } + + if (!$fileSizeValidator->isValid($fileSize)) { // Check if file size is exceeding allowed limit + throw new Exception(Exception::STORAGE_INVALID_FILE_SIZE); + } + + if (!$upload->isValid($fileTmpName)) { + throw new Exception(Exception::STORAGE_INVALID_FILE); + } + + // Save to storage + $fileSize ??= $deviceForLocal->getFileSize($fileTmpName); + $path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + $metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)]; + if (!$deployment->isEmpty()) { + $chunks = $deployment->getAttribute('chunksTotal', 1); + $metadata = $deployment->getAttribute('metadata', []); + if ($chunk === -1) { + $chunk = $chunks; + } + } + + $chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata); + + if (empty($chunksUploaded)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file'); + } + + $type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual'; + + if ($chunksUploaded === $chunks) { + if ($activate) { + // Remove deploy for all other deployments. + $activeDeployments = $dbForProject->find('deployments', [ + Query::equal('activate', [true]), + Query::equal('resourceId', [$functionId]), + Query::equal('resourceType', ['functions']) + ]); + + foreach ($activeDeployments as $activeDeployment) { + $activeDeployment->setAttribute('activate', false); + $dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment); + } + } + + $fileSize = $deviceForFunctions->getFileSize($path); + + if ($deployment->isEmpty()) { + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceInternalId' => $function->getInternalId(), + 'resourceId' => $function->getId(), + 'resourceType' => 'functions', + 'buildInternalId' => '', + 'entrypoint' => $entrypoint, + 'commands' => $commands, + 'path' => $path, + 'size' => $fileSize, + 'search' => implode(' ', [$deploymentId, $entrypoint]), + 'activate' => $activate, + 'metadata' => $metadata, + 'type' => $type + ])); + } else { + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('size', $fileSize)->setAttribute('metadata', $metadata)); + } + + // Start the build + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($function) + ->setDeployment($deployment); + } else { + if ($deployment->isEmpty()) { + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceInternalId' => $function->getInternalId(), + 'resourceId' => $function->getId(), + 'resourceType' => 'functions', + 'buildInternalId' => '', + 'entrypoint' => $entrypoint, + 'commands' => $commands, + 'path' => $path, + 'size' => $fileSize, + 'chunksTotal' => $chunks, + 'chunksUploaded' => $chunksUploaded, + 'search' => implode(' ', [$deploymentId, $entrypoint]), + 'activate' => $activate, + 'metadata' => $metadata, + 'type' => $type + ])); + } else { + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('chunksUploaded', $chunksUploaded)->setAttribute('metadata', $metadata)); + } + } + + $metadata = null; + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php new file mode 100644 index 0000000000..6c56b26214 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/CreateFunction.php @@ -0,0 +1,316 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/functions') + ->desc('Create function') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('event', 'functions.[functionId].create') + ->label('audits.event', 'function.create') + ->label('audits.resource', 'function/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'create') + ->label('sdk.description', '/docs/references/functions/create-function.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_FUNCTION) + ->param('functionId', '', new CustomId(), 'Function ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Function name. Max length: 128 chars.') + ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.') + ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) + ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) + ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) + ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true) + ->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true) + ->param('logging', true, new Boolean(), 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.', true) + ->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true) + ->param('commands', '', new Text(8192, 0), 'Build Commands.', true) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true) + ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) + ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the function.', true) + ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true) + ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) + ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) + ->param('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true) + ->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true) + ->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true) + ->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) + ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( + $plan, + Config::getParam('runtime-specifications', []), + App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), + App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) + ), 'Runtime specification for the function and builds.', true, ['plan']) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->inject('user') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('dbForConsole') + ->inject('gitHub') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) + { + $functionId = ($functionId == 'unique()') ? ID::unique() : $functionId; + + $allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', ''))); + + if (!empty($allowList) && !\in_array($runtime, $allowList)) { + throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $runtime . '" is not supported'); + } + + // build from template + $template = new Document([]); + if ( + !empty($templateRepository) + && !empty($templateOwner) + && !empty($templateRootDirectory) + && !empty($templateVersion) + ) { + $template->setAttribute('repositoryName', $templateRepository) + ->setAttribute('ownerName', $templateOwner) + ->setAttribute('rootDirectory', $templateRootDirectory) + ->setAttribute('version', $templateVersion); + } + + $installation = $dbForConsole->getDocument('installations', $installationId); + + if (!empty($installationId) && $installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".'); + } + + $function = $dbForProject->createDocument('functions', new Document([ + '$id' => $functionId, + 'execute' => $execute, + 'enabled' => $enabled, + 'live' => true, + 'logging' => $logging, + 'name' => $name, + 'runtime' => $runtime, + 'deploymentInternalId' => '', + 'deployment' => '', + 'events' => $events, + 'schedule' => $schedule, + 'scheduleInternalId' => '', + 'scheduleId' => '', + 'timeout' => $timeout, + 'entrypoint' => $entrypoint, + 'commands' => $commands, + 'scopes' => $scopes, + 'search' => implode(' ', [$functionId, $name, $runtime]), + 'version' => 'v4', + 'installationId' => $installation->getId(), + 'installationInternalId' => $installation->getInternalId(), + 'providerRepositoryId' => $providerRepositoryId, + 'repositoryId' => '', + 'repositoryInternalId' => '', + 'providerBranch' => $providerBranch, + 'providerRootDirectory' => $providerRootDirectory, + 'providerSilentMode' => $providerSilentMode, + 'specification' => $specification + ])); + + $schedule = Authorization::skip( + fn () => $dbForConsole->createDocument('schedules', new Document([ + 'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region + 'resourceType' => 'function', + 'resourceId' => $function->getId(), + 'resourceInternalId' => $function->getInternalId(), + 'resourceUpdatedAt' => DateTime::now(), + 'projectId' => $project->getId(), + 'schedule' => $function->getAttribute('schedule'), + 'active' => false, + ])) + ); + + $function->setAttribute('scheduleId', $schedule->getId()); + $function->setAttribute('scheduleInternalId', $schedule->getInternalId()); + + // Git connect logic + if (!empty($providerRepositoryId)) { + $teamId = $project->getAttribute('teamId', ''); + + $repository = $dbForConsole->createDocument('repositories', new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + Permission::update(Role::team(ID::custom($teamId), 'owner')), + Permission::update(Role::team(ID::custom($teamId), 'developer')), + Permission::delete(Role::team(ID::custom($teamId), 'owner')), + Permission::delete(Role::team(ID::custom($teamId), 'developer')), + ], + 'installationId' => $installation->getId(), + 'installationInternalId' => $installation->getInternalId(), + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'providerRepositoryId' => $providerRepositoryId, + 'resourceId' => $function->getId(), + 'resourceInternalId' => $function->getInternalId(), + 'resourceType' => 'function', + 'providerPullRequestIds' => [] + ])); + + $function->setAttribute('repositoryId', $repository->getId()); + $function->setAttribute('repositoryInternalId', $repository->getInternalId()); + } + + $function = $dbForProject->updateDocument('functions', $function->getId(), $function); + + if (!empty($providerRepositoryId)) { + // Deploy VCS + $this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, $template, $github); + } elseif (!$template->isEmpty()) { + // Deploy non-VCS from template + $deploymentId = ID::unique(); + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceId' => $function->getId(), + 'resourceInternalId' => $function->getInternalId(), + 'resourceType' => 'functions', + 'entrypoint' => $function->getAttribute('entrypoint', ''), + 'commands' => $function->getAttribute('commands', ''), + 'type' => 'manual', + 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint', '')]), + 'activate' => true, + ])); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($function) + ->setDeployment($deployment) + ->setTemplate($template); + } + + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + if (!empty($functionsDomain)) { + $ruleId = ID::unique(); + $routeSubdomain = ID::unique(); + $domain = "{$routeSubdomain}.{$functionsDomain}"; + + $rule = Authorization::skip( + fn () => $dbForConsole->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'resourceType' => 'function', + 'resourceId' => $function->getId(), + 'resourceInternalId' => $function->getInternalId(), + 'status' => 'verified', + 'certificateId' => '', + ])) + ); + + /** Trigger Webhook */ + $ruleModel = new Rule(); + $ruleCreate = + $queueForEvents + ->setClass(Event::WEBHOOK_CLASS_NAME) + ->setQueue(Event::WEBHOOK_QUEUE_NAME); + + $ruleCreate + ->setProject($project) + ->setEvent('rules.[ruleId].create') + ->setParam('ruleId', $rule->getId()) + ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) + ->trigger(); + + /** Trigger Functions */ + $ruleCreate + ->setClass(Event::FUNCTIONS_CLASS_NAME) + ->setQueue(Event::FUNCTIONS_QUEUE_NAME) + ->trigger(); + + /** Trigger realtime event */ + $allEvents = Event::generateEvents('rules.[ruleId].create', [ + 'ruleId' => $rule->getId(), + ]); + $target = Realtime::fromPayload( + // Pass first, most verbose event pattern + event: $allEvents[0], + payload: $rule, + project: $project + ); + Realtime::send( + projectId: 'console', + payload: $rule->getArrayCopy(), + events: $allEvents, + channels: $target['channels'], + roles: $target['roles'] + ); + Realtime::send( + projectId: $project->getId(), + payload: $rule->getArrayCopy(), + events: $allEvents, + channels: $target['channels'], + roles: $target['roles'] + ); + } + + $queueForEvents->setParam('functionId', $function->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($function, Response::MODEL_FUNCTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/UpdateFunction.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/UpdateFunction.php new file mode 100644 index 0000000000..e728034431 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/UpdateFunction.php @@ -0,0 +1,255 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) + ->setHttpPath('/v1/functions/:functionId') + ->desc('Update function') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('event', 'functions.[functionId].update') + ->label('audits.event', 'function.update') + ->label('audits.resource', 'function/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'update') + ->label('sdk.description', '/docs/references/functions/update-function.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('name', '', new Text(128), 'Function name. Max length: 128 chars.') + ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', true) + ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) + ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) + ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) + ->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Maximum execution time in seconds.', true) + ->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true) + ->param('logging', true, new Boolean(), 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.', true) + ->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true) + ->param('commands', '', new Text(8192, 0), 'Build Commands.', true) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true) + ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Controle System) deployment.', true) + ->param('providerRepositoryId', null, new Nullable(new Text(128, 0)), 'Repository ID of the repo linked to the function', true) + ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true) + ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) + ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) + ->param('specification', APP_FUNCTION_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( + $plan, + Config::getParam('runtime-specifications', []), + App::getEnv('_APP_FUNCTIONS_CPUS', APP_FUNCTION_CPUS_DEFAULT), + App::getEnv('_APP_FUNCTIONS_MEMORY', APP_FUNCTION_MEMORY_DEFAULT) + ), 'Runtime specification for the function and builds.', true, ['plan']) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('dbForConsole') + ->inject('gitHub') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) + { + // TODO: If only branch changes, re-deploy + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $installation = $dbForConsole->getDocument('installations', $installationId); + + if (!empty($installationId) && $installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".'); + } + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + if (empty($runtime)) { + $runtime = $function->getAttribute('runtime'); + } + + $enabled ??= $function->getAttribute('enabled', true); + + $repositoryId = $function->getAttribute('repositoryId', ''); + $repositoryInternalId = $function->getAttribute('repositoryInternalId', ''); + + if (empty($entrypoint)) { + $entrypoint = $function->getAttribute('entrypoint', ''); + } + + $isConnected = !empty($function->getAttribute('providerRepositoryId', '')); + + // Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git + if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) { + $repositories = $dbForConsole->find('repositories', [ + Query::equal('projectInternalId', [$project->getInternalId()]), + Query::equal('resourceInternalId', [$function->getInternalId()]), + Query::equal('resourceType', ['function']), + Query::limit(100), + ]); + + foreach ($repositories as $repository) { + $dbForConsole->deleteDocument('repositories', $repository->getId()); + } + + $providerRepositoryId = ''; + $installationId = ''; + $providerBranch = ''; + $providerRootDirectory = ''; + $providerSilentMode = true; + $repositoryId = ''; + $repositoryInternalId = ''; + } + + // Git connect logic + if (!$isConnected && !empty($providerRepositoryId)) { + $teamId = $project->getAttribute('teamId', ''); + + $repository = $dbForConsole->createDocument('repositories', new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + Permission::update(Role::team(ID::custom($teamId), 'owner')), + Permission::update(Role::team(ID::custom($teamId), 'developer')), + Permission::delete(Role::team(ID::custom($teamId), 'owner')), + Permission::delete(Role::team(ID::custom($teamId), 'developer')), + ], + 'installationId' => $installation->getId(), + 'installationInternalId' => $installation->getInternalId(), + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'providerRepositoryId' => $providerRepositoryId, + 'resourceId' => $function->getId(), + 'resourceInternalId' => $function->getInternalId(), + 'resourceType' => 'function', + 'providerPullRequestIds' => [] + ])); + + $repositoryId = $repository->getId(); + $repositoryInternalId = $repository->getInternalId(); + } + + $live = true; + + if ( + $function->getAttribute('name') !== $name || + $function->getAttribute('entrypoint') !== $entrypoint || + $function->getAttribute('commands') !== $commands || + $function->getAttribute('providerRootDirectory') !== $providerRootDirectory || + $function->getAttribute('runtime') !== $runtime + ) { + $live = false; + } + + $spec = Config::getParam('runtime-specifications')[$specification] ?? []; + + // Enforce Cold Start if spec limits change. + if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) { + $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + try { + $executor->deleteRuntime($project->getId(), $function->getAttribute('deployment')); + } catch (\Throwable $th) { + // Don't throw if the deployment doesn't exist + if ($th->getCode() !== 404) { + throw $th; + } + } + } + + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ + 'execute' => $execute, + 'name' => $name, + 'runtime' => $runtime, + 'events' => $events, + 'schedule' => $schedule, + 'timeout' => $timeout, + 'enabled' => $enabled, + 'live' => $live, + 'logging' => $logging, + 'entrypoint' => $entrypoint, + 'commands' => $commands, + 'scopes' => $scopes, + 'installationId' => $installation->getId(), + 'installationInternalId' => $installation->getInternalId(), + 'providerRepositoryId' => $providerRepositoryId, + 'repositoryId' => $repositoryId, + 'repositoryInternalId' => $repositoryInternalId, + 'providerBranch' => $providerBranch, + 'providerRootDirectory' => $providerRootDirectory, + 'providerSilentMode' => $providerSilentMode, + 'specification' => $specification, + 'search' => implode(' ', [$functionId, $name, $runtime]), + ]))); + + // Redeploy logic + if (!$isConnected && !empty($providerRepositoryId)) { + $this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github); + } + + // Inform scheduler if function is still active + $schedule = $dbForConsole->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 () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule)); + + $queueForEvents->setParam('functionId', $function->getId()); + + $response->dynamic($function, Response::MODEL_FUNCTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Module.php b/src/Appwrite/Platform/Modules/Functions/Module.php new file mode 100644 index 0000000000..6829452089 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Module.php @@ -0,0 +1,14 @@ +addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Services/Http.php b/src/Appwrite/Platform/Modules/Functions/Services/Http.php new file mode 100644 index 0000000000..0725ee0f88 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Services/Http.php @@ -0,0 +1,19 @@ +type = Service::TYPE_HTTP; + $this->addAction(CreateFunction::getName(), new CreateFunction()); + $this->addAction(UpdateFunction::getName(), new UpdateFunction()); + $this->addAction(CreateDeployment::getName(), new CreateDeployment()); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/CreateSite.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/CreateSite.php new file mode 100644 index 0000000000..086aeddc35 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/CreateSite.php @@ -0,0 +1,287 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/sites') + ->desc('Create site') + ->groups(['api', 'sites']) + ->label('scope', 'functions.write') // TODO: Update scope to sites.write + ->label('event', 'sites.[siteId].create') + ->label('audits.event', 'site.create') + ->label('audits.resource', 'site/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'create') + ->label('sdk.description', '/docs/references/sites/create-site.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SITE) + ->param('siteId', '', new CustomId(), 'Site ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Site name. Max length: 128 chars.') + ->param('framework', '', new WhiteList(Config::getParam('frameworks'), true), 'Sites framework.') + ->param('enabled', true, new Boolean(), 'Is site enabled? When set to \'disabled\', users cannot access the site but Server SDKs with and API key can still access the site. No data is lost when this is toggled.', true) // TODO: Add logging param later + ->param('installCommand', '', new Text(8192, 0), 'Install Command.', true) + ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) + ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) + ->param('fallbackRedirect', '', new Text(8192, 0), 'Fallback Redirect URL for site in case a route is not found.', true) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true) //TODO: Update description of scopes + ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) + ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true) + ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true) + ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true) + ->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true) + ->param('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true) + ->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true) + ->param('templateRootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.', true) + ->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the site template.', true) + ->param('specification', APP_SITE_SPECIFICATION_DEFAULT, fn (array $plan) => new FrameworkSpecification( + $plan, + Config::getParam('framework-specifications', []), + App::getEnv('_APP_SITES_CPUS', APP_SITE_CPUS_DEFAULT), + App::getEnv('_APP_SITES_MEMORY', APP_SITE_MEMORY_DEFAULT) + ), 'Runtime specification for the site and builds.', true, ['plan']) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->inject('user') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('dbForConsole') + ->inject('gitHub') + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $name, string $framework, bool $enabled, string $installCommand, string $buildCommand, string $outputDirectory, string $fallbackRedirect, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) + { + $siteId = ($siteId == 'unique()') ? ID::unique() : $siteId; + + $allowList = \array_filter(\explode(',', System::getEnv('_APP_SITES_FRAMEWORKS', ''))); + + if (!empty($allowList) && !\in_array($framework, $allowList)) { + throw new Exception(Exception::SITE_FRAMEWORK_UNSUPPORTED, 'Framework "' . $framework . '" is not supported'); + } + + // build from template + $template = new Document([]); + if ( + !empty($templateRepository) + && !empty($templateOwner) + && !empty($templateRootDirectory) + && !empty($templateVersion) + ) { + $template->setAttribute('repositoryName', $templateRepository) + ->setAttribute('ownerName', $templateOwner) + ->setAttribute('rootDirectory', $templateRootDirectory) + ->setAttribute('version', $templateVersion); + } + + $installation = $dbForConsole->getDocument('installations', $installationId); + + if (!empty($installationId) && $installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".'); + } + + $site = $dbForProject->createDocument('sites', new Document([ + '$id' => $siteId, + 'enabled' => $enabled, + 'live' => true, + 'name' => $name, + 'framework' => $framework, + 'deploymentInternalId' => '', + 'deploymentId' => '', + 'installCommand' => $installCommand, + 'buildCommand' => $buildCommand, + 'outputDirectory' => $outputDirectory, + 'fallbackRedirect' => $fallbackRedirect, + 'scopes' => $scopes, + 'search' => implode(' ', [$siteId, $name, $framework]), + 'installationId' => $installation->getId(), + 'installationInternalId' => $installation->getInternalId(), + 'providerRepositoryId' => $providerRepositoryId, + 'repositoryId' => '', + 'repositoryInternalId' => '', + 'providerBranch' => $providerBranch, + 'providerRootDirectory' => $providerRootDirectory, + 'providerSilentMode' => $providerSilentMode, + 'specification' => $specification + ])); + + // Git connect logic + if (!empty($providerRepositoryId)) { + $teamId = $project->getAttribute('teamId', ''); + + $repository = $dbForConsole->createDocument('repositories', new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + Permission::update(Role::team(ID::custom($teamId), 'owner')), + Permission::update(Role::team(ID::custom($teamId), 'developer')), + Permission::delete(Role::team(ID::custom($teamId), 'owner')), + Permission::delete(Role::team(ID::custom($teamId), 'developer')), + ], + 'installationId' => $installation->getId(), + 'installationInternalId' => $installation->getInternalId(), + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'providerRepositoryId' => $providerRepositoryId, + 'resourceId' => $site->getId(), + 'resourceInternalId' => $site->getInternalId(), + 'resourceType' => 'site', + 'providerPullRequestIds' => [] + ])); + + $site->setAttribute('repositoryId', $repository->getId()); + $site->setAttribute('repositoryInternalId', $repository->getInternalId()); + } + + $site = $dbForProject->updateDocument('sites', $site->getId(), $site); + + if (!empty($providerRepositoryId)) { + // Deploy VCS + $this->redeployVcsSite($request, $site, $project, $installation, $dbForProject, $queueForBuilds, $template, $github); + } elseif (!$template->isEmpty()) { + // Deploy non-VCS from template + $deploymentId = ID::unique(); + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceId' => $site->getId(), + 'resourceInternalId' => $site->getInternalId(), + 'resourceType' => 'sites', + 'installCommand' => $site->getAttribute('installCommand', ''), + 'buildCommand' => $site->getAttribute('buildCommand', ''), + 'outputDirectory' => $site->getAttribute('outputDirectory', ''), + 'type' => 'manual', + 'search' => implode(' ', [$deploymentId]), + 'activate' => true, + ])); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($site) + ->setDeployment($deployment) + ->setTemplate($template); + } + + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + if (!empty($sitesDomain)) { + $ruleId = ID::unique(); + $routeSubdomain = ID::unique(); + $domain = "{$routeSubdomain}.{$sitesDomain}"; + + $rule = Authorization::skip( + fn () => $dbForConsole->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'resourceType' => 'site', + 'resourceId' => $site->getId(), + 'resourceInternalId' => $site->getInternalId(), + 'status' => 'verified', + 'certificateId' => '', + ])) + ); + + /** Trigger Webhook */ + $ruleModel = new Rule(); + $ruleCreate = + $queueForEvents + ->setClass(Event::WEBHOOK_CLASS_NAME) + ->setQueue(Event::WEBHOOK_QUEUE_NAME); + + $ruleCreate + ->setProject($project) + ->setEvent('rules.[ruleId].create') + ->setParam('ruleId', $rule->getId()) + ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) + ->trigger(); + + /** Trigger Sites */ + $ruleCreate + ->setClass(Event::SITES_CLASS_NAME) + ->setQueue(Event::SITES_QUEUE_NAME) + ->trigger(); + + /** Trigger realtime event */ + $allEvents = Event::generateEvents('rules.[ruleId].create', [ + 'ruleId' => $rule->getId(), + ]); + $target = Realtime::fromPayload( + // Pass first, most verbose event pattern + event: $allEvents[0], + payload: $rule, + project: $project + ); + Realtime::send( + projectId: 'console', + payload: $rule->getArrayCopy(), + events: $allEvents, + channels: $target['channels'], + roles: $target['roles'] + ); + Realtime::send( + projectId: $project->getId(), + payload: $rule->getArrayCopy(), + events: $allEvents, + channels: $target['channels'], + roles: $target['roles'] + ); + } + + $queueForEvents->setParam('siteId', $site->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($site, Response::MODEL_SITE); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Module.php b/src/Appwrite/Platform/Modules/Sites/Module.php new file mode 100644 index 0000000000..86a6459a86 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Module.php @@ -0,0 +1,14 @@ +addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php new file mode 100644 index 0000000000..1730426a73 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -0,0 +1,15 @@ +type = Service::TYPE_HTTP; + $this->addAction(CreateSite::getName(), new CreateSite()); + } +} diff --git a/src/Appwrite/Sites/Validator/FrameworkSpecification.php b/src/Appwrite/Sites/Validator/FrameworkSpecification.php new file mode 100644 index 0000000000..b8dc3b5cfd --- /dev/null +++ b/src/Appwrite/Sites/Validator/FrameworkSpecification.php @@ -0,0 +1,112 @@ +plan = $plan; + $this->specifications = $specifications; + $this->maxCpus = $maxCpus; + $this->maxMemory = $maxMemory; + } + + /** + * Get Allowed Specifications. + * + * Get allowed specifications taking into account the limits set by the environment variables and the plan. + * + * @return array + */ + public function getAllowedSpecifications(): array + { + $allowedSpecifications = []; + + foreach ($this->specifications as $size => $values) { + if ($values['cpus'] <= $this->maxCpus && $values['memory'] <= $this->maxMemory) { + if (!empty($this->plan) && array_key_exists('frameworkSpecifications', $this->plan)) { + if (!\in_array($size, $this->plan['frameworkSpecifications'])) { + continue; + } + } + + $allowedSpecifications[] = $size; + } + } + + return $allowedSpecifications; + } + + /** + * Get Description. + * + * Returns validator description. + * + * @return string + */ + public function getDescription(): string + { + return 'Specification must be one of: ' . implode(', ', $this->getAllowedSpecifications()); + } + + /** + * Is valid. + * + * Returns true if valid or false if not. + * + * @param mixed $value + * + * @return bool + */ + public function isValid($value): bool + { + if (empty($value)) { + return false; + } + + if (!\is_string($value)) { + return false; + } + + if (!\in_array($value, $this->getAllowedSpecifications())) { + return false; + } + + return true; + } + + /** + * Is array. + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return false; + } + + /** + * Get Type. + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_STRING; + } +} diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 6cc2639f51..eaeeea96e4 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -84,6 +84,7 @@ use Appwrite\Utopia\Response\Model\ProviderRepository; use Appwrite\Utopia\Response\Model\Rule; use Appwrite\Utopia\Response\Model\Runtime; use Appwrite\Utopia\Response\Model\Session; +use Appwrite\Utopia\Response\Model\Site; use Appwrite\Utopia\Response\Model\Specification; use Appwrite\Utopia\Response\Model\Subscriber; use Appwrite\Utopia\Response\Model\Target; @@ -244,6 +245,10 @@ class Response extends SwooleResponse public const MODEL_VCS_CONTENT = 'vcsContent'; public const MODEL_VCS_CONTENT_LIST = 'vcsContentList'; + // Sites + public const MODEL_SITE = 'site'; + public const MODEL_SITE_LIST = 'siteList'; + // Functions public const MODEL_FUNCTION = 'function'; public const MODEL_FUNCTION_LIST = 'functionList'; @@ -351,6 +356,7 @@ class Response extends SwooleResponse ->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET)) ->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM)) ->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP)) + ->setModel(new BaseList('Sites List', self::MODEL_SITE_LIST, 'sites', self::MODEL_SITE)) ->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION)) ->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION)) ->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION)) @@ -422,6 +428,7 @@ class Response extends SwooleResponse ->setModel(new Bucket()) ->setModel(new Team()) ->setModel(new Membership()) + ->setModel(new Site()) ->setModel(new Func()) ->setModel(new TemplateFunction()) ->setModel(new TemplateRuntime()) diff --git a/src/Appwrite/Utopia/Response/Model/Site.php b/src/Appwrite/Utopia/Response/Model/Site.php new file mode 100644 index 0000000000..7c4a9a265d --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Site.php @@ -0,0 +1,157 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Site ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('$createdAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Site creation date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('$updatedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Site update date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'Site name.', + 'default' => '', + 'example' => 'My Site', + ]) + ->addRule('enabled', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Site enabled.', + 'default' => true, + 'example' => false, + ]) + ->addRule('live', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Is the site deployed with the latest configuration? This is set to false if you\'ve changed an environment variables, entrypoint, commands, or other settings that needs redeploy to be applied. When the value is false, redeploy the site to update it with the latest configuration.', + 'default' => true, + 'example' => false, + ]) + ->addRule('framework', [ + 'type' => self::TYPE_STRING, + 'description' => 'Site framework.', + 'default' => '', + 'example' => 'react', + ]) + ->addRule('deploymentId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Site\'s active deployment ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('scopes', [ + 'type' => self::TYPE_STRING, + 'description' => 'Allowed permission scopes.', + 'default' => [], + 'example' => 'users.read', + 'array' => true, + ]) + ->addRule('vars', [ + 'type' => Response::MODEL_VARIABLE, + 'description' => 'Site variables.', + 'default' => [], + 'example' => [], + 'array' => true + ]) + ->addRule('installCommand', [ + 'type' => self::TYPE_STRING, + 'description' => 'The install command used to install the site dependencies.', + 'default' => '', + 'example' => 'npm install', + ]) + ->addRule('buildCommand', [ + 'type' => self::TYPE_STRING, + 'description' => 'The build command used to build the site.', + 'default' => '', + 'example' => 'npm run build', + ]) + ->addRule('outputDirectory', [ + 'type' => self::TYPE_STRING, + 'description' => 'The directory where the site build output is located.', + 'default' => '', + 'example' => 'build', + ]) + ->addRule('fallbackRedirect', [ + 'type' => self::TYPE_STRING, + 'description' => 'The URL to redirect to if the route is not found.', //TODO: Update the description + 'default' => '', + 'example' => 'https://appwrite.io', + ]) + ->addRule('installationId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Site VCS (Version Control System) installation id.', + 'default' => '', + 'example' => '6m40at4ejk5h2u9s1hboo', + ]) + ->addRule('providerRepositoryId', [ + 'type' => self::TYPE_STRING, + 'description' => 'VCS (Version Control System) Repository ID', + 'default' => '', + 'example' => 'appwrite', + ]) + ->addRule('providerBranch', [ + 'type' => self::TYPE_STRING, + 'description' => 'VCS (Version Control System) branch name', + 'default' => '', + 'example' => 'main', + ]) + ->addRule('providerRootDirectory', [ + 'type' => self::TYPE_STRING, + 'description' => 'Path to site in VCS (Version Control System) repository', + 'default' => '', + 'example' => 'sites/helloWorld', + ]) + ->addRule('providerSilentMode', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Is VCS (Version Control System) connection is in silent mode? When in silence mode, no comments will be posted on the repository pull or merge requests', + 'default' => false, + 'example' => false, + ]) + ->addRule('specification', [ + 'type' => self::TYPE_STRING, + 'description' => 'Machine specification for builds and executions.', + 'default' => APP_SITE_SPECIFICATION_DEFAULT, + 'example' => APP_SITE_SPECIFICATION_DEFAULT, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Site'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_SITE; + } +}