mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 08:58:35 +00:00
Merge pull request #8527 from appwrite/fix-manual-templates
Fix: Manual templates
This commit is contained in:
commit
78af3ab867
17 changed files with 338 additions and 98 deletions
|
|
@ -553,6 +553,12 @@ To run end-2-end tests for a specific service use:
|
|||
docker compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName]
|
||||
```
|
||||
|
||||
To run one specific test:
|
||||
|
||||
```bash
|
||||
docker compose exec appwrite vendor/bin/phpunit --filter [FunctionName]
|
||||
```
|
||||
|
||||
## Benchmarking
|
||||
|
||||
You can use WRK Docker image to benchmark the server performance. Benchmarking is extremely useful when you want to compare how the server behaves before and after a change has been applied. Replace [APPWRITE_HOSTNAME_OR_IP] with your Appwrite server hostname or IP. Note that localhost is not accessible from inside the WRK container.
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [],
|
||||
'scopes' => ["users.read"]
|
||||
],
|
||||
|
|
@ -106,7 +106,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'UPSTASH_URL',
|
||||
|
|
@ -149,7 +149,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'REDIS_HOST',
|
||||
|
|
@ -191,7 +191,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'NEO4J_URI',
|
||||
|
|
@ -242,7 +242,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'MONGO_URI',
|
||||
|
|
@ -278,7 +278,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'PGHOST',
|
||||
|
|
@ -362,7 +362,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'OPENAI_API_KEY',
|
||||
|
|
@ -416,7 +416,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'DISCORD_PUBLIC_KEY',
|
||||
|
|
@ -466,7 +466,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'PERSPECTIVE_API_KEY',
|
||||
|
|
@ -513,7 +513,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'PANGEA_REDACT_TOKEN',
|
||||
|
|
@ -542,7 +542,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => []
|
||||
],
|
||||
[
|
||||
|
|
@ -568,7 +568,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'GITHUB_TOKEN',
|
||||
|
|
@ -610,7 +610,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'APPWRITE_DATABASE_ID',
|
||||
|
|
@ -673,7 +673,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'APPWRITE_DATABASE_ID',
|
||||
|
|
@ -766,7 +766,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'APPWRITE_DATABASE_ID',
|
||||
|
|
@ -865,7 +865,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'VONAGE_API_KEY',
|
||||
|
|
@ -920,7 +920,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'FCM_PROJECT_ID',
|
||||
|
|
@ -987,7 +987,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'SMTP_HOST',
|
||||
|
|
@ -1057,7 +1057,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'STRIPE_SECRET_KEY',
|
||||
|
|
@ -1098,7 +1098,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'STRIPE_SECRET_KEY',
|
||||
|
|
@ -1155,7 +1155,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'HUGGINGFACE_ACCESS_TOKEN',
|
||||
|
|
@ -1188,7 +1188,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'HUGGINGFACE_ACCESS_TOKEN',
|
||||
|
|
@ -1221,7 +1221,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'APPWRITE_DATABASE_ID',
|
||||
|
|
@ -1279,7 +1279,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'APPWRITE_DATABASE_ID',
|
||||
|
|
@ -1337,7 +1337,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'APPWRITE_DATABASE_ID',
|
||||
|
|
@ -1395,7 +1395,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'APPWRITE_DATABASE_ID',
|
||||
|
|
@ -1453,7 +1453,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'REPLICATE_API_KEY',
|
||||
|
|
@ -1487,7 +1487,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'TOGETHER_API_KEY',
|
||||
|
|
@ -1529,7 +1529,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'PERPLEXITY_API_KEY',
|
||||
|
|
@ -1569,7 +1569,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'REPLICATE_API_KEY',
|
||||
|
|
@ -1603,7 +1603,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'OPENAI_API_KEY',
|
||||
|
|
@ -1666,7 +1666,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'OPENAI_API_KEY',
|
||||
|
|
@ -1729,7 +1729,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'ELEVENLABS_API_KEY',
|
||||
|
|
@ -1784,7 +1784,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'LMNT_API_KEY',
|
||||
|
|
@ -1825,7 +1825,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'ANYSCALE_API_KEY',
|
||||
|
|
@ -1865,7 +1865,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'APPWRITE_BUCKET_ID',
|
||||
|
|
@ -1907,7 +1907,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'FAL_API_KEY',
|
||||
|
|
@ -1941,7 +1941,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'LEMON_SQUEEZY_API_KEY',
|
||||
|
|
@ -1996,7 +1996,7 @@ return [
|
|||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerBranch' => 'main',
|
||||
'providerVersion' => '0.2.*',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => 'APPWRITE_DATABASE_ID',
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -165,7 +165,7 @@ App::post('/v1/functions')
|
|||
->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('templateBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function template.', true)
|
||||
->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
|
|
@ -175,7 +175,7 @@ App::post('/v1/functions')
|
|||
->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 $templateBranch, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
|
||||
->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, 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', '')));
|
||||
|
|
@ -190,12 +190,12 @@ App::post('/v1/functions')
|
|||
!empty($templateRepository)
|
||||
&& !empty($templateOwner)
|
||||
&& !empty($templateRootDirectory)
|
||||
&& !empty($templateBranch)
|
||||
&& !empty($templateVersion)
|
||||
) {
|
||||
$template->setAttribute('repositoryName', $templateRepository)
|
||||
->setAttribute('ownerName', $templateOwner)
|
||||
->setAttribute('rootDirectory', $templateRootDirectory)
|
||||
->setAttribute('branch', $templateBranch);
|
||||
->setAttribute('version', $templateVersion);
|
||||
}
|
||||
|
||||
$installation = $dbForConsole->getDocument('installations', $installationId);
|
||||
|
|
@ -284,9 +284,34 @@ App::post('/v1/functions')
|
|||
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
|
||||
// Redeploy vcs logic
|
||||
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);
|
||||
}
|
||||
|
||||
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
||||
|
|
|
|||
12
composer.lock
generated
12
composer.lock
generated
|
|
@ -2759,16 +2759,16 @@
|
|||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/vcs.git",
|
||||
"reference": "3084aa93d24ed1e70f01e75f4318fc0d07f12596"
|
||||
"reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/3084aa93d24ed1e70f01e75f4318fc0d07f12596",
|
||||
"reference": "3084aa93d24ed1e70f01e75f4318fc0d07f12596",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18",
|
||||
"reference": "eb9b7eade1a46a4f660e0d5a6304f7fa26ec9d18",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -2802,9 +2802,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/vcs/issues",
|
||||
"source": "https://github.com/utopia-php/vcs/tree/0.8.1"
|
||||
"source": "https://github.com/utopia-php/vcs/tree/0.8.2"
|
||||
},
|
||||
"time": "2024-07-29T20:49:09+00:00"
|
||||
"time": "2024-08-13T14:36:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/websocket",
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ services:
|
|||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: appwrite/console:5.0.0-rc14
|
||||
image: appwrite/console:5.0.0-rc15
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="true"
|
||||
stopOnFailure="false"
|
||||
>
|
||||
<extensions>
|
||||
<extension class="Appwrite\Tests\TestHook" />
|
||||
|
|
|
|||
|
|
@ -207,7 +207,65 @@ class Builds extends Action
|
|||
}
|
||||
|
||||
try {
|
||||
if ($isNewBuild && $isVcsEnabled) {
|
||||
if($isNewBuild && !$isVcsEnabled) {
|
||||
// Non-vcs+Template
|
||||
|
||||
$templateRepositoryName = $template->getAttribute('repositoryName', '');
|
||||
$templateOwnerName = $template->getAttribute('ownerName', '');
|
||||
$templateVersion = $template->getAttribute('version', '');
|
||||
|
||||
$templateRootDirectory = $template->getAttribute('rootDirectory', '');
|
||||
$templateRootDirectory = \rtrim($templateRootDirectory, '/');
|
||||
$templateRootDirectory = \ltrim($templateRootDirectory, '.');
|
||||
$templateRootDirectory = \ltrim($templateRootDirectory, '/');
|
||||
|
||||
if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) {
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
|
||||
// Clone template repo
|
||||
$tmpTemplateDirectory = '/tmp/builds/' . $buildId . '-template';
|
||||
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory);
|
||||
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
throw new \Exception('Unable to clone code repository: ' . $stderr);
|
||||
}
|
||||
|
||||
// Ensure directories
|
||||
Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr);
|
||||
|
||||
$tmpPathFile = $tmpTemplateDirectory . '/code.tar.gz';
|
||||
|
||||
$localDevice = new Local();
|
||||
|
||||
if (substr($tmpTemplateDirectory, -1) !== '/') {
|
||||
$tmpTemplateDirectory .= '/';
|
||||
}
|
||||
|
||||
$directorySize = $localDevice->getDirectorySize($tmpTemplateDirectory);
|
||||
$functionsSizeLimit = (int)System::getEnv('_APP_FUNCTIONS_SIZE_LIMIT', '30000000');
|
||||
if ($directorySize > $functionsSizeLimit) {
|
||||
throw new \Exception('Repository directory size should be less than ' . number_format($functionsSizeLimit / 1048576, 2) . ' MBs.');
|
||||
}
|
||||
|
||||
$tarParamDirectory = \escapeshellarg('/tmp/builds/' . $buildId . '-template' . (empty($templateRootDirectory) ? '' : '/' . $templateRootDirectory));
|
||||
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
|
||||
|
||||
$source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
|
||||
$result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions);
|
||||
|
||||
if (!$result) {
|
||||
throw new \Exception("Unable to move file");
|
||||
}
|
||||
|
||||
Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $stdout, $stderr);
|
||||
|
||||
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source));
|
||||
}
|
||||
} elseif ($isNewBuild && $isVcsEnabled) {
|
||||
// VCS and VCS+Temaplte
|
||||
$tmpDirectory = '/tmp/builds/' . $buildId . '/code';
|
||||
$rootDirectory = $function->getAttribute('providerRootDirectory', '');
|
||||
$rootDirectory = \rtrim($rootDirectory, '/');
|
||||
|
|
@ -233,7 +291,8 @@ class Builds extends Action
|
|||
$gitCloneCommand = $github->generateCloneCommand($cloneOwner, $cloneRepository, $cloneVersion, $cloneType, $tmpDirectory, $rootDirectory);
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
Console::execute('mkdir -p /tmp/builds/' . \escapeshellcmd($buildId), '', $stdout, $stderr);
|
||||
|
||||
Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $buildId), '', $stdout, $stderr);
|
||||
|
||||
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
|
||||
Console::info('Build has been canceled');
|
||||
|
|
@ -263,23 +322,18 @@ class Builds extends Action
|
|||
// Build from template
|
||||
$templateRepositoryName = $template->getAttribute('repositoryName', '');
|
||||
$templateOwnerName = $template->getAttribute('ownerName', '');
|
||||
$templateBranch = $template->getAttribute('branch', '');
|
||||
$templateVersion = $template->getAttribute('version', '');
|
||||
|
||||
$templateRootDirectory = $template->getAttribute('rootDirectory', '');
|
||||
$templateRootDirectory = \rtrim($templateRootDirectory, '/');
|
||||
$templateRootDirectory = \ltrim($templateRootDirectory, '.');
|
||||
$templateRootDirectory = \ltrim($templateRootDirectory, '/');
|
||||
|
||||
if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateBranch)) {
|
||||
if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) {
|
||||
// Clone template repo
|
||||
$tmpTemplateDirectory = '/tmp/builds/' . \escapeshellcmd($buildId) . '/template';
|
||||
$tmpTemplateDirectory = '/tmp/builds/' . $buildId . '/template';
|
||||
|
||||
$cloneType = GitHub::CLONE_TYPE_BRANCH;
|
||||
if(\str_starts_with($templateBranch, '0.1.')) { // Temporary fix for 1.5. In future versions this only support tag names
|
||||
$cloneType = GitHub::CLONE_TYPE_TAG;
|
||||
}
|
||||
|
||||
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateBranch, $cloneType, $tmpTemplateDirectory, $templateRootDirectory);
|
||||
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory);
|
||||
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
|
|
@ -287,20 +341,20 @@ class Builds extends Action
|
|||
}
|
||||
|
||||
// Ensure directories
|
||||
Console::execute('mkdir -p ' . $tmpTemplateDirectory . '/' . $templateRootDirectory, '', $stdout, $stderr);
|
||||
Console::execute('mkdir -p ' . $tmpDirectory . '/' . $rootDirectory, '', $stdout, $stderr);
|
||||
Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr);
|
||||
Console::execute('mkdir -p ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr);
|
||||
|
||||
// Merge template into user repo
|
||||
Console::execute('rsync -av --exclude \'.git\' ' . $tmpTemplateDirectory . '/' . $templateRootDirectory . '/ ' . $tmpDirectory . '/' . $rootDirectory, '', $stdout, $stderr);
|
||||
Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr);
|
||||
|
||||
// Commit and push
|
||||
$exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . $tmpDirectory . ' && git add . && git commit -m "Create \'' . \escapeshellcmd($function->getAttribute('name', '')) . '\' function" && git push origin ' . \escapeshellcmd($branchName), '', $stdout, $stderr);
|
||||
$exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
throw new \Exception('Unable to push code repository: ' . $stderr);
|
||||
}
|
||||
|
||||
$exit = Console::execute('cd ' . $tmpDirectory . ' && git rev-parse HEAD', '', $stdout, $stderr);
|
||||
$exit = Console::execute('cd ' . \escapeshellarg($tmpDirectory) . ' && git rev-parse HEAD', '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
throw new \Exception('Unable to get vcs commit SHA: ' . $stderr);
|
||||
|
|
@ -334,7 +388,7 @@ class Builds extends Action
|
|||
);
|
||||
}
|
||||
|
||||
$tmpPath = '/tmp/builds/' . \escapeshellcmd($buildId);
|
||||
$tmpPath = '/tmp/builds/' . $buildId;
|
||||
$tmpPathFile = $tmpPath . '/code.tar.gz';
|
||||
$localDevice = new Local();
|
||||
|
||||
|
|
@ -348,21 +402,17 @@ class Builds extends Action
|
|||
throw new \Exception('Repository directory size should be less than ' . number_format($functionsSizeLimit / 1048576, 2) . ' MBs.');
|
||||
}
|
||||
|
||||
Console::execute('tar --exclude code.tar.gz -czf ' . $tmpPathFile . ' -C /tmp/builds/' . \escapeshellcmd($buildId) . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory) . ' .', '', $stdout, $stderr);
|
||||
$tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory);
|
||||
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
|
||||
|
||||
$path = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
|
||||
$result = $localDevice->transfer($tmpPathFile, $path, $deviceForFunctions);
|
||||
$source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
|
||||
$result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions);
|
||||
|
||||
if (!$result) {
|
||||
throw new \Exception("Unable to move file");
|
||||
}
|
||||
|
||||
$deployment->setAttribute('path', $path);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
|
||||
Console::execute('rm -rf ' . $tmpPath, '', $stdout, $stderr);
|
||||
|
||||
$source = $path;
|
||||
Console::execute('rm -rf ' . \escapeshellarg($tmpPath), '', $stdout, $stderr);
|
||||
|
||||
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source));
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ class V18 extends Filter
|
|||
case 'account.deleteMfaAuthenticator':
|
||||
unset($content['otp']);
|
||||
break;
|
||||
|
||||
case 'functions.create':
|
||||
$content['templateVersion'] = $content['templateBranch'];
|
||||
unset($content['templateBranch']);
|
||||
break;
|
||||
}
|
||||
|
||||
return $content;
|
||||
|
|
|
|||
|
|
@ -98,9 +98,9 @@ class TemplateFunction extends Model
|
|||
'default' => '',
|
||||
'example' => 'appwrite',
|
||||
])
|
||||
->addRule('providerBranch', [
|
||||
->addRule('providerVersion', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'VCS (Version Control System) branch name',
|
||||
'description' => 'VCS (Version Control System) branch version (tag).',
|
||||
'default' => '',
|
||||
'example' => 'main',
|
||||
])
|
||||
|
|
|
|||
|
|
@ -250,8 +250,8 @@ class FunctionsCustomClientTest extends Scope
|
|||
break;
|
||||
}
|
||||
|
||||
if (\microtime(true) - $start > 5) {
|
||||
$this->fail('Execution did not complete within 5 seconds of schedule');
|
||||
if (\microtime(true) - $start > 10) {
|
||||
$this->fail('Execution did not complete within 10 seconds of schedule in status ' . $execution['body']['status'] . ': ' . \json_encode($execution));
|
||||
}
|
||||
|
||||
usleep(500000); // 0.5 seconds
|
||||
|
|
@ -848,6 +848,7 @@ class FunctionsCustomClientTest extends Scope
|
|||
$this->assertEquals(200, $templates['headers']['status-code']);
|
||||
$this->assertGreaterThan(0, $templates['body']['total']);
|
||||
$this->assertIsArray($templates['body']['templates']);
|
||||
|
||||
$this->assertArrayHasKey('runtimes', $templates['body']['templates'][0]);
|
||||
$this->assertArrayHasKey('useCases', $templates['body']['templates'][0]);
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
|
|
|
|||
|
|
@ -439,6 +439,135 @@ class FunctionsCustomServerTest extends Scope
|
|||
$this->assertEquals('cli', $functionDetails['body']['type']);
|
||||
}
|
||||
|
||||
public function testCreateDeploymentFromTemplate()
|
||||
{
|
||||
$runtimeName = 'php-8.0';
|
||||
|
||||
// Fetch starter template (used to create function later)
|
||||
$template = $this->client->call(Client::METHOD_GET, '/functions/templates/starter', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $template['headers']['status-code']);
|
||||
|
||||
$entrypoint = null;
|
||||
$rootDirectory = null;
|
||||
$commands = null;
|
||||
foreach($template['body']['runtimes'] as $runtime) {
|
||||
if($runtime["name"] !== $runtimeName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entrypoint = $runtime["entrypoint"];
|
||||
$rootDirectory = $runtime["providerRootDirectory"];
|
||||
$commands = $runtime["commands"];
|
||||
break;
|
||||
}
|
||||
|
||||
$this->assertNotNull($entrypoint);
|
||||
|
||||
/**
|
||||
* If below test ever starts failing, it means temaplate used in
|
||||
* this test now has some variables. This test currently doesnt test variables.
|
||||
* Remove bellow assertion and update test to crete variable,
|
||||
* and ensure variable works as expected in execution.
|
||||
*/
|
||||
$this->assertEmpty($template['body']['variables']);
|
||||
|
||||
// Create function using settings from template.
|
||||
// Deployment is automatically created from template inside endpoint
|
||||
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), [
|
||||
'functionId' => ID::unique(),
|
||||
'name' => $template['body']['name'],
|
||||
'runtime' => $runtimeName,
|
||||
'execute' => $template['body']['permissions'],
|
||||
'entrypoint' => $entrypoint,
|
||||
'events' => $template['body']['events'],
|
||||
'schedule' => $template['body']['cron'],
|
||||
'timeout' => $template['body']['timeout'],
|
||||
'commands' => $commands,
|
||||
'scopes' => $template['body']['scopes'],
|
||||
'templateRepository' => $template['body']['providerRepositoryId'],
|
||||
'templateOwner' => $template['body']['providerOwner'],
|
||||
'templateRootDirectory' => $rootDirectory,
|
||||
'templateVersion' => $template['body']['providerVersion'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $function['headers']['status-code']);
|
||||
$this->assertNotEmpty($function['body']['$id']);
|
||||
|
||||
$functionId = $function['body']['$id'];
|
||||
|
||||
// List deployments so we can await deployment build
|
||||
$deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], []);
|
||||
|
||||
$this->assertEquals(200, $deployments['headers']['status-code']);
|
||||
$this->assertEquals(1, $deployments['body']['total']);
|
||||
$this->assertNotEmpty($deployments['body']['deployments'][0]['$id']);
|
||||
|
||||
$deploymentId = $deployments['body']['deployments'][0]['$id'];
|
||||
|
||||
// Wait for deployment build to finish
|
||||
// Deployment is automatically activated
|
||||
$this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId);
|
||||
|
||||
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(200, $function['headers']['status-code']);
|
||||
$this->assertEquals($deploymentId, $function['body']['deployment']);
|
||||
|
||||
// Execute function to ensure starter code is used
|
||||
// Also tests if dynamic keys works as expected
|
||||
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'path' => '/ping'
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $execution['headers']['status-code']);
|
||||
$this->assertEquals("completed", $execution['body']['status']);
|
||||
$this->assertEquals(200, $execution['body']['responseStatusCode']);
|
||||
$this->assertEquals("Pong", $execution['body']['responseBody']);
|
||||
$this->assertEmpty($execution['body']['errors']);
|
||||
|
||||
// Get users to ensure execution logged correct total users
|
||||
$users = $this->client->call(Client::METHOD_GET, '/users', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(200, $users['headers']['status-code']);
|
||||
$this->assertIsInt($users['body']['total']);
|
||||
|
||||
$totalusers = $users['body']['total'];
|
||||
|
||||
$this->assertStringContainsString("Total users: " . $totalusers, $execution['body']['logs']);
|
||||
|
||||
// Cleanup : Delete function
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], []);
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testUpdate
|
||||
*/
|
||||
|
|
@ -1268,7 +1397,11 @@ class FunctionsCustomServerTest extends Scope
|
|||
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::equal('trigger', ['http'])->toString(),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals($executions['headers']['status-code'], 200);
|
||||
$this->assertEquals($executions['body']['total'], 1);
|
||||
|
|
@ -1284,17 +1417,39 @@ class FunctionsCustomServerTest extends Scope
|
|||
$this->assertEquals($executions['body']['executions'][0]['logs'], '');
|
||||
$this->assertStringContainsString('timed out', $executions['body']['executions'][0]['errors']);
|
||||
|
||||
sleep(75); // Wait for scheduled execution to be created and time out
|
||||
$start = \microtime(true);
|
||||
while (true) {
|
||||
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::equal('trigger', ['schedule'])->toString(),
|
||||
],
|
||||
]);
|
||||
|
||||
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(200, $executions['headers']['status-code']);
|
||||
|
||||
if (\count($executions['body']['executions']) > 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 0s would mean instant execution
|
||||
// +60 seconds, maximum possible waiting time before next minute
|
||||
// +10 seconds, maximum update interval time
|
||||
// +60 seconds, possible overlap between update and schedule tick
|
||||
// +10 seconds, maximum execution time including cold-start
|
||||
// Result: We allow maximum
|
||||
if (\microtime(true) - $start > 140) {
|
||||
$this->fail('Execution did not create within 140 seconds of schedule: ' . \json_encode($executions));
|
||||
}
|
||||
|
||||
usleep(1000000); // 1 second
|
||||
}
|
||||
|
||||
$this->assertEquals(200, $executions['headers']['status-code']);
|
||||
$this->assertCount(2, $executions['body']['executions']);
|
||||
$this->assertIsArray($executions['body']['executions']);
|
||||
$this->assertEquals($executions['body']['executions'][1]['trigger'], 'schedule');
|
||||
$this->assertGreaterThanOrEqual(1, \count($executions['body']['executions']));
|
||||
$this->assertEquals($executions['body']['executions'][0]['trigger'], 'schedule');
|
||||
|
||||
// Cleanup : Delete function
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [
|
||||
|
|
|
|||
Loading…
Reference in a new issue