Compare commits

..

3 Commits

Author SHA1 Message Date
Pablo Zmdl 29b4bad3ff Run playwright tests via compose setup
Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
2026-06-03 16:18:54 +02:00
Pablo Zmdl b55ab5eef4 Enable running playwright tests locally
They now are running via a docker compose setup, which can be executed via
./php/tests/run.sh locally, and also gets called from the github workflow.

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
2026-06-03 16:18:54 +02:00
Pablo Zmdl fa98d22724 Use test path that exists on macos, too
Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
2026-06-03 11:05:53 +02:00
18 changed files with 292 additions and 334 deletions
+2 -90
View File
@@ -30,99 +30,11 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.15.0
- name: Install dependencies
run: cd php/tests && npm ci
- name: Install Playwright Browsers
run: cd php/tests && npx playwright install --with-deps chromium
- name: Set up php 8.5
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
with:
extensions: apcu
php-version: 8.5
coverage: none
ini-file: development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Adjust some things and fix permissions
run: |
cd php
rm -r ./data
rm -r ./session
composer install --no-dev
composer clear-cache
sudo chmod 777 -R ../
- name: Start fresh development server
run: |
docker rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup} || true
docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} || true
docker pull ghcr.io/nextcloud-releases/all-in-one:develop
docker run \
-d \
--init \
--name nextcloud-aio-mastercontainer \
--restart always \
--publish 8080:8080 \
--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \
--volume ./php:/var/www/docker-aio/php \
--volume ./Containers/mastercontainer/internal.Caddyfile:/internal.Caddyfile \
--volume ./Containers/mastercontainer/headers.Caddyfile:/headers.Caddyfile \
--volume ./Containers/mastercontainer/start.sh:/start.sh \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
--env SKIP_DOMAIN_VALIDATION=true \
--env APACHE_PORT=11000 \
ghcr.io/nextcloud-releases/all-in-one:develop
echo Waiting for 10 seconds for the development container to start ...
sleep 10
- name: Run Playwright tests for initial setup
run: |
cd php/tests
export DEBUG=pw:api
if ! npx playwright test tests/initial-setup.spec.js; then
docker logs nextcloud-aio-mastercontainer
docker logs nextcloud-aio-borgbackup
exit 1
fi
- name: Start fresh development server
run: |
docker rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup} || true
docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} || true
docker run \
-d \
--init \
--name nextcloud-aio-mastercontainer \
--restart always \
--publish 8080:8080 \
--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \
--volume ./php:/var/www/docker-aio/php \
--volume ./Containers/mastercontainer/internal.Caddyfile:/internal.Caddyfile \
--volume ./Containers/mastercontainer/headers.Caddyfile:/headers.Caddyfile \
--volume ./Containers/mastercontainer/start.sh:/start.sh \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
--env SKIP_DOMAIN_VALIDATION=false \
--env APACHE_PORT=11000 \
ghcr.io/nextcloud-releases/all-in-one:develop
echo Waiting for 10 seconds for the development container to start ...
sleep 10
run: ./php/tests/run.sh ./tests/initial-setup.spec.js
- name: Run Playwright tests for backup restore
run: |
cd php/tests
export DEBUG=pw:api
if ! npx playwright test tests/restore-instance.spec.js; then
docker logs nextcloud-aio-mastercontainer
docker logs nextcloud-aio-borgbackup
exit 1
fi
run: ./php/tests/run.sh ./tests/restore-instance.spec.js
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ !cancelled() }}
@@ -15,72 +15,15 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.15.0
- name: Install dependencies
run: cd php/tests && npm ci
- name: Install Playwright Browsers
run: cd php/tests && npx playwright install --with-deps chromium
- name: Start fresh development server
run: |
docker rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup} || true
docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} || true
docker pull ghcr.io/nextcloud-releases/all-in-one:develop
docker run \
-d \
--init \
--name nextcloud-aio-mastercontainer \
--restart always \
--publish 8080:8080 \
--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
--env SKIP_DOMAIN_VALIDATION=true \
--env APACHE_PORT=11000 \
ghcr.io/nextcloud-releases/all-in-one:develop
echo Waiting for 10 seconds for the development container to start ...
sleep 10
- name: Run Playwright tests for initial setup
run: |
cd php/tests
export DEBUG=pw:api
if ! npx playwright test tests/initial-setup.spec.js; then
docker logs nextcloud-aio-mastercontainer
docker logs nextcloud-aio-borgbackup
exit 1
fi
- name: Start fresh development server
run: |
docker rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup} || true
docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} || true
docker run \
-d \
--init \
--name nextcloud-aio-mastercontainer \
--restart always \
--publish 8080:8080 \
--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
--env SKIP_DOMAIN_VALIDATION=false \
--env APACHE_PORT=11000 \
ghcr.io/nextcloud-releases/all-in-one:develop
echo Waiting for 10 seconds for the development container to start ...
sleep 10
env:
TEST_CODE_FROM_IMAGE: yes
run: ./run.sh ./tests/initial-setup.spec.js
- name: Run Playwright tests for backup restore
run: |
cd php/tests
export DEBUG=pw:api
if ! npx playwright test tests/restore-instance.spec.js; then
docker logs nextcloud-aio-mastercontainer
docker logs nextcloud-aio-borgbackup
exit 1
fi
env:
TEST_CODE_FROM_IMAGE: yes
run: ./php/tests/run.sh ./tests/restore-instance.spec.js
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ !cancelled() }}
@@ -88,4 +31,4 @@ jobs:
name: playwright-report
path: php/tests/playwright-report/
retention-days: 14
overwrite: true
overwrite: true
@@ -2,5 +2,4 @@
$CONFIG = array (
'one-click-instance' => true,
'one-click-instance.user-limit' => 100,
'update_channel' => 'stable',
);
+31 -2
View File
@@ -419,12 +419,41 @@ EOF
# AIO update to latest start # Do not remove or change this line!
if [ "$INSTALL_LATEST_MAJOR" = yes ]; then
if ! bash /upgrade-latest-major.sh; then
echo "Upgrade to latest major version failed! Check the output above for details."
php /var/www/html/occ config:system:set updatedirectory --value="/nc-updater"
INSTALLED_AT="$(php /var/www/html/occ config:app:get core installedat)"
if [ -n "${INSTALLED_AT}" ]; then
# Set the installdat to 00 which will allow to skip staging and install the next major directly
# shellcheck disable=SC2001
INSTALLED_AT="$(echo "${INSTALLED_AT}" | sed "s|[0-9][0-9]$|00|")"
php /var/www/html/occ config:app:set core installedat --value="${INSTALLED_AT}"
fi
php /var/www/html/updater/updater.phar --no-interaction --no-backup
if ! php /var/www/html/occ -V || php /var/www/html/occ status | grep maintenance | grep -q 'true'; then
echo "Installation of Nextcloud failed!"
touch "$NEXTCLOUD_DATA_DIR/install.failed"
exit 1
fi
# shellcheck disable=SC2016
installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')"
INSTALLED_MAJOR="${installed_version%%.*}"
IMAGE_MAJOR="${image_version%%.*}"
# If a valid upgrade path, trigger the Nextcloud built-in Updater
if ! [ "$INSTALLED_MAJOR" -gt "$IMAGE_MAJOR" ]; then
php /var/www/html/updater/updater.phar --no-interaction --no-backup
if ! php /var/www/html/occ -V || php /var/www/html/occ status | grep maintenance | grep -q 'true'; then
echo "Installation of Nextcloud failed!"
# TODO: Add a hint here about what to do / where to look / updater.log?
touch "$NEXTCLOUD_DATA_DIR/install.failed"
exit 1
fi
# shellcheck disable=SC2016
installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')"
fi
php /var/www/html/occ config:system:set updatechecker --type=bool --value=true
php /var/www/html/occ app:enable nextcloud-aio --force
php /var/www/html/occ db:add-missing-columns
php /var/www/html/occ db:add-missing-primary-keys
yes | php /var/www/html/occ db:convert-filecache-bigint
fi
# AIO update to latest end # Do not remove or change this line!
@@ -1,43 +0,0 @@
#!/bin/bash
PHP_CLI="php"
if [[ "$EUID" = 0 ]]; then
PHP_CLI="sudo -u www-data -E $PHP_CLI"
fi
# shellcheck disable=SC2016
image_version="$($PHP_CLI -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')"
export IMAGE_MAJOR="${image_version%%.*}"
$PHP_CLI /var/www/html/occ config:system:set updatedirectory --value="/nc-updater"
INSTALLED_AT="$($PHP_CLI /var/www/html/occ config:app:get core installedat)"
if [ -n "${INSTALLED_AT}" ]; then
# Set the installedat to 00 which will allow to skip staging and install the next major directly
# shellcheck disable=SC2001
INSTALLED_AT="$(echo "${INSTALLED_AT}" | sed "s|[0-9][0-9]$|00|")"
$PHP_CLI /var/www/html/occ config:app:set core installedat --value="${INSTALLED_AT}"
fi
$PHP_CLI /var/www/html/updater/updater.phar --no-interaction --no-backup
if ! $PHP_CLI /var/www/html/occ -V || $PHP_CLI /var/www/html/occ status | grep maintenance | grep -q 'true'; then
echo "Installation of Nextcloud failed!"
touch "$NEXTCLOUD_DATA_DIR/install.failed"
exit 1
fi
# shellcheck disable=SC2016
installed_version="$($PHP_CLI -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')"
export INSTALLED_MAJOR="${installed_version%%.*}"
# If a valid upgrade path, trigger the Nextcloud built-in Updater
if ! $PHP_CLI -r "version_compare(getenv('INSTALLED_MAJOR'), getenv('IMAGE_MAJOR'), '>') || exit(1);"; then
$PHP_CLI /var/www/html/updater/updater.phar --no-interaction --no-backup
if ! $PHP_CLI /var/www/html/occ -V || $PHP_CLI /var/www/html/occ status | grep maintenance | grep -q 'true'; then
echo "Installation of Nextcloud failed!"
# TODO: Add a hint here about what to do / where to look / updater.log?
touch "$NEXTCLOUD_DATA_DIR/install.failed"
exit 1
fi
fi
$PHP_CLI /var/www/html/occ config:system:set updatechecker --type=bool --value=true
$PHP_CLI /var/www/html/occ app:enable nextcloud-aio --force
$PHP_CLI /var/www/html/occ db:add-missing-columns
$PHP_CLI /var/www/html/occ db:add-missing-primary-keys
yes | $PHP_CLI /var/www/html/occ db:convert-filecache-bigint
+1 -1
View File
@@ -4,4 +4,4 @@ if [ "$AIO_LOG_LEVEL" = 'debug' ]; then
set -x
fi
nc -z 127.0.0.1 1234 || exit 1
nc -z 127.0.0.1 1234 || nc -z ::1 1234 || exit 1
+9 -1
View File
@@ -58,13 +58,21 @@ extensionaudio = .m4a
extensionvideo = .mp4"
fi
# Detect IPv6 availability to choose the right listen address
RECORDING_LISTEN="0.0.0.0:1234"
if ! grep -q "1" /sys/module/ipv6/parameters/disable 2>/dev/null \
&& ! grep -q "1" /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null \
&& ! grep -q "1" /proc/sys/net/ipv6/conf/default/disable_ipv6 2>/dev/null; then
RECORDING_LISTEN="[::]:1234"
fi
cat << RECORDING_CONF > "/conf/recording.conf"
[logs]
# 30 means Warning
level = ${TALK_RECORDING_LOG_LEVEL}
[http]
listen = 0.0.0.0:1234
listen = ${RECORDING_LISTEN}
[backend]
allowall = ${ALLOW_ALL}
-1
View File
@@ -104,7 +104,6 @@ $app->post('/api/docker/backup-test', AIO\Controller\DockerController::class . '
$app->post('/api/docker/restore', AIO\Controller\DockerController::class . ':StartBackupContainerRestore');
$app->post('/api/docker/stop', AIO\Controller\DockerController::class . ':StopContainer');
$app->post('/api/docker/backup-reset-location', AIO\Controller\DockerController::class . ':DeleteBorgBackupConfig');
$app->post('/api/docker/nextcloud-upgrade-to-latest-major', AIO\Controller\DockerController::class . ':RunNextcloudUpgradeToLatestMajor');
$app->post('/api/docker/prune', AIO\Controller\DockerController::class . ':SystemPrune');
$app->get('/api/docker/logs', AIO\Controller\DockerController::class . ':GetLogs');
$app->post('/api/auth/login', AIO\Controller\LoginController::class . ':TryLogin');
+1 -1
View File
@@ -483,7 +483,7 @@ input[type="checkbox"]:disabled:not(:checked) + label {
visibility: hidden;
opacity: 0;
align-self: start;
width: min(600px, calc(100vw - 4rem));
width: min(700px, calc(100vw - 4rem));
height: min(400px, calc(100vh - 14rem));
border-radius: var(--border-radius-large);
border: solid thin rgb(192, 192, 192);
+8 -30
View File
@@ -14,7 +14,6 @@ use Slim\Psr7\NonBufferedBody;
readonly class DockerController {
private const string TOP_CONTAINER = 'nextcloud-aio-apache';
private const string LATEST_MAJOR_VERSION = '34';
public function __construct(
private DockerActionManager $dockerActionManager,
@@ -222,7 +221,7 @@ readonly class DockerController {
}
if (isset($request->getParsedBody()['install_latest_major'])) {
$installLatestMajor = self::LATEST_MAJOR_VERSION;
$installLatestMajor = '34';
} else {
$installLatestMajor = '';
}
@@ -299,7 +298,7 @@ readonly class DockerController {
}
if ($addToStreamingResponseBody !== null) {
$addToStreamingResponseBody("Stopping container", $container);
$addToStreamingResponseBody($container, "Stopping container");
}
// Stop itself first and then all the dependencies
@@ -334,30 +333,14 @@ readonly class DockerController {
return $response->withStatus(201)->withHeader('Location', '.');
}
public function RunNextcloudUpgradeToLatestMajor(Request $request, Response $response, array $args) : Response {
$this->configurationManager->installLatestMajor = self::LATEST_MAJOR_VERSION;
// Get streaming response start and closure
$nonbufResp = $this->startStreamingResponse($response);
$addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp);
$this->dockerActionManager->RunNextcloudUpgradeToLatestMajor($addToStreamingResponseBody);
// We automatically reload after 10s so that the output can be read or copied if necessary
$addToStreamingResponseBody("Automatically reloading the page after 10s.");
sleep(10);
// End streaming response
$this->finalizeStreamingResponse($nonbufResp);
return $nonbufResp;
}
public function SystemPrune(Request $request, Response $response, array $args) : Response {
// Get streaming response start and closure
$nonbufResp = $this->startStreamingResponse($response);
$body = $nonbufResp->getBody();
$addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp);
$addToStreamingResponseBody = function (string $message) use ($body) : void {
$body->write("<div>$message</div>");
};
$this->dockerActionManager->SystemPrune($addToStreamingResponseBody);
@@ -443,17 +426,12 @@ readonly class DockerController {
return $nonbufResp;
}
private function getAddToStreamingResponseBody(Response $nonbufResp) : \Closure {
private function getAddToStreamingResponseBody(Response $nonbufResp) : ?\Closure {
// Create a closure to pass around to the code, which should to the logging (because it e.g. decides
// if it'll actually pull an image), but which should not need to know anything about the
// wanted markup or formatting.
$addToStreamingResponseBody = function (string $message, ?Container $container = null) use ($nonbufResp) : void {
// Strip ANSI codes.
$message = preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $message);
if ($container) {
$message = "{$container->displayName}: {$message}";
}
$nonbufResp->getBody()->write("<div>" . htmlspecialchars("{$message}", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</div>");
$addToStreamingResponseBody = function (Container $container, string $message) use ($nonbufResp) : void {
$nonbufResp->getBody()->write("<div>{$container->displayName}: {$message}</div>");
};
return $addToStreamingResponseBody;
+56 -91
View File
@@ -12,7 +12,6 @@ use AIO\Data\DataConst;
use AIO\Helper\NetworkHelper;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Utils;
use http\Env\Response;
readonly class DockerActionManager {
@@ -49,7 +48,7 @@ readonly class DockerActionManager {
public function GetContainerRunningState(Container $container): ContainerState {
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->identifier)));
try {
$response = $this->sendHttpRequest('GET', $url);
$response = $this->guzzleClient->get($url);
} catch (RequestException $e) {
if ($e->getCode() === 404) {
return ContainerState::ImageDoesNotExist;
@@ -69,7 +68,7 @@ readonly class DockerActionManager {
public function GetContainerRestartingState(Container $container): ContainerState {
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->identifier)));
try {
$response = $this->sendHttpRequest('GET', $url);
$response = $this->guzzleClient->get($url);
} catch (RequestException $e) {
if ($e->getCode() === 404) {
return ContainerState::ImageDoesNotExist;
@@ -139,7 +138,7 @@ readonly class DockerActionManager {
public function DeleteContainer(Container $container): void {
$url = $this->BuildApiUrl(sprintf('containers/%s?v=true', urlencode($container->identifier)));
try {
$this->sendHttpRequest('DELETE', $url);
$this->guzzleClient->delete($url);
} catch (RequestException $e) {
if ($e->getCode() !== 404) {
throw $e;
@@ -156,7 +155,7 @@ readonly class DockerActionManager {
// Delete the borg cache volume
$url = $this->BuildApiUrl('volumes/nextcloud_aio_backup_cache');
try {
$this->sendHttpRequest('DELETE', $url);
$this->guzzleClient->delete($url);
error_log('nextcloud_aio_backup_cache volume deleted successfully.');
} catch (RequestException $e) {
if ($e->getCode() !== 404) {
@@ -175,7 +174,7 @@ readonly class DockerActionManager {
urlencode($id),
$since
));
$responseBody = (string)$this->sendHttpRequest('GET', $url)->getBody();
$responseBody = (string)$this->guzzleClient->get($url)->getBody();
$response = "";
$separator = "\r\n";
@@ -195,9 +194,9 @@ readonly class DockerActionManager {
$url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->identifier)));
try {
if ($addToStreamingResponseBody !== null) {
$addToStreamingResponseBody("Starting container", $container);
$addToStreamingResponseBody($container, "Starting container");
}
$this->sendHttpRequest('POST', $url);
$this->guzzleClient->post($url);
} catch (RequestException $e) {
throw new \Exception("Could not start container " . $container->identifier . ": " . $e->getResponse()?->getBody()->getContents());
}
@@ -216,7 +215,7 @@ readonly class DockerActionManager {
$firstChar = substr($volume->name, 0, 1);
if (!in_array($firstChar, $forbiddenChars)) {
$this->sendHttpRequest(
$this->guzzleClient->request(
'POST',
$url,
[
@@ -495,7 +494,7 @@ readonly class DockerActionManager {
$url = $this->BuildApiUrl('containers/create?name=' . $container->identifier);
try {
$this->sendHttpRequest(
$this->guzzleClient->request(
'POST',
$url,
[
@@ -552,10 +551,10 @@ readonly class DockerActionManager {
$imageIsThere = true;
try {
if ($addToStreamingResponseBody) {
$addToStreamingResponseBody("Pulling image", $container);
$addToStreamingResponseBody($container, "Pulling image");
}
$imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $encodedImageName));
$this->sendHttpRequest('GET', $imageUrl)->getBody()->getContents();
$this->guzzleClient->get($imageUrl)->getBody()->getContents();
} catch (\Throwable $e) {
$imageIsThere = false;
}
@@ -563,7 +562,7 @@ readonly class DockerActionManager {
$maxRetries = 3;
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
try {
$this->sendHttpRequest('POST', $url);
$this->guzzleClient->post($url);
break;
} catch (RequestException $e) {
$message = "Could not pull image " . $imageName . " (attempt $attempt/$maxRetries): " . $e->getResponse()?->getBody()->getContents();
@@ -648,11 +647,11 @@ readonly class DockerActionManager {
private function GetRepoDigestsOfContainer(string $containerName): ?array {
try {
$containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
$containerOutput = json_decode($this->sendHttpRequest('GET', $containerUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$imageName = $containerOutput['Image'];
$imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName));
$imageOutput = json_decode($this->sendHttpRequest('GET', $imageUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
if (!isset($imageOutput['RepoDigests'])) {
error_log('RepoDigests is not set of container ' . $containerName);
@@ -696,7 +695,7 @@ readonly class DockerActionManager {
$containerName = 'nextcloud-aio-mastercontainer';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
try {
$output = json_decode($this->sendHttpRequest('GET', $url)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$imageNameArray = explode(':', $output['Config']['Image']);
if (count($imageNameArray) === 2) {
$imageName = $imageNameArray[0];
@@ -723,7 +722,7 @@ readonly class DockerActionManager {
$containerName = 'nextcloud-aio-mastercontainer';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
try {
$output = json_decode($this->sendHttpRequest('GET', $url)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$tagArray = explode(':', $output['Config']['Image']);
if (count($tagArray) === 2) {
$tag = $tagArray[1];
@@ -764,69 +763,48 @@ readonly class DockerActionManager {
}
public function sendNotification(Container $container, string $subject, string $message, string $file = '/notify.sh'): void {
$this->execCommandInContainer($container, ['bash', $file, $subject, $message]);
}
if ($this->GetContainerStartingState($container) === ContainerState::Running) {
public function execCommandInContainer(Container $container, array $cmd, ?\Closure $outputCallback = null): void {
if ($cmd === []) {
throw new \InvalidArgumentException('$cmd must not be empty.');
}
foreach ($cmd as $arg) {
if (!is_string($arg) || $arg === '') {
throw new \InvalidArgumentException('Every element of $cmd must be a non-empty string.');
}
}
$containerName = $container->identifier;
if ($this->GetContainerStartingState($container) !== ContainerState::Running) {
return;
}
// schedule the exec
$url = $this->BuildApiUrl(sprintf('containers/%s/exec', urlencode($containerName)));
$response = json_decode(
$this->guzzleClient->request(
'POST',
$url,
[
'json' => [
'AttachStdout' => true,
'Tty' => true,
'Cmd' => [
'bash',
$file,
$subject,
$message
],
],
]
)->getBody()->getContents(),
true,
512,
JSON_THROW_ON_ERROR,
);
$containerName = $container->identifier;
$id = $response['Id'];
// Create exec instance
$url = $this->BuildApiUrl(sprintf('containers/%s/exec', urlencode($containerName)));
$response = json_decode(
$this->sendHttpRequest(
// start the exec
$url = $this->BuildApiUrl(sprintf('exec/%s/start', $id));
$this->guzzleClient->request(
'POST',
$url,
[
'json' => [
'AttachStdout' => true,
'AttachStderr' => true,
'Detach' => false,
'Tty' => true,
'Cmd' => $cmd,
],
]
)->getBody()->getContents(),
true,
512,
JSON_THROW_ON_ERROR,
);
$execId = $response['Id'];
// Start exec
$url = $this->BuildApiUrl(sprintf('exec/%s/start', $execId));
$requestOptions = [
'json' => [
'Detach' => false,
'Tty' => true,
],
];
if ($outputCallback !== null) {
$requestOptions['stream'] = true;
}
$startResponse = $this->sendHttpRequest('POST', $url, $requestOptions);
if ($outputCallback !== null) {
$body = $startResponse->getBody();
while (!$body->eof()) {
$line = rtrim(Utils::readLine($body), "\r");;
if ($line !== '') {
$outputCallback($line);
}
}
);
}
}
@@ -837,7 +815,7 @@ readonly class DockerActionManager {
);
try {
$this->sendHttpRequest(
$this->guzzleClient->request(
'POST',
$url,
[
@@ -858,7 +836,7 @@ readonly class DockerActionManager {
if ($createNetwork) {
$url = $this->BuildApiUrl('networks/create');
try {
$this->sendHttpRequest(
$this->guzzleClient->request(
'POST',
$url,
[
@@ -887,7 +865,7 @@ readonly class DockerActionManager {
}
try {
$this->sendHttpRequest(
$this->guzzleClient->request(
'POST',
$url,
[
@@ -932,7 +910,7 @@ readonly class DockerActionManager {
}
$url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->identifier), $maxShutDownTime));
try {
$this->sendHttpRequest('POST', $url);
$this->guzzleClient->post($url);
} catch (RequestException $e) {
if ($e->getCode() !== 404 && $e->getCode() !== 304) {
throw $e;
@@ -944,7 +922,7 @@ readonly class DockerActionManager {
$containerName = 'nextcloud-aio-borgbackup';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName)));
try {
$response = $this->sendHttpRequest('GET', $url);
$response = $this->guzzleClient->get($url);
} catch (RequestException $e) {
if ($e->getCode() === 404) {
return -1;
@@ -966,7 +944,7 @@ readonly class DockerActionManager {
$containerName = 'nextcloud-aio-database';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName)));
try {
$response = $this->sendHttpRequest('GET', $url);
$response = $this->guzzleClient->get($url);
} catch (RequestException $e) {
if ($e->getCode() === 404) {
return -1;
@@ -1006,7 +984,7 @@ readonly class DockerActionManager {
$imageName = $imageName . ':' . $this->GetCurrentChannel();
try {
$imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName));
$imageOutput = json_decode($this->sendHttpRequest('GET', $imageUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
if (!isset($imageOutput['Created'])) {
error_log('Created is not set of image ' . $imageName);
@@ -1051,11 +1029,6 @@ readonly class DockerActionManager {
}
}
public function RunNextcloudUpgradeToLatestMajor(\Closure $addToStreamingResponseBody): void {
$container = $this->containerDefinitionFetcher->GetContainerById('nextcloud-aio-nextcloud');
$this->execCommandInContainer($container, ['bash', '/upgrade-latest-major.sh'], $addToStreamingResponseBody);
}
public function SystemPrune(?\Closure $addToStreamingResponseBody = null): void {
$endpoints = [
// Remove stopped containers
@@ -1084,7 +1057,7 @@ readonly class DockerActionManager {
}
try {
$response = $this->sendHttpRequest('POST', $url);
$response = $this->guzzleClient->post($url);
if ($addToStreamingResponseBody !== null) {
$data = json_decode((string)$response->getBody(), true);
$deleted = 0;
@@ -1122,12 +1095,4 @@ readonly class DockerActionManager {
sleep(10);
}
}
protected function sendHttpRequest(string $httpMethod, string $url, array $requestOptions = []): \Psr\Http\Message\ResponseInterface {
if (($requestOptions['stream'] ?? null) === true) {
$requestOptions['proxy'] = 'unix:///var/run/docker.sock';
}
return $this->guzzleClient->request($httpMethod, $url, $requestOptions);
}
}
}
+1 -6
View File
@@ -298,12 +298,7 @@
{% if newMajorVersionString != '' and isAnyRunning == true and isApacheStarting != true %}
<details>
<summary>Note about <strong>Nextcloud Hub {{ newMajorVersionString }}</strong></summary>
<p>If you haven't upgraded to Nextcloud Hub {{ newMajorVersionString }} yet and want to do that now, feel free to click the button below. ⚠️ Warning: make sure to create a backup before clicking the button as the update can go wrong and will leave your instance in a broken state!</p>
<form method="POST" action="api/docker/nextcloud-upgrade-to-latest-major" target="overlay-log">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input type="submit" value="Upgrade to Nextcloud Hub {{ newMajorVersionString }}" data-confirm="Upgrade to Nextcloud Hub {{ newMajorVersionString }}? You should consider creating a backup first." />
</form>
<p>If you haven't upgraded to Nextcloud Hub {{ newMajorVersionString }} yet and want to do that now, feel free to follow <strong><a target="_blank" href="https://github.com/nextcloud/all-in-one/discussions/8223">this documentation</a></strong></p>
</details>
{% endif %}
{% endif %}
+1 -1
View File
@@ -1 +1 @@
13.2.0
13.1.0
+4
View File
@@ -0,0 +1,4 @@
FROM docker.io/library/composer:latest
RUN pecl bundle -d /usr/src/php/ext apcu \
&& docker-php-ext-install apcu
+107
View File
@@ -0,0 +1,107 @@
# This setup expects that you run the services via profiles!
# Usage: docker compose --profile local-code up
# or: docker compose --profile code-from-image up
name: nextcloud-aio
services:
composer:
image: localhost/composer:latest
build: Containers/composer
pull_policy: never
volumes:
- ..:/app
working_dir: /app
command: |-
bash -c '
test -d ./data && rm -r ./data
test -d ./session && rm -r ./session
composer install --no-dev
composer clear-cache
'
app-base:
image: ghcr.io/nextcloud-releases/all-in-one:develop${ARM64_SUFFIX-}
pull_policy: always # Always pull so we don't risk to run into the "Update for mastercontainer" page.
init: true
restart: always
network_mode: bridge
ports:
- "8080:8080"
volumes:
- nextcloud_aio_mastercontainer:/mnt/docker-aio-config
- /var/run/docker.sock:/var/run/docker.sock:ro
- backup_vol:/mnt/test
profiles:
- none
environment:
SKIP_DOMAIN_VALIDATION: ${SKIP_DOMAIN_VALIDATION-true}
APACHE_PORT: 11000
entrypoint: bash /start.sh
app-code-from-image:
extends: app-base
container_name: nextcloud-aio-mastercontainer
profiles:
- code-from-image
app-local-code:
extends: app-base
container_name: nextcloud-aio-mastercontainer
depends_on:
composer:
condition: service_completed_successfully
volumes:
- ..:/var/www/docker-aio/php
- ../../Containers/mastercontainer/internal.Caddyfile:/internal.Caddyfile
- ../../Containers/mastercontainer/headers.Caddyfile:/headers.Caddyfile
- ../../Containers/mastercontainer/start.sh:/start.sh
profiles:
- local-code
test-runner-base:
image: mcr.microsoft.com/playwright:v1.56.1
volumes:
- ..:/app
working_dir: /app
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- '9323:9323' # to view test reports
profiles:
- none
environment:
BASE_URL: "https://host.docker.internal:8080"
DEBUG: "pw:api"
command: |-
bash -c "
cd tests
# Install dependencies
npm ci
# Run the initial setup tests
npx playwright test "${TESTS_FILE-}"
exit $?
"
test-runner-code-from-image:
extends: test-runner-base
container_name: test-runner
profiles:
- code-from-image
depends_on:
app:
condition: service_healthy
test-runner-local-code:
extends: test-runner-base
container_name: test-runner
profiles:
- local-code
depends_on:
app-local-code:
condition: service_healthy
volumes:
nextcloud_aio_mastercontainer:
name: nextcloud_aio_mastercontainer
backup_vol:
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
if [[ "$1" = -* ]]; then
echo "Usage $(basename $0) [PLAYWRIGHT_TESTS_FILE]"
exit 1
fi
cd $(dirname $0)/../..
DOCO="docker compose -f ./php/tests/compose.yaml"
if [[ $(uname -m) = 'arm64' ]]; then
export ARM64_SUFFIX='-arm64'
fi
run_tests() {
export TESTS_FILE="$1"
export SKIP_DOMAIN_VALIDATION
if [[ -n "$TEST_CODE_FROM_IMAGE" ]]; then
profile="code-from-image"
else
profile="local-code"
fi
# Clean up old containers and volumes
docker container rm --force nextcloud-aio-{mastercontainer,apache,notify-push,nextcloud,redis,database,domaincheck,whiteboard,imaginary,talk,collabora,borgbackup} > /dev/null 2>&1
docker volume rm nextcloud_aio_{mastercontainer,apache,database,database_dump,nextcloud,nextcloud_data,redis,backup_cache,elasticsearch} > /dev/null 2>&1
$DOCO --profile $profile down -v
sleep 1
echo -e "\n 📣 Running playwright tests for ${TESTS_FILE}\n"
if ! $DOCO --profile $profile run --remove-orphans test-runner-$profile; then
for container in nextcloud-aio-{mastercontainer,borgbackup}; do
if docker container list --format="{{ .Names }}" | grep -q "$container"; then
echo -e "\n 📣 Log output from container ${container}:\n"
docker logs nextcloud-aio-mastercontainer
fi
done
fi
}
if [[ -n "$1" ]]; then
if [[ ! -f "$1" ]]; then
echo "Error: file '$1' does not exist."
exit 1
fi
# Not using coreutil's `realpath --relative-to` here since that is not available on BSD/mac systems.
fullpath="$(realpath "$1")"
prefix="$(realpath ./php/tests)"
relpath="${fullpath#"$prefix"/}"
: ${SKIP_DOMAIN_VALIDATION:-false}
run_tests "$relpath"
else
SKIP_DOMAIN_VALIDATION=true
run_tests tests/initial-setup.spec.js
sleep 1
SKIP_DOMAIN_VALIDATION=false
run_tests tests/restore-instance.spec.js
fi
+1 -1
View File
@@ -69,7 +69,7 @@ test('Initial setup', async ({ page: setupPage }) => {
const initialNextcloudPassword = await containersPage.locator('#initial-nextcloud-password').innerText();
// Set backup location and create backup
const borgBackupLocation = `/mnt/test/aio-${Math.floor(Math.random() * 2147483647)}`
const borgBackupLocation = `/tmp/test/aio-${Math.floor(Math.random() * 2147483647)}`
await containersPage.locator('#borg_backup_host_location').click();
await containersPage.locator('#borg_backup_host_location').fill(borgBackupLocation);
await containersPage.getByRole('button', { name: 'Submit backup location' }).click();
+1 -1
View File
@@ -32,7 +32,7 @@ test('Restore instance', async ({ page: setupPage }) => {
// Reject invalid backup location
await containersPage.locator('#borg_restore_host_location').click();
await containersPage.locator('#borg_restore_host_location').fill('/mnt/test/aio-incorrect-path');
await containersPage.locator('#borg_restore_host_location').fill('/tmp/test/aio-incorrect-path');
await containersPage.locator('#borg_restore_password').click();
await containersPage.locator('#borg_restore_password').fill(borgBackupPassword);
await containersPage.getByRole('button', { name: 'Submit location and encryption password' }).click()