Merge pull request #319048 from Ma27/nc-objectstore-and-cleanup

nixos/nextcloud: add objectstore test, refactor testing structure
This commit is contained in:
Maximilian Bosch 2024-06-18 14:40:55 +00:00 committed by GitHub
commit 3734012f61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 263 additions and 204 deletions

View File

@ -1,27 +1,27 @@
args@{ pkgs, nextcloudVersion ? 22, ... }:
{ name, pkgs, testBase, system,... }:
(import ../make-test-python.nix ({ pkgs, ...}: let
adminpass = "notproduction";
adminuser = "root";
in {
name = "nextcloud-basic";
with import ../../lib/testing-python.nix { inherit system pkgs; };
runTest ({ config, ... }: {
inherit name;
meta = with pkgs.lib.maintainers; {
maintainers = [ globin eqyiel ];
maintainers = [ globin eqyiel ma27 ];
};
nodes = rec {
imports = [ testBase ];
nodes = {
# The only thing the client needs to do is download a file.
client = { ... }: {
services.davfs2.enable = true;
systemd.tmpfiles.settings.nextcloud = {
"/tmp/davfs2-secrets"."f+" = {
mode = "0600";
argument = "http://nextcloud/remote.php/dav/files/${adminuser} ${adminuser} ${adminpass}";
argument = "http://nextcloud/remote.php/dav/files/${config.adminuser} ${config.adminuser} ${config.adminpass}";
};
};
virtualisation.fileSystems = {
"/mnt/dav" = {
device = "http://nextcloud/remote.php/dav/files/${adminuser}";
device = "http://nextcloud/remote.php/dav/files/${config.adminuser}";
fsType = "davfs";
options = let
davfs2Conf = (pkgs.writeText "davfs2.conf" "secrets /tmp/davfs2-secrets");
@ -30,11 +30,7 @@ in {
};
};
nextcloud = { config, pkgs, ... }: let
cfg = config;
in {
networking.firewall.allowedTCPPorts = [ 80 ];
nextcloud = { config, pkgs, ... }: {
systemd.tmpfiles.rules = [
"d /var/lib/nextcloud-data 0750 nextcloud nginx - -"
];
@ -42,14 +38,7 @@ in {
services.nextcloud = {
enable = true;
datadir = "/var/lib/nextcloud-data";
hostName = "nextcloud";
database.createLocally = true;
config = {
# Don't inherit adminuser since "root" is supposed to be the default
adminpassFile = "${pkgs.writeText "adminpass" adminpass}"; # Don't try this at home!
dbtableprefix = "nixos_";
};
package = pkgs.${"nextcloud" + (toString nextcloudVersion)};
config.dbtableprefix = "nixos_";
autoUpdateApps = {
enable = true;
startAt = "20:00";
@ -57,64 +46,31 @@ in {
phpExtraExtensions = all: [ all.bz2 ];
};
environment.systemPackages = [ cfg.services.nextcloud.occ ];
specialisation.withoutMagick.configuration = {
services.nextcloud.enableImagemagick = false;
};
};
nextcloudWithoutMagick = args@{ config, pkgs, lib, ... }:
lib.mkMerge
[ (nextcloud args)
{ services.nextcloud.enableImagemagick = false; } ];
};
testScript = { nodes, ... }: let
withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
#!${pkgs.runtimeShell}
export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/dav/files/${adminuser}"
export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
export RCLONE_CONFIG_NEXTCLOUD_USER="${adminuser}"
export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${adminpass})"
"''${@}"
'';
copySharedFile = pkgs.writeScript "copy-shared-file" ''
#!${pkgs.runtimeShell}
echo 'hi' | ${withRcloneEnv} ${pkgs.rclone}/bin/rclone rcat nextcloud:test-shared-file
'';
diffSharedFile = pkgs.writeScript "diff-shared-file" ''
#!${pkgs.runtimeShell}
diff <(echo 'hi') <(${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file)
'';
test-helpers.extraTests = { nodes, ... }: let
findInClosure = what: drv: pkgs.runCommand "find-in-closure" { exportReferencesGraph = [ "graph" drv ]; inherit what; } ''
test -e graph
grep "$what" graph >$out || true
'';
nextcloudUsesImagick = findInClosure "imagick" nodes.nextcloud.system.build.vm;
nextcloudWithoutDoesntUseIt = findInClosure "imagick" nodes.nextcloudWithoutMagick.system.build.vm;
nexcloudWithImagick = findInClosure "imagick" nodes.nextcloud.system.build.vm;
nextcloudWithoutImagick = findInClosure "imagick" nodes.nextcloud.specialisation.withoutMagick.configuration.system.build.vm;
in ''
assert open("${nextcloudUsesImagick}").read() != ""
assert open("${nextcloudWithoutDoesntUseIt}").read() == ""
with subtest("File is in proper nextcloud home"):
nextcloud.succeed("test -f ${nodes.nextcloud.services.nextcloud.datadir}/data/root/files/test-shared-file")
nextcloud.start()
client.start()
nextcloud.wait_for_unit("multi-user.target")
# This is just to ensure the nextcloud-occ program is working
nextcloud.succeed("nextcloud-occ status")
nextcloud.succeed("curl -sSf http://nextcloud/login")
# Ensure that no OpenSSL 1.1 is used.
nextcloud.succeed(
"${nodes.nextcloud.services.phpfpm.pools.nextcloud.phpPackage}/bin/php -i | grep 'OpenSSL Library Version' | awk -F'=>' '{ print $2 }' | awk '{ print $2 }' | grep -v 1.1"
)
nextcloud.succeed(
"${withRcloneEnv} ${copySharedFile}"
)
client.wait_for_unit("multi-user.target")
nextcloud.succeed("test -f /var/lib/nextcloud-data/data/root/files/test-shared-file")
client.succeed(
"${withRcloneEnv} ${diffSharedFile}"
)
assert "hi" in client.succeed("cat /mnt/dav/test-shared-file")
nextcloud.succeed("grep -vE '^HBEGIN:oc_encryption_module' /var/lib/nextcloud-data/data/root/files/test-shared-file")
with subtest("Closure checks"):
assert open("${nexcloudWithImagick}").read() != ""
assert open("${nextcloudWithoutImagick}").read() == ""
with subtest("Davfs2"):
assert "hi" in client.succeed("cat /mnt/dav/test-shared-file")
with subtest("Ensure SSE is disabled by default"):
nextcloud.succeed("grep -vE '^HBEGIN:oc_encryption_module' /var/lib/nextcloud-data/data/root/files/test-shared-file")
'';
})) args
})

View File

@ -5,21 +5,108 @@
with pkgs.lib;
foldl
(matrix: ver: matrix // {
"basic${toString ver}" = import ./basic.nix { inherit system pkgs; nextcloudVersion = ver; };
"with-postgresql-and-redis${toString ver}" = import ./with-postgresql-and-redis.nix {
inherit system pkgs;
nextcloudVersion = ver;
let
baseModule = { config, ... }: {
imports = [
{
options.test-helpers = {
rclone = mkOption { type = types.str; };
upload-sample = mkOption { type = types.str; };
check-sample = mkOption { type = types.str; };
init = mkOption { type = types.str; default = ""; };
extraTests = mkOption { type = types.either types.str (types.functionTo types.str); default = ""; };
};
options.adminuser = mkOption { type = types.str; };
options.adminpass = mkOption { type = types.str; };
}
];
adminuser = "root";
adminpass = "hunter2";
test-helpers.rclone = "${pkgs.writeShellScript "rclone" ''
set -euo pipefail
export PATH="${pkgs.rclone}/bin:$PATH"
export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/dav/files/${config.adminuser}"
export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
export RCLONE_CONFIG_NEXTCLOUD_USER="${config.adminuser}"
export RCLONE_CONFIG_NEXTCLOUD_PASS="$(rclone obscure ${config.adminpass})"
exec "$@"
''}";
test-helpers.upload-sample = "${pkgs.writeShellScript "rclone-upload" ''
<<<'hi' rclone rcat nextcloud:test-shared-file
''}";
test-helpers.check-sample = "${pkgs.writeShellScript "check-sample" ''
set -e
diff <(echo 'hi') <(rclone cat nextcloud:test-shared-file)
''}";
nodes = {
client = { ... }: {};
nextcloud = {
networking.firewall.allowedTCPPorts = [ 80 ];
services.nextcloud = {
enable = true;
hostName = "nextcloud";
https = false;
database.createLocally = true;
config = {
adminpassFile = "${pkgs.writeText "adminpass" config.adminpass}"; # Don't try this at home!
};
};
};
};
"with-mysql-and-memcached${toString ver}" = import ./with-mysql-and-memcached.nix {
inherit system pkgs;
nextcloudVersion = ver;
};
"with-declarative-redis-and-secrets${toString ver}" = import ./with-declarative-redis-and-secrets.nix {
inherit system pkgs;
nextcloudVersion = ver;
};
})
{ }
[ 27 28 29 ]
testScript = args@{ nodes, ... }: let
inherit (config) test-helpers;
in mkBefore ''
nextcloud.start()
client.start()
nextcloud.wait_for_unit("multi-user.target")
${test-helpers.init}
with subtest("Ensure nextcloud-occ is working"):
nextcloud.succeed("nextcloud-occ status")
nextcloud.succeed("curl -sSf http://nextcloud/login")
with subtest("Upload/Download test"):
nextcloud.succeed(
"${test-helpers.rclone} ${test-helpers.upload-sample}"
)
client.wait_for_unit("multi-user.target")
client.succeed(
"${test-helpers.rclone} ${test-helpers.check-sample}"
)
${if builtins.isFunction test-helpers.extraTests then test-helpers.extraTests args else test-helpers.extraTests}
'';
};
genTests = version:
let
testBase.imports = [
baseModule
{
nodes.nextcloud = { pkgs, ... }: {
services.nextcloud.package = pkgs.${"nextcloud${toString version}"};
};
}
];
callNextcloudTest = path:
let
name = "${removeSuffix ".nix" (baseNameOf path)}${toString version}";
in nameValuePair name (import path {
inherit system pkgs testBase;
name = "nextcloud-${name}";
});
in map callNextcloudTest [
./basic.nix
./with-mysql-and-memcached.nix
./with-postgresql-and-redis.nix
./with-objectstore.nix
];
in
listToAttrs (concatMap genTests [ 27 28 29 ])

View File

@ -1,79 +1,37 @@
args@{ pkgs, nextcloudVersion ? 22, ... }:
{ pkgs, testBase, system, ... }:
(import ../make-test-python.nix ({ pkgs, ...}: let
adminpass = "hunter2";
adminuser = "root";
in {
with import ../../lib/testing-python.nix { inherit system pkgs; };
runTest ({ config, ... }: {
name = "nextcloud-with-mysql-and-memcached";
meta = with pkgs.lib.maintainers; {
maintainers = [ eqyiel ];
};
imports = [ testBase ];
nodes = {
# The only thing the client needs to do is download a file.
client = { ... }: {};
nextcloud = { config, pkgs, ... }: {
networking.firewall.allowedTCPPorts = [ 80 ];
services.nextcloud = {
enable = true;
hostName = "nextcloud";
https = true;
package = pkgs.${"nextcloud" + (toString nextcloudVersion)};
caching = {
apcu = true;
redis = false;
memcached = true;
};
database.createLocally = true;
config = {
dbtype = "mysql";
# Don't inherit adminuser since "root" is supposed to be the default
adminpassFile = "${pkgs.writeText "adminpass" adminpass}"; # Don't try this at home!
};
config.dbtype = "mysql";
};
services.memcached.enable = true;
};
};
testScript = let
test-helpers.init = let
configureMemcached = pkgs.writeScript "configure-memcached" ''
#!${pkgs.runtimeShell}
nextcloud-occ config:system:set memcached_servers 0 0 --value 127.0.0.1 --type string
nextcloud-occ config:system:set memcached_servers 0 1 --value 11211 --type integer
nextcloud-occ config:system:set memcache.local --value '\OC\Memcache\APCu' --type string
nextcloud-occ config:system:set memcache.distributed --value '\OC\Memcache\Memcached' --type string
'';
withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
#!${pkgs.runtimeShell}
export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/dav/files/${adminuser}"
export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
export RCLONE_CONFIG_NEXTCLOUD_USER="${adminuser}"
export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${adminpass})"
'';
copySharedFile = pkgs.writeScript "copy-shared-file" ''
#!${pkgs.runtimeShell}
echo 'hi' | ${pkgs.rclone}/bin/rclone rcat nextcloud:test-shared-file
'';
diffSharedFile = pkgs.writeScript "diff-shared-file" ''
#!${pkgs.runtimeShell}
diff <(echo 'hi') <(${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file)
'';
in ''
start_all()
nextcloud.wait_for_unit("multi-user.target")
nextcloud.succeed("${configureMemcached}")
nextcloud.succeed("curl -sSf http://nextcloud/login")
nextcloud.succeed(
"${withRcloneEnv} ${copySharedFile}"
)
client.wait_for_unit("multi-user.target")
client.succeed(
"${withRcloneEnv} ${diffSharedFile}"
)
'';
})) args
})

View File

@ -0,0 +1,96 @@
{ name, pkgs, testBase, system, ... }:
with import ../../lib/testing-python.nix { inherit system pkgs; };
runTest ({ config, lib, ... }: let
accessKey = "BKIKJAA5BMMU2RHO6IBB";
secretKey = "V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12";
rootCredentialsFile = pkgs.writeText "minio-credentials-full" ''
MINIO_ROOT_USER=${accessKey}
MINIO_ROOT_PASSWORD=${secretKey}
'';
in {
inherit name;
meta = with pkgs.lib.maintainers; {
maintainers = [ onny ma27 ];
};
imports = [ testBase ];
nodes = {
nextcloud = { config, pkgs, ... }: {
networking.firewall.allowedTCPPorts = [ 9000 ];
environment.systemPackages = [ pkgs.minio-client ];
services.nextcloud.config.objectstore.s3 = {
enable = true;
bucket = "nextcloud";
autocreate = true;
key = accessKey;
secretFile = "${pkgs.writeText "secretKey" secretKey}";
hostname = "nextcloud";
useSsl = false;
port = 9000;
usePathStyle = true;
region = "us-east-1";
};
services.minio = {
enable = true;
listenAddress = "0.0.0.0:9000";
consoleAddress = "0.0.0.0:9001";
inherit rootCredentialsFile;
};
};
};
test-helpers.init = ''
nextcloud.wait_for_open_port(9000)
'';
test-helpers.extraTests = { nodes, ... }: ''
with subtest("File is not on the filesystem"):
nextcloud.succeed("test ! -e ${nodes.nextcloud.services.nextcloud.home}/data/root/files/test-shared-file")
with subtest("Check if file is in S3"):
nextcloud.succeed(
"mc config host add minio http://localhost:9000 ${accessKey} ${secretKey} --api s3v4"
)
files = nextcloud.succeed('mc ls minio/nextcloud|sort').strip().split('\n')
# Cannot assert an exact number here, nc27 writes more stuff initially into S3.
# For now let's assume it's always the most recently added file.
assert len(files) > 0, f"""
Expected to have at least one object in minio/nextcloud. But `mc ls` gave output:
'{files}'
"""
import re
ptrn = re.compile("^\[[A-Z0-9 :-]+\] +(?P<details>[A-Za-z0-9 :]+)$")
match = ptrn.match(files[-1].strip())
assert match, "Cannot match mc client output!"
size, type_, file = tuple(match.group('details').split(' '))
assert size == "3B", f"""
Expected size of uploaded file to be 3 bytes, got {size}
"""
assert type_ == 'STANDARD', f"""
Expected type of bucket entry to be a file, i.e. 'STANDARD'. Got {type_}
"""
assert file.startswith('urn:oid'), """
Expected filename to start with 'urn:oid', instead got '{file}.
"""
with subtest("Test download from S3"):
client.succeed(
"env AWS_ACCESS_KEY_ID=${accessKey} AWS_SECRET_ACCESS_KEY=${secretKey} "
+ f"${lib.getExe pkgs.awscli2} s3 cp s3://nextcloud/{file} test --endpoint-url http://nextcloud:9000 "
+ "--region us-east-1"
)
client.succeed("test hi = $(cat test)")
'';
})

View File

@ -1,45 +1,30 @@
args@{ pkgs, nextcloudVersion ? 22, ... }:
{ name, pkgs, testBase, system, ... }:
(import ../make-test-python.nix ({ pkgs, ...}: let
adminpass = "hunter2";
adminuser = "custom-admin-username";
in {
name = "nextcloud-with-postgresql-and-redis";
with import ../../lib/testing-python.nix { inherit system pkgs; };
runTest ({ config, ... }: {
inherit name;
meta = with pkgs.lib.maintainers; {
maintainers = [ eqyiel ];
maintainers = [ eqyiel ma27 ];
};
imports = [ testBase ];
nodes = {
# The only thing the client needs to do is download a file.
client = { ... }: {};
nextcloud = { config, pkgs, lib, ... }: {
networking.firewall.allowedTCPPorts = [ 80 ];
services.nextcloud = {
enable = true;
hostName = "nextcloud";
package = pkgs.${"nextcloud" + (toString nextcloudVersion)};
caching = {
apcu = false;
redis = true;
memcached = false;
};
database.createLocally = true;
config = {
dbtype = "pgsql";
inherit adminuser;
adminpassFile = toString (pkgs.writeText "admin-pass-file" ''
${adminpass}
'');
};
config.dbtype = "pgsql";
notify_push = {
enable = true;
logLevel = "debug";
};
extraAppsEnable = true;
extraApps = {
inherit (pkgs."nextcloud${lib.versions.major config.services.nextcloud.package.version}Packages".apps) notify_push notes;
extraApps = with config.services.nextcloud.package.packages.apps; {
inherit notify_push notes;
};
settings.trusted_proxies = [ "::1" ];
};
@ -49,50 +34,27 @@ in {
};
};
testScript = let
test-helpers.init = let
configureRedis = pkgs.writeScript "configure-redis" ''
#!${pkgs.runtimeShell}
nextcloud-occ config:system:set redis 'host' --value 'localhost' --type string
nextcloud-occ config:system:set redis 'port' --value 6379 --type integer
nextcloud-occ config:system:set memcache.local --value '\OC\Memcache\Redis' --type string
nextcloud-occ config:system:set memcache.locking --value '\OC\Memcache\Redis' --type string
'';
withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
#!${pkgs.runtimeShell}
export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/dav/files/${adminuser}"
export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
export RCLONE_CONFIG_NEXTCLOUD_USER="${adminuser}"
export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${adminpass})"
"''${@}"
'';
copySharedFile = pkgs.writeScript "copy-shared-file" ''
#!${pkgs.runtimeShell}
echo 'hi' | ${pkgs.rclone}/bin/rclone rcat nextcloud:test-shared-file
'';
diffSharedFile = pkgs.writeScript "diff-shared-file" ''
#!${pkgs.runtimeShell}
diff <(echo 'hi') <(${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file)
'';
in ''
start_all()
nextcloud.wait_for_unit("multi-user.target")
nextcloud.succeed("${configureRedis}")
nextcloud.succeed("curl -sSf http://nextcloud/login")
nextcloud.succeed(
"${withRcloneEnv} ${copySharedFile}"
)
client.wait_for_unit("multi-user.target")
client.execute("${pkgs.lib.getExe pkgs.nextcloud-notify_push.passthru.test_client} http://nextcloud ${adminuser} ${adminpass} >&2 &")
client.succeed(
"${withRcloneEnv} ${diffSharedFile}"
)
nextcloud.wait_until_succeeds("journalctl -u nextcloud-notify_push | grep -q \"Sending ping to ${adminuser}\"")
# redis cache should not be empty
nextcloud.fail('test "[]" = "$(redis-cli --json KEYS "*")"')
nextcloud.fail("curl -f http://nextcloud/nix-apps/notes/lib/AppInfo/Application.php")
'';
})) args
test-helpers.extraTests = ''
with subtest("notify-push"):
client.execute("${pkgs.lib.getExe pkgs.nextcloud-notify_push.passthru.test_client} http://nextcloud ${config.adminuser} ${config.adminpass} >&2 &")
nextcloud.wait_until_succeeds("journalctl -u nextcloud-notify_push | grep -q \"Sending ping to ${config.adminuser}\"")
with subtest("Redis is used for caching"):
# redis cache should not be empty
nextcloud.fail('test "[]" = "$(redis-cli --json KEYS "*")"')
with subtest("No code is returned when requesting PHP files (regression test)"):
nextcloud.fail("curl -f http://nextcloud/nix-apps/notes/lib/AppInfo/Application.php")
'';
})