nixos/opencloud: init module

Co-authored-by: Christoph Heiss <christoph@c8h4.io>

K900 b504cc13 3c20fdf1

Changed files
+426
nixos
+6
nixos/doc/manual/redirects.json
···
"module-services-crab-hole-upstream-options": [
"index.html#module-services-crab-hole-upstream-options"
],
"module-services-strfry": [
"index.html#module-services-strfry"
],
···
"module-services-crab-hole-upstream-options": [
"index.html#module-services-crab-hole-upstream-options"
],
+
"module-services-opencloud": [
+
"index.html#module-services-opencloud"
+
],
+
"module-services-opencloud-basic-usage": [
+
"index.html#module-services-opencloud-basic-usage"
+
],
"module-services-strfry": [
"index.html#module-services-strfry"
],
+2
nixos/doc/manual/release-notes/rl-2505.section.md
···
- [`g3proxy`](https://github.com/bytedance/g3), an open source enterprise forward proxy from ByteDance, similar to Squid or tinyproxy. Available as [services.g3proxy](#opt-services.g3proxy.enable).
- [echoip](https://github.com/mpolden/echoip), a simple service for looking up your IP address. Available as [services.echoip](#opt-services.echoip.enable).
- [whoami](https://github.com/traefik/whoami), a tiny Go server that prints OS information and HTTP request to output. Available as [services.whoami](#opt-services.whoami.enable).
···
- [`g3proxy`](https://github.com/bytedance/g3), an open source enterprise forward proxy from ByteDance, similar to Squid or tinyproxy. Available as [services.g3proxy](#opt-services.g3proxy.enable).
+
- [OpenCloud](https://opencloud.eu/), an open-source, modern file-sync and sharing platform. It is a fork of oCIS, a ground-up rewrite of the well-known PHP-based NextCloud server. Available as [services.opencloud](#opt-services.opencloud.enable).
+
- [echoip](https://github.com/mpolden/echoip), a simple service for looking up your IP address. Available as [services.echoip](#opt-services.echoip.enable).
- [whoami](https://github.com/traefik/whoami), a tiny Go server that prints OS information and HTTP request to output. Available as [services.whoami](#opt-services.whoami.enable).
+1
nixos/modules/module-list.nix
···
./services/web-apps/oncall.nix
./services/web-apps/onlyoffice.nix
./services/web-apps/open-web-calendar.nix
./services/web-apps/openvscode-server.nix
./services/web-apps/openwebrx.nix
./services/web-apps/outline.nix
···
./services/web-apps/oncall.nix
./services/web-apps/onlyoffice.nix
./services/web-apps/open-web-calendar.nix
+
./services/web-apps/opencloud.nix
./services/web-apps/openvscode-server.nix
./services/web-apps/openwebrx.nix
./services/web-apps/outline.nix
+64
nixos/modules/services/web-apps/opencloud.md
···
···
+
# OpenCloud {#module-services-opencloud}
+
+
[OpenCloud](https://opencloud.eu/en) is an open-source, modern file-sync and
+
sharing platform. It is a fork of oCIS, a ground-up rewrite of the well-known
+
PHP-based NextCloud server.
+
+
The service can be configured using a combination of [](#opt-services.opencloud.settings),
+
[](#opt-services.opencloud.environment) and [](#opt-services.opencloud.environmentFile).
+
+
## Basic usage {#module-services-opencloud-basic-usage}
+
+
OpenCloud is configured using a combination of YAML and environment
+
variables. The full documentation can be found at
+
[OpenCloud Admin Docs](https://docs.opencloud.eu/docs/admin/intro).
+
+
The general flow of configuring OpenCloud is:
+
- configure services with `services.opencloud.settings.<service>` when possible
+
- configure global settings that affect multiple services via `services.opencloud.environment`
+
- allow NixOS to provision a default `opencloud.yaml` for you, containing default credentials
+
for communication between the microservices
+
- provide additional secrets via `environmentFile`, provisioned out of band
+
+
Please note that current NixOS module for OpenCloud is configured to run in
+
`fullstack` mode, which starts all the services for OpenCloud in a single
+
instance, in so called supervised mode. This will start multiple OpenCloud
+
services and listen on multiple other ports.
+
+
Current known services and their ports are as below:
+
+
| Service | Group | Port |
+
|--------------------|---------|-------|
+
| gateway | api | 9142 |
+
| sharing | api | 9150 |
+
| app-registry | api | 9242 |
+
| ocdav | web | 45023 |
+
| auth-machine | api | 9166 |
+
| storage-system | api | 9215 |
+
| webdav | web | 9115 |
+
| webfinger | web | 46871 |
+
| storage-system | web | 9216 |
+
| web | web | 9100 |
+
| eventhistory | api | 33177 |
+
| ocs | web | 9110 |
+
| storage-publiclink | api | 9178 |
+
| settings | web | 9190 |
+
| ocm | api | 9282 |
+
| settings | api | 9191 |
+
| ocm | web | 9280 |
+
| app-provider | api | 9164 |
+
| storage-users | api | 9157 |
+
| auth-service | api | 9199 |
+
| thumbnails | web | 9186 |
+
| thumbnails | api | 9185 |
+
| storage-shares | api | 9154 |
+
| sse | sse | 46833 |
+
| userlog | userlog | 45363 |
+
| search | api | 9220 |
+
| proxy | web | 9200 |
+
| idp | web | 9130 |
+
| frontend | web | 9140 |
+
| groups | api | 9160 |
+
| graph | graph | 9120 |
+
| users | api | 9144 |
+
| auth-basic | api | 9146 |
+242
nixos/modules/services/web-apps/opencloud.nix
···
···
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
+
let
+
inherit (lib) types;
+
cfg = config.services.opencloud;
+
+
defaultUser = "opencloud";
+
defaultGroup = defaultUser;
+
+
settingsFormat = pkgs.formats.yaml { };
+
in
+
{
+
options = {
+
services.opencloud = {
+
enable = lib.mkEnableOption "OpenCloud";
+
+
package = lib.mkPackageOption pkgs "opencloud" { };
+
webPackage = lib.mkPackageOption pkgs [ "opencloud" "web" ] { };
+
idpWebPackage = lib.mkPackageOption pkgs [ "opencloud" "idp-web" ] { };
+
+
user = lib.mkOption {
+
type = types.str;
+
default = defaultUser;
+
example = "mycloud";
+
description = ''
+
The user to run OpenCloud as.
+
By default, a user named `${defaultUser}` will be created whose home
+
directory is [](#opt-services.opencloud.stateDir).
+
'';
+
};
+
+
group = lib.mkOption {
+
type = types.str;
+
default = defaultGroup;
+
example = "mycloud";
+
description = ''
+
The group to run OpenCloud under.
+
By default, a group named `${defaultGroup}` will be created.
+
'';
+
};
+
+
address = lib.mkOption {
+
type = types.str;
+
default = "127.0.0.1";
+
description = "Web server bind address.";
+
};
+
+
port = lib.mkOption {
+
type = types.port;
+
default = 9200;
+
description = "Web server port.";
+
};
+
+
url = lib.mkOption {
+
type = types.str;
+
default = "https://localhost:9200";
+
example = "https://cloud.example.com";
+
description = "Web interface root public URL, including scheme and port (if non-default).";
+
};
+
+
stateDir = lib.mkOption {
+
default = "/var/lib/opencloud";
+
type = types.str;
+
description = "OpenCloud data directory.";
+
};
+
+
settings = lib.mkOption {
+
type = lib.types.attrsOf settingsFormat.type;
+
default = { };
+
description = ''
+
Additional YAML configuration for OpenCloud services.
+
+
Every item in this attrset will be mapped to a .yaml file in /etc/opencloud.
+
+
The possible config options are currently not well documented, see source code:
+
https://github.com/opencloud-eu/opencloud/blob/main/pkg/config/config.go
+
'';
+
};
+
+
environmentFile = lib.mkOption {
+
type = types.nullOr types.path;
+
default = null;
+
example = "/run/keys/opencloud.env";
+
description = ''
+
An environment file as defined in {manpage}`systemd.exec(5)`.
+
+
Use this to inject secrets, e.g. database or auth credentials out of band.
+
+
Configuration provided here will override `settings` and `environment`.
+
'';
+
};
+
+
environment = lib.mkOption {
+
type = types.attrsOf types.str;
+
default = {
+
OC_INSECURE = "true";
+
};
+
description = ''
+
Extra environment variables to set for the service.
+
+
Use this to set configuration that may affect multiple microservices.
+
+
Configuration provided here will override `settings`.
+
'';
+
example = {
+
OC_INSECURE = "false";
+
OC_LOG_LEVEL = "error";
+
};
+
};
+
};
+
};
+
+
config = lib.mkIf cfg.enable {
+
users.users.${defaultUser} = lib.mkIf (cfg.user == defaultUser) {
+
group = cfg.group;
+
home = cfg.stateDir;
+
isSystemUser = true;
+
createHome = true;
+
description = "OpenCloud daemon user";
+
};
+
+
users.groups = lib.mkIf (cfg.group == defaultGroup) { ${defaultGroup} = { }; };
+
+
systemd = {
+
services =
+
let
+
environment = {
+
PROXY_HTTP_ADDR = "${cfg.address}:${toString cfg.port}";
+
OC_URL = cfg.url;
+
OC_BASE_DATA_PATH = cfg.stateDir;
+
WEB_ASSET_CORE_PATH = "${cfg.webPackage}";
+
IDP_ASSET_PATH = "${cfg.idpWebPackage}/assets";
+
OC_CONFIG_DIR = "/etc/opencloud";
+
} // cfg.environment;
+
commonServiceConfig = {
+
EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
+
MemoryDenyWriteExecute = true;
+
NoNewPrivileges = true;
+
PrivateTmp = true;
+
PrivateDevices = true;
+
ProtectSystem = "strict";
+
ProtectHome = true;
+
ProtectControlGroups = true;
+
ProtectKernelModules = true;
+
ProtectKernelTunables = true;
+
ProtectKernelLogs = true;
+
RestrictAddressFamilies = [
+
"AF_UNIX"
+
"AF_INET"
+
"AF_INET6"
+
];
+
RestrictNamespaces = true;
+
RestrictRealtime = true;
+
RestrictSUIDSGID = true;
+
LockPersonality = true;
+
SystemCallArchitectures = "native";
+
};
+
in
+
{
+
opencloud-init-config = lib.mkIf (cfg.settings.opencloud or { } == { }) {
+
description = "Provision initial OpenCloud config";
+
before = [ "opencloud.service" ];
+
wantedBy = [ "multi-user.target" ];
+
+
inherit environment;
+
+
serviceConfig = {
+
Type = "oneshot";
+
ReadWritePaths = [ "/etc/opencloud" ];
+
} // commonServiceConfig;
+
+
path = [ cfg.package ];
+
script = ''
+
set -x
+
config="''${OC_CONFIG_DIR}/opencloud.yaml"
+
if [ ! -e "$config" ]; then
+
echo "Provisioning initial OpenCloud config..."
+
opencloud init --insecure "''${OC_INSECURE:false}" --config-path "''${OC_CONFIG_DIR}"
+
chown ${cfg.user}:${cfg.group} "$config"
+
fi
+
'';
+
};
+
+
opencloud = {
+
description = "OpenCloud - a secure and private way to store, access, and share your files";
+
after = [ "network.target" ];
+
wantedBy = [ "multi-user.target" ];
+
+
inherit environment;
+
+
serviceConfig = {
+
Type = "simple";
+
ExecStart = "${lib.getExe cfg.package} server";
+
WorkingDirectory = cfg.stateDir;
+
User = cfg.user;
+
Group = cfg.group;
+
Restart = "always";
+
ReadWritePaths = [ cfg.stateDir ];
+
} // commonServiceConfig;
+
+
restartTriggers = lib.mapAttrsToList (
+
name: _: config.environment.etc."opencloud/${name}.yaml".source
+
) cfg.settings;
+
};
+
};
+
};
+
+
systemd.tmpfiles.settings."10-opencloud" = {
+
${cfg.stateDir}.d = {
+
inherit (cfg) user group;
+
mode = "0750";
+
};
+
"${cfg.stateDir}/idm".d = {
+
inherit (cfg) user group;
+
mode = "0750";
+
};
+
};
+
+
environment.etc =
+
(lib.mapAttrs' (name: value: {
+
name = "opencloud/${name}.yaml";
+
value.source = settingsFormat.generate "${name}.yaml" value;
+
}) cfg.settings)
+
// {
+
# ensure /etc/opencloud gets created, so we can provision the config
+
"opencloud/.keep".text = "";
+
};
+
};
+
+
meta = {
+
doc = ./opencloud.md;
+
maintainers = with lib.maintainers; [
+
christoph-heiss
+
k900
+
];
+
};
+
}
+1
nixos/tests/all-tests.nix
···
ombi = handleTest ./ombi.nix { };
openarena = handleTest ./openarena.nix { };
openbao = runTest ./openbao.nix;
openldap = handleTest ./openldap.nix { };
opensearch = discoverTests (import ./opensearch.nix);
openresty-lua = handleTest ./openresty-lua.nix { };
···
ombi = handleTest ./ombi.nix { };
openarena = handleTest ./openarena.nix { };
openbao = runTest ./openbao.nix;
+
opencloud = runTest ./opencloud.nix;
openldap = handleTest ./openldap.nix { };
opensearch = discoverTests (import ./opensearch.nix);
openresty-lua = handleTest ./openresty-lua.nix { };
+110
nixos/tests/opencloud.nix
···
···
+
{ lib, pkgs, ... }:
+
+
let
+
certs = import ./common/acme/server/snakeoil-certs.nix;
+
inherit (certs) domain;
+
+
# this is a demo user created by IDM_CREATE_DEMO_USERS=true
+
demoUser = "alan";
+
demoPassword = "demo";
+
+
adminUser = "admin";
+
adminPassword = "hunter2";
+
testRunner =
+
pkgs.writers.writePython3Bin "test-runner"
+
{
+
libraries = [ pkgs.python3Packages.selenium ];
+
flakeIgnore = [ "E501" ];
+
}
+
''
+
import sys
+
from selenium.webdriver.common.by import By
+
from selenium.webdriver import Firefox
+
from selenium.webdriver.firefox.options import Options
+
from selenium.webdriver.support.ui import WebDriverWait
+
from selenium.webdriver.support import expected_conditions as EC
+
+
options = Options()
+
options.add_argument('--headless')
+
driver = Firefox(options=options)
+
+
host = sys.argv[1]
+
user = sys.argv[2]
+
password = sys.argv[3]
+
+
driver.get(f"https://{host}/")
+
wait = WebDriverWait(driver, 60)
+
wait.until(EC.title_contains("Sign in"))
+
wait.until(EC.url_contains(f"https://{host}/signin/v1/identifier"))
+
wait.until(EC.visibility_of_element_located((By.ID, 'oc-login-username')))
+
driver.find_element(By.ID, 'oc-login-username').send_keys(user)
+
driver.find_element(By.ID, 'oc-login-password').send_keys(password)
+
wait.until(EC.visibility_of_element_located((By.XPATH, '//button[@type="submit"]')))
+
driver.find_element(By.XPATH, '//button[@type="submit"]').click()
+
wait.until(EC.visibility_of_element_located((By.ID, 'new-file-menu-btn')))
+
wait.until(EC.title_contains("Personal"))
+
'';
+
in
+
+
{
+
name = "opencloud";
+
+
meta.maintainers = with lib.maintainers; [
+
christoph-heiss
+
k900
+
];
+
+
nodes.machine = {
+
virtualisation.memorySize = 2048;
+
environment.systemPackages = [
+
pkgs.firefox-unwrapped
+
pkgs.geckodriver
+
testRunner
+
];
+
+
networking.hosts."127.0.0.1" = [ domain ];
+
security.pki.certificateFiles = [ certs.ca.cert ];
+
+
services.opencloud = {
+
enable = true;
+
url = "https://${domain}:9200";
+
environment = {
+
ADMIN_PASSWORD = adminPassword;
+
IDM_CREATE_DEMO_USERS = "true";
+
IDM_LDAPS_CERT = "${certs.${domain}.cert}";
+
IDM_LDAPS_KEY = "${certs.${domain}.key}";
+
OC_INSECURE = "false";
+
OC_LDAP_URI = "ldaps://${domain}:9235";
+
OC_LDAP_CACERT = "${certs.${domain}.cert}";
+
OC_HTTP_TLS_ENABLED = "true";
+
OC_HTTP_TLS_CERTIFICATE = "${certs.${domain}.cert}";
+
OC_HTTP_TLS_KEY = "${certs.${domain}.key}";
+
PROXY_TLS = "true";
+
PROXY_TRANSPORT_TLS_CERT = "${certs.${domain}.cert}";
+
PROXY_TRANSPORT_TLS_KEY = "${certs.${domain}.key}";
+
PROXY_INSECURE_BACKENDS = "true";
+
};
+
};
+
};
+
+
testScript = ''
+
start_all()
+
machine.wait_for_unit("opencloud.service")
+
machine.wait_for_open_port(9200)
+
+
# wait for OpenCloud to fully come up
+
machine.sleep(10)
+
+
with subtest("opencloud bin works"):
+
machine.succeed("${lib.getExe pkgs.opencloud} version")
+
+
with subtest("web interface presents start page"):
+
machine.succeed("curl -sSf https://${domain}:9200 | grep '<title>OpenCloud</title>'")
+
+
with subtest("use the web interface to log in with the provisioned admin user"):
+
machine.succeed("PYTHONUNBUFFERED=1 systemd-cat -t test-runner test-runner ${domain}:9200 ${adminUser} ${adminPassword}")
+
+
with subtest("use the web interface to log in with a demo user"):
+
machine.succeed("PYTHONUNBUFFERED=1 systemd-cat -t test-runner test-runner ${domain}:9200 ${demoUser} ${demoPassword}")
+
'';
+
}