1
0
Fork 0
mirror of synced 2024-09-30 17:26:48 +13:00

Merge branch 'feat-functions-refactor' of https://github.com/appwrite/appwrite into feat-functions-refactor

This commit is contained in:
Bradley Schofield 2022-01-24 09:37:46 +00:00
commit 9a0b7ce142
20 changed files with 336 additions and 318 deletions

View file

@ -1,10 +1,9 @@
name: "Tests" name: "Tests"
on: [pull_request] on: [pull_request]
jobs: jobs:
tests: tests:
name: Unit & E2E name: Unit & E2E
runs-on: self-hosted runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
@ -19,16 +18,29 @@ jobs:
- run: git checkout HEAD^2 - run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
- name: Build Appwrite - name: Prepare Docker
# Upstream bug causes buildkit pulls to fail so prefetch base images
# https://github.com/moby/moby/issues/41864
run: | run: |
export COMPOSE_INTERACTIVE_NO_CLI
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
echo "_APP_FUNCTIONS_RUNTIMES=php-8.0" >> .env echo "_APP_FUNCTIONS_RUNTIMES=php-8.0" >> .env
docker pull composer:2.0 docker pull composer:2.0
docker pull php:8.0-cli-alpine docker pull php:8.0-cli-alpine
docker compose build --progress=plain docker compose pull
- name: Prepare Cache
uses: satackey/action-docker-layer-caching@v0.0.11
# Ignore the failure of a step and avoid terminating the job.
continue-on-error: true
- name: Build Appwrite
run: docker compose build --progress=plain
- name: Start Appwrite
run: |
docker compose up -d docker compose up -d
sleep 30 sleep 30
- name: Doctor - name: Doctor
run: docker compose exec -T appwrite doctor run: docker compose exec -T appwrite doctor
@ -37,9 +49,3 @@ jobs:
- name: Run Tests - name: Run Tests
run: docker compose exec -T appwrite test --debug run: docker compose exec -T appwrite test --debug
- name: Teardown
if: always()
run: |
docker compose down -v
docker ps -aq | xargs docker rm --force

View file

@ -1,29 +0,0 @@
#!/bin/bash bash
RED='\033[0;31m'
NC='\033[0m' # No Color
if [ -z "$1" ]
then
echo "Missing tag number"
exit 1
fi
if [ -z "$2" ]
then
echo "Missing version number"
exit 1
fi
if test $(find "./app/db/DBIP/dbip-country-lite-2021-12.mmdb" -mmin +259200)
then
printf "${RED}GEO country DB has not been updated for more than 6 months. Go to https://db-ip.com/db/download/ip-to-country-lite to download a newer version${NC}\n"
fi
echo 'Starting build...'
docker build --build-arg VERSION="$2" --tag appwrite/appwrite:"$1" .
echo 'Pushing build to registry...'
docker push appwrite/appwrite:"$1"

View file

@ -1 +0,0 @@
echo 'Nothing to deploy right now.'

View file

@ -1,87 +0,0 @@
dist: focal
arch:
- amd64
os: linux
vm:
size: large
language: shell
notifications:
email:
- team@appwrite.io
before_install:
# Install latest Docker
- curl -fsSL https://get.docker.com | sh
# Enable Buildkit in Docker config
- echo '{"experimental":"enabled"}' | sudo tee /etc/docker/daemon.json
- mkdir -p $HOME/.docker
- echo '{"experimental":"enabled"}' | sudo tee $HOME/.docker/config.json
- sudo service docker start
# Login to increase Docker Hub ratelimit
- >
if [ ! -z "${DOCKERHUB_PULL_USERNAME:-}" ]; then
echo "${DOCKERHUB_PULL_PASSWORD}" | docker login --username "${DOCKERHUB_PULL_USERNAME}" --password-stdin
fi
- docker --version
# Install latest Compose
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/1.29.2/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
- docker-compose --version
# Enable Buildkit
- docker buildx create --name travis_builder --use
- export COMPOSE_INTERACTIVE_NO_CLI
- export DOCKER_BUILDKIT=1
- export COMPOSE_DOCKER_CLI_BUILD=1
- export BUILDKIT_PROGRESS=plain
# Only pass a single runtime for CI stability
- echo "_APP_FUNCTIONS_RUNTIMES=php-8.0" >> .env
# Ensure Travis scripts are executable
- chmod -R u+x ./.travis-ci
install:
- docker-compose pull
# Upstream bug causes buildkit pulls to fail so prefetch base images
# https://github.com/moby/moby/issues/41864
- docker pull composer:2.0
- docker pull php:8.0-cli-alpine
- docker-compose build
- docker-compose up -d
- sleep 60
script:
- docker ps -a
# Tests should fail if any container is in exited status
# - ALL_UP=`docker ps -aq --filter "status=exited"`
# - >
# if [[ "$ALL_UP" != "" ]]; then
# exit 1
# fi
- docker-compose logs appwrite
- docker-compose logs appwrite-realtime
- docker-compose logs mariadb
- docker-compose logs appwrite-worker-functions
- docker-compose exec appwrite doctor
- docker-compose exec appwrite vars
- docker-compose exec appwrite test --debug
after_script:
# travis re-uses their build nodes so clean them up
- docker buildx rm travis_builder
after_failure:
- docker-compose logs appwrite
deploy:
- provider: script
edge: true
script: ./.travis-ci/deploy.sh
on:
repo: appwrite/appwrite
branch: deploy

View file

@ -1,6 +1,8 @@
# Unreleased Version 0.13.0 # Unreleased Version 0.13.0
- Added ability to create syncronous function executions - Added ability to create syncronous function executions
- Introduced new execution model for functions - Introduced new execution model for functions
- Improved functions execution times
- Improved functions execution times
# Version 0.12.1 # Version 0.12.1
## Bugs ## Bugs

View file

@ -24,7 +24,7 @@ COPY public /usr/local/src/public
RUN npm ci RUN npm ci
RUN npm run build RUN npm run build
FROM php:8.0-cli-alpine as compile FROM php:8.0.14-cli-alpine as compile
ARG DEBUG=false ARG DEBUG=false
ENV DEBUG=$DEBUG ENV DEBUG=$DEBUG
@ -123,7 +123,7 @@ RUN \
./configure && \ ./configure && \
make && make install make && make install
FROM php:8.0-cli-alpine as final FROM php:8.0.14-cli-alpine as final
LABEL maintainer="team@appwrite.io" LABEL maintainer="team@appwrite.io"

View file

@ -230,7 +230,7 @@ App::get('/v1/database/collections')
$queries = []; $queries = [];
if (!empty($search)) { if (!empty($search)) {
$queries[] = new Query('name', Query::TYPE_SEARCH, [$search]); $queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
} }
$usage->setParam('database.collections.read', 1); $usage->setParam('database.collections.read', 1);

View file

@ -45,7 +45,7 @@ App::post('/v1/functions')
->param('name', '', new Text(128), 'Function name. Max length: 128 chars.') ->param('name', '', new Text(128), 'Function name. Max length: 128 chars.')
->param('execute', [], new ArrayList(new Text(64)), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.') ->param('execute', [], new ArrayList(new Text(64)), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.') ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.')
->param('vars', new stdClass(), new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true) ->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)), 'Events list.', true) ->param('events', [], new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)), 'Events list.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
->param('timeout', 15, new Range(1, 900), 'Function maximum execution time in seconds.', true) ->param('timeout', 15, new Range(1, 900), 'Function maximum execution time in seconds.', true)
@ -863,7 +863,7 @@ App::post('/v1/functions/:functionId/executions')
throw new Exception('Tag not found. Deploy tag before trying to execute a function', 404); throw new Exception('Tag not found. Deploy tag before trying to execute a function', 404);
} }
$validator = new Authorization($function, 'execute'); $validator = new Authorization('execute');
if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function
throw new Exception($validator->getDescription(), 401); throw new Exception($validator->getDescription(), 401);

View file

@ -25,6 +25,7 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\JSON; use Utopia\Validator\JSON;
use Utopia\Validator\Text; use Utopia\Validator\Text;
use Cron\CronExpression; use Cron\CronExpression;
use Swoole\ConnectionPool;
use Utopia\Storage\Device\Local; use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage; use Utopia\Storage\Storage;
use Swoole\Coroutine as Co; use Swoole\Coroutine as Co;
@ -32,6 +33,8 @@ use Utopia\Cache\Cache;
use Utopia\Database\Query; use Utopia\Database\Query;
use Utopia\Orchestration\Adapter\DockerCLI; use Utopia\Orchestration\Adapter\DockerCLI;
use Utopia\Logger\Log; use Utopia\Logger\Log;
use Utopia\Orchestration\Adapter\DockerAPI;
use Utopia\Registry\Registry;
require_once __DIR__ . '/init.php'; require_once __DIR__ . '/init.php';
@ -81,18 +84,22 @@ function logError(Throwable $error, string $action, Utopia\Route $route = null)
Console::error('[Error] Line: ' . $error->getLine()); Console::error('[Error] Line: ' . $error->getLine());
}; };
try { $orchestrationPool = new ConnectionPool(function () {
$dockerUser = App::getEnv('DOCKERHUB_PULL_USERNAME', null); $dockerUser = App::getEnv('DOCKERHUB_PULL_USERNAME', null);
$dockerPass = App::getEnv('DOCKERHUB_PULL_PASSWORD', null); $dockerPass = App::getEnv('DOCKERHUB_PULL_PASSWORD', null);
$dockerEmail = App::getEnv('DOCKERHUB_PULL_EMAIL', null);
$orchestration = new Orchestration(new DockerCLI($dockerUser, $dockerPass)); $orchestration = new Orchestration(new DockerCLI($dockerUser, $dockerPass));
return $orchestration;
}, 6);
try {
$runtimes = Config::getParam('runtimes'); $runtimes = Config::getParam('runtimes');
// Warmup: make sure images are ready to run fast 🚀 // Warmup: make sure images are ready to run fast 🚀
Co\run(function () use ($runtimes, $orchestration) { Co\run(function () use ($runtimes, $orchestrationPool) {
foreach ($runtimes as $runtime) { foreach ($runtimes as $runtime) {
go(function () use ($runtime, $orchestration) { go(function () use ($runtime, $orchestrationPool) {
$orchestration = $orchestrationPool->get();
Console::info('Warming up ' . $runtime['name'] . ' ' . $runtime['version'] . ' environment...'); Console::info('Warming up ' . $runtime['name'] . ' ' . $runtime['version'] . ' environment...');
$response = $orchestration->pull($runtime['image']); $response = $orchestration->pull($runtime['image']);
@ -102,6 +109,8 @@ try {
} else { } else {
Console::warning("Failed to Warmup {$runtime['name']} {$runtime['version']}!"); Console::warning("Failed to Warmup {$runtime['name']} {$runtime['version']}!");
} }
$orchestrationPool->put($orchestration);
}); });
} }
}); });
@ -113,10 +122,12 @@ try {
$activeFunctions->column('key', Swoole\Table::TYPE_STRING, 4096); $activeFunctions->column('key', Swoole\Table::TYPE_STRING, 4096);
$activeFunctions->create(); $activeFunctions->create();
Co\run(function () use ($orchestration, $activeFunctions) { Co\run(function () use ($orchestrationPool, $activeFunctions) {
$orchestration = $orchestrationPool->get();
$executionStart = \microtime(true); $executionStart = \microtime(true);
$residueList = $orchestration->list(['label' => 'appwrite-type=function']); $residueList = $orchestration->list(['label' => 'appwrite-type=function']);
$orchestrationPool->put($orchestration);
foreach ($residueList as $value) { foreach ($residueList as $value) {
go(fn () => $activeFunctions->set($value->getName(), [ go(fn () => $activeFunctions->set($value->getName(), [
@ -128,7 +139,6 @@ try {
} }
$executionEnd = \microtime(true); $executionEnd = \microtime(true);
Console::info(count($activeFunctions) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds'); Console::info(count($activeFunctions) . ' functions listed in ' . ($executionEnd - $executionStart) . ' seconds');
}); });
} catch (\Throwable $error) { } catch (\Throwable $error) {
@ -137,164 +147,172 @@ try {
function createRuntimeServer(string $functionId, string $projectId, string $tagId, Database $database): void function createRuntimeServer(string $functionId, string $projectId, string $tagId, Database $database): void
{ {
global $orchestration; global $orchestrationPool;
global $runtimes; global $runtimes;
global $activeFunctions; global $activeFunctions;
$function = $database->getDocument('functions', $functionId); try {
$tag = $database->getDocument('tags', $tagId); $orchestration = $orchestrationPool->get();
$function = $database->getDocument('functions', $functionId);
$tag = $database->getDocument('tags', $tagId);
if ($tag->getAttribute('buildId') === null) { if ($tag->getAttribute('buildId') === null) {
throw new Exception('Tag has no buildId'); throw new Exception('Tag has no buildId');
}
// Grab Build Document
$build = $database->getDocument('builds', $tag->getAttribute('buildId'));
// Check if function isn't already created
$functions = $orchestration->list(['label' => 'appwrite-type=function', 'name' => 'appwrite-function-' . $tag->getId()]);
if (\count($functions) > 0) {
return;
}
// Generate random secret key
$secret = \bin2hex(\random_bytes(16));
// Check if runtime is active
$runtime = $runtimes[$function->getAttribute('runtime', '')] ?? null;
if ($tag->getAttribute('functionId') !== $function->getId()) {
throw new Exception('Tag not found', 404);
}
if (\is_null($runtime)) {
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
}
// Process environment variables
$vars = \array_merge($function->getAttribute('vars', []), [
'APPWRITE_FUNCTION_ID' => $function->getId(),
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
'APPWRITE_FUNCTION_TAG' => $tag->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'],
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'],
'APPWRITE_FUNCTION_PROJECT_ID' => $projectId,
'INTERNAL_RUNTIME_KEY' => $secret
]);
$vars = \array_merge($vars, $build->getAttribute('vars', [])); // for gettng endpoint.
$container = 'appwrite-function-' . $tag->getId();
if ($activeFunctions->exists($container) && !(\substr($activeFunctions->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online
// If container is online then stop and remove it
try {
$orchestration->remove($container, true);
} catch (Exception $e) {
try {
throw new Exception('Failed to remove container: ' . $e->getMessage());
} catch (Throwable $error) {
logError($error, "createRuntimeServer");
}
} }
$activeFunctions->del($container); // Grab Build Document
} $build = $database->getDocument('builds', $tag->getAttribute('buildId'));
// Check if tag hasn't failed // Check if function isn't already created
if ($build->getAttribute('status') === 'failed') { $functions = $orchestration->list(['label' => 'appwrite-type=function', 'name' => 'appwrite-function-' . $tag->getId()]);
throw new Exception('Tag build failed, please check your logs.', 500);
}
// Check if tag is built yet. if (\count($functions) > 0) {
if ($build->getAttribute('status') !== 'ready') { return;
throw new Exception('Tag is not built yet', 500);
}
// Grab Tag Files
$tagPath = $build->getAttribute('outputPath', '');
$tagPathTarget = '/tmp/project-' . $projectId . '/' . $build->getId() . '/builtCode/code.tar.gz';
$tagPathTargetDir = \pathinfo($tagPathTarget, PATHINFO_DIRNAME);
$container = 'appwrite-function-' . $tag->getId();
$device = Storage::getDevice('builds');
if (!\file_exists($tagPathTargetDir)) {
if (!\mkdir($tagPathTargetDir, 0777, true)) {
throw new Exception('Can\'t create directory ' . $tagPathTargetDir);
}
}
if (!\file_exists($tagPathTarget)) {
if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
if (!\copy($tagPath, $tagPathTarget)) {
throw new Exception('Can\'t create temporary code file ' . $tagPathTarget);
}
} else {
$buffer = $device->read($tagPath);
\file_put_contents($tagPathTarget, $buffer);
}
};
/**
* Limit CPU Usage - DONE
* Limit Memory Usage - DONE
* Limit Network Usage
* Limit Storage Usage (//--storage-opt size=120m \)
* Make sure no access to redis, mariadb, influxdb or other system services
* Make sure no access to NFS server / storage volumes
* Access Appwrite REST from internal network for improved performance
*/
if (!$activeFunctions->exists($container)) { // Create contianer if not ready
$executionStart = \microtime(true);
$executionTime = \time();
$orchestration
->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', '1'))
->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', '256'))
->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', '256'));
foreach ($vars as $key => $value) {
$vars[$key] = strval($value);
} }
// Launch runtime server // Generate random secret key
$id = $orchestration->run( $secret = \bin2hex(\random_bytes(16));
image: $runtime['image'],
name: $container,
vars: $vars,
labels: [
'appwrite-type' => 'function',
'appwrite-created' => strval($executionTime),
'appwrite-runtime' => $function->getAttribute('runtime', ''),
'appwrite-project' => $projectId,
'appwrite-tag' => $tag->getId(),
],
hostname: $container,
mountFolder: $tagPathTargetDir,
);
if (empty($id)) { // Check if runtime is active
throw new Exception('Failed to create container'); $runtime = $runtimes[$function->getAttribute('runtime', '')] ?? null;
if ($tag->getAttribute('functionId') !== $function->getId()) {
throw new Exception('Tag not found', 404);
} }
// Add to network if (\is_null($runtime)) {
$orchestration->networkConnect($container, 'appwrite_runtimes'); throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
}
$executionEnd = \microtime(true); // Process environment variables
$vars = \array_merge($function->getAttribute('vars', []), [
$activeFunctions->set($container, [ 'APPWRITE_FUNCTION_ID' => $function->getId(),
'id' => $id, 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
'name' => $container, 'APPWRITE_FUNCTION_TAG' => $tag->getId(),
'status' => 'Up ' . \round($executionEnd - $executionStart, 2) . 's', 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'],
'key' => $secret, 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'],
'APPWRITE_FUNCTION_PROJECT_ID' => $projectId,
'INTERNAL_RUNTIME_KEY' => $secret
]); ]);
Console::info('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds'); $vars = \array_merge($vars, $build->getAttribute('vars', [])); // for gettng endpoint.
} else {
Console::info('Runtime server is ready to run'); $container = 'appwrite-function-' . $tag->getId();
if ($activeFunctions->exists($container) && !(\substr($activeFunctions->get($container)['status'], 0, 2) === 'Up')) { // Remove container if not online
// If container is online then stop and remove it
try {
$orchestration->remove($container, true);
} catch (Exception $e) {
try {
throw new Exception('Failed to remove container: ' . $e->getMessage());
} catch (Throwable $error) {
logError($error, "createRuntimeServer");
}
}
$activeFunctions->del($container);
}
// Check if tag hasn't failed
if ($build->getAttribute('status') === 'failed') {
throw new Exception('Tag build failed, please check your logs.', 500);
}
// Check if tag is built yet.
if ($build->getAttribute('status') !== 'ready') {
throw new Exception('Tag is not built yet', 500);
}
// Grab Tag Files
$tagPath = $build->getAttribute('outputPath', '');
$tagPathTarget = '/tmp/project-' . $projectId . '/' . $build->getId() . '/builtCode/code.tar.gz';
$tagPathTargetDir = \pathinfo($tagPathTarget, PATHINFO_DIRNAME);
$container = 'appwrite-function-' . $tag->getId();
$device = Storage::getDevice('builds');
if (!\file_exists($tagPathTargetDir)) {
if (@\mkdir($tagPathTargetDir, 0777, true)) {
\chmod($tagPathTargetDir, 0777);
} else {
throw new Exception('Can\'t create directory ' . $tagPathTargetDir);
}
}
if (!\file_exists($tagPathTarget)) {
if (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
if (!\copy($tagPath, $tagPathTarget)) {
throw new Exception('Can\'t create temporary code file ' . $tagPathTarget);
}
} else {
$buffer = $device->read($tagPath);
\file_put_contents($tagPathTarget, $buffer);
}
};
/**
* Limit CPU Usage - DONE
* Limit Memory Usage - DONE
* Limit Network Usage
* Limit Storage Usage (//--storage-opt size=120m \)
* Make sure no access to redis, mariadb, influxdb or other system services
* Make sure no access to NFS server / storage volumes
* Access Appwrite REST from internal network for improved performance
*/
if (!$activeFunctions->exists($container)) { // Create contianer if not ready
$executionStart = \microtime(true);
$executionTime = \time();
$orchestration
->setCpus(App::getEnv('_APP_FUNCTIONS_CPUS', '1'))
->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', '256'))
->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', '256'));
$vars = array_map(fn ($v) => strval($v), $vars);
// Launch runtime server
$id = $orchestration->run(
image: $runtime['image'],
name: $container,
vars: $vars,
labels: [
'appwrite-type' => 'function',
'appwrite-created' => strval($executionTime),
'appwrite-runtime' => $function->getAttribute('runtime', ''),
'appwrite-project' => $projectId,
'appwrite-tag' => $tag->getId(),
],
hostname: $container,
mountFolder: $tagPathTargetDir,
);
if (empty($id)) {
throw new Exception('Failed to create container');
}
// Add to network
$orchestration->networkConnect($container, 'appwrite_runtimes');
$executionEnd = \microtime(true);
$activeFunctions->set($container, [
'id' => $id,
'name' => $container,
'status' => 'Up ' . \round($executionEnd - $executionStart, 2) . 's',
'key' => $secret,
]);
Console::info('Runtime Server created in ' . ($executionEnd - $executionStart) . ' seconds');
} else {
Console::info('Runtime server is ready to run');
}
} catch (\Throwable $th) {
$orchestrationPool->put($orchestration);
throw $th;
} finally {
$orchestrationPool->put($orchestration);
} }
}; };
@ -410,7 +428,7 @@ function execute(string $trigger, string $projectId, string $executionId, string
$database->updateDocument('tags', $tag->getId(), $tag); $database->updateDocument('tags', $tag->getId(), $tag);
runBuildStage($buildId, $projectId, $database); runBuildStage($buildId, $projectId);
} }
} catch (Exception $e) { } catch (Exception $e) {
$execution $execution
@ -671,8 +689,11 @@ App::post('/v1/cleanup/function')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->action( ->action(
function (string $functionId, Response $response, Database $dbForProject) use ($orchestration) { function (string $functionId, Response $response, Database $dbForProject) use ($orchestrationPool) {
try { try {
/** @var Orchestration $orchestration */
$orchestration = $orchestrationPool->get();
// Get function document // Get function document
$function = $dbForProject->getDocument('functions', $functionId); $function = $dbForProject->getDocument('functions', $functionId);
@ -712,9 +733,11 @@ App::post('/v1/cleanup/function')
return $response->json(['success' => true]); return $response->json(['success' => true]);
} catch (Exception $e) { } catch (Exception $e) {
logError($e, "cleanupFunction"); logError($e, "cleanupFunction");
$orchestrationPool->put($orchestration);
return $response->json(['error' => $e->getMessage()]); return $response->json(['error' => $e->getMessage()]);
} }
$orchestrationPool->put($orchestration);
} }
); );
@ -722,8 +745,11 @@ App::post('/v1/cleanup/tag')
->param('tagId', '', new UID(), 'Tag unique ID.') ->param('tagId', '', new UID(), 'Tag unique ID.')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->action(function (string $tagId, Response $response, Database $dbForProject) use ($orchestration) { ->action(function (string $tagId, Response $response, Database $dbForProject) use ($orchestrationPool) {
try { try {
/** @var Orchestration $orchestration */
$orchestration = $orchestrationPool->get();
// Get tag document // Get tag document
$tag = $dbForProject->getDocument('tags', $tagId); $tag = $dbForProject->getDocument('tags', $tagId);
@ -752,8 +778,11 @@ App::post('/v1/cleanup/tag')
} }
} catch (Exception $e) { } catch (Exception $e) {
logError($e, "cleanupFunction"); logError($e, "cleanupFunction");
$orchestrationPool->put($orchestration);
return $response->json(['error' => $e->getMessage()]); return $response->json(['error' => $e->getMessage()]);
} }
$orchestrationPool->put($orchestration);
return $response->json(['success' => true]); return $response->json(['success' => true]);
}); });
@ -765,7 +794,8 @@ App::post('/v1/tag')
->inject('response') ->inject('response')
->inject('dbForProject') ->inject('dbForProject')
->inject('projectID') ->inject('projectID')
->action(function (string $functionId, string $tagId, string $userId, Response $response, Database $dbForProject, string $projectID) use ($runtimes) { ->inject('register')
->action(function (string $functionId, string $tagId, string $userId, Response $response, Database $dbForProject, string $projectID, Registry $register) use ($runtimes) {
// Get function document // Get function document
$function = $dbForProject->getDocument('functions', $functionId); $function = $dbForProject->getDocument('functions', $functionId);
// Get tag document // Get tag document
@ -826,9 +856,16 @@ App::post('/v1/tag')
} }
// Build Code // Build Code
go(function () use ($dbForProject, $projectID, $tagId, $buildId, $functionId, $function) { go(function () use ($projectID, $tagId, $buildId, $functionId, $function, $register) {
$db = $register->get('dbPool')->get();
$redis = $register->get('redisPool')->get();
$cache = new Cache(new RedisCache($redis));
$dbForProject = new Database(new MariaDB($db), $cache);
$dbForProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$dbForProject->setNamespace('_project_' . $projectID);
// Build Code // Build Code
runBuildStage($buildId, $projectID, $dbForProject); runBuildStage($buildId, $projectID);
// Update the schedule // Update the schedule
$schedule = $function->getAttribute('schedule', ''); $schedule = $function->getAttribute('schedule', '');
@ -856,6 +893,9 @@ App::post('/v1/tag')
// Deploy Runtime Server // Deploy Runtime Server
createRuntimeServer($functionId, $projectID, $tagId, $dbForProject); createRuntimeServer($functionId, $projectID, $tagId, $dbForProject);
$register->get('dbPool')->put($db);
$register->get('redisPool')->put($redis);
}); });
if (false === $function) { if (false === $function) {
@ -867,7 +907,8 @@ App::post('/v1/tag')
App::get('/v1/') App::get('/v1/')
->inject('response') ->inject('response')
->action(function (Response $response) { ->action(
function (Response $response) {
$response $response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0') ->addHeader('Expires', '0')
@ -920,12 +961,15 @@ App::post('/v1/build/:buildId') // Start a Build
} }
}); });
function runBuildStage(string $buildId, string $projectID, Database $database): Document function runBuildStage(string $buildId, string $projectID): Document
{ {
global $runtimes; global $runtimes;
global $orchestration; global $orchestrationPool;
global $register; global $register;
/** @var Orchestration $orchestration */
$orchestration = $orchestrationPool->get();
$buildStdout = ''; $buildStdout = '';
$buildStderr = ''; $buildStderr = '';
@ -971,7 +1015,9 @@ function runBuildStage(string $buildId, string $projectID, Database $database):
// Perform various checks // Perform various checks
if (!\file_exists($tagPathTargetDir)) { if (!\file_exists($tagPathTargetDir)) {
if (!\mkdir($tagPathTargetDir, 0777, true)) { if (@\mkdir($tagPathTargetDir, 0777, true)) {
\chmod($tagPathTargetDir, 0777);
} else {
throw new Exception('Can\'t create directory ' . $tagPathTargetDir); throw new Exception('Can\'t create directory ' . $tagPathTargetDir);
} }
} }
@ -1002,12 +1048,13 @@ function runBuildStage(string $buildId, string $projectID, Database $database):
->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256)) ->setMemory(App::getEnv('_APP_FUNCTIONS_MEMORY', 256))
->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256)); ->setSwap(App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 256));
foreach ($vars as &$value) { $vars = array_map(fn ($v) => strval($v), $vars);
$value = strval($value); $path = '/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode';
}
if (!\file_exists('/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode')) { if (!\file_exists($path)) {
if (!\mkdir('/tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode', 0777, true)) { if (@\mkdir($path, 0777, true)) {
\chmod($path, 0777);
} else {
throw new Exception('Can\'t create directory /tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode'); throw new Exception('Can\'t create directory /tmp/project-' . $projectID . '/' . $build->getId() . '/builtCode');
} }
}; };
@ -1108,7 +1155,9 @@ function runBuildStage(string $buildId, string $projectID, Database $database):
$path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); $path = $device->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
if (!\file_exists(\dirname($path))) { // Checks if directory path to file exists if (!\file_exists(\dirname($path))) { // Checks if directory path to file exists
if (!@\mkdir(\dirname($path), 0777, true)) { if (@\mkdir(\dirname($path), 0777, true)) {
\chmod(\dirname($path), 0777);
} else {
throw new Exception('Can\'t create directory: ' . \dirname($path)); throw new Exception('Can\'t create directory: ' . \dirname($path));
} }
} }
@ -1149,12 +1198,18 @@ function runBuildStage(string $buildId, string $projectID, Database $database):
$build = $database->updateDocument('builds', $buildId, $build); $build = $database->updateDocument('builds', $buildId, $build);
// also remove the container if it exists // also remove the container if it exists
if ($id) { if (isset($id)) {
$orchestration->remove($id, true); $orchestration->remove($id, true);
} }
$orchestrationPool->put($orchestration);
$register->get('dbPool')->put($db);
$register->get('redisPool')->put($redis);
throw new Exception('Build failed: ' . $e->getMessage()); throw new Exception('Build failed: ' . $e->getMessage());
} finally { } finally {
$orchestrationPool->put($orchestration);
$register->get('dbPool')->put($db); $register->get('dbPool')->put($db);
$register->get('redisPool')->put($redis); $register->get('redisPool')->put($redis);
} }
@ -1166,8 +1221,9 @@ App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode
$http = new Server("0.0.0.0", 8080); $http = new Server("0.0.0.0", 8080);
function handleShutdown() { function handleShutdown()
global $orchestration; {
global $orchestrationPool;
global $register; global $register;
try { try {
@ -1175,6 +1231,8 @@ function handleShutdown() {
// Remove all containers. // Remove all containers.
/** @var Orchestration $orchestration */
$orchestration = $orchestrationPool->get();
$functionsToRemove = $orchestration->list(['label' => 'appwrite-type=function']); $functionsToRemove = $orchestration->list(['label' => 'appwrite-type=function']);
@ -1210,6 +1268,8 @@ function handleShutdown() {
} }
} catch (\Throwable $error) { } catch (\Throwable $error) {
logError($error, 'shutdownError'); logError($error, 'shutdownError');
} finally {
$orchestrationPool->put($orchestration);
} }
}; };

View file

@ -620,6 +620,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert" data-failure="alert"
data-failure-param-alert-text="Failed to create attribute" data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error" data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }"> x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" /> <input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -675,6 +676,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert" data-failure="alert"
data-failure-param-alert-text="Failed to create attribute" data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error" data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }"> x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" /> <input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -739,6 +741,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert" data-failure="alert"
data-failure-param-alert-text="Failed to create attribute" data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error" data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }"> x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" /> <input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -803,6 +806,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert" data-failure="alert"
data-failure-param-alert-text="Failed to create attribute" data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error" data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }"> x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" /> <input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -855,6 +859,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert" data-failure="alert"
data-failure-param-alert-text="Failed to create attribute" data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error" data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }"> x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" /> <input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -911,6 +916,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert" data-failure="alert"
data-failure-param-alert-text="Failed to create attribute" data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error" data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }"> x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" /> <input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -963,6 +969,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert" data-failure="alert"
data-failure-param-alert-text="Failed to create attribute" data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error" data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }"> x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" /> <input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -1015,6 +1022,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert" data-failure="alert"
data-failure-param-alert-text="Failed to create attribute" data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error" data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }"> x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" /> <input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />

View file

@ -174,7 +174,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<input type="email" class="full-width" id="user-email" name="email" required autocomplete="off" /> <input type="email" class="full-width" id="user-email" name="email" required autocomplete="off" />
<label for="user-password">Password</label> <label for="user-password">Password</label>
<input type="password" class="full-width" id="user-password" name="password" required pattern=".{6,}" title="Six or more characters" autocomplete="off" /> <input type="password" class="full-width" id="user-password" name="password" required minlength="8" title="Eight or more characters" autocomplete="off" />
<hr /> <hr />

View file

@ -28,10 +28,10 @@
<input type="hidden" name="secret" data-ls-bind="{{router.params.secret}}"> <input type="hidden" name="secret" data-ls-bind="{{router.params.secret}}">
<label>Password</label> <label>Password</label>
<input name="password" type="password" autocomplete="off" placeholder="" required data-forms-password-meter pattern=".{6,}" title="Six or more characters"> <input name="password" type="password" autocomplete="off" placeholder="" required data-forms-password-meter minlength="8" title="Eight or more characters">
<label>Confirm Password</label> <label>Confirm Password</label>
<input name="passwordAgain" type="password" autocomplete="off" placeholder="" required data-forms-password-meter pattern=".{6,}" title="Six or more characters"> <input name="passwordAgain" type="password" autocomplete="off" placeholder="" required data-forms-password-meter minlength="8" title="Eight or more characters">
<button type="submit" class="btn btn-primary"><i class="fa fa-sign-in"></i> Apply</button> <button type="submit" class="btn btn-primary"><i class="fa fa-sign-in"></i> Apply</button>
</form> </form>

View file

@ -37,7 +37,7 @@ $root = ($this->getParam('root') !== 'disabled');
<input name="email" type="email" class="full-width" autocomplete="email" placeholder="Email" required> <input name="email" type="email" class="full-width" autocomplete="email" placeholder="Email" required>
<input name="password" type="password" class="full-width" autocomplete="off" placeholder="Password" required pattern=".{6,}" title="Six or more characters"> <input name="password" type="password" class="full-width" autocomplete="off" placeholder="Password" required minlength="8" title="Eight or more characters">
<button>Sign In</button> <button>Sign In</button>
</form> </form>

View file

@ -46,7 +46,7 @@ $root = ($this->getParam('root') !== 'disabled');
<input name="email" type="email" autocomplete="email" placeholder="" required data-ls-bind="{{router.params.email}}"> <input name="email" type="email" autocomplete="email" placeholder="" required data-ls-bind="{{router.params.email}}">
<label>Password</label> <label>Password</label>
<input name="password" type="password" autocomplete="off" placeholder="" required data-forms-password-meter pattern=".{8,}" title="Eight or more characters"> <input name="password" type="password" autocomplete="off" placeholder="" required data-forms-password-meter minlength="8" title="Eight or more characters">
<div class="agree margin-top-large margin-bottom-large"> <div class="agree margin-top-large margin-bottom-large">
<div class="pull-start margin-end-small margin-bottom"> <div class="pull-start margin-end-small margin-bottom">

View file

@ -72,7 +72,7 @@
"require-dev": { "require-dev": {
"appwrite/sdk-generator": "0.17.1", "appwrite/sdk-generator": "0.17.1",
"phpunit/phpunit": "9.5.10", "phpunit/phpunit": "9.5.10",
"swoole/ide-helper": "4.8.3", "swoole/ide-helper": "4.8.5",
"textalk/websocket": "1.5.5", "textalk/websocket": "1.5.5",
"vimeo/psalm": "4.13.1" "vimeo/psalm": "4.13.1"
}, },

14
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "adf8727742248da9d7143546e513f96d", "content-hash": "cba39f50398d5ae2b121db34c9e4c529",
"packages": [ "packages": [
{ {
"name": "adhocore/jwt", "name": "adhocore/jwt",
@ -5676,16 +5676,16 @@
}, },
{ {
"name": "swoole/ide-helper", "name": "swoole/ide-helper",
"version": "4.8.3", "version": "4.8.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/swoole/ide-helper.git", "url": "https://github.com/swoole/ide-helper.git",
"reference": "3ac4971814273889933b871e03b2a6b340e58f79" "reference": "d03c707d4dc803228e93b4884c72949c4d28e8b8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/swoole/ide-helper/zipball/3ac4971814273889933b871e03b2a6b340e58f79", "url": "https://api.github.com/repos/swoole/ide-helper/zipball/d03c707d4dc803228e93b4884c72949c4d28e8b8",
"reference": "3ac4971814273889933b871e03b2a6b340e58f79", "reference": "d03c707d4dc803228e93b4884c72949c4d28e8b8",
"shasum": "" "shasum": ""
}, },
"type": "library", "type": "library",
@ -5702,7 +5702,7 @@
"description": "IDE help files for Swoole.", "description": "IDE help files for Swoole.",
"support": { "support": {
"issues": "https://github.com/swoole/ide-helper/issues", "issues": "https://github.com/swoole/ide-helper/issues",
"source": "https://github.com/swoole/ide-helper/tree/4.8.3" "source": "https://github.com/swoole/ide-helper/tree/4.8.5"
}, },
"funding": [ "funding": [
{ {
@ -5714,7 +5714,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2021-12-01T08:11:40+00:00" "time": "2021-12-24T22:44:20+00:00"
}, },
{ {
"name": "symfony/console", "name": "symfony/console",

View file

@ -1 +1 @@
Use this endpoint to log out the currently logged in user from all their account sessions across all of their different devices. When using the option id argument, only the session unique ID provider will be deleted. Use this endpoint to log out the currently logged in user from all their account sessions across all of their different devices. When using the Session ID argument, only the unique session ID provided is deleted.

View file

@ -33,7 +33,7 @@ abstract class Scope extends TestCase
protected function getLastEmail():array protected function getLastEmail():array
{ {
sleep(5); sleep(3);
$emails = json_decode(file_get_contents('http://maildev:1080/email'), true); $emails = json_decode(file_get_contents('http://maildev:1080/email'), true);
@ -46,7 +46,7 @@ abstract class Scope extends TestCase
protected function getLastRequest():array protected function getLastRequest():array
{ {
sleep(5); sleep(2);
$resquest = json_decode(file_get_contents('http://request-catcher:5000/__last_request__'), true); $resquest = json_decode(file_get_contents('http://request-catcher:5000/__last_request__'), true);
$resquest['data'] = json_decode($resquest['data'], true); $resquest['data'] = json_decode($resquest['data'], true);

View file

@ -125,6 +125,39 @@ class DatabaseCustomServerTest extends Scope
$this->assertCount(0, $collections['body']['collections']); $this->assertCount(0, $collections['body']['collections']);
$this->assertEmpty($collections['body']['collections']); $this->assertEmpty($collections['body']['collections']);
/**
* Test for Search
*/
$collections = $this->client->call(Client::METHOD_GET, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'first'
]);
$this->assertEquals(1, $collections['body']['sum']);
$this->assertEquals('first', $collections['body']['collections'][0]['$id']);
$collections = $this->client->call(Client::METHOD_GET, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'Test'
]);
$this->assertEquals(2, $collections['body']['sum']);
$this->assertEquals('Test 1', $collections['body']['collections'][0]['name']);
$this->assertEquals('Test 2', $collections['body']['collections'][1]['name']);
$collections = $this->client->call(Client::METHOD_GET, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'Nonexistent'
]);
$this->assertEquals(0, $collections['body']['sum']);
/** /**
* Test for FAILURE * Test for FAILURE
*/ */

View file

@ -224,6 +224,32 @@ class FunctionsCustomClientTest extends Scope
]; ];
} }
public function testCreateExecutionUnauthorized():array
{
$function = $this->client->call(Client::METHOD_POST, '/functions', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'functionId' => 'unique()',
'name' => 'Test',
'execute' => [],
'runtime' => 'php-8.0',
'timeout' => 10,
]);
$execution = $this->client->call(Client::METHOD_POST, '/functions/'.$function['body']['$id'].'/executions', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'async' => 1,
]);
$this->assertEquals(401, $execution['headers']['status-code']);
return [];
}
/** /**
* @depends testCreateCustomExecution * @depends testCreateCustomExecution
*/ */