Skip to content

Commit

Permalink
WIP: Test deploy draft (1)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
  • Loading branch information
andrey18106 committed Apr 25, 2024
1 parent 0844622 commit b4156e6
Show file tree
Hide file tree
Showing 12 changed files with 1,644 additions and 608 deletions.
6 changes: 5 additions & 1 deletion appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
['name' => 'ExAppsPage#enableApp', 'url' => '/apps/enable/{appId}', 'verb' => 'GET' , 'root' => ''],
['name' => 'ExAppsPage#enableApp', 'url' => '/apps/enable/{appId}', 'verb' => 'POST' , 'root' => ''],
['name' => 'ExAppsPage#getAppStatus', 'url' => '/apps/status/{appId}', 'verb' => 'GET' , 'root' => ''],
['name' => 'ExAppsPage#getAppLogs', 'url' => '/apps/logs/{appId}', 'verb' => 'GET' , 'root' => ''],
['name' => 'ExAppsPage#disableApp', 'url' => '/apps/disable/{appId}', 'verb' => 'GET' , 'root' => ''],
['name' => 'ExAppsPage#updateApp', 'url' => '/apps/update/{appId}', 'verb' => 'GET' , 'root' => ''],
['name' => 'ExAppsPage#uninstallApp', 'url' => '/apps/uninstall/{appId}', 'verb' => 'GET' , 'root' => ''],
Expand All @@ -46,7 +47,10 @@
['name' => 'DaemonConfig#unregisterDaemonConfig', 'url' => '/daemons/{name}', 'verb' => 'DELETE'],
['name' => 'DaemonConfig#verifyDaemonConnection', 'url' => '/daemons/{name}/check', 'verb' => 'POST'],
['name' => 'DaemonConfig#checkDaemonConnection', 'url' => '/daemons/verify_connection', 'verb' => 'POST'],
['name' => 'DaemonConfig#updateDaemonConfig', 'url' => '/daemons', 'verb' => 'PUT'],

// Test Deploy actions
['name' => 'DaemonConfig#startTestDeploy', 'url' => '/daemons/{name}/test_deploy', 'verb' => 'POST'],
['name' => 'DaemonConfig#stopTestDeploy', 'url' => '/daemons/{name}/test_deploy', 'verb' => 'DELETE'],
],
'ocs' => [
// Logging
Expand Down
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@

class Application extends App implements IBootstrap {
public const APP_ID = 'app_api';
public const TEST_DEPLOY_APPID = 'test-deploy';
public const TEST_DEPLOY_INFO_XML = 'https://raw.githubusercontent.com/cloud-py-api/test-deploy/main/appinfo/info.xml';

public function __construct(array $urlParams = []) {
parent::__construct(self::APP_ID, $urlParams);
Expand Down
27 changes: 20 additions & 7 deletions lib/Command/ExApp/Register.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,23 @@ protected function configure(): void {
$this->addOption('json-info', null, InputOption::VALUE_REQUIRED, 'ExApp info.xml in JSON format');
$this->addOption('wait-finish', null, InputOption::VALUE_NONE, 'Wait until finish');
$this->addOption('silent', null, InputOption::VALUE_NONE, 'Do not print to console');
$this->addOption('test-deploy-mode', null, InputOption::VALUE_NONE, 'Test deploy mode with additional status checks and slightly different logic');
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$outputConsole = !$input->getOption('silent');
$isTestDeployMode = $input->getOption('test-deploy-mode');
$appId = $input->getArgument('appid');

if ($this->exAppService->getExApp($appId) !== null) {
$this->logger->error(sprintf('ExApp %s is already registered.', $appId));
if ($outputConsole) {
$output->writeln(sprintf('ExApp %s is already registered.', $appId));
if (!$isTestDeployMode) {
$this->logger->error(sprintf('ExApp %s is already registered.', $appId));
if ($outputConsole) {
$output->writeln(sprintf('ExApp %s is already registered.', $appId));
}
return 3;
}
return 3;
$this->exAppService->unregisterExApp($appId);
}

$appInfo = $this->exAppService->getAppInfo(
Expand Down Expand Up @@ -147,7 +152,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($outputConsole) {
$output->writeln(sprintf('Error while registering API scopes for %s.', $appId));
}
$this->exAppService->unregisterExApp($appId);
$this->_unregisterExApp($appId, $isTestDeployMode);
return 1;
}
$this->logger->info(
Expand All @@ -167,7 +172,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($outputConsole) {
$output->writeln(sprintf('Failed to install translations for %s. Reason: %s', $appId, $result));
}
$this->exAppService->unregisterExApp($appId);
$this->_unregisterExApp($appId, $isTestDeployMode);
return 3;
}
}
Expand All @@ -181,7 +186,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($outputConsole) {
$output->writeln(sprintf('ExApp %s deployment failed. Error: %s', $appId, $deployResult));
}
$this->exAppService->unregisterExApp($appId);
$this->exAppService->setStatusError($exApp, $deployResult);
$this->_unregisterExApp($appId, $isTestDeployMode);
return 1;
}

Expand Down Expand Up @@ -241,4 +247,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
return 0;
}

private function _unregisterExApp(string $appId, bool $testDeployMode = false): void {
if ($testDeployMode) {
return;
}
$this->exAppService->unregisterExApp($appId);
}
}
51 changes: 48 additions & 3 deletions lib/Controller/DaemonConfigController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@

namespace OCA\AppAPI\Controller;

use OC\AppFramework\Http;
use OCA\AppAPI\AppInfo\Application;
use OCA\AppAPI\Db\DaemonConfig;

use OCA\AppAPI\DeployActions\DockerActions;
use OCA\AppAPI\Service\AppAPIService;
use OCA\AppAPI\Service\DaemonConfigService;
use OCA\AppAPI\Service\ExAppService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;

/**
Expand All @@ -28,16 +31,17 @@ public function __construct(
private readonly DaemonConfigService $daemonConfigService,
private readonly DockerActions $dockerActions,
private readonly AppAPIService $service,
private readonly ExAppService $exAppService,
private readonly IL10N $l10n,
) {
parent::__construct(Application::APP_ID, $request);
}

#[NoCSRFRequired]
public function getAllDaemonConfigs(): Response {
$daemonConfigs = $this->daemonConfigService->getRegisteredDaemonConfigs();
return new JSONResponse([
'daemons' => $daemonConfigs,
'default_daemon_config' => $this->config->getAppValue(Application::APP_ID, 'default_daemon_config', ''),
'daemons' => $this->daemonConfigService->getDaemonConfigsWithAppsCount(),
'default_daemon_config' => $this->config->getAppValue(Application::APP_ID, 'default_daemon_config'),
]);
}

Expand Down Expand Up @@ -116,4 +120,45 @@ public function checkDaemonConnection(array $daemonParams): Response {
'success' => $dockerDaemonAccessible,
]);
}

#[NoCSRFRequired]
public function startTestDeploy(string $name): Response {
$daemonConfig = $this->daemonConfigService->getDaemonConfigByName($name);
if (!$daemonConfig) {
return new JSONResponse(['error' => $this->l10n->t('Daemon config not found')], Http::STATUS_NOT_FOUND);

Check failure on line 128 in lib/Controller/DaemonConfigController.php

View workflow job for this annotation

GitHub Actions / php-psalm-analysis (8.1)

UndefinedClass

lib/Controller/DaemonConfigController.php:128:84: UndefinedClass: Class, interface or enum named OC\AppFramework\Http does not exist (see https://psalm.dev/019)
}

if (!$this->service->runOccCommand(
sprintf("app_api:app:register --force-scopes --silent %s %s --info-xml %s --test-deploy-mode",
Application::TEST_DEPLOY_APPID, $daemonConfig->getName(), Application::TEST_DEPLOY_INFO_XML)
)) {
return new JSONResponse(['error' => $this->l10n->t('Error starting install of ExApp')], Http::STATUS_INTERNAL_SERVER_ERROR);

Check failure on line 135 in lib/Controller/DaemonConfigController.php

View workflow job for this annotation

GitHub Actions / php-psalm-analysis (8.1)

UndefinedClass

lib/Controller/DaemonConfigController.php:135:92: UndefinedClass: Class, interface or enum named OC\AppFramework\Http does not exist (see https://psalm.dev/019)
}

$elapsedTime = 0;
while ($elapsedTime < 5000000 && !$this->exAppService->getExApp(Application::TEST_DEPLOY_APPID)) {
usleep(150000); // 0.15
$elapsedTime += 150000;
}

$exApp = $this->exAppService->getExApp(Application::TEST_DEPLOY_APPID);
$status = $exApp->getStatus();

return new JSONResponse([
'success' => $exApp !== null,

Check failure on line 148 in lib/Controller/DaemonConfigController.php

View workflow job for this annotation

GitHub Actions / php-psalm-analysis (8.1)

RedundantCondition

lib/Controller/DaemonConfigController.php:148:17: RedundantCondition: OCA\AppAPI\Db\ExApp can never contain null (see https://psalm.dev/122)
'status' => $status,
]);
}

#[NoCSRFRequired]
public function stopTestDeploy(string $name): Response {
$exApp = $this->exAppService->getExApp(Application::TEST_DEPLOY_APPID);
if ($exApp !== null) {
$this->service->runOccCommand(sprintf("app_api:app:unregister --silent --force %s", Application::TEST_DEPLOY_APPID));
}
$exApp = $this->exAppService->getExApp(Application::TEST_DEPLOY_APPID);
return new JSONResponse([
'success' => $exApp === null,
]);
}
}
25 changes: 25 additions & 0 deletions lib/Controller/ExAppsPageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
Expand Down Expand Up @@ -524,6 +525,7 @@ public function listCategories(): JSONResponse {
/**
* Get ExApp status, that includes initialization information
*/
#[NoCSRFRequired]
public function getAppStatus(string $appId): JSONResponse {
$exApp = $this->exAppService->getExApp($appId);
if (is_null($exApp)) {
Expand All @@ -532,6 +534,29 @@ public function getAppStatus(string $appId): JSONResponse {
return new JSONResponse($exApp->getStatus());
}

#[NoCSRFRequired]
public function getAppLogs(string $appId): DataDownloadResponse {
$exApp = $this->exAppService->getExApp($appId);
if (is_null($exApp)) {
return new DataDownloadResponse(json_encode(['error' => $this->l10n->t('ExApp not found, failed to get logs')]), $this->dockerActions->buildExAppContainerName($appId) . '_logs.txt', 'text/plain');
}
$daemonConfig = $this->daemonConfigService->getDaemonConfigByName($exApp->getDaemonConfigName());
$this->dockerActions->initGuzzleClient($daemonConfig);
try {
$logs = $this->dockerActions->getContainerLogs(
$this->dockerActions->buildDockerUrl($daemonConfig),
$this->dockerActions->buildExAppContainerName($appId)
);
return new DataDownloadResponse(
$logs,
$this->dockerActions->buildExAppContainerName($appId) . '_logs.txt', 'text/plain',
Http::STATUS_OK
);
} catch (Exception) {
return new DataDownloadResponse(json_encode(['error' => $this->l10n->t('Failed to get logs')]), $this->dockerActions->buildExAppContainerName($appId) . '_logs.txt', 'text/plain');
}
}

/**
* Using default methods to fetch App Store categories as they are the same for ExApps
*
Expand Down
35 changes: 34 additions & 1 deletion lib/DeployActions/DockerActions.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,40 @@ public function getContainerLogs(string $dockerUrl, string $containerId, string
$dockerUrl, sprintf('containers/%s/logs?stdout=true&stderr=true&tail=%s', $containerId, $tail)
);
$response = $this->guzzleClient->get($url);
return (string) $response->getBody();
return array_reduce($this->processDockerLogs((string) $response->getBody()), function ($carry, $logEntry) {
return $carry . $logEntry['content'];
}, '');
}

private function processDockerLogs($binaryData): array {
$offset = 0;
$length = strlen($binaryData);
$logs = [];

while ($offset < $length) {
if ($offset + 8 > $length) {
break; // Incomplete header, handle this case as needed
}

// Unpack the header
$header = unpack('C1type/C3skip/N1size', substr($binaryData, $offset, 8));
$offset += 8; // Move past the header

// Extract the log data based on the size from header
$logSize = $header['size'];
if ($offset + $logSize > $length) {
break; // Incomplete data, handle this case as needed
}

$logs[] = [
'stream_type' => $header['type'] === 1 ? 'stdout' : 'stderr',
'content' => substr($binaryData, $offset, $logSize)
];

$offset += $logSize; // Move to the next log entry
}

return $logs;
}

public function createVolume(string $dockerUrl, string $volume): array {
Expand Down
19 changes: 19 additions & 0 deletions lib/Service/DaemonConfigService.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class DaemonConfigService {
public function __construct(
private readonly LoggerInterface $logger,
private readonly DaemonConfigMapper $mapper,
private readonly ExAppService $exAppService,
) {
}

Expand Down Expand Up @@ -72,6 +73,24 @@ public function getRegisteredDaemonConfigs(): array {
}
}

public function getDaemonConfigsWithAppsCount(): array {
$exApps = $this->exAppService->getExAppsList('all');
$daemonsExAppsCount = [];
foreach ($exApps as $app) {
$exApp = $this->exAppService->getExApp($app['id']);
if (!isset($daemonsExAppsCount[$exApp->getDaemonConfigName()])) {
$daemonsExAppsCount[$exApp->getDaemonConfigName()] = 0;
}
$daemonsExAppsCount[$exApp->getDaemonConfigName()] += 1;
}
return array_map(function (DaemonConfig $daemonConfig) use ($daemonsExAppsCount) {
return [
...$daemonConfig->jsonSerialize(),
'exAppsCount' => isset($daemonsExAppsCount[$daemonConfig->getName()]) ? $daemonsExAppsCount[$daemonConfig->getName()] : 0,
];
}, $this->getRegisteredDaemonConfigs());
}

public function getDaemonConfigByName(string $name): ?DaemonConfig {
try {
return $this->mapper->findByName($name);
Expand Down
20 changes: 1 addition & 19 deletions lib/Settings/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@

use OCA\AppAPI\AppInfo\Application;

use OCA\AppAPI\Db\DaemonConfig;
use OCA\AppAPI\DeployActions\DockerActions;
use OCA\AppAPI\Service\DaemonConfigService;
use OCA\AppAPI\Service\ExAppService;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
Expand All @@ -23,29 +21,13 @@ public function __construct(
private readonly DaemonConfigService $daemonConfigService,
private readonly IConfig $config,
private readonly DockerActions $dockerActions,
private readonly ExAppService $service,
private readonly LoggerInterface $logger,
) {
}

public function getForm(): TemplateResponse {
$exApps = $this->service->getExAppsList('all');
$daemonsExAppsCount = [];
foreach ($exApps as $app) {
$exApp = $this->service->getExApp($app['id']);
if (!isset($daemonsExAppsCount[$exApp->getDaemonConfigName()])) {
$daemonsExAppsCount[$exApp->getDaemonConfigName()] = 0;
}
$daemonsExAppsCount[$exApp->getDaemonConfigName()] += 1;
}
$daemons = array_map(function (DaemonConfig $daemonConfig) use ($daemonsExAppsCount) {
return [
...$daemonConfig->jsonSerialize(),
'exAppsCount' => isset($daemonsExAppsCount[$daemonConfig->getName()]) ? $daemonsExAppsCount[$daemonConfig->getName()] : 0,
];
}, $this->daemonConfigService->getRegisteredDaemonConfigs());
$adminInitialData = [
'daemons' => $daemons,
'daemons' => $this->daemonConfigService->getDaemonConfigsWithAppsCount(),
'default_daemon_config' => $this->config->getAppValue(Application::APP_ID, 'default_daemon_config'),
'init_timeout' => $this->config->getAppValue(Application::APP_ID, 'init_timeout', '40'),
'container_restart_policy' => $this->config->getAppValue(Application::APP_ID, 'container_restart_policy', 'unless-stopped'),
Expand Down
Loading

0 comments on commit b4156e6

Please sign in to comment.