feat: add executor class
This commit is contained in:
parent
0b36c7e21c
commit
6610bf9873
|
@ -759,6 +759,13 @@ App::post('/v1/functions/:functionId/executions')
|
|||
throw new Exception('Function not found', 404);
|
||||
}
|
||||
|
||||
$runtimes = Config::getParam('runtimes', []);
|
||||
$key = $function->getAttribute('runtime', '');
|
||||
$runtime = isset($runtimes[$key]) ? $runtimes[$key] : null;
|
||||
if (\is_null($runtime)) {
|
||||
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported', 400);
|
||||
}
|
||||
|
||||
$deployment = Authorization::skip(fn() => $dbForProject->getDocument('deployments', $function->getAttribute('deployment')));
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
|
||||
|
@ -834,7 +841,7 @@ App::post('/v1/functions/:functionId/executions')
|
|||
'data' => $data,
|
||||
'runtime' => $function->getAttribute('runtime', ''),
|
||||
'timeout' => $function->getAttribute('timeout', 0),
|
||||
'baseImage' => '',
|
||||
'baseImage' => $runtime['image'],
|
||||
'webhooks' => $project->getAttribute('webhooks', []),
|
||||
'userId' => $user->getId(),
|
||||
'functionId' => $function->getId(),
|
||||
|
@ -849,21 +856,19 @@ App::post('/v1/functions/:functionId/executions')
|
|||
}
|
||||
|
||||
/** Send variables */
|
||||
// $vars = \array_merge($function->getAttribute('vars', []), [
|
||||
// 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''),
|
||||
// 'APPWRITE_FUNCTION_ID' => $function->getId(),
|
||||
// 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
|
||||
// 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
|
||||
// 'APPWRITE_FUNCTION_TRIGGER' => 'http',
|
||||
// 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'],
|
||||
// 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'],
|
||||
// 'APPWRITE_FUNCTION_EVENT' => $event,
|
||||
// 'APPWRITE_FUNCTION_EVENT_DATA' => $eventData,
|
||||
// 'APPWRITE_FUNCTION_DATA' => $data,
|
||||
// 'APPWRITE_FUNCTION_USER_ID' => $userId,
|
||||
// 'APPWRITE_FUNCTION_JWT' => $jwt,
|
||||
// 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId
|
||||
// ]);
|
||||
$vars = \array_merge($function->getAttribute('vars', []), [
|
||||
'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''),
|
||||
'APPWRITE_FUNCTION_ID' => $function->getId(),
|
||||
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
|
||||
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
|
||||
'APPWRITE_FUNCTION_TRIGGER' => 'http',
|
||||
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'],
|
||||
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'],
|
||||
'APPWRITE_FUNCTION_DATA' => $data,
|
||||
'APPWRITE_FUNCTION_USER_ID' => $user->getId(),
|
||||
'APPWRITE_FUNCTION_JWT' => $jwt,
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId()
|
||||
]);
|
||||
|
||||
// Directly execute function.
|
||||
$ch = \curl_init();
|
||||
|
@ -873,11 +878,11 @@ App::post('/v1/functions/:functionId/executions')
|
|||
'deploymentId' => $deployment->getId(),
|
||||
'buildId' => $deployment->getAttribute('buildId', ''),
|
||||
'path' => $build->getAttribute('outputPath', ''),
|
||||
'vars' => $function->getAttribute('vars', []),
|
||||
'vars' => $vars,
|
||||
'data' => $data,
|
||||
'runtime' => $function->getAttribute('runtime', ''),
|
||||
'timeout' => $function->getAttribute('timeout', 0),
|
||||
'baseImage' => '',
|
||||
'baseImage' => $runtime['image'],
|
||||
'webhooks' => $project->getAttribute('webhooks', []),
|
||||
'userId' => $user->getId(),
|
||||
]));
|
||||
|
@ -896,12 +901,21 @@ App::post('/v1/functions/:functionId/executions')
|
|||
if (!empty($error)) {
|
||||
Console::error('Curl error: '.$error);
|
||||
}
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
$responseExecute = json_decode($responseExecute, true);
|
||||
$execution->setAttribute('status', $responseExecute['status']);
|
||||
$execution->setAttribute('statusCode', $responseExecute['statusCode']);
|
||||
$execution->setAttribute('stdout', $responseExecute['stdout']);
|
||||
$execution->setAttribute('stderr', $responseExecute['stderr']);
|
||||
$execution->setAttribute('time', $responseExecute['time']);
|
||||
Authorization::skip(fn() => $dbForProject->updateDocument('executions', $executionId, $execution));
|
||||
|
||||
$responseExecute['response'] = ($responseExecute['status'] !== 'completed') ? $responseExecute['stderr'] : $responseExecute['stdout'];
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic(new Document(json_decode($responseExecute, true)), Response::MODEL_SYNC_EXECUTION);
|
||||
->dynamic(new Document($responseExecute), Response::MODEL_SYNC_EXECUTION);
|
||||
});
|
||||
|
||||
App::get('/v1/functions/:functionId/executions')
|
||||
|
|
|
@ -276,23 +276,6 @@ function execute(string $projectId, string $functionId, string $deploymentId, ar
|
|||
|
||||
$key = $activeFunctions->get('appwrite-function-' . $deploymentId, 'key');
|
||||
|
||||
// Process environment variables
|
||||
// $vars = \array_merge($function->getAttribute('vars', []), [
|
||||
// 'ENTRYPOINT_NAME' => $deployment->getAttribute('entrypoint', ''),
|
||||
// 'APPWRITE_FUNCTION_ID' => $function->getId(),
|
||||
// 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
|
||||
// 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
|
||||
// 'APPWRITE_FUNCTION_TRIGGER' => $trigger,
|
||||
// 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'],
|
||||
// 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'],
|
||||
// 'APPWRITE_FUNCTION_EVENT' => $event,
|
||||
// 'APPWRITE_FUNCTION_EVENT_DATA' => $eventData,
|
||||
// 'APPWRITE_FUNCTION_DATA' => $data,
|
||||
// 'APPWRITE_FUNCTION_USER_ID' => $userId,
|
||||
// 'APPWRITE_FUNCTION_JWT' => $jwt,
|
||||
// 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId
|
||||
// ]);
|
||||
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
|
||||
|
@ -365,6 +348,8 @@ function execute(string $projectId, string $functionId, string $deploymentId, ar
|
|||
throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 500);
|
||||
}
|
||||
|
||||
var_dump($executorResponse);
|
||||
|
||||
$executionData = [];
|
||||
|
||||
if (!empty($executorResponse)) {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use Appwrite\Resque\Worker;
|
||||
use Cron\CronExpression;
|
||||
use Executor\Executor;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
|
@ -20,28 +21,19 @@ Console::success(APP_NAME.' build worker v1 has started');
|
|||
// TODO: Executor should return appropriate response codes.
|
||||
class BuildsV1 extends Worker
|
||||
{
|
||||
const METHOD_GET = 'GET';
|
||||
const METHOD_POST = 'POST';
|
||||
const METHOD_PUT = 'PUT';
|
||||
const METHOD_PATCH = 'PATCH';
|
||||
const METHOD_DELETE = 'DELETE';
|
||||
const METHOD_HEAD = 'HEAD';
|
||||
const METHOD_OPTIONS = 'OPTIONS';
|
||||
const METHOD_CONNECT = 'CONNECT';
|
||||
const METHOD_TRACE = 'TRACE';
|
||||
|
||||
protected $selfSigned = false;
|
||||
private $endpoint = 'http://appwrite-executor/v1';
|
||||
protected $headers = [
|
||||
'content-type' => '',
|
||||
];
|
||||
/**
|
||||
* @var Executor
|
||||
*/
|
||||
private $executor = null;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return "builds";
|
||||
}
|
||||
|
||||
public function init(): void {}
|
||||
public function init(): void {
|
||||
$this->executor = new Executor();
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
|
@ -70,32 +62,6 @@ class BuildsV1 extends Worker
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
protected function createBuild(string $projectId, string $functionId, string $deploymentId, string $buildId, string $path, array $vars, string $runtime, string $baseImage)
|
||||
{
|
||||
$route = "/functions/$functionId/deployments/$deploymentId/builds/$buildId";
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '')
|
||||
];
|
||||
$params = [
|
||||
'path' => $path,
|
||||
'vars' => $vars,
|
||||
'runtime' => $runtime,
|
||||
'baseImage' => $baseImage
|
||||
];
|
||||
|
||||
$response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30);
|
||||
|
||||
$status = $response['headers']['status-code'];
|
||||
if ($status >= 400) {
|
||||
throw new \Exception('Error creating build: ', $status);
|
||||
}
|
||||
|
||||
return $response['body'];
|
||||
}
|
||||
|
||||
protected function buildDeployment(string $projectId, string $functionId, string $deploymentId)
|
||||
{
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
|
@ -150,8 +116,17 @@ class BuildsV1 extends Worker
|
|||
$path = $deployment->getAttribute('path');
|
||||
$vars = $function->getAttribute('vars', []);
|
||||
$baseImage = $runtime['image'];
|
||||
$response = $this->createBuild($projectId, $functionId, $deploymentId, $buildId, $path, $vars, $key, $baseImage);
|
||||
|
||||
$response = $this->executor->createRuntime(
|
||||
projectId: $projectId,
|
||||
functionId: $functionId,
|
||||
deploymentId: $deploymentId,
|
||||
buildId: $buildId,
|
||||
path: $path,
|
||||
vars: $vars,
|
||||
runtime: $key,
|
||||
baseImage: $baseImage
|
||||
);
|
||||
|
||||
/** Update the build document */
|
||||
$build->setAttribute('endTime', $response['endTime']);
|
||||
$build->setAttribute('duration', $response['duration']);
|
||||
|
@ -181,151 +156,4 @@ class BuildsV1 extends Worker
|
|||
}
|
||||
|
||||
public function shutdown(): void {}
|
||||
|
||||
/**
|
||||
* Call
|
||||
*
|
||||
* Make an API call
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $path
|
||||
* @param array $params
|
||||
* @param array $headers
|
||||
* @param bool $decode
|
||||
* @return array|string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15)
|
||||
{
|
||||
$headers = array_merge($this->headers, $headers);
|
||||
$ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : ''));
|
||||
$responseHeaders = [];
|
||||
$responseStatus = -1;
|
||||
$responseType = '';
|
||||
$responseBody = '';
|
||||
|
||||
switch ($headers['content-type']) {
|
||||
case 'application/json':
|
||||
$query = json_encode($params);
|
||||
break;
|
||||
|
||||
case 'multipart/form-data':
|
||||
$query = $this->flatten($params);
|
||||
break;
|
||||
|
||||
default:
|
||||
$query = http_build_query($params);
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($headers as $i => $header) {
|
||||
$headers[] = $i . ':' . $header;
|
||||
unset($headers[$i]);
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36');
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
|
||||
$len = strlen($header);
|
||||
$header = explode(':', $header, 2);
|
||||
|
||||
if (count($header) < 2) { // ignore invalid headers
|
||||
return $len;
|
||||
}
|
||||
|
||||
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
|
||||
|
||||
return $len;
|
||||
});
|
||||
|
||||
if ($method != self::METHOD_GET) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
|
||||
}
|
||||
|
||||
// Allow self signed certificates
|
||||
if ($this->selfSigned) {
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
}
|
||||
|
||||
$responseBody = curl_exec($ch);
|
||||
$responseType = $responseHeaders['content-type'] ?? '';
|
||||
$responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if($decode) {
|
||||
switch (substr($responseType, 0, strpos($responseType, ';'))) {
|
||||
case 'application/json':
|
||||
$json = json_decode($responseBody, true);
|
||||
|
||||
if ($json === null) {
|
||||
throw new Exception('Failed to parse response: '.$responseBody);
|
||||
}
|
||||
|
||||
$responseBody = $json;
|
||||
$json = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((curl_errno($ch)/* || 200 != $responseStatus*/)) {
|
||||
throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
$responseHeaders['status-code'] = $responseStatus;
|
||||
|
||||
if ($responseStatus === 500) {
|
||||
echo 'Server error('.$method.': '.$path.'. Params: '.json_encode($params).'): '.json_encode($responseBody)."\n";
|
||||
}
|
||||
|
||||
return [
|
||||
'headers' => $responseHeaders,
|
||||
'body' => $responseBody
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Cookie String
|
||||
*
|
||||
* @param string $cookie
|
||||
* @return array
|
||||
*/
|
||||
public function parseCookie(string $cookie): array
|
||||
{
|
||||
$cookies = [];
|
||||
|
||||
parse_str(strtr($cookie, array('&' => '%26', '+' => '%2B', ';' => '&')), $cookies);
|
||||
|
||||
return $cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten params array to PHP multiple format
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $prefix
|
||||
* @return array
|
||||
*/
|
||||
protected function flatten(array $data, string $prefix = ''): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$finalKey = $prefix ? "{$prefix}[{$key}]" : $key;
|
||||
|
||||
if (is_array($value)) {
|
||||
$output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed
|
||||
} else {
|
||||
$output[$finalKey] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ use Utopia\Database\Document;
|
|||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Executor\Executor;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\Abuse\Adapters\TimeLimit;
|
||||
|
@ -354,22 +355,10 @@ class DeletesV1 extends Worker
|
|||
/**
|
||||
* Request executor to delete all deployment containers
|
||||
*/
|
||||
$executor = new Executor();
|
||||
foreach ($deploymentIds as $deploymentId) {
|
||||
try {
|
||||
$route = "/deployments/$deploymentId";
|
||||
$headers = [
|
||||
'content-Type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '')
|
||||
];
|
||||
$params = [
|
||||
'buildIds' => $buildIds[$deploymentId] ?? [],
|
||||
];
|
||||
$response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30);
|
||||
$status = $response['headers']['status-code'];
|
||||
if ($status >= 400) {
|
||||
throw new \Exception('Error deleting deplyoment: ' . $document->getId() , $status);
|
||||
}
|
||||
$executor->deleteRuntime($deploymentId, $projectId);
|
||||
} catch (Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
}
|
||||
|
@ -415,21 +404,8 @@ class DeletesV1 extends Worker
|
|||
* Request executor to delete the deployment container
|
||||
*/
|
||||
try {
|
||||
$route = "/deployments/{$document->getId()}";
|
||||
$headers = [
|
||||
'content-Type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '')
|
||||
];
|
||||
$params = [
|
||||
'buildIds' => $buildIds ?? []
|
||||
];
|
||||
|
||||
$response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30);
|
||||
$status = $response['headers']['status-code'];
|
||||
if ($status >= 400) {
|
||||
throw new \Exception('Error deleting deplyoment: ' . $document->getId() , $status);
|
||||
}
|
||||
$executor = new Executor();
|
||||
$executor->deleteRuntime($document->getId(), $projectId);
|
||||
} catch (Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
}
|
||||
|
@ -554,167 +530,4 @@ class DeletesV1 extends Worker
|
|||
Console::info("No certificate files found for {$domain}");
|
||||
}
|
||||
}
|
||||
|
||||
const METHOD_GET = 'GET';
|
||||
const METHOD_POST = 'POST';
|
||||
const METHOD_PUT = 'PUT';
|
||||
const METHOD_PATCH = 'PATCH';
|
||||
const METHOD_DELETE = 'DELETE';
|
||||
const METHOD_HEAD = 'HEAD';
|
||||
const METHOD_OPTIONS = 'OPTIONS';
|
||||
const METHOD_CONNECT = 'CONNECT';
|
||||
const METHOD_TRACE = 'TRACE';
|
||||
|
||||
protected $selfSigned = false;
|
||||
private $endpoint = 'http://appwrite-executor/v1';
|
||||
protected $headers = [
|
||||
'content-type' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Call
|
||||
*
|
||||
* Make an API call
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $path
|
||||
* @param array $params
|
||||
* @param array $headers
|
||||
* @param bool $decode
|
||||
* @return array|string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15)
|
||||
{
|
||||
$headers = array_merge($this->headers, $headers);
|
||||
$ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : ''));
|
||||
$responseHeaders = [];
|
||||
$responseStatus = -1;
|
||||
$responseType = '';
|
||||
$responseBody = '';
|
||||
|
||||
switch ($headers['content-type']) {
|
||||
case 'application/json':
|
||||
$query = json_encode($params);
|
||||
break;
|
||||
|
||||
case 'multipart/form-data':
|
||||
$query = $this->flatten($params);
|
||||
break;
|
||||
|
||||
default:
|
||||
$query = http_build_query($params);
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($headers as $i => $header) {
|
||||
$headers[] = $i . ':' . $header;
|
||||
unset($headers[$i]);
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36');
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
|
||||
$len = strlen($header);
|
||||
$header = explode(':', $header, 2);
|
||||
|
||||
if (count($header) < 2) { // ignore invalid headers
|
||||
return $len;
|
||||
}
|
||||
|
||||
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
|
||||
|
||||
return $len;
|
||||
});
|
||||
|
||||
if ($method != self::METHOD_GET) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
|
||||
}
|
||||
|
||||
// Allow self signed certificates
|
||||
if ($this->selfSigned) {
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
}
|
||||
|
||||
$responseBody = curl_exec($ch);
|
||||
$responseType = $responseHeaders['content-type'] ?? '';
|
||||
$responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if($decode) {
|
||||
switch (substr($responseType, 0, strpos($responseType, ';'))) {
|
||||
case 'application/json':
|
||||
$json = json_decode($responseBody, true);
|
||||
|
||||
if ($json === null) {
|
||||
throw new Exception('Failed to parse response: '.$responseBody);
|
||||
}
|
||||
|
||||
$responseBody = $json;
|
||||
$json = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((curl_errno($ch)/* || 200 != $responseStatus*/)) {
|
||||
throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
$responseHeaders['status-code'] = $responseStatus;
|
||||
|
||||
if ($responseStatus === 500) {
|
||||
echo 'Server error('.$method.': '.$path.'. Params: '.json_encode($params).'): '.json_encode($responseBody)."\n";
|
||||
}
|
||||
|
||||
return [
|
||||
'headers' => $responseHeaders,
|
||||
'body' => $responseBody
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Cookie String
|
||||
*
|
||||
* @param string $cookie
|
||||
* @return array
|
||||
*/
|
||||
public function parseCookie(string $cookie): array
|
||||
{
|
||||
$cookies = [];
|
||||
|
||||
parse_str(strtr($cookie, array('&' => '%26', '+' => '%2B', ';' => '&')), $cookies);
|
||||
|
||||
return $cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten params array to PHP multiple format
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $prefix
|
||||
* @return array
|
||||
*/
|
||||
protected function flatten(array $data, string $prefix = ''): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$finalKey = $prefix ? "{$prefix}[{$key}]" : $key;
|
||||
|
||||
if (is_array($value)) {
|
||||
$output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed
|
||||
} else {
|
||||
$output[$finalKey] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Appwrite\\": "src/Appwrite"
|
||||
"Appwrite\\": "src/Appwrite",
|
||||
"Executor\\": "src/Executor"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
|
|
278
src/Executor/Executor.php
Normal file
278
src/Executor/Executor.php
Normal file
|
@ -0,0 +1,278 @@
|
|||
<?php
|
||||
|
||||
namespace Executor;
|
||||
|
||||
use Exception;
|
||||
use Utopia\App;
|
||||
|
||||
class Executor
|
||||
{
|
||||
const METHOD_GET = 'GET';
|
||||
const METHOD_POST = 'POST';
|
||||
const METHOD_PUT = 'PUT';
|
||||
const METHOD_PATCH = 'PATCH';
|
||||
const METHOD_DELETE = 'DELETE';
|
||||
const METHOD_HEAD = 'HEAD';
|
||||
const METHOD_OPTIONS = 'OPTIONS';
|
||||
const METHOD_CONNECT = 'CONNECT';
|
||||
const METHOD_TRACE = 'TRACE';
|
||||
|
||||
private $endpoint;
|
||||
|
||||
private $selfSigned = false;
|
||||
|
||||
protected $headers = [
|
||||
'content-type' => '',
|
||||
];
|
||||
|
||||
public function __construct(string $endpoint = 'http://appwrite-executor/v1')
|
||||
{
|
||||
$this->endpoint = $endpoint;
|
||||
}
|
||||
|
||||
public function createRuntime(
|
||||
string $functionId,
|
||||
string $deploymentId,
|
||||
string $buildId,
|
||||
string $projectId,
|
||||
string $path,
|
||||
array $vars,
|
||||
string $runtime,
|
||||
string $baseImage)
|
||||
{
|
||||
$route = "/functions/$functionId/deployments/$deploymentId/builds/$buildId";
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '')
|
||||
];
|
||||
$params = [
|
||||
'path' => $path,
|
||||
'vars' => $vars,
|
||||
'runtime' => $runtime,
|
||||
'baseImage' => $baseImage
|
||||
];
|
||||
|
||||
$response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30);
|
||||
|
||||
$status = $response['headers']['status-code'];
|
||||
if ($status >= 400) {
|
||||
throw new \Exception('Error creating build: ', $status);
|
||||
}
|
||||
|
||||
return $response['body'];
|
||||
}
|
||||
|
||||
public function deleteRuntime(string $deploymentId, string $projectId)
|
||||
{
|
||||
$route = "/deployments/$deploymentId";
|
||||
$headers = [
|
||||
'content-Type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '')
|
||||
];
|
||||
$params = [
|
||||
'buildIds' => $buildIds[$deploymentId] ?? [],
|
||||
];
|
||||
|
||||
$response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30);
|
||||
|
||||
$status = $response['headers']['status-code'];
|
||||
if ($status >= 400) {
|
||||
throw new \Exception('Error deleting deplyoment: ' . $deploymentId , $status);
|
||||
}
|
||||
|
||||
return $response['body'];
|
||||
}
|
||||
|
||||
public function createExecution(
|
||||
string $projectId,
|
||||
string $functionId,
|
||||
string $deploymentId,
|
||||
string $buildId,
|
||||
string $path,
|
||||
array $vars,
|
||||
string $data,
|
||||
string $runtime,
|
||||
string $baseImage,
|
||||
$timeout,
|
||||
$webhooks,
|
||||
string $userId
|
||||
)
|
||||
{
|
||||
$route = "/functions/$functionId/executions";
|
||||
$headers = [
|
||||
'content-Type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '')
|
||||
];
|
||||
$params = [
|
||||
'deploymentId' => $deploymentId,
|
||||
'buildId' => $buildId,
|
||||
'path' => $path,
|
||||
'vars' => $vars,
|
||||
'data' => $data,
|
||||
'runtime' => $runtime,
|
||||
'timeout' => $timeout,
|
||||
'baseImage' => $baseImage,
|
||||
'webhooks' => $webhooks,
|
||||
'userId' => $userId,
|
||||
];
|
||||
|
||||
$response = $this->call(self::METHOD_POST, $route, $headers, $params, true, 30);
|
||||
|
||||
$status = $response['headers']['status-code'];
|
||||
if ($status >= 400) {
|
||||
throw new \Exception('Error creating execution: ', $status);
|
||||
}
|
||||
|
||||
return $response['body'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Call
|
||||
*
|
||||
* Make an API call
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $path
|
||||
* @param array $params
|
||||
* @param array $headers
|
||||
* @param bool $decode
|
||||
* @return array|string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15)
|
||||
{
|
||||
$headers = array_merge($this->headers, $headers);
|
||||
$ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : ''));
|
||||
$responseHeaders = [];
|
||||
$responseStatus = -1;
|
||||
$responseType = '';
|
||||
$responseBody = '';
|
||||
|
||||
switch ($headers['content-type']) {
|
||||
case 'application/json':
|
||||
$query = json_encode($params);
|
||||
break;
|
||||
|
||||
case 'multipart/form-data':
|
||||
$query = $this->flatten($params);
|
||||
break;
|
||||
|
||||
default:
|
||||
$query = http_build_query($params);
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($headers as $i => $header) {
|
||||
$headers[] = $i . ':' . $header;
|
||||
unset($headers[$i]);
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36');
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
|
||||
$len = strlen($header);
|
||||
$header = explode(':', $header, 2);
|
||||
|
||||
if (count($header) < 2) { // ignore invalid headers
|
||||
return $len;
|
||||
}
|
||||
|
||||
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
|
||||
|
||||
return $len;
|
||||
});
|
||||
|
||||
if ($method != self::METHOD_GET) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
|
||||
}
|
||||
|
||||
// Allow self signed certificates
|
||||
if ($this->selfSigned) {
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
}
|
||||
|
||||
$responseBody = curl_exec($ch);
|
||||
$responseType = $responseHeaders['content-type'] ?? '';
|
||||
$responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if($decode) {
|
||||
switch (substr($responseType, 0, strpos($responseType, ';'))) {
|
||||
case 'application/json':
|
||||
$json = json_decode($responseBody, true);
|
||||
|
||||
if ($json === null) {
|
||||
throw new Exception('Failed to parse response: '.$responseBody);
|
||||
}
|
||||
|
||||
$responseBody = $json;
|
||||
$json = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((curl_errno($ch)/* || 200 != $responseStatus*/)) {
|
||||
throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
$responseHeaders['status-code'] = $responseStatus;
|
||||
|
||||
if ($responseStatus === 500) {
|
||||
echo 'Server error('.$method.': '.$path.'. Params: '.json_encode($params).'): '.json_encode($responseBody)."\n";
|
||||
}
|
||||
|
||||
return [
|
||||
'headers' => $responseHeaders,
|
||||
'body' => $responseBody
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Cookie String
|
||||
*
|
||||
* @param string $cookie
|
||||
* @return array
|
||||
*/
|
||||
public function parseCookie(string $cookie): array
|
||||
{
|
||||
$cookies = [];
|
||||
|
||||
parse_str(strtr($cookie, array('&' => '%26', '+' => '%2B', ';' => '&')), $cookies);
|
||||
|
||||
return $cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten params array to PHP multiple format
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $prefix
|
||||
* @return array
|
||||
*/
|
||||
protected function flatten(array $data, string $prefix = ''): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$finalKey = $prefix ? "{$prefix}[{$key}]" : $key;
|
||||
|
||||
if (is_array($value)) {
|
||||
$output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed
|
||||
} else {
|
||||
$output[$finalKey] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue