2021-08-24 21:32:27 +12:00
< ? php
2022-05-24 02:54:50 +12:00
2021-08-24 21:32:27 +12:00
require_once __DIR__ . '/../vendor/autoload.php' ;
2022-02-15 05:37:56 +13:00
use Appwrite\Runtimes\Runtimes ;
2022-01-22 03:33:58 +13:00
use Swoole\ConnectionPool ;
2021-08-24 21:32:27 +12:00
use Swoole\Http\Request as SwooleRequest ;
use Swoole\Http\Response as SwooleResponse ;
2022-01-24 06:21:23 +13:00
use Swoole\Http\Server ;
use Swoole\Process ;
2022-02-15 10:26:31 +13:00
use Swoole\Runtime ;
use Swoole\Timer ;
2022-01-24 06:21:23 +13:00
use Utopia\App ;
use Utopia\CLI\Console ;
2022-01-19 22:33:48 +13:00
use Utopia\Logger\Log ;
2022-02-19 04:58:04 +13:00
use Utopia\Logger\Logger ;
2022-01-24 06:21:23 +13:00
use Utopia\Orchestration\Adapter\DockerCLI ;
use Utopia\Orchestration\Orchestration ;
2022-02-18 00:40:02 +13:00
use Utopia\Storage\Device ;
2022-01-24 06:21:23 +13:00
use Utopia\Storage\Device\Local ;
2022-05-10 23:15:56 +12:00
use Utopia\Storage\Device\Backblaze ;
2022-02-18 00:40:02 +13:00
use Utopia\Storage\Device\DOSpaces ;
2022-03-19 05:03:04 +13:00
use Utopia\Storage\Device\Linode ;
2022-03-19 06:17:43 +13:00
use Utopia\Storage\Device\Wasabi ;
2022-02-18 00:40:02 +13:00
use Utopia\Storage\Device\S3 ;
2022-01-24 06:21:23 +13:00
use Utopia\Storage\Storage ;
use Utopia\Swoole\Request ;
2022-02-14 01:03:25 +13:00
use Utopia\Swoole\Response ;
2022-02-18 03:51:36 +13:00
use Utopia\Validator\ArrayList ;
2022-02-03 07:58:03 +13:00
use Utopia\Validator\Assoc ;
2022-02-18 13:43:04 +13:00
use Utopia\Validator\Boolean ;
2022-03-04 10:14:44 +13:00
use Utopia\Validator\Range ;
2022-01-24 06:21:23 +13:00
use Utopia\Validator\Text ;
2021-08-27 21:21:28 +12:00
2022-02-15 05:37:56 +13:00
2022-02-16 04:59:26 +13:00
Runtime :: enableCoroutine ( true , SWOOLE_HOOK_ALL );
2022-01-21 00:34:50 +13:00
2022-02-15 13:23:20 +13:00
/** Constants */
2022-03-04 10:14:44 +13:00
const MAINTENANCE_INTERVAL = 3600 ; // 3600 seconds = 1 hour
2022-02-15 13:23:20 +13:00
2022-02-15 11:15:08 +13:00
/**
2022-05-24 02:54:50 +12:00
* Create a Swoole table to store runtime information
2022-02-15 11:15:08 +13:00
*/
2022-02-15 13:23:20 +13:00
$activeRuntimes = new Swoole\Table ( 1024 );
2022-02-15 13:49:15 +13:00
$activeRuntimes -> column ( 'id' , Swoole\Table :: TYPE_STRING , 256 );
2022-02-15 13:23:20 +13:00
$activeRuntimes -> column ( 'created' , Swoole\Table :: TYPE_INT , 8 );
$activeRuntimes -> column ( 'updated' , Swoole\Table :: TYPE_INT , 8 );
2022-02-15 13:49:15 +13:00
$activeRuntimes -> column ( 'name' , Swoole\Table :: TYPE_STRING , 128 );
$activeRuntimes -> column ( 'status' , Swoole\Table :: TYPE_STRING , 128 );
$activeRuntimes -> column ( 'key' , Swoole\Table :: TYPE_STRING , 256 );
2022-02-15 13:23:20 +13:00
$activeRuntimes -> create ();
2022-02-15 11:15:08 +13:00
/**
* Create orchestration pool
*/
$orchestrationPool = new ConnectionPool ( function () {
$dockerUser = App :: getEnv ( 'DOCKERHUB_PULL_USERNAME' , null );
$dockerPass = App :: getEnv ( 'DOCKERHUB_PULL_PASSWORD' , null );
$orchestration = new Orchestration ( new DockerCLI ( $dockerUser , $dockerPass ));
return $orchestration ;
}, 10 );
2022-02-19 04:58:04 +13:00
/**
* Create logger instance
*/
$providerName = App :: getEnv ( '_APP_LOGGING_PROVIDER' , '' );
$providerConfig = App :: getEnv ( '_APP_LOGGING_CONFIG' , '' );
$logger = null ;
2022-05-24 02:54:50 +12:00
if ( ! empty ( $providerName ) && ! empty ( $providerConfig ) && Logger :: hasProvider ( $providerName )) {
$classname = '\\Utopia\\Logger\\Adapter\\' . \ucfirst ( $providerName );
2022-02-19 04:58:04 +13:00
$adapter = new $classname ( $providerConfig );
$logger = new Logger ( $adapter );
}
2022-01-21 23:42:12 +13:00
function logError ( Throwable $error , string $action , Utopia\Route $route = null )
{
2022-02-19 04:58:04 +13:00
global $logger ;
2022-01-21 00:34:50 +13:00
2022-01-21 23:42:12 +13:00
if ( $logger ) {
2022-01-19 05:55:53 +13:00
$version = App :: getEnv ( '_APP_VERSION' , 'UNKNOWN' );
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
$log = new Log ();
$log -> setNamespace ( " executor " );
$log -> setServer ( \gethostname ());
$log -> setVersion ( $version );
$log -> setType ( Log :: TYPE_ERROR );
$log -> setMessage ( $error -> getMessage ());
2021-08-27 21:21:28 +12:00
2022-01-21 23:42:12 +13:00
if ( $route ) {
2022-01-19 05:55:53 +13:00
$log -> addTag ( 'method' , $route -> getMethod ());
2022-05-24 02:54:50 +12:00
$log -> addTag ( 'url' , $route -> getPath ());
2022-01-19 05:55:53 +13:00
}
2021-09-28 01:39:20 +13:00
2022-01-19 05:55:53 +13:00
$log -> addTag ( 'code' , $error -> getCode ());
$log -> addTag ( 'verboseType' , get_class ( $error ));
2021-09-28 01:39:20 +13:00
2022-01-19 05:55:53 +13:00
$log -> addExtra ( 'file' , $error -> getFile ());
$log -> addExtra ( 'line' , $error -> getLine ());
$log -> addExtra ( 'trace' , $error -> getTraceAsString ());
2022-03-18 23:05:38 +13:00
$log -> addExtra ( 'detailedTrace' , $error -> getTrace ());
2022-01-19 05:55:53 +13:00
$log -> setAction ( $action );
$isProduction = App :: getEnv ( '_APP_ENV' , 'development' ) === 'production' ;
$log -> setEnvironment ( $isProduction ? Log :: ENVIRONMENT_PRODUCTION : Log :: ENVIRONMENT_STAGING );
$responseCode = $logger -> addLog ( $log );
2022-01-21 23:42:12 +13:00
Console :: info ( 'Executor log pushed with status code: ' . $responseCode );
2021-09-28 01:39:20 +13:00
}
2021-08-24 21:32:27 +12:00
2022-01-19 05:55:53 +13:00
Console :: error ( '[Error] Type: ' . get_class ( $error ));
Console :: error ( '[Error] Message: ' . $error -> getMessage ());
Console :: error ( '[Error] File: ' . $error -> getFile ());
Console :: error ( '[Error] Line: ' . $error -> getLine ());
2022-05-31 05:41:25 +12:00
}
2022-01-17 23:18:31 +13:00
2022-05-24 02:54:50 +12:00
function getStorageDevice ( $root ) : Device
{
2022-02-18 00:40:02 +13:00
switch ( App :: getEnv ( '_APP_STORAGE_DEVICE' , Storage :: DEVICE_LOCAL )) {
2022-05-24 02:54:50 +12:00
case Storage :: DEVICE_LOCAL :
default :
2022-02-18 00:40:02 +13:00
return new Local ( $root );
case Storage :: DEVICE_S3 :
2022-02-23 01:33:35 +13:00
$s3AccessKey = App :: getEnv ( '_APP_STORAGE_S3_ACCESS_KEY' , '' );
$s3SecretKey = App :: getEnv ( '_APP_STORAGE_S3_SECRET' , '' );
$s3Region = App :: getEnv ( '_APP_STORAGE_S3_REGION' , '' );
$s3Bucket = App :: getEnv ( '_APP_STORAGE_S3_BUCKET' , '' );
2022-02-18 00:40:02 +13:00
$s3Acl = 'private' ;
return new S3 ( $root , $s3AccessKey , $s3SecretKey , $s3Bucket , $s3Region , $s3Acl );
case Storage :: DEVICE_DO_SPACES :
2022-02-23 01:33:35 +13:00
$doSpacesAccessKey = App :: getEnv ( '_APP_STORAGE_DO_SPACES_ACCESS_KEY' , '' );
$doSpacesSecretKey = App :: getEnv ( '_APP_STORAGE_DO_SPACES_SECRET' , '' );
$doSpacesRegion = App :: getEnv ( '_APP_STORAGE_DO_SPACES_REGION' , '' );
$doSpacesBucket = App :: getEnv ( '_APP_STORAGE_DO_SPACES_BUCKET' , '' );
2022-02-18 00:40:02 +13:00
$doSpacesAcl = 'private' ;
return new DOSpaces ( $root , $doSpacesAccessKey , $doSpacesSecretKey , $doSpacesBucket , $doSpacesRegion , $doSpacesAcl );
2022-05-13 10:01:53 +12:00
case Storage :: DEVICE_BACKBLAZE :
$backblazeAccessKey = App :: getEnv ( '_APP_STORAGE_BACKBLAZE_ACCESS_KEY' , '' );
$backblazeSecretKey = App :: getEnv ( '_APP_STORAGE_BACKBLAZE_SECRET' , '' );
$backblazeRegion = App :: getEnv ( '_APP_STORAGE_BACKBLAZE_REGION' , '' );
$backblazeBucket = App :: getEnv ( '_APP_STORAGE_BACKBLAZE_BUCKET' , '' );
$backblazeAcl = 'private' ;
return new Backblaze ( $root , $backblazeAccessKey , $backblazeSecretKey , $backblazeBucket , $backblazeRegion , $backblazeAcl );
2022-03-19 05:03:04 +13:00
case Storage :: DEVICE_LINODE :
$linodeAccessKey = App :: getEnv ( '_APP_STORAGE_LINODE_ACCESS_KEY' , '' );
$linodeSecretKey = App :: getEnv ( '_APP_STORAGE_LINODE_SECRET' , '' );
$linodeRegion = App :: getEnv ( '_APP_STORAGE_LINODE_REGION' , '' );
$linodeBucket = App :: getEnv ( '_APP_STORAGE_LINODE_BUCKET' , '' );
$linodeAcl = 'private' ;
return new Linode ( $root , $linodeAccessKey , $linodeSecretKey , $linodeBucket , $linodeRegion , $linodeAcl );
2022-03-19 06:17:43 +13:00
case Storage :: DEVICE_WASABI :
$wasabiAccessKey = App :: getEnv ( '_APP_STORAGE_WASABI_ACCESS_KEY' , '' );
$wasabiSecretKey = App :: getEnv ( '_APP_STORAGE_WASABI_SECRET' , '' );
$wasabiRegion = App :: getEnv ( '_APP_STORAGE_WASABI_REGION' , '' );
$wasabiBucket = App :: getEnv ( '_APP_STORAGE_WASABI_BUCKET' , '' );
$wasabiAcl = 'private' ;
return new Wasabi ( $root , $wasabiAccessKey , $wasabiSecretKey , $wasabiBucket , $wasabiRegion , $wasabiAcl );
2022-02-18 00:40:02 +13:00
}
}
2022-02-14 01:31:44 +13:00
App :: post ( '/v1/runtimes' )
-> desc ( " Create a new runtime server " )
2022-02-19 13:15:03 +13:00
-> param ( 'runtimeId' , '' , new Text ( 64 ), 'Unique runtime ID.' )
2022-02-14 01:31:44 +13:00
-> param ( 'source' , '' , new Text ( 0 ), 'Path to source files.' )
2022-02-18 13:43:04 +13:00
-> param ( 'destination' , '' , new Text ( 0 ), 'Destination folder to store build files into.' , true )
2022-04-25 19:23:25 +12:00
-> param ( 'vars' , [], new Assoc (), 'Environment Variables required for the build.' )
2022-05-03 20:47:50 +12:00
-> param ( 'commands' , [], new ArrayList ( new Text ( 1024 ), 100 ), 'Commands required to build the container. Maximum of 100 commands are allowed, each 1024 characters long.' )
2022-04-25 19:23:25 +12:00
-> param ( 'runtime' , '' , new Text ( 128 ), 'Runtime for the cloud function.' )
-> param ( 'baseImage' , '' , new Text ( 128 ), 'Base image name of the runtime.' )
-> param ( 'entrypoint' , '' , new Text ( 256 ), 'Entrypoint of the code file.' , true )
-> param ( 'remove' , false , new Boolean (), 'Remove a runtime after execution.' )
-> param ( 'workdir' , '' , new Text ( 256 ), 'Working directory.' , true )
2022-02-15 06:18:48 +13:00
-> inject ( 'orchestrationPool' )
2022-02-15 13:23:20 +13:00
-> inject ( 'activeRuntimes' )
2022-02-14 01:31:44 +13:00
-> inject ( 'response' )
2022-03-25 00:01:30 +13:00
-> action ( function ( string $runtimeId , string $source , string $destination , array $vars , array $commands , string $runtime , string $baseImage , string $entrypoint , bool $remove , string $workdir , $orchestrationPool , $activeRuntimes , Response $response ) {
2022-02-19 13:15:03 +13:00
if ( $activeRuntimes -> exists ( $runtimeId )) {
2022-06-21 00:35:43 +12:00
if ( $activeRuntimes -> get ( $runtimeId )[ 'status' ] == 'pending' ) {
2022-06-09 03:15:17 +12:00
throw new \Exception ( 'A runtime with the same ID is already being created. Attempt a execution soon.' , 500 );
2022-06-08 01:55:44 +12:00
}
2022-02-16 06:39:03 +13:00
throw new Exception ( 'Runtime already exists.' , 409 );
}
2022-02-14 01:31:44 +13:00
2022-02-19 04:58:04 +13:00
$container = [];
$containerId = '' ;
$stdout = '' ;
$stderr = '' ;
$startTime = \time ();
$endTime = 0 ;
2022-03-01 03:15:10 +13:00
$orchestration = $orchestrationPool -> get ();
2022-02-14 01:31:44 +13:00
2022-06-08 01:55:44 +12:00
$secret = \bin2hex ( \random_bytes ( 16 ));
2022-06-09 03:15:17 +12:00
if ( ! $remove ) {
$activeRuntimes -> set ( $runtimeId , [
'id' => $containerId ,
'name' => $runtimeId ,
'created' => $startTime ,
'updated' => $endTime ,
'status' => 'pending' ,
'key' => $secret ,
]);
}
2022-06-08 01:55:44 +12:00
2022-02-14 01:31:44 +13:00
try {
2022-02-19 13:15:03 +13:00
Console :: info ( 'Building container : ' . $runtimeId );
2022-05-24 02:54:50 +12:00
/**
* Temporary file paths in the executor
2022-02-14 01:31:44 +13:00
*/
2022-02-24 01:22:18 +13:00
$tmpSource = " /tmp/ $runtimeId /src/code.tar.gz " ;
2022-02-14 01:31:44 +13:00
$tmpBuild = " /tmp/ $runtimeId /builds/code.tar.gz " ;
/**
2022-02-14 10:26:36 +13:00
* Copy code files from source to a temporary location on the executor
2022-02-14 01:31:44 +13:00
*/
2022-02-22 18:56:50 +13:00
$sourceDevice = getStorageDevice ( " / " );
$localDevice = new Local ();
$buffer = $sourceDevice -> read ( $source );
2022-05-24 02:54:50 +12:00
if ( ! $localDevice -> write ( $tmpSource , $buffer )) {
2022-02-16 06:39:03 +13:00
throw new Exception ( 'Failed to copy source code to temporary directory' , 500 );
2022-02-14 10:26:36 +13:00
};
2021-12-07 03:12:41 +13:00
2022-02-14 01:31:44 +13:00
/**
2022-02-18 10:40:28 +13:00
* Create the mount folder
2022-02-14 01:31:44 +13:00
*/
2022-02-18 10:40:28 +13:00
if ( ! \file_exists ( \dirname ( $tmpBuild ))) {
if ( !@ \mkdir ( \dirname ( $tmpBuild ), 0755 , true )) {
throw new Exception ( " Failed to create temporary directory " , 500 );
2022-02-14 01:31:44 +13:00
}
}
2021-08-24 21:32:27 +12:00
2022-02-14 01:31:44 +13:00
/**
* Create container
*/
2022-02-18 13:43:04 +13:00
$vars = \array_merge ( $vars , [
2022-02-19 12:32:09 +13:00
'INTERNAL_RUNTIME_KEY' => $secret ,
'INTERNAL_RUNTIME_ENTRYPOINT' => $entrypoint ,
2022-02-18 13:43:04 +13:00
]);
2022-02-14 01:31:44 +13:00
$vars = array_map ( fn ( $v ) => strval ( $v ), $vars );
$orchestration
2022-02-26 00:00:16 +13:00
-> setCpus (( int ) App :: getEnv ( '_APP_FUNCTIONS_CPUS' , 0 ))
-> setMemory (( int ) App :: getEnv ( '_APP_FUNCTIONS_MEMORY' , 0 ))
-> setSwap (( int ) App :: getEnv ( '_APP_FUNCTIONS_MEMORY_SWAP' , 0 ));
2022-05-24 02:54:50 +12:00
2022-02-22 01:15:09 +13:00
/** Keep the container alive if we have commands to be executed */
$entrypoint = ! empty ( $commands ) ? [
2022-02-21 09:25:35 +13:00
'tail' ,
'-f' ,
'/dev/null'
2022-02-22 01:15:09 +13:00
] : [];
2022-02-22 00:08:58 +13:00
2022-02-19 04:58:04 +13:00
$containerId = $orchestration -> run (
2022-02-14 01:31:44 +13:00
image : $baseImage ,
2022-02-19 13:15:03 +13:00
name : $runtimeId ,
hostname : $runtimeId ,
2022-02-14 01:31:44 +13:00
vars : $vars ,
2022-02-21 09:25:35 +13:00
command : $entrypoint ,
2022-02-14 01:31:44 +13:00
labels : [
'openruntimes-id' => $runtimeId ,
2022-02-19 04:25:54 +13:00
'openruntimes-type' => 'runtime' ,
2022-02-19 04:58:04 +13:00
'openruntimes-created' => strval ( $startTime ),
2022-02-14 01:31:44 +13:00
'openruntimes-runtime' => $runtime ,
],
2022-02-18 10:40:28 +13:00
workdir : $workdir ,
2022-02-14 01:31:44 +13:00
volumes : [
2022-05-24 02:54:50 +12:00
\dirname ( $tmpSource ) . ':/tmp:rw' ,
\dirname ( $tmpBuild ) . ':/usr/code:rw'
2022-02-14 01:31:44 +13:00
]
);
2021-08-27 22:55:22 +12:00
2022-02-19 04:58:04 +13:00
if ( empty ( $containerId )) {
2022-02-14 01:31:44 +13:00
throw new Exception ( 'Failed to create build container' , 500 );
}
2021-09-21 03:52:12 +12:00
2022-04-04 20:14:14 +12:00
$orchestration -> networkConnect ( $runtimeId , App :: getEnv ( 'OPEN_RUNTIMES_NETWORK' , 'appwrite_runtimes' ));
2022-02-18 13:43:04 +13:00
2022-05-24 02:54:50 +12:00
/**
2022-02-18 10:40:28 +13:00
* Execute any commands if they were provided
2022-02-14 01:31:44 +13:00
*/
2022-02-18 10:40:28 +13:00
if ( ! empty ( $commands )) {
$status = $orchestration -> execute (
2022-02-19 13:15:03 +13:00
name : $runtimeId ,
2022-02-18 10:40:28 +13:00
command : $commands ,
2022-02-19 04:58:04 +13:00
stdout : $stdout ,
stderr : $stderr ,
2022-03-15 10:02:24 +13:00
timeout : App :: getEnv ( '_APP_FUNCTIONS_BUILD_TIMEOUT' , 900 )
2022-02-18 10:40:28 +13:00
);
if ( ! $status ) {
2022-02-19 04:58:04 +13:00
throw new Exception ( 'Failed to build dependenices ' . $stderr , 500 );
2022-02-18 10:40:28 +13:00
}
2022-02-14 01:31:44 +13:00
}
/**
2022-02-14 10:26:36 +13:00
* Move built code to expected build directory
2022-02-14 01:31:44 +13:00
*/
2022-02-18 10:40:28 +13:00
if ( ! empty ( $destination )) {
// Check if the build was successful by checking if file exists
if ( ! \file_exists ( $tmpBuild )) {
throw new Exception ( 'Something went wrong during the build process' , 500 );
2022-02-14 01:31:44 +13:00
}
2022-02-18 10:40:28 +13:00
2022-02-24 01:23:48 +13:00
$destinationDevice = getStorageDevice ( $destination );
2022-02-22 18:56:50 +13:00
$outputPath = $destinationDevice -> getPath ( \uniqid () . '.' . \pathinfo ( 'code.tar.gz' , PATHINFO_EXTENSION ));
2022-02-19 12:32:09 +13:00
2022-02-22 18:56:50 +13:00
$buffer = $localDevice -> read ( $tmpBuild );
2022-05-24 02:54:50 +12:00
if ( ! $destinationDevice -> write ( $outputPath , $buffer , $localDevice -> getFileMimeType ( $tmpBuild ))) {
2022-02-19 12:32:09 +13:00
throw new Exception ( 'Failed to move built code to storage' , 500 );
};
2022-02-18 10:40:28 +13:00
2022-02-19 04:58:04 +13:00
$container [ 'outputPath' ] = $outputPath ;
2022-02-14 01:31:44 +13:00
}
2021-09-06 12:37:20 +12:00
2022-02-19 04:58:04 +13:00
if ( empty ( $stdout )) {
$stdout = 'Build Successful!' ;
2022-02-14 01:31:44 +13:00
}
2022-02-19 04:58:04 +13:00
$endTime = \time ();
$container = array_merge ( $container , [
2022-02-14 01:31:44 +13:00
'status' => 'ready' ,
2022-05-13 06:41:04 +12:00
'response' => \mb_strcut ( $stdout , 0 , 1000000 ), // Limit to 1MB
2022-05-05 21:01:08 +12:00
'stderr' => \mb_strcut ( $stderr , 0 , 1000000 ), // Limit to 1MB
2022-02-19 04:58:04 +13:00
'startTime' => $startTime ,
'endTime' => $endTime ,
'duration' => $endTime - $startTime ,
2022-02-18 10:40:28 +13:00
]);
2022-02-14 01:31:44 +13:00
2022-02-18 13:43:04 +13:00
if ( ! $remove ) {
2022-02-19 13:15:03 +13:00
$activeRuntimes -> set ( $runtimeId , [
2022-02-19 04:58:04 +13:00
'id' => $containerId ,
2022-02-19 13:15:03 +13:00
'name' => $runtimeId ,
2022-02-19 04:58:04 +13:00
'created' => $startTime ,
'updated' => $endTime ,
'status' => 'Up ' . \round ( $endTime - $startTime , 2 ) . 's' ,
2022-02-18 13:43:04 +13:00
'key' => $secret ,
]);
}
2022-02-19 04:58:04 +13:00
Console :: success ( 'Build Stage completed in ' . ( $endTime - $startTime ) . ' seconds' );
2022-02-14 01:31:44 +13:00
} catch ( Throwable $th ) {
2022-02-25 22:10:00 +13:00
Console :: error ( 'Build failed: ' . $th -> getMessage () . $stdout );
2022-06-09 23:46:08 +12:00
2022-02-25 22:10:00 +13:00
throw new Exception ( $th -> getMessage () . $stdout , 500 );
2022-02-14 01:31:44 +13:00
} finally {
2022-03-19 00:42:13 +13:00
// Container cleanup
2022-05-24 02:54:50 +12:00
if ( $remove ) {
2022-03-19 00:42:13 +13:00
if ( ! empty ( $containerId )) {
// If container properly created
$orchestration -> remove ( $containerId , true );
2022-06-09 23:46:08 +12:00
$activeRuntimes -> del ( $runtimeId );
2022-03-19 00:42:13 +13:00
} else {
// If whole creation failed, but container might have been initialized
try {
// Try to remove with contaier name instead of ID
$orchestration -> remove ( $runtimeId , true );
2022-06-09 23:46:08 +12:00
$activeRuntimes -> del ( $runtimeId );
2022-03-19 00:42:13 +13:00
} catch ( Throwable $th ) {
// If fails, means initialization also failed.
// Contianer is not there, no need to remove
}
}
2022-02-14 01:31:44 +13:00
}
2022-03-19 00:42:13 +13:00
// Release orchestration back to pool, we are done with it
2022-02-16 06:39:03 +13:00
$orchestrationPool -> put ( $orchestration );
2022-02-14 10:26:36 +13:00
}
2022-02-11 23:31:53 +13:00
$response
2022-02-16 06:39:03 +13:00
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2022-02-19 04:58:04 +13:00
-> json ( $container );
2022-02-11 23:31:53 +13:00
});
App :: get ( '/v1/runtimes' )
2022-02-15 06:52:44 +13:00
-> desc ( " List currently active runtimes " )
2022-02-15 13:23:20 +13:00
-> inject ( 'activeRuntimes' )
2022-02-11 23:31:53 +13:00
-> inject ( 'response' )
2022-02-15 13:23:20 +13:00
-> action ( function ( $activeRuntimes , Response $response ) {
2022-02-11 23:31:53 +13:00
$runtimes = [];
2022-05-24 02:54:50 +12:00
foreach ( $activeRuntimes as $runtime ) {
2022-02-15 06:52:44 +13:00
$runtimes [] = $runtime ;
}
2022-02-11 23:31:53 +13:00
$response
2022-02-19 04:58:04 +13:00
-> setStatusCode ( Response :: STATUS_CODE_OK )
2022-02-11 23:31:53 +13:00
-> json ( $runtimes );
});
App :: get ( '/v1/runtimes/:runtimeId' )
-> desc ( " Get a runtime by its ID " )
2022-02-19 13:15:03 +13:00
-> param ( 'runtimeId' , '' , new Text ( 64 ), 'Runtime unique ID.' )
2022-02-15 13:23:20 +13:00
-> inject ( 'activeRuntimes' )
2022-02-11 23:31:53 +13:00
-> inject ( 'response' )
2022-02-15 13:23:20 +13:00
-> action ( function ( $runtimeId , $activeRuntimes , Response $response ) {
2022-02-15 06:52:44 +13:00
2022-05-24 02:54:50 +12:00
if ( ! $activeRuntimes -> exists ( $runtimeId )) {
2022-02-15 06:52:44 +13:00
throw new Exception ( 'Runtime not found' , 404 );
}
2022-02-19 13:15:03 +13:00
$runtime = $activeRuntimes -> get ( $runtimeId );
2022-02-11 23:31:53 +13:00
$response
2022-02-19 04:58:04 +13:00
-> setStatusCode ( Response :: STATUS_CODE_OK )
2022-02-11 23:31:53 +13:00
-> json ( $runtime );
});
2022-02-13 11:34:16 +13:00
App :: delete ( '/v1/runtimes/:runtimeId' )
-> desc ( 'Delete a runtime' )
2022-02-19 13:15:03 +13:00
-> param ( 'runtimeId' , '' , new Text ( 64 ), 'Runtime unique ID.' , false )
2022-02-15 06:18:48 +13:00
-> inject ( 'orchestrationPool' )
2022-02-15 13:23:20 +13:00
-> inject ( 'activeRuntimes' )
2022-01-24 06:21:23 +13:00
-> inject ( 'response' )
2022-02-15 13:58:12 +13:00
-> action ( function ( string $runtimeId , $orchestrationPool , $activeRuntimes , Response $response ) {
2022-02-03 08:38:47 +13:00
2022-05-24 02:54:50 +12:00
if ( ! $activeRuntimes -> exists ( $runtimeId )) {
2022-02-16 06:39:03 +13:00
throw new Exception ( 'Runtime not found' , 404 );
}
2022-02-19 13:15:03 +13:00
Console :: info ( 'Deleting runtime: ' . $runtimeId );
2022-02-16 06:39:03 +13:00
2022-02-15 13:58:12 +13:00
try {
$orchestration = $orchestrationPool -> get ();
2022-02-19 13:15:03 +13:00
$orchestration -> remove ( $runtimeId , true );
$activeRuntimes -> del ( $runtimeId );
Console :: success ( 'Removed runtime container: ' . $runtimeId );
2022-02-15 13:58:12 +13:00
} finally {
$orchestrationPool -> put ( $orchestration );
2022-02-03 08:38:47 +13:00
}
2022-02-13 11:34:16 +13:00
// Remove all the build containers with that same ID
2022-02-13 14:29:28 +13:00
// TODO:: Delete build containers
// foreach ($buildIds as $buildId) {
// try {
// Console::info('Deleting build container : ' . $buildId);
// $status = $orchestration->remove('build-' . $buildId, true);
// } catch (Throwable $th) {
// Console::error($th->getMessage());
// }
// }
2022-02-06 23:00:44 +13:00
2022-01-28 14:40:53 +13:00
$response
-> setStatusCode ( Response :: STATUS_CODE_OK )
-> send ();
2022-01-24 06:21:23 +13:00
});
2022-02-13 11:34:16 +13:00
App :: post ( '/v1/execution' )
-> desc ( 'Create an execution' )
2022-04-25 19:23:25 +12:00
-> param ( 'runtimeId' , '' , new Text ( 64 ), 'The runtimeID to execute.' )
-> param ( 'vars' , [], new Assoc (), 'Environment variables required for the build.' )
2022-06-10 22:31:37 +12:00
-> param ( 'data' , '' , new Text ( 8192 ), 'Data to be forwarded to the function, this is user specified.' , true )
2022-03-04 10:14:44 +13:00
-> param ( 'timeout' , 15 , new Range ( 1 , ( int ) App :: getEnv ( '_APP_FUNCTIONS_TIMEOUT' , 900 )), 'Function maximum execution time in seconds.' )
2022-02-15 13:23:20 +13:00
-> inject ( 'activeRuntimes' )
2022-02-13 11:34:16 +13:00
-> inject ( 'response' )
-> action (
2022-02-25 03:31:30 +13:00
function ( string $runtimeId , array $vars , string $data , $timeout , $activeRuntimes , Response $response ) {
2022-02-19 13:15:03 +13:00
if ( ! $activeRuntimes -> exists ( $runtimeId )) {
2022-02-15 05:37:56 +13:00
throw new Exception ( 'Runtime not found. Please create the runtime.' , 404 );
}
2022-06-09 03:15:17 +12:00
for ( $i = 0 ; $i < 5 ; $i ++ ) {
if ( $activeRuntimes -> get ( $runtimeId )[ 'status' ] === 'pending' ) {
Console :: info ( 'Waiting for runtime to be ready...' );
sleep ( 1 );
} else {
break ;
}
if ( $i === 4 ) {
throw new Exception ( 'Runtime failed to launch in allocated time.' , 500 );
}
}
2022-02-19 13:15:03 +13:00
$runtime = $activeRuntimes -> get ( $runtimeId );
2022-02-15 12:22:16 +13:00
$secret = $runtime [ 'key' ];
2022-02-15 05:37:56 +13:00
if ( empty ( $secret )) {
2022-02-17 09:11:05 +13:00
throw new Exception ( 'Runtime secret not found. Please re-create the runtime.' , 500 );
2022-02-15 05:37:56 +13:00
}
Console :: info ( 'Executing Runtime: ' . $runtimeId );
2022-05-13 06:41:04 +12:00
2022-02-17 00:52:54 +13:00
$execution = [];
2022-02-15 05:37:56 +13:00
$executionStart = \microtime ( true );
$stdout = '' ;
$stderr = '' ;
$statusCode = 0 ;
$errNo = - 1 ;
$executorResponse = '' ;
2022-03-04 23:37:56 +13:00
$timeout ? ? = ( int ) App :: getEnv ( '_APP_FUNCTIONS_TIMEOUT' , 900 );
2022-03-04 10:14:44 +13:00
2022-02-17 00:52:54 +13:00
$ch = \curl_init ();
$body = \json_encode ([
'env' => $vars ,
'payload' => $data ,
2022-03-04 10:14:44 +13:00
'timeout' => $timeout
2022-02-17 00:52:54 +13:00
]);
2022-02-19 13:15:03 +13:00
\curl_setopt ( $ch , CURLOPT_URL , " http:// " . $runtimeId . " :3000/ " );
2022-02-17 00:52:54 +13:00
\curl_setopt ( $ch , CURLOPT_POST , true );
\curl_setopt ( $ch , CURLOPT_POSTFIELDS , $body );
\curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , true );
2022-03-04 10:14:44 +13:00
\curl_setopt ( $ch , CURLOPT_TIMEOUT , $timeout );
2022-02-17 00:52:54 +13:00
\curl_setopt ( $ch , CURLOPT_CONNECTTIMEOUT , 10 );
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
\curl_setopt ( $ch , CURLOPT_HTTPHEADER , [
'Content-Type: application/json' ,
'Content-Length: ' . \strlen ( $body ),
'x-internal-challenge: ' . $secret ,
'host: null'
]);
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
$executorResponse = \curl_exec ( $ch );
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
$statusCode = \curl_getinfo ( $ch , CURLINFO_HTTP_CODE );
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
$error = \curl_error ( $ch );
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
$errNo = \curl_errno ( $ch );
2022-05-24 02:54:50 +12:00
2022-02-17 00:52:54 +13:00
\curl_close ( $ch );
2022-03-16 00:56:00 +13:00
switch ( true ) {
/** No Error. */
2022-05-24 02:54:50 +12:00
case $errNo === 0 :
2022-03-16 03:51:32 +13:00
break ;
2022-03-16 01:04:43 +13:00
/** Runtime not ready for requests yet. 111 is the swoole error code for Connection Refused - see https://openswoole.com/docs/swoole-error-code */
2022-03-16 00:56:00 +13:00
case $errNo === 111 :
throw new Exception ( 'An internal curl error has occurred within the executor! Error Msg: ' . $error , 406 );
2022-03-16 01:04:43 +13:00
/** Any other CURL error */
2022-05-24 02:54:50 +12:00
default :
2022-03-16 00:56:00 +13:00
throw new Exception ( 'An internal curl error has occurred within the executor! Error Msg: ' . $error , 500 );
2022-02-17 00:52:54 +13:00
}
2022-03-16 00:56:00 +13:00
switch ( true ) {
case $statusCode >= 500 :
$stderr = $executorResponse ? ? 'Internal Runtime error.' ;
break ;
2022-03-16 02:06:05 +13:00
case $statusCode >= 100 :
2022-03-16 00:56:00 +13:00
$stdout = $executorResponse ;
break ;
default :
$stderr = $executorResponse ? ? 'Execution failed.' ;
break ;
2022-02-15 05:37:56 +13:00
}
2022-05-13 06:41:04 +12:00
2022-02-17 00:52:54 +13:00
$executionEnd = \microtime ( true );
$executionTime = ( $executionEnd - $executionStart );
2022-03-16 00:56:00 +13:00
$functionStatus = ( $statusCode >= 500 ) ? 'failed' : 'completed' ;
2022-05-13 06:41:04 +12:00
2022-02-17 00:52:54 +13:00
Console :: success ( 'Function executed in ' . $executionTime . ' seconds, status: ' . $functionStatus );
2022-04-28 01:57:03 +12:00
2022-02-17 00:52:54 +13:00
$execution = [
'status' => $functionStatus ,
'statusCode' => $statusCode ,
2022-05-13 06:41:04 +12:00
'response' => \mb_strcut ( $stdout , 0 , 1000000 ), // Limit to 1MB
2022-04-28 01:57:03 +12:00
'stderr' => \mb_strcut ( $stderr , 0 , 1000000 ), // Limit to 1MB
2022-02-17 00:52:54 +13:00
'time' => $executionTime ,
];
/** Update swoole table */
$runtime [ 'updated' ] = \time ();
2022-02-19 13:15:03 +13:00
$activeRuntimes -> set ( $runtimeId , $runtime );
2022-02-13 11:34:16 +13:00
$response
-> setStatusCode ( Response :: STATUS_CODE_OK )
-> json ( $execution );
}
);
2022-01-19 05:55:53 +13:00
App :: setMode ( App :: MODE_TYPE_PRODUCTION ); // Define Mode
2021-10-14 22:37:00 +13:00
2022-01-24 06:21:23 +13:00
$http = new Server ( " 0.0.0.0 " , 80 );
2021-08-24 21:32:27 +12:00
2022-02-15 06:18:48 +13:00
/** Set Resources */
App :: setResource ( 'orchestrationPool' , fn () => $orchestrationPool );
2022-02-15 13:23:20 +13:00
App :: setResource ( 'activeRuntimes' , fn () => $activeRuntimes );
2022-02-15 06:18:48 +13:00
/** Set callbacks */
2022-07-25 00:06:34 +12:00
App :: error ()
-> inject ( 'utopia' )
-> inject ( 'error' )
-> inject ( 'request' )
-> inject ( 'response' )
2022-07-28 19:05:23 +12:00
-> action ( function ( App $utopia , throwable $error , Request $request , Response $response ) {
2022-07-25 00:06:34 +12:00
$route = $utopia -> match ( $request );
logError ( $error , " httpError " , $route );
switch ( $error -> getCode ()) {
case 400 : // Error allowed publicly
case 401 : // Error allowed publicly
case 402 : // Error allowed publicly
case 403 : // Error allowed publicly
case 404 : // Error allowed publicly
case 406 : // Error allowed publicly
case 409 : // Error allowed publicly
case 412 : // Error allowed publicly
case 425 : // Error allowed publicly
case 429 : // Error allowed publicly
case 501 : // Error allowed publicly
case 503 : // Error allowed publicly
$code = $error -> getCode ();
break ;
default :
$code = 500 ; // All other errors get the generic 500 server error status code
}
2022-05-24 02:54:50 +12:00
2022-07-25 00:06:34 +12:00
$output = [
'message' => $error -> getMessage (),
'code' => $error -> getCode (),
'file' => $error -> getFile (),
'line' => $error -> getLine (),
'trace' => $error -> getTrace (),
'version' => App :: getEnv ( '_APP_VERSION' , 'UNKNOWN' )
];
$response
-> addHeader ( 'Cache-Control' , 'no-cache, no-store, must-revalidate' )
-> addHeader ( 'Expires' , '0' )
-> addHeader ( 'Pragma' , 'no-cache' )
-> setStatusCode ( $code );
$response -> json ( $output );
});
2022-02-14 13:00:16 +13:00
2022-07-22 18:00:42 +12:00
App :: init ()
-> inject ( 'request' )
2022-07-28 19:05:23 +12:00
-> action ( function ( Request $request ) {
2022-07-22 18:00:42 +12:00
$secretKey = $request -> getHeader ( 'x-appwrite-executor-key' , '' );
if ( empty ( $secretKey )) {
throw new Exception ( 'Missing executor key' , 401 );
}
2022-02-15 05:37:56 +13:00
2022-07-22 18:00:42 +12:00
if ( $secretKey !== App :: getEnv ( '_APP_EXECUTOR_SECRET' , '' )) {
throw new Exception ( 'Missing executor key' , 401 );
}
});
2022-02-15 05:37:56 +13:00
2022-02-15 10:26:31 +13:00
2022-02-15 12:22:16 +13:00
$http -> on ( 'start' , function ( $http ) {
2022-02-15 13:40:16 +13:00
global $orchestrationPool ;
global $activeRuntimes ;
2022-05-17 01:05:58 +12:00
2022-05-24 02:54:50 +12:00
/**
2022-02-15 11:15:08 +13:00
* Warmup : make sure images are ready to run fast 🚀
*/
2022-05-17 01:05:58 +12:00
$runtimes = new Runtimes ( 'v1' );
2022-02-15 11:15:08 +13:00
$allowList = empty ( App :: getEnv ( '_APP_FUNCTIONS_RUNTIMES' )) ? [] : \explode ( ',' , App :: getEnv ( '_APP_FUNCTIONS_RUNTIMES' ));
$runtimes = $runtimes -> getAll ( true , $allowList );
foreach ( $runtimes as $runtime ) {
go ( function () use ( $runtime , $orchestrationPool ) {
try {
$orchestration = $orchestrationPool -> get ();
Console :: info ( 'Warming up ' . $runtime [ 'name' ] . ' ' . $runtime [ 'version' ] . ' environment...' );
2022-02-28 07:41:43 +13:00
$response = $orchestration -> pull ( $runtime [ 'image' ]);
if ( $response ) {
Console :: success ( " Successfully Warmed up { $runtime [ 'name' ] } { $runtime [ 'version' ] } ! " );
} else {
Console :: warning ( " Failed to Warmup { $runtime [ 'name' ] } { $runtime [ 'version' ] } ! " );
}
2022-02-15 11:15:08 +13:00
} catch ( \Throwable $th ) {
} finally {
$orchestrationPool -> put ( $orchestration );
}
});
}
/**
2022-02-15 13:40:16 +13:00
* Remove residual runtimes
2022-02-15 11:15:08 +13:00
*/
2022-02-15 13:40:16 +13:00
Console :: info ( 'Removing orphan runtimes...' );
2022-02-15 11:15:08 +13:00
try {
$orchestration = $orchestrationPool -> get ();
2022-02-19 04:25:54 +13:00
$orphans = $orchestration -> list ([ 'label' => 'openruntimes-type=runtime' ]);
2022-02-15 11:15:08 +13:00
} finally {
$orchestrationPool -> put ( $orchestration );
}
2022-02-15 13:40:16 +13:00
foreach ( $orphans as $runtime ) {
go ( function () use ( $runtime , $orchestrationPool ) {
try {
$orchestration = $orchestrationPool -> get ();
$orchestration -> remove ( $runtime -> getName (), true );
Console :: success ( " Successfully removed { $runtime -> getName () } " );
} catch ( \Throwable $th ) {
Console :: error ( 'Orphan runtime deletion failed: ' . $th -> getMessage ());
} finally {
$orchestrationPool -> put ( $orchestration );
}
});
2022-02-15 11:15:08 +13:00
}
2022-02-15 10:26:31 +13:00
/**
* Register handlers for shutdown
*/
2022-01-21 23:42:12 +13:00
@ Process :: signal ( SIGINT , function () use ( $http ) {
2021-09-06 12:37:20 +12:00
$http -> shutdown ();
});
2022-01-21 23:42:12 +13:00
@ Process :: signal ( SIGQUIT , function () use ( $http ) {
2021-09-06 12:37:20 +12:00
$http -> shutdown ();
});
2022-01-21 23:42:12 +13:00
@ Process :: signal ( SIGKILL , function () use ( $http ) {
2021-09-06 12:37:20 +12:00
$http -> shutdown ();
});
2022-01-21 23:42:12 +13:00
@ Process :: signal ( SIGTERM , function () use ( $http ) {
2021-09-06 12:37:20 +12:00
$http -> shutdown ();
});
2022-02-15 10:26:31 +13:00
/**
2022-02-15 13:23:20 +13:00
* Run a maintenance worker every MAINTENANCE_INTERVAL seconds to remove inactive runtimes
2022-02-15 10:26:31 +13:00
*/
2022-02-15 13:23:20 +13:00
Timer :: tick ( MAINTENANCE_INTERVAL * 1000 , function () use ( $orchestrationPool , $activeRuntimes ) {
Console :: warning ( " Running maintenance task ... " );
foreach ( $activeRuntimes as $runtime ) {
2022-02-26 03:34:43 +13:00
$inactiveThreshold = \time () - App :: getEnv ( '_APP_FUNCTIONS_INACTIVE_THRESHOLD' , 60 );
2022-02-15 13:23:20 +13:00
if ( $runtime [ 'updated' ] < $inactiveThreshold ) {
go ( function () use ( $runtime , $orchestrationPool , $activeRuntimes ) {
try {
$orchestration = $orchestrationPool -> get ();
$orchestration -> remove ( $runtime [ 'name' ], true );
$activeRuntimes -> del ( $runtime [ 'name' ]);
Console :: success ( " Successfully removed { $runtime [ 'name' ] } " );
} catch ( \Throwable $th ) {
Console :: error ( 'Inactive Runtime deletion failed: ' . $th -> getMessage ());
} finally {
$orchestrationPool -> put ( $orchestration );
}
});
}
}
});
2021-09-06 12:37:20 +12:00
});
2022-02-15 10:26:31 +13:00
2022-05-24 02:54:50 +12:00
$http -> on ( 'beforeShutdown' , function () {
2022-02-15 10:26:31 +13:00
global $orchestrationPool ;
Console :: info ( 'Cleaning up containers before shutdown...' );
$orchestration = $orchestrationPool -> get ();
2022-02-19 04:25:54 +13:00
$functionsToRemove = $orchestration -> list ([ 'label' => 'openruntimes-type=runtime' ]);
2022-02-15 10:26:31 +13:00
$orchestrationPool -> put ( $orchestration );
foreach ( $functionsToRemove as $container ) {
2022-05-24 02:54:50 +12:00
go ( function () use ( $orchestrationPool , $container ) {
2022-02-15 10:26:31 +13:00
try {
$orchestration = $orchestrationPool -> get ();
$orchestration -> remove ( $container -> getId (), true );
Console :: info ( 'Removed container ' . $container -> getName ());
} catch ( \Throwable $th ) {
Console :: error ( 'Failed to remove container: ' . $container -> getName ());
} finally {
$orchestrationPool -> put ( $orchestration );
}
});
}
});
2022-01-21 23:42:12 +13:00
$http -> on ( 'request' , function ( SwooleRequest $swooleRequest , SwooleResponse $swooleResponse ) {
2021-08-24 21:32:27 +12:00
$request = new Request ( $swooleRequest );
$response = new Response ( $swooleResponse );
$app = new App ( 'UTC' );
try {
$app -> run ( $request , $response );
2022-02-15 05:37:56 +13:00
} catch ( \Throwable $th ) {
2022-02-19 04:58:04 +13:00
logError ( $th , " serverError " );
2022-02-15 05:37:56 +13:00
$swooleResponse -> setStatusCode ( 500 );
$output = [
2022-05-24 02:54:50 +12:00
'message' => 'Error: ' . $th -> getMessage (),
2022-02-15 05:37:56 +13:00
'code' => 500 ,
'file' => $th -> getFile (),
'line' => $th -> getLine (),
'trace' => $th -> getTrace ()
];
$swooleResponse -> end ( \json_encode ( $output ));
2021-08-24 21:32:27 +12:00
}
});
2022-05-05 21:01:08 +12:00
$http -> start ();