diff --git a/app/config/runtimes-v2.php b/app/config/runtimes-v2.php new file mode 100644 index 0000000000..c24aaa109e --- /dev/null +++ b/app/config/runtimes-v2.php @@ -0,0 +1,16 @@ +getAll(true, $allowList); + +return $runtimes; diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 5c8499cdf3..4da67328cd 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -1523,7 +1523,8 @@ App::post('/v1/functions/:functionId/executions') throw new Exception(Exception::FUNCTION_NOT_FOUND); } - $runtimes = Config::getParam('runtimes', []); + $version = $function->getAttribute('version', 'v2'); + $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; @@ -1684,7 +1685,9 @@ App::post('/v1/functions/:functionId/executions') /** Execute function */ $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); try { + $version = $function->getAttribute('version', 'v2'); $command = $runtime['startCommand']; + $command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"'; $executionResponse = $executor->createExecution( projectId: $project->getId(), deploymentId: $deployment->getId(), @@ -1694,11 +1697,11 @@ App::post('/v1/functions/:functionId/executions') image: $runtime['image'], source: $build->getAttribute('path', ''), entrypoint: $deployment->getAttribute('entrypoint', ''), - version: $function->getAttribute('version'), + version: $version, path: $path, method: $method, headers: $headers, - runtimeEntrypoint: 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"' + runtimeEntrypoint: $command ); $headersFiltered = []; diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index c6d9b2d4e3..00520ae8d7 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -750,7 +750,7 @@ App::put('/v1/users/:userId/labels') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USER) ->param('userId', '', new UID(), 'User ID.') - ->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), 5), 'Array of user labels. Replaces the previous labels. Maximum of 5 labels are allowed, each up to 36 alphanumeric characters long.') + ->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of user labels. Replaces the previous labels. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' labels are allowed, each up to 36 alphanumeric characters long.') ->inject('response') ->inject('dbForProject') ->inject('events') diff --git a/app/controllers/mock.php b/app/controllers/mock.php index a763df82c0..8a4d5650d7 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -8,12 +8,14 @@ use Utopia\Validator\Host; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Utopia\App; +use Utopia\Database\Database; use Utopia\Validator\ArrayList; use Utopia\Validator\Integer; use Utopia\Validator\Text; use Utopia\Storage\Validator\File; use Utopia\Validator\WhiteList; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\UID; use Utopia\Validator\Nullable; App::get('/v1/mock/tests/foo') @@ -605,6 +607,32 @@ App::get('/v1/mock/tests/general/oauth2/failure') ]); }); +App::patch('/v1/mock/functions-v2') + ->desc('Update Function Version to V2 (outdated code syntax)') + ->groups(['mock', 'api', 'functions']) + ->label('scope', 'functions.write') + ->label('docs', false) + ->param('functionId', '', new UID(), 'Function ID.') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $functionId, Response $response, Database $dbForProject) { + $isDevelopment = App::getEnv('_APP_ENV', 'development') === 'development'; + + if (!$isDevelopment) { + throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); + } + + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('version', 'v2')); + + $response->noContent(); + }); + App::shutdown() ->groups(['mock']) ->inject('utopia') diff --git a/app/init.php b/app/init.php index 439cef7a80..91ded9f017 100644 --- a/app/init.php +++ b/app/init.php @@ -237,6 +237,7 @@ Config::load('providers', __DIR__ . '/config/providers.php'); Config::load('platforms', __DIR__ . '/config/platforms.php'); Config::load('collections', __DIR__ . '/config/collections.php'); Config::load('runtimes', __DIR__ . '/config/runtimes.php'); +Config::load('runtimes-v2', __DIR__ . '/config/runtimes-v2.php'); Config::load('roles', __DIR__ . '/config/roles.php'); // User roles and scopes Config::load('scopes', __DIR__ . '/config/scopes.php'); // User roles and scopes Config::load('services', __DIR__ . '/config/services.php'); // List of services diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 7a97ab20de..289f03c94b 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -597,7 +597,7 @@ services: hostname: appwrite-executor <<: *x-logging stop_signal: SIGINT - image: openruntimes/executor:0.3.5 + image: openruntimes/executor:0.4.0 networks: - appwrite - runtimes diff --git a/app/workers/builds.php b/app/workers/builds.php index 41867a5dd3..2088db7b8b 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -90,7 +90,8 @@ class BuildsV1 extends Worker throw new Exception('Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".', 500); } - $runtimes = Config::getParam('runtimes', []); + $version = $function->getAttribute('version', 'v2'); + $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); $key = $function->getAttribute('runtime'); $runtime = isset($runtimes[$key]) ? $runtimes[$key] : null; if (\is_null($runtime)) { @@ -358,17 +359,20 @@ class BuildsV1 extends Worker Co::join([ Co\go(function () use (&$response, $project, $deployment, $source, $function, $runtime, $vars, $command, &$err) { try { + $version = $function->getAttribute('version', 'v2'); + $command = $version === 'v2' ? 'tar -zxf /tmp/code.tar.gz -C /usr/code && cd /usr/local/src/ && ./build.sh' : 'tar -zxf /tmp/code.tar.gz -C /mnt/code && helpers/build.sh "' . $command . '"'; + $response = $this->executor->createRuntime( deploymentId: $deployment->getId(), projectId: $project->getId(), source: $source, image: $runtime['image'], - version: $function->getAttribute('version'), + version: $version, remove: true, entrypoint: $deployment->getAttribute('entrypoint'), destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}", variables: $vars, - command: 'tar -zxf /tmp/code.tar.gz -C /mnt/code && helpers/build.sh "' . $command . '"' + command: $command ); } catch (Exception $error) { $err = $error; diff --git a/app/workers/functions.php b/app/workers/functions.php index 38923b5129..d35c8c791a 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -74,7 +74,8 @@ Server::setResource('execute', function () { } /** Check if runtime is supported */ - $runtimes = Config::getParam('runtimes', []); + $version = $function->getAttribute('version', 'v2'); + $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); if (!\array_key_exists($function->getAttribute('runtime'), $runtimes)) { throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); @@ -171,8 +172,10 @@ Server::setResource('execute', function () { /** Execute function */ try { + $version = $function->getAttribute('version', 'v2'); $command = $runtime['startCommand']; $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + $command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"'; $executionResponse = $executor->createExecution( projectId: $project->getId(), deploymentId: $deploymentId, @@ -182,11 +185,11 @@ Server::setResource('execute', function () { image: $runtime['image'], source: $build->getAttribute('path', ''), entrypoint: $deployment->getAttribute('entrypoint', ''), - version: $function->getAttribute('version'), + version: $version, path: $path, method: $method, headers: $headers, - runtimeEntrypoint: 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"' + runtimeEntrypoint: $command ); $status = $executionResponse['statusCode'] >= 400 ? 'failed' : 'completed'; @@ -381,9 +384,9 @@ $server->job() data: $data, user: $user, jwt: $jwt, - path: $payload['path'], - method: $payload['method'], - headers: $payload['headers'], + path: $payload['path'] ?? '', + method: $payload['method'] ?? 'POST', + headers: $payload['headers'] ?? [], statsd: $statsd, ); break; @@ -401,9 +404,9 @@ $server->job() data: null, user: null, jwt: null, - path: $payload['path'], - method: $payload['method'], - headers: $payload['headers'], + path: $payload['path'] ?? '/', + method: $payload['method'] ?? 'POST', + headers: $payload['headers'] ?? [], statsd: $statsd, ); break; diff --git a/docker-compose.yml b/docker-compose.yml index 37a22b4177..8fdaf34024 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -693,7 +693,7 @@ services: hostname: appwrite-executor <<: *x-logging stop_signal: SIGINT - image: openruntimes/executor:0.3.5 + image: openruntimes/executor:0.4.0 networks: - appwrite - runtimes @@ -742,7 +742,7 @@ services: hostname: proxy <<: *x-logging stop_signal: SIGINT - image: openruntimes/proxy:0.3.0 + image: openruntimes/proxy:0.3.1 networks: - appwrite - runtimes diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 052c3ca936..5350d9d0e5 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -975,7 +975,7 @@ class FunctionsCustomServerTest extends Scope */ public function testCreateCustomExecution(string $folder, string $name, string $entrypoint, string $runtimeName, string $runtimeVersion) { - $timeout = 2; + $timeout = 5; $code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz"; $this->packageCode($folder); @@ -1094,6 +1094,104 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); } + public function testv2Function() + { + $timeout = 5; + $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-v2/code.tar.gz"; + $this->packageCode('php-v2'); + + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test PHP V2', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'events' => [], + 'timeout' => $timeout, + ]); + + $functionId = $function['body']['$id'] ?? ''; + + $this->assertEquals(201, $function['headers']['status-code']); + + $headers = [ + 'content-type' => 'application/json', + 'origin' => 'http://localhost', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-mode' => 'admin', + ]; + + $variable = $this->client->call(Client::METHOD_PATCH, '/mock/functions-v2', $headers, [ + 'functionId' => $functionId + ]); + + $this->assertEquals(204, $variable['headers']['status-code']); + + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'entrypoint' => 'index.php', + 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'activate' => true + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + $this->assertEquals(202, $deployment['headers']['status-code']); + + // Poll until deployment is built + while (true) { + $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + if ( + $deployment['headers']['status-code'] >= 400 + || \in_array($deployment['body']['status'], ['ready', 'failed']) + ) { + break; + } + + \sleep(1); + } + + $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $deployment['headers']['status-code']); + + $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'body' => 'foobar', + 'async' => false + ]); + + $output = json_decode($execution['body']['responseBody'], true); + + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertEquals(true, $output['v2Woks']); + + // 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']); + } + public function testGetRuntimes() { $runtimes = $this->client->call(Client::METHOD_GET, '/functions/runtimes', array_merge([ diff --git a/tests/resources/functions/php-v2/index.php b/tests/resources/functions/php-v2/index.php new file mode 100644 index 0000000000..2bf5d83304 --- /dev/null +++ b/tests/resources/functions/php-v2/index.php @@ -0,0 +1,7 @@ +json([ + 'v2Woks' => true + ]); +};