diff --git a/.github/workflows/tests-deploy.yml b/.github/workflows/tests-deploy.yml
index 636eec79..15b687e1 100644
--- a/.github/workflows/tests-deploy.yml
+++ b/.github/workflows/tests-deploy.yml
@@ -709,6 +709,166 @@ jobs:
path: data/nextcloud.log
if-no-files-found: warn
+ nc-host-app-docker-redis-deploy-options:
+ runs-on: ubuntu-22.04
+ name: NC In Host(Redis) Deploy options • master • 🐘8.3
+
+ services:
+ postgres:
+ image: ghcr.io/nextcloud/continuous-integration-postgres-14:latest
+ ports:
+ - 4444:5432/tcp
+ env:
+ POSTGRES_USER: root
+ POSTGRES_PASSWORD: rootpassword
+ POSTGRES_DB: nextcloud
+ options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
+ redis:
+ image: redis
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ --name redis
+ ports:
+ - 6379:6379
+
+ steps:
+ - name: Set app env
+ run: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
+
+ - name: Checkout server
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ with:
+ submodules: true
+ repository: nextcloud/server
+ ref: master
+
+ - name: Checkout AppAPI
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ with:
+ path: apps/${{ env.APP_NAME }}
+
+ - name: Set up php 8.3
+ uses: shivammathur/setup-php@4bd44f22a98a19e0950cbad5f31095157cc9621b # v2
+ with:
+ php-version: 8.3
+ 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
+ coverage: none
+ ini-file: development
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Check composer file existence
+ id: check_composer
+ uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2
+ with:
+ files: apps/${{ env.APP_NAME }}/composer.json
+
+ - name: Set up dependencies
+ if: steps.check_composer.outputs.files_exists == 'true'
+ working-directory: apps/${{ env.APP_NAME }}
+ run: composer i
+
+ - name: Set up Nextcloud
+ env:
+ DB_PORT: 4444
+ REDIS_HOST: localhost
+ REDIS_PORT: 6379
+ run: |
+ mkdir data
+ ./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 \
+ --database-port=$DB_PORT --database-user=root --database-pass=rootpassword \
+ --admin-user admin --admin-pass admin
+ ./occ config:system:set loglevel --value=0 --type=integer
+ ./occ config:system:set debug --value=true --type=boolean
+
+ ./occ config:system:set memcache.local --value "\\OC\\Memcache\\Redis"
+ ./occ config:system:set memcache.distributed --value "\\OC\\Memcache\\Redis"
+ ./occ config:system:set memcache.locking --value "\\OC\\Memcache\\Redis"
+ ./occ config:system:set redis host --value ${{ env.REDIS_HOST }}
+ ./occ config:system:set redis port --value ${{ env.REDIS_PORT }}
+
+ ./occ app:enable --force ${{ env.APP_NAME }}
+
+ - name: Test deploy
+ run: |
+ PHP_CLI_SERVER_WORKERS=2 php -S 127.0.0.1:8080 &
+ ./occ app_api:daemon:register docker_local_sock Docker docker-install http /var/run/docker.sock http://127.0.0.1:8080/index.php
+ ./occ app_api:daemon:list
+ mkdir -p ./test_mount
+ TEST_MOUNT_ABS_PATH=$(pwd)/test_mount
+ ./occ app_api:app:register app-skeleton-python docker_local_sock \
+ --info-xml https://raw.githubusercontent.com/nextcloud/app-skeleton-python/main/appinfo/info.xml \
+ --env='TEST_ENV_2=2' \
+ --mount "$TEST_MOUNT_ABS_PATH:/test_mount:rw"
+ ./occ app_api:app:enable app-skeleton-python
+ ./occ app_api:app:disable app-skeleton-python
+
+ - name: Check logs
+ run: |
+ grep -q 'Hello from app-skeleton-python :)' data/nextcloud.log || error
+ grep -q 'Bye bye from app-skeleton-python :(' data/nextcloud.log || error
+
+ - name: Check docker inspect TEST_ENV_1
+ run: |
+ docker inspect --format '{{ json .Config.Env }}' nc_app_app-skeleton-python | grep -q 'TEST_ENV_1=0' || error
+
+ - name: Check docker inspect TEST_ENV_2
+ run: |
+ docker inspect --format '{{ json .Config.Env }}' nc_app_app-skeleton-python | grep -q 'TEST_ENV_2=2' || error
+
+ - name: Check docker inspect TEST_ENV_3
+ run: |
+ docker inspect --format '{{ json .Config.Env }}' nc_app_app-skeleton-python | grep -q 'TEST_ENV_3=' && error || true
+
+ - name: Check docker inspect TEST_MOUNT
+ run: |
+ 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; }
+
+ - name: Save container info & logs
+ if: always()
+ run: |
+ docker inspect nc_app_app-skeleton-python | json_pp > container.json
+ docker logs nc_app_app-skeleton-python > container.log 2>&1
+
+ - name: Unregister Skeleton & Daemon
+ run: |
+ ./occ app_api:app:unregister app-skeleton-python
+ ./occ app_api:daemon:unregister docker_local_sock
+
+ - name: Test OCC commands(docker)
+ run: python3 apps/${{ env.APP_NAME }}/tests/test_occ_commands_docker.py
+
+ - name: Check redis keys
+ run: |
+ docker exec redis redis-cli keys '*app_api*' || error
+
+ - name: Upload Container info
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: nc_host_app_docker_redis_deploy_options_master_8.3_container.json
+ path: container.json
+ if-no-files-found: warn
+
+ - name: Upload Container logs
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: nc_host_app_docker_redis_deploy_options_master_8.3_container.log
+ path: container.log
+ if-no-files-found: warn
+
+ - name: Upload NC logs
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: nc_host_app_docker_redis_deploy_options_master_8.3_nextcloud.log
+ path: data/nextcloud.log
+ if-no-files-found: warn
+
nc-host-network-host:
runs-on: ubuntu-22.04
name: NC In Host(network=host) • master • 🐘8.3
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 7eb020a8..756b4a81 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -38,7 +38,7 @@ to join us in shaping a more versatile, stable, and secure app landscape.
*Your insights, suggestions, and contributions are invaluable to us.*
]]>
- 32.0.0-dev.1
+ 32.0.0-dev.2
agpl
Andrey Borysenko
Alexander Piskun
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 11789d74..0d2c7493 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -38,6 +38,7 @@
['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#getAppDeployOptions', 'url' => '/apps/deploy-options/{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' => ''],
diff --git a/lib/Command/ExApp/Register.php b/lib/Command/ExApp/Register.php
index 0a422396..f3d2d9fa 100644
--- a/lib/Command/ExApp/Register.php
+++ b/lib/Command/ExApp/Register.php
@@ -55,6 +55,10 @@ protected function configure(): void {
$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');
+
+ // Advanced deploy options
+ $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');
+ $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');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
@@ -73,8 +77,35 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->exAppService->unregisterExApp($appId);
}
+ $deployOptions = [];
+ $envs = $input->getOption('env') ?? [];
+ // Parse array of deploy options strings (ENV_NAME=ENV_VALUE) to array key => value
+ $envs = array_reduce($envs, function ($carry, $item) {
+ $parts = explode('=', $item, 2);
+ if (count($parts) === 2) {
+ $carry[$parts[0]] = $parts[1];
+ }
+ return $carry;
+ }, []);
+ $deployOptions['environment_variables'] = $envs;
+
+ $mounts = $input->getOption('mount') ?? [];
+ // Parse array of mount options strings (HOST_PATH:CONTAINER_PATH:ro|rw)
+ // to array of arrays ['source' => HOST_PATH, 'target' => CONTAINER_PATH, 'mode' => ro|rw]
+ $mounts = array_reduce($mounts, function ($carry, $item) {
+ $parts = explode(':', $item, 3);
+ if (count($parts) === 3) {
+ $carry[] = ['source' => $parts[0], 'target' => $parts[1], 'mode' => $parts[2]];
+ } elseif (count($parts) === 2) {
+ $carry[] = ['source' => $parts[0], 'target' => $parts[1], 'mode' => 'rw'];
+ }
+ return $carry;
+ }, );
+ $deployOptions['mounts'] = $mounts;
+
$appInfo = $this->exAppService->getAppInfo(
- $appId, $input->getOption('info-xml'), $input->getOption('json-info')
+ $appId, $input->getOption('info-xml'), $input->getOption('json-info'),
+ $deployOptions
);
if (isset($appInfo['error'])) {
$this->logger->error($appInfo['error']);
@@ -86,7 +117,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$appId = $appInfo['id']; # value from $appInfo should have higher priority
$daemonConfigName = $input->getArgument('daemon-config-name');
- if (!isset($daemonConfigName)) {
+ if (!isset($daemonConfigName) || $daemonConfigName === '') {
$daemonConfigName = $this->config->getAppValue(Application::APP_ID, 'default_daemon_config');
}
$daemonConfig = $this->daemonConfigService->getDaemonConfigByName($daemonConfigName);
diff --git a/lib/Command/ExApp/Update.php b/lib/Command/ExApp/Update.php
index 50b52ec3..0e862537 100644
--- a/lib/Command/ExApp/Update.php
+++ b/lib/Command/ExApp/Update.php
@@ -16,6 +16,7 @@
use OCA\AppAPI\Service\AppAPIService;
use OCA\AppAPI\Service\DaemonConfigService;
+use OCA\AppAPI\Service\ExAppDeployOptionsService;
use OCA\AppAPI\Service\ExAppService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
@@ -27,14 +28,15 @@
class Update extends Command {
public function __construct(
- private readonly AppAPIService $service,
- private readonly ExAppService $exAppService,
- private readonly DaemonConfigService $daemonConfigService,
- private readonly DockerActions $dockerActions,
- private readonly ManualActions $manualActions,
- private readonly LoggerInterface $logger,
- private readonly ExAppArchiveFetcher $exAppArchiveFetcher,
- private readonly ExAppFetcher $exAppFetcher,
+ private readonly AppAPIService $service,
+ private readonly ExAppService $exAppService,
+ private readonly DaemonConfigService $daemonConfigService,
+ private readonly DockerActions $dockerActions,
+ private readonly ManualActions $manualActions,
+ private readonly LoggerInterface $logger,
+ private readonly ExAppArchiveFetcher $exAppArchiveFetcher,
+ private readonly ExAppFetcher $exAppFetcher,
+ private readonly ExAppDeployOptionsService $exAppDeployOptionsService,
) {
parent::__construct();
}
@@ -90,8 +92,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
private function updateExApp(InputInterface $input, OutputInterface $output, string $appId): int {
$outputConsole = !$input->getOption('silent');
+ $deployOptions = $this->exAppDeployOptionsService->formatDeployOptions(
+ $this->exAppDeployOptionsService->getDeployOptions()
+ );
$appInfo = $this->exAppService->getAppInfo(
- $appId, $input->getOption('info-xml'), $input->getOption('json-info')
+ $appId, $input->getOption('info-xml'), $input->getOption('json-info'),
+ $deployOptions
);
if (isset($appInfo['error'])) {
$this->logger->error($appInfo['error']);
diff --git a/lib/Controller/ExAppsPageController.php b/lib/Controller/ExAppsPageController.php
index c2c99d20..9a63ea36 100644
--- a/lib/Controller/ExAppsPageController.php
+++ b/lib/Controller/ExAppsPageController.php
@@ -21,6 +21,7 @@
use OCA\AppAPI\Fetcher\ExAppFetcher;
use OCA\AppAPI\Service\AppAPIService;
use OCA\AppAPI\Service\DaemonConfigService;
+use OCA\AppAPI\Service\ExAppDeployOptionsService;
use OCA\AppAPI\Service\ExAppService;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
@@ -53,6 +54,7 @@ public function __construct(
private readonly LoggerInterface $logger,
private readonly IAppManager $appManager,
private readonly ExAppService $exAppService,
+ private readonly ExAppDeployOptionsService $exAppDeployOptionsService,
) {
parent::__construct(Application::APP_ID, $request);
}
@@ -312,12 +314,29 @@ private function buildLocalAppsList(array $apps, array $exApps): array {
}
#[PasswordConfirmationRequired]
- public function enableApp(string $appId): JSONResponse {
+ public function enableApp(string $appId, array $deployOptions = []): JSONResponse {
$updateRequired = false;
$exApp = $this->exAppService->getExApp($appId);
+
+ $envOptions = isset($deployOptions['environment_variables'])
+ ? array_keys($deployOptions['environment_variables']) : [];
+ $envOptionsString = '';
+ foreach ($envOptions as $envOption) {
+ $envOptionsString .= sprintf(' --env %s=%s', $envOption, $deployOptions['environment_variables'][$envOption]);
+ }
+ $envOptionsString = trim($envOptionsString);
+
+ $mountOptions = $deployOptions['mounts'] ?? [];
+ $mountOptionsString = '';
+ foreach ($mountOptions as $mountOption) {
+ $readonlyModifier = $mountOption['readonly'] ? 'ro' : 'rw';
+ $mountOptionsString .= sprintf(' --mount %s:%s:%s', $mountOption['hostPath'], $mountOption['containerPath'], $readonlyModifier);
+ }
+ $mountOptionsString = trim($mountOptionsString);
+
// If ExApp is not registered - then it's a "Deploy and Enable" action.
if (!$exApp) {
- if (!$this->service->runOccCommand(sprintf("app_api:app:register --silent %s", $appId))) {
+ if (!$this->service->runOccCommand(sprintf("app_api:app:register --silent %s %s %s", $appId, $envOptionsString, $mountOptionsString))) {
return new JSONResponse(['data' => ['message' => $this->l10n->t('Error starting install of ExApp')]], Http::STATUS_INTERNAL_SERVER_ERROR);
}
$elapsedTime = 0;
@@ -481,6 +500,38 @@ public function getAppLogs(string $appId, string $tail = 'all'): DataDownloadRes
}
}
+ public function getAppDeployOptions(string $appId) {
+ $exApp = $this->exAppService->getExApp($appId);
+ if (is_null($exApp)) {
+ return new JSONResponse(['error' => $this->l10n->t('ExApp not found, failed to get deploy options')], Http::STATUS_NOT_FOUND);
+ }
+
+ $deployOptions = $this->exAppDeployOptionsService->formatDeployOptions(
+ $this->exAppDeployOptionsService->getDeployOptions($appId)
+ );
+
+ $envs = [];
+ if (isset($deployOptions['environment_variables'])) {
+ $envs = $deployOptions['environment_variables'];
+ }
+
+ $mounts = [];
+ if (isset($deployOptions['mounts'])) {
+ foreach ($deployOptions['mounts'] as $mount) {
+ $mounts[] = [
+ 'hostPath' => $mount['source'],
+ 'containerPath' => $mount['target'],
+ 'readonly' => $mount['mode'] === 'ro'
+ ];
+ }
+ }
+
+ return new JSONResponse([
+ 'environment_variables' => $envs,
+ 'mounts' => $mounts,
+ ]);
+ }
+
/**
* Using default methods to fetch App Store categories as they are the same for ExApps
*
diff --git a/lib/Db/ExAppDeployOption.php b/lib/Db/ExAppDeployOption.php
new file mode 100644
index 00000000..54bbc627
--- /dev/null
+++ b/lib/Db/ExAppDeployOption.php
@@ -0,0 +1,64 @@
+addType('appid', 'string');
+ $this->addType('name', 'string');
+ $this->addType('type', 'string');
+ $this->addType('value', 'json');
+
+ if (isset($params['id'])) {
+ $this->setId($params['id']);
+ }
+ if (isset($params['appid'])) {
+ $this->setAppid($params['appid']);
+ }
+ if (isset($params['type'])) {
+ $this->setType($params['type']);
+ }
+ if (isset($params['value'])) {
+ $this->setValue($params['value']);
+ }
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'id' => $this->getId(),
+ 'appid' => $this->getAppid(),
+ 'type' => $this->getType(),
+ 'value' => $this->getValue(),
+ ];
+ }
+}
diff --git a/lib/Db/ExAppDeployOptionsMapper.php b/lib/Db/ExAppDeployOptionsMapper.php
new file mode 100644
index 00000000..44d6759f
--- /dev/null
+++ b/lib/Db/ExAppDeployOptionsMapper.php
@@ -0,0 +1,47 @@
+
+ */
+class ExAppDeployOptionsMapper extends QBMapper {
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, 'ex_deploy_options');
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function findAll(): array {
+ $qb = $this->db->getQueryBuilder();
+ $result = $qb->select('exs.*')
+ ->from($this->tableName, 'exs')
+ ->executeQuery();
+ return $result->fetchAll();
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function removeAllByAppId(string $appId): int {
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete($this->tableName)
+ ->where(
+ $qb->expr()->eq('appid', $qb->createNamedParameter($appId, IQueryBuilder::PARAM_STR))
+ );
+ return $qb->executeStatement();
+ }
+}
diff --git a/lib/DeployActions/DockerActions.php b/lib/DeployActions/DockerActions.php
index f2a34d9b..27754b93 100644
--- a/lib/DeployActions/DockerActions.php
+++ b/lib/DeployActions/DockerActions.php
@@ -17,6 +17,7 @@
use OCA\AppAPI\Db\ExApp;
use OCA\AppAPI\Service\AppAPICommonService;
+use OCA\AppAPI\Service\ExAppDeployOptionsService;
use OCA\AppAPI\Service\ExAppService;
use OCP\App\IAppManager;
@@ -38,15 +39,16 @@ class DockerActions implements IDeployActions {
private bool $useSocket = false; # for `pullImage` function, to detect can be stream used or not.
public function __construct(
- private readonly LoggerInterface $logger,
- private readonly IConfig $config,
- private readonly ICertificateManager $certificateManager,
- private readonly IAppManager $appManager,
- private readonly IURLGenerator $urlGenerator,
- private readonly AppAPICommonService $service,
- private readonly ExAppService $exAppService,
- private readonly ITempManager $tempManager,
- private readonly ICrypto $crypto,
+ private readonly LoggerInterface $logger,
+ private readonly IConfig $config,
+ private readonly ICertificateManager $certificateManager,
+ private readonly IAppManager $appManager,
+ private readonly IURLGenerator $urlGenerator,
+ private readonly AppAPICommonService $service,
+ private readonly ExAppService $exAppService,
+ private readonly ITempManager $tempManager,
+ private readonly ICrypto $crypto,
+ private readonly ExAppDeployOptionsService $exAppDeployOptionsService,
) {
}
@@ -95,6 +97,10 @@ public function deployExApp(ExApp $exApp, DaemonConfig $daemonConfig, array $par
if (isset($result['error'])) {
return $result['error'];
}
+
+ $this->exAppDeployOptionsService->removeExAppDeployOptions($exApp->getAppid());
+ $this->exAppDeployOptionsService->addExAppDeployOptions($exApp->getAppid(), $params['deploy_options']);
+
$this->exAppService->setAppDeployProgress($exApp, 99);
if (!$this->waitTillContainerStart($containerName, $daemonConfig)) {
return 'container startup failed';
@@ -350,6 +356,20 @@ public function createContainer(string $dockerUrl, string $imageId, DaemonConfig
}
}
+ if (isset($params['mounts'])) {
+ $containerParams['HostConfig']['Mounts'] = array_merge(
+ $containerParams['HostConfig']['Mounts'] ?? [],
+ array_map(function ($mount) {
+ return [
+ 'Source' => $mount['source'],
+ 'Target' => $mount['target'],
+ 'Type' => 'bind', // we don't support other types for now
+ 'ReadOnly' => $mount['mode'] === 'ro',
+ ];
+ }, $params['mounts'])
+ );
+ }
+
$url = $this->buildApiUrl($dockerUrl, sprintf('containers/create?name=%s', urlencode($this->buildExAppContainerName($params['name']))));
try {
$options['json'] = $containerParams;
@@ -677,6 +697,7 @@ public function buildDeployParams(DaemonConfig $daemonConfig, array $appInfo): a
'port' => $appInfo['port'],
'storage' => $storage,
'secret' => $appInfo['secret'],
+ 'environment_variables' => $appInfo['external-app']['environment-variables'] ?? [],
], $deployConfig);
$containerParams = [
@@ -688,11 +709,16 @@ public function buildDeployParams(DaemonConfig $daemonConfig, array $appInfo): a
'computeDevice' => $deployConfig['computeDevice'] ?? null,
'devices' => $devices,
'deviceRequests' => $deviceRequests,
+ 'mounts' => $appInfo['external-app']['mounts'] ?? [],
];
return [
'image_params' => $imageParams,
'container_params' => $containerParams,
+ 'deploy_options' => [
+ 'environment_variables' => $appInfo['external-app']['environment-variables'] ?? [],
+ 'mounts' => $appInfo['external-app']['mounts'] ?? [],
+ ]
];
}
@@ -718,6 +744,12 @@ public function buildDeployEnvs(array $params, array $deployConfig): array {
$autoEnvs[] = sprintf('NVIDIA_DRIVER_CAPABILITIES=%s', 'compute,utility');
}
}
+
+ // Appending additional deploy options to container envs
+ foreach (array_keys($params['environment_variables']) as $envKey) {
+ $autoEnvs[] = sprintf('%s=%s', $envKey, $params['environment_variables'][$envKey]['value'] ?? '');
+ }
+
return $autoEnvs;
}
diff --git a/lib/Migration/Version032001Date20250115164140.php b/lib/Migration/Version032001Date20250115164140.php
new file mode 100644
index 00000000..04cb1cbe
--- /dev/null
+++ b/lib/Migration/Version032001Date20250115164140.php
@@ -0,0 +1,59 @@
+hasTable('ex_deploy_options')) {
+ $table = $schema->createTable('ex_deploy_options');
+
+ $table->addColumn('id', Types::BIGINT, [
+ 'notnull' => true,
+ 'autoincrement' => true,
+ ]);
+ $table->addColumn('appid', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 32,
+ ]);
+ $table->addColumn('type', Types::STRING, [ // environment_variables/mounts/ports
+ 'notnull' => true,
+ 'length' => 32,
+ ]);
+ $table->addColumn('value', Types::JSON, [
+ 'notnull' => true,
+ ]);
+
+ $table->setPrimaryKey(['id']);
+ $table->addUniqueIndex(['appid', 'type'], 'deploy_options__idx');
+ }
+
+
+ return $schema;
+ }
+}
diff --git a/lib/Service/ExAppDeployOptionsService.php b/lib/Service/ExAppDeployOptionsService.php
new file mode 100644
index 00000000..bc41e100
--- /dev/null
+++ b/lib/Service/ExAppDeployOptionsService.php
@@ -0,0 +1,126 @@
+isAvailable()) {
+ $this->cache = $cacheFactory->createDistributed(Application::APP_ID . '/ex_deploy_options');
+ }
+ }
+
+ public function addExAppDeployOptions(string $appId, array $deployOptions): array {
+ $added = [];
+ foreach (array_keys($deployOptions) as $type) {
+ if ($this->addExAppDeployOption($appId, $type, $deployOptions[$type])) {
+ $added[$type] = $deployOptions[$type];
+ }
+ }
+ return $added;
+ }
+
+ public function addExAppDeployOption(
+ string $appId,
+ string $type,
+ mixed $value,
+ ): ?ExAppDeployOption {
+ $deployOptionEntry = $this->getDeployOption($appId, $type);
+ try {
+ $newExAppDeployOption = new ExAppDeployOption([
+ 'appid' => $appId,
+ 'type' => $type,
+ 'value' => $value,
+ ]);
+ if ($deployOptionEntry !== null) {
+ $newExAppDeployOption->setId($deployOptionEntry->getId());
+ }
+ $exAppDeployOption = $this->mapper->insertOrUpdate($newExAppDeployOption);
+ $this->resetCache();
+ } catch (Exception $e) {
+ $this->logger->error(
+ sprintf('Failed to register ExApp Deploy option for %s. Error: %s', $appId, $e->getMessage()), ['exception' => $e]
+ );
+ return null;
+ }
+ return $exAppDeployOption;
+ }
+
+ public function getDeployOption(string $appId, string $type): ?ExAppDeployOption {
+ foreach ($this->getDeployOptions() as $deployOption) {
+ if (($deployOption->getAppid() === $appId) && ($deployOption->getType() === $type)) {
+ return $deployOption;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get list of all registered ExApp Deploy Options
+ *
+ * @return ExAppDeployOption[]
+ */
+ public function getDeployOptions(?string $appId = null): array {
+ try {
+ $cacheKey = '/ex_deploy_options';
+ $records = $this->cache?->get($cacheKey);
+ if ($records === null) {
+ $records = $this->mapper->findAll();
+ $this->cache?->set($cacheKey, $records);
+ }
+ if ($appId !== null) {
+ $records = array_values(array_filter($records, function ($record) use ($appId) {
+ return $record['appid'] === $appId;
+ }));
+ }
+ return array_map(function ($record) {
+ return new ExAppDeployOption($record);
+ }, $records);
+ } catch (Exception) {
+ return [];
+ }
+ }
+
+ public function formatDeployOptions(array $deployOptions): array {
+ $formattedDeployOptions = [];
+ foreach ($deployOptions as $deployOption) {
+ $formattedDeployOptions[$deployOption->getType()] = $deployOption->getValue();
+ }
+ return $formattedDeployOptions;
+ }
+
+ public function removeExAppDeployOptions(string $appId): int {
+ try {
+ $result = $this->mapper->removeAllByAppId($appId);
+ } catch (Exception) {
+ $result = -1;
+ }
+ $this->resetCache();
+ return $result;
+ }
+
+ public function resetCache(): void {
+ $this->cache?->remove('/ex_deploy_options');
+ }
+}
diff --git a/lib/Service/ExAppService.php b/lib/Service/ExAppService.php
index 059008d9..cd8d90db 100644
--- a/lib/Service/ExAppService.php
+++ b/lib/Service/ExAppService.php
@@ -60,6 +60,7 @@ public function __construct(
private readonly SettingsService $settingsService,
private readonly ExAppEventsListenerService $eventsListenerService,
private readonly ExAppOccService $occService,
+ private readonly ExAppDeployOptionsService $deployOptionsService,
private readonly IConfig $config,
) {
if ($cacheFactory->isAvailable()) {
@@ -128,6 +129,7 @@ public function unregisterExApp(string $appId): bool {
$this->exAppArchiveFetcher->removeExAppFolder($appId);
$this->eventsListenerService->unregisterExAppEventListeners($appId);
$this->occService->unregisterExAppOccCommands($appId);
+ $this->deployOptionsService->removeExAppDeployOptions($appId);
$this->unregisterExAppWebhooks($appId);
$r = $this->exAppMapper->deleteExApp($appId);
if ($r !== 1) {
@@ -256,9 +258,10 @@ private function resetCaches(): void {
$this->settingsService->resetCacheEnabled();
$this->eventsListenerService->resetCacheEnabled();
$this->occService->resetCacheEnabled();
+ $this->deployOptionsService->resetCache();
}
- public function getAppInfo(string $appId, ?string $infoXml, ?string $jsonInfo): array {
+ public function getAppInfo(string $appId, ?string $infoXml, ?string $jsonInfo, ?array $deployOptions = null): array {
$extractedDir = '';
if ($jsonInfo !== null) {
$appInfo = json_decode($jsonInfo, true);
@@ -300,6 +303,34 @@ public function getAppInfo(string $appId, ?string $infoXml, ?string $jsonInfo):
}
}, $appInfo['external-app']['routes']);
}
+ // Advanced deploy options
+ if (isset($appInfo['external-app']['environment-variables']['variable'])) {
+ $envVars = [];
+ foreach ($appInfo['external-app']['environment-variables']['variable'] as $envVar) {
+ $envVars[$envVar['name']] = [
+ 'name' => $envVar['name'],
+ 'displayName' => $envVar['display-name'] ?? '',
+ 'description' => $envVar['description'] ?? '',
+ 'default' => $envVar['default'] ?? '',
+ 'value' => $envVar['default'] ?? '',
+ ];
+ }
+ if (isset($deployOptions['environment_variables']) && count(array_keys($deployOptions['environment_variables'])) > 0) {
+ // override with given deploy options values
+ foreach ($deployOptions['environment_variables'] as $key => $value) {
+ if (array_key_exists($key, $envVars)) {
+ $envVars[$key]['value'] = $value['value'] ?? $value ?? '';
+ }
+ }
+ }
+ $envVars = array_filter($envVars, function ($envVar) {
+ return $envVar['value'] !== '';
+ });
+ $appInfo['external-app']['environment-variables'] = $envVars;
+ }
+ if (isset($deployOptions['mounts'])) {
+ $appInfo['external-app']['mounts'] = $deployOptions['mounts'];
+ }
if ($extractedDir) {
if (file_exists($extractedDir . '/l10n')) {
$appInfo['translations_folder'] = $extractedDir . '/l10n';
diff --git a/lib/Service/ExAppsPageService.php b/lib/Service/ExAppsPageService.php
index 5d7d9f5a..12d3e7b8 100644
--- a/lib/Service/ExAppsPageService.php
+++ b/lib/Service/ExAppsPageService.php
@@ -14,6 +14,7 @@
use OCP\App\IAppManager;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
+use OCP\IURLGenerator;
use Psr\Log\LoggerInterface;
class ExAppsPageService {
@@ -25,6 +26,7 @@ public function __construct(
private readonly IConfig $config,
private readonly IAppManager $appManager,
private readonly LoggerInterface $logger,
+ private readonly IURLGenerator $urlGenerator,
) {
}
@@ -61,5 +63,8 @@ public function provideAppApiState(IInitialState $initialState): void {
if ($defaultDaemonConfig !== null) {
$initialState->provideInitialState('defaultDaemonConfig', $defaultDaemonConfig);
}
+
+ $deployOptionsDocsUrl = $this->urlGenerator->linkToDocs('admin-deploy-options');
+ $initialState->provideInitialState('deployOptionsDocsUrl', $deployOptionsDocsUrl);
}
}