Merge branch 'feat-functions-refactor' of https://github.com/appwrite/appwrite into feat-functions-refactor
This commit is contained in:
commit
9a0b7ce142
20 changed files with 336 additions and 318 deletions
|
@ -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
|
|
|
@ -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"
|
|
|
@ -1 +0,0 @@
|
||||||
echo 'Nothing to deploy right now.'
|
|
87
.travis.yml
87
.travis.yml
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
394
app/executor.php
394
app/executor.php
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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}}" />
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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
14
composer.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue