From 16dd539401ff8f9d6073f0e2539ab8af48d15d0b Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Thu, 9 Jun 2022 10:39:04 +0100 Subject: [PATCH] Create Executor.php --- src/Executor/Executor.php | 347 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/Executor/Executor.php diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php new file mode 100644 index 000000000..82b09ffe6 --- /dev/null +++ b/src/Executor/Executor.php @@ -0,0 +1,347 @@ + '', + ]; + + public function __construct(string $endpoint) + { + if (!filter_var($endpoint, FILTER_VALIDATE_URL)) { + throw new Exception('Unsupported endpoint'); + } + $this->endpoint = $endpoint; + } + + /** + * Create runtime + * + * Launches a runtime container for a deployment ready for execution + * + * @param string $deploymentId + * @param string $projectId + * @param string $source + * @param string $runtime + * @param string $baseImage + * @param bool $remove + * @param string $entrypoint + * @param string $workdir + * @param string $destinaction + * @param string $network + * @param array $vars + * @param array $commands + */ + public function createRuntime( + string $deploymentId, + string $projectId, + string $source, + string $runtime, + string $baseImage, + bool $remove = false, + string $entrypoint = '', + string $workdir = '', + string $destination = '', + array $vars = [], + array $commands = [] + ) { + $route = "/runtimes"; + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + ]; + $params = [ + 'runtimeId' => "$projectId-$deploymentId", + 'source' => $source, + 'destination' => $destination, + 'runtime' => $runtime, + 'baseImage' => $baseImage, + 'entrypoint' => $entrypoint, + 'workdir' => $workdir, + 'vars' => $vars, + 'remove' => $remove, + 'commands' => $commands + ]; + + $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); + + $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, $timeout); + + $status = $response['headers']['status-code']; + if ($status >= 400) { + throw new \Exception($response['body']['message'], $status); + } + + return $response['body']; + } + + /** + * Delete Runtime + * + * Deletes a runtime and cleans up any containers remaining. + * + * @param string $projectId + * @param string $deploymentId + */ + public function deleteRuntime(string $projectId, string $deploymentId) + { + $runtimeId = "$projectId-$deploymentId"; + $route = "/runtimes/$runtimeId"; + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + ]; + + $params = []; + + $response = $this->call(self::METHOD_DELETE, $route, $headers, $params, true, 30); + + $status = $response['headers']['status-code']; + if ($status >= 400) { + throw new \Exception($response['body']['message'], $status); + } + + return $response['body']; + } + + /** + * Create an execution + * + * @param string $projectId + * @param string $deploymentId + * @param string $path + * @param array $vars + * @param string $entrypoint + * @param string $data + * @param string runtime + * @param string $baseImage + * @param int $timeout + * + * @return array + */ + public function createExecution( + string $projectId, + string $deploymentId, + string $path, + array $vars, + string $entrypoint, + string $data, + string $runtime, + string $baseImage, + $timeout + ) { + $route = "/execution"; + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-executor-key' => App::getEnv('_APP_EXECUTOR_SECRET', '') + ]; + $params = [ + 'runtimeId' => "$projectId-$deploymentId", + 'vars' => $vars, + 'data' => $data, + 'timeout' => $timeout, + ]; + + /* Add 2 seconds as a buffer to the actual timeout value since there can be a slight variance*/ + $requestTimeout = $timeout + 2; + + $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, $requestTimeout); + $status = $response['headers']['status-code']; + + for ($attempts = 0; $attempts < 10; $attempts++) { + try { + switch (true) { + case $status < 400: + return $response['body']; + case $status === 404: + $response = $this->createRuntime( + deploymentId: $deploymentId, + projectId: $projectId, + source: $path, + runtime: $runtime, + baseImage: $baseImage, + vars: $vars, + entrypoint: $entrypoint, + commands: [] + ); + $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, $requestTimeout); + $status = $response['headers']['status-code']; + break; + case $status === 406: + $response = $this->call(self::METHOD_POST, $route, $headers, $params, true, $requestTimeout); + $status = $response['headers']['status-code']; + break; + default: + throw new \Exception($response['body']['message'], $status); + } + } catch (\Exception $e) { + throw new \Exception($e->getMessage(), $e->getCode()); + } + sleep(2); + } + + throw new Exception($response['body']['message'], 503); + } + + /** + * 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_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; + + 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; + } +}