Skip to content

Commit 93544ed

Browse files
authored
Merge pull request #499 from nextcloud/backport/497/stable31
[stable31] feat: advanced deploy options
2 parents a5f3dce + 9bb55a3 commit 93544ed

File tree

13 files changed

+637
-24
lines changed

13 files changed

+637
-24
lines changed

.github/workflows/tests-deploy.yml

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,166 @@ jobs:
709709
path: data/nextcloud.log
710710
if-no-files-found: warn
711711

712+
nc-host-app-docker-redis-deploy-options:
713+
runs-on: ubuntu-22.04
714+
name: NC In Host(Redis) Deploy options • master • 🐘8.3
715+
716+
services:
717+
postgres:
718+
image: ghcr.io/nextcloud/continuous-integration-postgres-14:latest
719+
ports:
720+
- 4444:5432/tcp
721+
env:
722+
POSTGRES_USER: root
723+
POSTGRES_PASSWORD: rootpassword
724+
POSTGRES_DB: nextcloud
725+
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
726+
redis:
727+
image: redis
728+
options: >-
729+
--health-cmd "redis-cli ping"
730+
--health-interval 10s
731+
--health-timeout 5s
732+
--health-retries 5
733+
--name redis
734+
ports:
735+
- 6379:6379
736+
737+
steps:
738+
- name: Set app env
739+
run: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
740+
741+
- name: Checkout server
742+
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
743+
with:
744+
submodules: true
745+
repository: nextcloud/server
746+
ref: master
747+
748+
- name: Checkout AppAPI
749+
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
750+
with:
751+
path: apps/${{ env.APP_NAME }}
752+
753+
- name: Set up php 8.3
754+
uses: shivammathur/setup-php@4bd44f22a98a19e0950cbad5f31095157cc9621b # v2
755+
with:
756+
php-version: 8.3
757+
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql, redis
758+
coverage: none
759+
ini-file: development
760+
env:
761+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
762+
763+
- name: Check composer file existence
764+
id: check_composer
765+
uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2
766+
with:
767+
files: apps/${{ env.APP_NAME }}/composer.json
768+
769+
- name: Set up dependencies
770+
if: steps.check_composer.outputs.files_exists == 'true'
771+
working-directory: apps/${{ env.APP_NAME }}
772+
run: composer i
773+
774+
- name: Set up Nextcloud
775+
env:
776+
DB_PORT: 4444
777+
REDIS_HOST: localhost
778+
REDIS_PORT: 6379
779+
run: |
780+
mkdir data
781+
./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 \
782+
--database-port=$DB_PORT --database-user=root --database-pass=rootpassword \
783+
--admin-user admin --admin-pass admin
784+
./occ config:system:set loglevel --value=0 --type=integer
785+
./occ config:system:set debug --value=true --type=boolean
786+
787+
./occ config:system:set memcache.local --value "\\OC\\Memcache\\Redis"
788+
./occ config:system:set memcache.distributed --value "\\OC\\Memcache\\Redis"
789+
./occ config:system:set memcache.locking --value "\\OC\\Memcache\\Redis"
790+
./occ config:system:set redis host --value ${{ env.REDIS_HOST }}
791+
./occ config:system:set redis port --value ${{ env.REDIS_PORT }}
792+
793+
./occ app:enable --force ${{ env.APP_NAME }}
794+
795+
- name: Test deploy
796+
run: |
797+
PHP_CLI_SERVER_WORKERS=2 php -S 127.0.0.1:8080 &
798+
./occ app_api:daemon:register docker_local_sock Docker docker-install http /var/run/docker.sock http://127.0.0.1:8080/index.php
799+
./occ app_api:daemon:list
800+
mkdir -p ./test_mount
801+
TEST_MOUNT_ABS_PATH=$(pwd)/test_mount
802+
./occ app_api:app:register app-skeleton-python docker_local_sock \
803+
--info-xml https://raw.githubusercontent.com/nextcloud/app-skeleton-python/main/appinfo/info.xml \
804+
--env='TEST_ENV_2=2' \
805+
--mount "$TEST_MOUNT_ABS_PATH:/test_mount:rw"
806+
./occ app_api:app:enable app-skeleton-python
807+
./occ app_api:app:disable app-skeleton-python
808+
809+
- name: Check logs
810+
run: |
811+
grep -q 'Hello from app-skeleton-python :)' data/nextcloud.log || error
812+
grep -q 'Bye bye from app-skeleton-python :(' data/nextcloud.log || error
813+
814+
- name: Check docker inspect TEST_ENV_1
815+
run: |
816+
docker inspect --format '{{ json .Config.Env }}' nc_app_app-skeleton-python | grep -q 'TEST_ENV_1=0' || error
817+
818+
- name: Check docker inspect TEST_ENV_2
819+
run: |
820+
docker inspect --format '{{ json .Config.Env }}' nc_app_app-skeleton-python | grep -q 'TEST_ENV_2=2' || error
821+
822+
- name: Check docker inspect TEST_ENV_3
823+
run: |
824+
docker inspect --format '{{ json .Config.Env }}' nc_app_app-skeleton-python | grep -q 'TEST_ENV_3=' && error || true
825+
826+
- name: Check docker inspect TEST_MOUNT
827+
run: |
828+
docker inspect --format '{{ json .Mounts }}' nc_app_app-skeleton-python | grep -q "Source\":\"$(printf '%s' "$TEST_MOUNT_ABS_PATH" | sed 's/[][\.*^$]/\\&/g')" || { echo "Error: TEST_MOUNT_ABS_PATH not found"; exit 1; }
829+
830+
- name: Save container info & logs
831+
if: always()
832+
run: |
833+
docker inspect nc_app_app-skeleton-python | json_pp > container.json
834+
docker logs nc_app_app-skeleton-python > container.log 2>&1
835+
836+
- name: Unregister Skeleton & Daemon
837+
run: |
838+
./occ app_api:app:unregister app-skeleton-python
839+
./occ app_api:daemon:unregister docker_local_sock
840+
841+
- name: Test OCC commands(docker)
842+
run: python3 apps/${{ env.APP_NAME }}/tests/test_occ_commands_docker.py
843+
844+
- name: Check redis keys
845+
run: |
846+
docker exec redis redis-cli keys '*app_api*' || error
847+
848+
- name: Upload Container info
849+
if: always()
850+
uses: actions/upload-artifact@v4
851+
with:
852+
name: nc_host_app_docker_redis_deploy_options_master_8.3_container.json
853+
path: container.json
854+
if-no-files-found: warn
855+
856+
- name: Upload Container logs
857+
if: always()
858+
uses: actions/upload-artifact@v4
859+
with:
860+
name: nc_host_app_docker_redis_deploy_options_master_8.3_container.log
861+
path: container.log
862+
if-no-files-found: warn
863+
864+
- name: Upload NC logs
865+
if: always()
866+
uses: actions/upload-artifact@v4
867+
with:
868+
name: nc_host_app_docker_redis_deploy_options_master_8.3_nextcloud.log
869+
path: data/nextcloud.log
870+
if-no-files-found: warn
871+
712872
nc-host-network-host:
713873
runs-on: ubuntu-22.04
714874
name: NC In Host(network=host) • master • 🐘8.3

appinfo/info.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ to join us in shaping a more versatile, stable, and secure app landscape.
4747
*Your insights, suggestions, and contributions are invaluable to us.*
4848
4949
]]></description>
50-
<version>5.0.1</version>
50+
<version>5.0.2</version>
5151
<licence>agpl</licence>
5252
<author mail="[email protected]" homepage="https://github.com/andrey18106">Andrey Borysenko</author>
5353
<author mail="[email protected]" homepage="https://github.com/bigcat88">Alexander Piskun</author>

appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
['name' => 'ExAppsPage#enableApp', 'url' => '/apps/enable/{appId}', 'verb' => 'POST' , 'root' => ''],
3939
['name' => 'ExAppsPage#getAppStatus', 'url' => '/apps/status/{appId}', 'verb' => 'GET' , 'root' => ''],
4040
['name' => 'ExAppsPage#getAppLogs', 'url' => '/apps/logs/{appId}', 'verb' => 'GET' , 'root' => ''],
41+
['name' => 'ExAppsPage#getAppDeployOptions', 'url' => '/apps/deploy-options/{appId}', 'verb' => 'GET' , 'root' => ''],
4142
['name' => 'ExAppsPage#disableApp', 'url' => '/apps/disable/{appId}', 'verb' => 'GET' , 'root' => ''],
4243
['name' => 'ExAppsPage#updateApp', 'url' => '/apps/update/{appId}', 'verb' => 'GET' , 'root' => ''],
4344
['name' => 'ExAppsPage#uninstallApp', 'url' => '/apps/uninstall/{appId}', 'verb' => 'GET' , 'root' => ''],

lib/Command/ExApp/Register.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ protected function configure(): void {
5555
$this->addOption('wait-finish', null, InputOption::VALUE_NONE, 'Wait until finish');
5656
$this->addOption('silent', null, InputOption::VALUE_NONE, 'Do not print to console');
5757
$this->addOption('test-deploy-mode', null, InputOption::VALUE_NONE, 'Test deploy mode with additional status checks and slightly different logic');
58+
59+
// Advanced deploy options
60+
$this->addOption('env', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Optional deploy options (ENV_NAME=ENV_VALUE), passed to ExApp container as environment variables');
61+
$this->addOption('mount', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Optional mount options (SRC_PATH:DST_PATH or SRC_PATH:DST_PATH:ro|rw), passed to ExApp container as volume mounts only if the app declares those variables in its info.xml');
5862
}
5963

6064
protected function execute(InputInterface $input, OutputInterface $output): int {
@@ -73,8 +77,35 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7377
$this->exAppService->unregisterExApp($appId);
7478
}
7579

80+
$deployOptions = [];
81+
$envs = $input->getOption('env') ?? [];
82+
// Parse array of deploy options strings (ENV_NAME=ENV_VALUE) to array key => value
83+
$envs = array_reduce($envs, function ($carry, $item) {
84+
$parts = explode('=', $item, 2);
85+
if (count($parts) === 2) {
86+
$carry[$parts[0]] = $parts[1];
87+
}
88+
return $carry;
89+
}, []);
90+
$deployOptions['environment_variables'] = $envs;
91+
92+
$mounts = $input->getOption('mount') ?? [];
93+
// Parse array of mount options strings (HOST_PATH:CONTAINER_PATH:ro|rw)
94+
// to array of arrays ['source' => HOST_PATH, 'target' => CONTAINER_PATH, 'mode' => ro|rw]
95+
$mounts = array_reduce($mounts, function ($carry, $item) {
96+
$parts = explode(':', $item, 3);
97+
if (count($parts) === 3) {
98+
$carry[] = ['source' => $parts[0], 'target' => $parts[1], 'mode' => $parts[2]];
99+
} elseif (count($parts) === 2) {
100+
$carry[] = ['source' => $parts[0], 'target' => $parts[1], 'mode' => 'rw'];
101+
}
102+
return $carry;
103+
}, );
104+
$deployOptions['mounts'] = $mounts;
105+
76106
$appInfo = $this->exAppService->getAppInfo(
77-
$appId, $input->getOption('info-xml'), $input->getOption('json-info')
107+
$appId, $input->getOption('info-xml'), $input->getOption('json-info'),
108+
$deployOptions
78109
);
79110
if (isset($appInfo['error'])) {
80111
$this->logger->error($appInfo['error']);
@@ -86,7 +117,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
86117
$appId = $appInfo['id']; # value from $appInfo should have higher priority
87118

88119
$daemonConfigName = $input->getArgument('daemon-config-name');
89-
if (!isset($daemonConfigName)) {
120+
if (!isset($daemonConfigName) || $daemonConfigName === '') {
90121
$daemonConfigName = $this->config->getAppValue(Application::APP_ID, 'default_daemon_config');
91122
}
92123
$daemonConfig = $this->daemonConfigService->getDaemonConfigByName($daemonConfigName);

lib/Command/ExApp/Update.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use OCA\AppAPI\Service\AppAPIService;
1717
use OCA\AppAPI\Service\DaemonConfigService;
1818

19+
use OCA\AppAPI\Service\ExAppDeployOptionsService;
1920
use OCA\AppAPI\Service\ExAppService;
2021
use Psr\Log\LoggerInterface;
2122
use Symfony\Component\Console\Command\Command;
@@ -27,14 +28,15 @@
2728
class Update extends Command {
2829

2930
public function __construct(
30-
private readonly AppAPIService $service,
31-
private readonly ExAppService $exAppService,
32-
private readonly DaemonConfigService $daemonConfigService,
33-
private readonly DockerActions $dockerActions,
34-
private readonly ManualActions $manualActions,
35-
private readonly LoggerInterface $logger,
36-
private readonly ExAppArchiveFetcher $exAppArchiveFetcher,
37-
private readonly ExAppFetcher $exAppFetcher,
31+
private readonly AppAPIService $service,
32+
private readonly ExAppService $exAppService,
33+
private readonly DaemonConfigService $daemonConfigService,
34+
private readonly DockerActions $dockerActions,
35+
private readonly ManualActions $manualActions,
36+
private readonly LoggerInterface $logger,
37+
private readonly ExAppArchiveFetcher $exAppArchiveFetcher,
38+
private readonly ExAppFetcher $exAppFetcher,
39+
private readonly ExAppDeployOptionsService $exAppDeployOptionsService,
3840
) {
3941
parent::__construct();
4042
}
@@ -90,8 +92,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
9092

9193
private function updateExApp(InputInterface $input, OutputInterface $output, string $appId): int {
9294
$outputConsole = !$input->getOption('silent');
95+
$deployOptions = $this->exAppDeployOptionsService->formatDeployOptions(
96+
$this->exAppDeployOptionsService->getDeployOptions()
97+
);
9398
$appInfo = $this->exAppService->getAppInfo(
94-
$appId, $input->getOption('info-xml'), $input->getOption('json-info')
99+
$appId, $input->getOption('info-xml'), $input->getOption('json-info'),
100+
$deployOptions
95101
);
96102
if (isset($appInfo['error'])) {
97103
$this->logger->error($appInfo['error']);

lib/Controller/ExAppsPageController.php

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use OCA\AppAPI\Fetcher\ExAppFetcher;
2222
use OCA\AppAPI\Service\AppAPIService;
2323
use OCA\AppAPI\Service\DaemonConfigService;
24+
use OCA\AppAPI\Service\ExAppDeployOptionsService;
2425
use OCA\AppAPI\Service\ExAppService;
2526
use OCP\App\IAppManager;
2627
use OCP\AppFramework\Controller;
@@ -53,6 +54,7 @@ public function __construct(
5354
private readonly LoggerInterface $logger,
5455
private readonly IAppManager $appManager,
5556
private readonly ExAppService $exAppService,
57+
private readonly ExAppDeployOptionsService $exAppDeployOptionsService,
5658
) {
5759
parent::__construct(Application::APP_ID, $request);
5860
}
@@ -312,12 +314,29 @@ private function buildLocalAppsList(array $apps, array $exApps): array {
312314
}
313315

314316
#[PasswordConfirmationRequired]
315-
public function enableApp(string $appId): JSONResponse {
317+
public function enableApp(string $appId, array $deployOptions = []): JSONResponse {
316318
$updateRequired = false;
317319
$exApp = $this->exAppService->getExApp($appId);
320+
321+
$envOptions = isset($deployOptions['environment_variables'])
322+
? array_keys($deployOptions['environment_variables']) : [];
323+
$envOptionsString = '';
324+
foreach ($envOptions as $envOption) {
325+
$envOptionsString .= sprintf(' --env %s=%s', $envOption, $deployOptions['environment_variables'][$envOption]);
326+
}
327+
$envOptionsString = trim($envOptionsString);
328+
329+
$mountOptions = $deployOptions['mounts'] ?? [];
330+
$mountOptionsString = '';
331+
foreach ($mountOptions as $mountOption) {
332+
$readonlyModifier = $mountOption['readonly'] ? 'ro' : 'rw';
333+
$mountOptionsString .= sprintf(' --mount %s:%s:%s', $mountOption['hostPath'], $mountOption['containerPath'], $readonlyModifier);
334+
}
335+
$mountOptionsString = trim($mountOptionsString);
336+
318337
// If ExApp is not registered - then it's a "Deploy and Enable" action.
319338
if (!$exApp) {
320-
if (!$this->service->runOccCommand(sprintf("app_api:app:register --silent %s", $appId))) {
339+
if (!$this->service->runOccCommand(sprintf("app_api:app:register --silent %s %s %s", $appId, $envOptionsString, $mountOptionsString))) {
321340
return new JSONResponse(['data' => ['message' => $this->l10n->t('Error starting install of ExApp')]], Http::STATUS_INTERNAL_SERVER_ERROR);
322341
}
323342
$elapsedTime = 0;
@@ -481,6 +500,38 @@ public function getAppLogs(string $appId, string $tail = 'all'): DataDownloadRes
481500
}
482501
}
483502

503+
public function getAppDeployOptions(string $appId) {
504+
$exApp = $this->exAppService->getExApp($appId);
505+
if (is_null($exApp)) {
506+
return new JSONResponse(['error' => $this->l10n->t('ExApp not found, failed to get deploy options')], Http::STATUS_NOT_FOUND);
507+
}
508+
509+
$deployOptions = $this->exAppDeployOptionsService->formatDeployOptions(
510+
$this->exAppDeployOptionsService->getDeployOptions($appId)
511+
);
512+
513+
$envs = [];
514+
if (isset($deployOptions['environment_variables'])) {
515+
$envs = $deployOptions['environment_variables'];
516+
}
517+
518+
$mounts = [];
519+
if (isset($deployOptions['mounts'])) {
520+
foreach ($deployOptions['mounts'] as $mount) {
521+
$mounts[] = [
522+
'hostPath' => $mount['source'],
523+
'containerPath' => $mount['target'],
524+
'readonly' => $mount['mode'] === 'ro'
525+
];
526+
}
527+
}
528+
529+
return new JSONResponse([
530+
'environment_variables' => $envs,
531+
'mounts' => $mounts,
532+
]);
533+
}
534+
484535
/**
485536
* Using default methods to fetch App Store categories as they are the same for ExApps
486537
*

0 commit comments

Comments
 (0)