Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Test deploy feature #279

Merged
merged 9 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [2.5.1 - 2024-0x-xx]

### Added

- Test deploy button in Admin settings for each Daemon configuration. #279

## [2.5.0 - 2024-04-23]

### Added
Expand Down
5 changes: 4 additions & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ to join us in shaping a more versatile, stable, and secure app landscape.
*Your insights, suggestions, and contributions are invaluable to us.*

]]></description>
<version>2.5.0</version>
<version>2.5.1</version>
<licence>agpl</licence>
<author mail="andrey18106x@gmail.com" homepage="https://github.com/andrey18106">Andrey Borysenko</author>
<author mail="bigcat88@icloud.com" homepage="https://github.com/bigcat88">Alexander Piskun</author>
Expand All @@ -70,6 +70,9 @@ to join us in shaping a more versatile, stable, and secure app landscape.
<job>OCA\AppAPI\BackgroundJob\ProvidersAICleanUpJob</job>
</background-jobs>
<repair-steps>
<post-migration>
<step>OCA\AppAPI\Migration\DaemonUpdateGPUSRepairStep</step>
</post-migration>
<install>
<step>OCA\AppAPI\Migration\DataInitializationStep</step>
<step>OCA\AppAPI\Migration\DaemonUpdateV2RepairStep</step>
Expand Down
7 changes: 6 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,11 @@
['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'],
['name' => 'DaemonConfig#getTestDeployStatus', 'url' => '/daemons/{name}/test_deploy/status', 'verb' => 'GET'],
],
'ocs' => [
// Logging
Expand Down
94 changes: 94 additions & 0 deletions docs/TestDeploy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
.. _test_deploy:

Test Deploy Daemon
------------------

You can test each Daemon configuration deployment from the AppAPI Admin settings.

.. image:: ./img/test_deploy.png


Status Checks
^^^^^^^^^^^^^

The Deploy test installs a `test-deploy <https://github.com/cloud-py-api/test-deploy>`_ ExApp
to verify each step of the deployment process, including a hardware support check -
for each compute device, there is a separate Docker image.

.. note::
The Test Deploy ExApp container is not removed after the test as it's needed for logs and status checks.
You can remove it after testing from the External Apps page.
The Docker images are also not removed from the Daemon; you can clean up unused images with the ``docker image prune`` command.

.. image:: ./img/test_deploy_modal_4.png


Register
********

The Register step is the first step; it checks if the ExApp is registered in Nextcloud.

Image Pull
**********

The Image Pull step downloads the ExApp Docker image.

Possible errors:

- Image not found
- Image pull failed (e.g., due to network issues)
- Image pull timeout

Container Started
*****************

The Container Started step verifies that the ExApp container is created and started successfully.

Possible errors:

- Container failed to start with GPU support
- For NVIDIA, refer to the `NVIDIA Docker configuration docs <https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html>`_.
- For AMD, refer to the `ROCm Docker configuration docs <https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html>`_.


Heartbeat
*********

The Heartbeat step checks if the container's health check is finished and the container is healthy.
The ExApp might have additional pre-configuration logic during this step.

Possible errors:

- ExApp failed to start a web server, e.g., if the port is already in use (this should be visible in the container logs)


Init
****

The Init step checks if the ExApp is initialized and ready to use.
During the init step, the ExApp may perform downloads of extra stuff required for it.

Possible errors:

- Initialization failed (e.g., due to network issues or timeout)


Enabled
*******

The Enabled step checks if the ExApp is enabled and ready to use.
During this step, the ExApp registers all the required and available APIs of the Nextcloud AppFramework.

Possible errors:

- ExApp did not respond to the enable request
- ExApp failed to enable due to a failure in registering AppAPI Nextcloud AppFramework APIs (this should be visible both in the container logs and in the Nextcloud logs if there are any errors)


Download Logs
^^^^^^^^^^^^^

You can download the logs of the last test deploy attempt container.

.. note::
Downloading Docker container logs is only possible for containers using the json-file or journald logging drivers.
Binary file added docs/img/test_deploy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/test_deploy_modal_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/test_deploy_modal_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and handle complex problems through issues.
Installation
DeployConfigurations
CreationOfDeployDaemon
TestDeploy
ManagingExternalApplications
Concepts
tech_details/index.rst
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);
}
}
65 changes: 61 additions & 4 deletions lib/Controller/DaemonConfigController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@

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;
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,57 @@ 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);
}

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);
}

$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([
'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));
$elapsedTime = 0;
while ($elapsedTime < 5000000 && $this->exAppService->getExApp(Application::TEST_DEPLOY_APPID) === null) {
andrey18106 marked this conversation as resolved.
Show resolved Hide resolved
usleep(150000); // 0.15
$elapsedTime += 150000;
}
}
$exApp = $this->exAppService->getExApp(Application::TEST_DEPLOY_APPID);
return new JSONResponse([
'success' => $exApp === null,
]);
}

public function getTestDeployStatus(string $name): Response {
$exApp = $this->exAppService->getExApp(Application::TEST_DEPLOY_APPID);
if (is_null($exApp) || $exApp->getDaemonConfigName() !== $name) {
return new JSONResponse(['error' => $this->l10n->t('ExApp not found, failed to get status')], Http::STATUS_NOT_FOUND);
}
return new JSONResponse($exApp->getStatus());
}
}
35 changes: 35 additions & 0 deletions lib/Controller/ExAppsPageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace OCA\AppAPI\Controller;

use Exception;
use GuzzleHttp\Exception\GuzzleException;
use OC\App\AppStore\Fetcher\CategoryFetcher;
use OC\App\AppStore\Version\VersionParser;
use OC\App\DependencyAnalyzer;
Expand All @@ -26,6 +27,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 +526,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 +535,38 @@ public function getAppStatus(string $appId): JSONResponse {
return new JSONResponse($exApp->getStatus());
}

#[NoCSRFRequired]
public function getAppLogs(string $appId, string $tail = 'all'): 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),
$tail
);
return new DataDownloadResponse(
$logs,
$this->dockerActions->buildExAppContainerName($appId) . '_logs.txt', 'text/plain',
Http::STATUS_OK
);
} catch (GuzzleException $e) {
return new DataDownloadResponse(
json_encode(['error' => $this->l10n->t('Failed to get container logs. Note: Downloading Docker container works only for containers with the json-file or journald logging driver. Error: %s', [$e->getMessage()])]),
$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
Loading
Loading