From 803e4ccbaffa8d2269f43aab910f3c185a85953d Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Wed, 2 Feb 2022 23:38:47 +0400 Subject: [PATCH] feat: remove database dependencies from the delete deployment endpoint --- app/executor.php | 55 +++++------ app/workers/deletes.php | 206 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 207 insertions(+), 54 deletions(-) diff --git a/app/executor.php b/app/executor.php index 22e379335c..0c4b5c8f04 100644 --- a/app/executor.php +++ b/app/executor.php @@ -926,44 +926,33 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/runtime') App::delete('/v1/deployments/:deploymentId') ->desc('Delete a deployment') - ->param('deploymentId', '', new UID(), 'Deployment unique ID.') - ->inject('projectId') + ->param('deploymentId', '', new UID(), 'Deployment unique ID.', false) + ->param('buildIds', [], new ArrayList(new Text(0), 100), 'List of build IDs to delete.', false) ->inject('response') - ->action(function (string $deploymentId, string $projectId, Response $response) use ($orchestrationPool) { + ->action(function (string $deploymentId, array $buildIds, Response $response) use ($orchestrationPool) { + Console::info('Deleting deployment: ' . $deploymentId); - global $register; - go(function () use ($projectId, $orchestrationPool, $register, $deploymentId) { - try { - $orchestration = $orchestrationPool->get(); - // Remove the container of the deployment - $orchestration->remove('appwrite-function-' . $deploymentId , true); - Console::success('Removed container for deployment: ' . $deploymentId); + $orchestration = $orchestrationPool->get(); - $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); + // Remove the container of the deployment + $status = $orchestration->remove('appwrite-function-' . $deploymentId , true); + if ($status) { + Console::success('Removed container for deployment: ' . $deploymentId); + } else { + Console::error('Failed to remove container for deployment: ' . $deploymentId); + } - $builds = $dbForProject->find('builds', [ - new Query('deploymentId', Query::TYPE_EQUAL, [$deploymentId]), - new Query('status', Query::TYPE_EQUAL, ['building']) - ], 999); - - // Remove all the build containers - foreach ($builds as $build) { - $orchestration->remove('build-stage-' . $build['$id'], true); - Console::success("Removed build container: $build for deployment: " . $deploymentId); - } - } catch (\Throwable $th) { - Console::error($th->getMessage()); - } finally { - $orchestrationPool->put($orchestration); - $register->get('dbPool')->put($db); - $register->get('redisPool')->put($redis); + // Remove all the build containers + foreach ($buildIds as $buildId) { + $status = $orchestration->remove('build-stage-' . $buildId, true); + if ($status) { + Console::success("Removed build container: $buildId for deployment: " . $deploymentId); + } else { + Console::error("Failed to remove build container: $buildId for deployment: " . $deploymentId); } - }); + } + + $orchestrationPool->put($orchestration); $response ->setStatusCode(Response::STATUS_CODE_OK) diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 7e46325cc3..af244906e9 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -389,33 +389,34 @@ class DeletesV1 extends Worker { $dbForProject = $this->getProjectDB($projectId); + $builds = $dbForProject->find('builds', [ + new Query('deploymentId', Query::TYPE_EQUAL, [$document->getId()]) + ], 999); + + $buildIds = []; + foreach ($builds as $build) { + $buildIds[] = $build['$id']; + } /** * Request executor to delete the deployment containers */ try { - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - // TODO: Implement coroutines. - \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor/v1/deployments/{$document->getId()}"); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'x-appwrite-project: '. $projectId, - 'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '') - ]); + $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 ?? [] + ]; - $executorResponse = \curl_exec($ch); - $error = \curl_error($ch); - if (!empty($error)) { - throw new Exception($error, 500); - } + $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); + } - $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($statusCode >= 400) { - throw new Exception('Executor error: ' . $executorResponse, $statusCode); - } - - \curl_close($ch); } catch (Throwable $th) { Console::error($th->getMessage()); } @@ -565,4 +566,167 @@ 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; + } }