diff --git a/CHANGES.md b/CHANGES.md index 4ef91e3ac9..2d178e00a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,8 @@ # Version 0.8.0 (Not Released Yet) ## Features - -- Anonymous login +- Anonymous login (#914) +- Added events for functions and executions (#971) ## Breaking Changes diff --git a/app/config/events.php b/app/config/events.php index 601b502163..cb0df42a06 100644 --- a/app/config/events.php +++ b/app/config/events.php @@ -97,6 +97,46 @@ return [ 'model' => Response::MODEL_ANY, 'note' => '', ], + 'functions.create' => [ + 'description' => 'This event triggers when a function is created.', + 'model' => Response::MODEL_FUNCTION, + 'note' => 'version >= 0.7', + ], + 'functions.update' => [ + 'description' => 'This event triggers when a function is updated.', + 'model' => Response::MODEL_FUNCTION, + 'note' => 'version >= 0.7', + ], + 'functions.delete' => [ + 'description' => 'This event triggers when a function is deleted.', + 'model' => Response::MODEL_ANY, + 'note' => 'version >= 0.7', + ], + 'functions.tags.create' => [ + 'description' => 'This event triggers when a function tag is created.', + 'model' => Response::MODEL_TAG, + 'note' => 'version >= 0.7', + ], + 'functions.tags.update' => [ + 'description' => 'This event triggers when a function tag is updated.', + 'model' => Response::MODEL_FUNCTION, + 'note' => 'version >= 0.7', + ], + 'functions.tags.delete' => [ + 'description' => 'This event triggers when a function tag is deleted.', + 'model' => Response::MODEL_ANY, + 'note' => 'version >= 0.7', + ], + 'functions.executions.create' => [ + 'description' => 'This event triggers when a function execution is created.', + 'model' => Response::MODEL_EXECUTION, + 'note' => 'version >= 0.7', + ], + 'functions.executions.update' => [ + 'description' => 'This event triggers when a function execution is updated.', + 'model' => Response::MODEL_EXECUTION, + 'note' => 'version >= 0.7', + ], 'storage.files.create' => [ 'description' => 'This event triggers when a storage file is created.', 'model' => Response::MODEL_FILE, @@ -167,4 +207,4 @@ return [ 'model' => Response::MODEL_MEMBERSHIP, 'note' => 'version >= 0.7', ], -]; \ No newline at end of file +]; diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 8d49963cd8..15e591a60f 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -27,6 +27,7 @@ App::post('/v1/functions') ->groups(['api', 'functions']) ->desc('Create Function') ->label('scope', 'functions.write') + ->label('event', 'functions.create') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'create') @@ -265,6 +266,7 @@ App::put('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Update Function') ->label('scope', 'functions.write') + ->label('event', 'functions.update') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'update') @@ -330,6 +332,7 @@ App::patch('/v1/functions/:functionId/tag') ->groups(['api', 'functions']) ->desc('Update Function Tag') ->label('scope', 'functions.write') + ->label('event', 'functions.tags.update') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'updateTag') @@ -387,6 +390,7 @@ App::delete('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Delete Function') ->label('scope', 'functions.write') + ->label('event', 'functions.delete') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'delete') @@ -424,6 +428,7 @@ App::post('/v1/functions/:functionId/tags') ->groups(['api', 'functions']) ->desc('Create Tag') ->label('scope', 'functions.write') + ->label('event', 'functions.tags.create') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'createTag') @@ -601,6 +606,7 @@ App::delete('/v1/functions/:functionId/tags/:tagId') ->groups(['api', 'functions']) ->desc('Delete Tag') ->label('scope', 'functions.write') + ->label('event', 'functions.tags.delete') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'deleteTag') @@ -662,6 +668,7 @@ App::post('/v1/functions/:functionId/executions') ->groups(['api', 'functions']) ->desc('Create Execution') ->label('scope', 'execution.write') + ->label('event', 'functions.executions.create') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'createExecution') diff --git a/app/workers/functions.php b/app/workers/functions.php index 87e3a86245..e88c2688be 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -148,6 +148,7 @@ class FunctionsV1 $event = $this->args['event'] ?? ''; $scheduleOriginal = $this->args['scheduleOriginal'] ?? ''; $payload = (!empty($this->args['payload'])) ? json_encode($this->args['payload']) : ''; + $userId = $this->args['userId'] ?? ''; $database = new Database(); $database->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); @@ -195,7 +196,7 @@ class FunctionsV1 Console::success('Triggered function: '.$event); - $this->execute('event', $projectId, '', $database, $function, $event, $payload); + $this->execute('event', $projectId, '', $database, $function, $event, $payload, $userId); } } break; @@ -251,8 +252,8 @@ class FunctionsV1 'scheduleOriginal' => $function->getAttribute('schedule', ''), ]); // Async task rescheduale - $this->execute($trigger, $projectId, $executionId, $database, $function); + $this->execute($trigger, $projectId, $executionId, $database, $function, $userId); break; case 'http': @@ -264,7 +265,7 @@ class FunctionsV1 throw new Exception('Function not found ('.$functionId.')'); } - $this->execute($trigger, $projectId, $executionId, $database, $function); + $this->execute($trigger, $projectId, $executionId, $database, $function, $userId); break; default: @@ -286,7 +287,7 @@ class FunctionsV1 * * @return void */ - public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = ''): void + public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = '', string $userId = ''): void { global $list; @@ -469,6 +470,26 @@ class FunctionsV1 throw new Exception('Failed saving execution to DB', 500); } + $executionUpdate = new Event('v1-webhooks', 'WebhooksV1'); + + $executionUpdate + ->setParam('projectId', $projectId) + ->setParam('userId', $userId) + ->setParam('event', 'functions.executions.update') + ->setParam('payload', [ + '$id' => $execution['$id'], + 'functionId' => $execution['functionId'], + 'dateCreated' => $execution['dateCreated'], + 'trigger' => $execution['trigger'], + 'status' => $execution['status'], + 'exitCode' => $execution['exitCode'], + 'stdout' => $execution['stdout'], + 'stderr' => $execution['stderr'], + 'time' => $execution['time'] + ]); + + $executionUpdate->trigger(); + $usage = new Event('v1-usage', 'UsageV1'); $usage diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 0596aee09f..fc35f1ddae 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -113,6 +113,14 @@ trait ProjectCustom 'database.documents.create', 'database.documents.update', 'database.documents.delete', + 'functions.create', + 'functions.update', + 'functions.delete', + 'functions.tags.create', + 'functions.tags.update', + 'functions.tags.delete', + 'functions.executions.create', + 'functions.executions.update', 'storage.files.create', 'storage.files.update', 'storage.files.delete', diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php index d2d5549134..c8e3ba747d 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php @@ -2,6 +2,7 @@ namespace Tests\E2E\Services\Webhooks; +use CURLFile; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; @@ -294,4 +295,265 @@ class WebhooksCustomServerTest extends Scope return $data; } + + public function testCreateFunction():array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Test', + 'env' => 'php-7.4', + 'execute' => ['*'], + 'timeout' => 10, + ]); + + $functionId = $function['body']['$id'] ?? ''; + + $this->assertEquals($function['headers']['status-code'], 201); + $this->assertNotEmpty($function['body']['$id']); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'functions.create'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + + /** + * Test for FAILURE + */ + + return [ + 'functionId' => $functionId, + ]; + } + + /** + * @depends testCreateFunction + */ + public function testUpdateFunction($data):array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_PUT, '/functions/'.$data['functionId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Test', + 'env' => 'php-7.4', + 'execute' => ['*'], + 'vars' => [ + 'key1' => 'value1', + ] + ]); + + $this->assertEquals($function['headers']['status-code'], 200); + $this->assertEquals($function['body']['$id'], $data['functionId']); + $this->assertEquals($function['body']['vars'], ['key1' => 'value1']); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'functions.update'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + + return $data; + } + + /** + * @depends testUpdateFunction + */ + public function testCreateTag($data):array + { + /** + * Test for SUCCESS + */ + $tag = $this->client->call(Client::METHOD_POST, '/functions/'.$data['functionId'].'/tags', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'command' => 'php index.php', + 'code' => new CURLFile(realpath(__DIR__ . '/../../../resources/functions/timeout.tar.gz'), 'application/x-gzip', 'php-fx.tar.gz'), + ]); + + $tagId = $tag['body']['$id'] ?? ''; + + $this->assertEquals($tag['headers']['status-code'], 201); + $this->assertNotEmpty($tag['body']['$id']); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'functions.tags.create'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + + /** + * Test for FAILURE + */ + + return array_merge($data, ['tagId' => $tagId]); + } + + /** + * @depends testCreateTag + */ + public function testUpdateTag($data):array + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$data['functionId'].'/tag', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'tag' => $data['tagId'], + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']['$id']); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'functions.tags.update'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testUpdateTag + */ + public function testExecutions($data):array + { + /** + * Test for SUCCESS + */ + $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$data['functionId'].'/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals($execution['headers']['status-code'], 201); + $this->assertNotEmpty($execution['body']['$id']); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'functions.executions.create'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + + // wait for timeout function to complete (sleep(5);) + sleep(6); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'functions.executions.update'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testExecutions + */ + public function testDeleteTag($data):array + { + /** + * Test for SUCCESS + */ + $tag = $this->client->call(Client::METHOD_DELETE, '/functions/'.$data['functionId'].'/tags/'.$data['tagId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($tag['headers']['status-code'], 204); + $this->assertEmpty($tag['body']); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'functions.tags.delete'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testDeleteTag + */ + public function testDeleteFunction($data):array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_DELETE, '/functions/'.$data['functionId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $function['headers']['status-code']); + $this->assertEmpty($function['body']); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'functions.delete'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + + /** + * Test for FAILURE + */ + + return $data; + } }