mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-08-25 17:55:21 +00:00
nixos/rundeck: init (#375846)
This commit is contained in:
@@ -108,6 +108,8 @@
|
||||
|
||||
- [Umbriel](https://docs.noctalia.dev/umbriel/), a Wayland compositor built on wlroots and SceneFX. Available as [programs.umbriel](#opt-programs.umbriel.enable).
|
||||
|
||||
- [Rundeck](https://www.rundeck.com), Self-Service Operations [services.rundeck](#opt-services.rundeck.enable).
|
||||
|
||||
## Backward Incompatibilities {#sec-release-26.11-incompatibilities}
|
||||
|
||||
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
|
||||
|
||||
@@ -1824,6 +1824,7 @@
|
||||
./services/web-apps/romm.nix
|
||||
./services/web-apps/rss-bridge.nix
|
||||
./services/web-apps/rsshub.nix
|
||||
./services/web-apps/rundeck.nix
|
||||
./services/web-apps/rustical.nix
|
||||
./services/web-apps/rutorrent.nix
|
||||
./services/web-apps/screego.nix
|
||||
|
||||
651
nixos/modules/services/web-apps/rundeck.nix
Normal file
651
nixos/modules/services/web-apps/rundeck.nix
Normal file
@@ -0,0 +1,651 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.services.rundeck;
|
||||
settingsFormat = pkgs.formats.javaProperties { };
|
||||
effectivePort = if cfg.ssl.enable then cfg.ssl.port else cfg.serverPort;
|
||||
scheme = if cfg.ssl.enable then "https" else "http";
|
||||
|
||||
configFile = settingsFormat.generate "rundeck-config.properties" cfg.settings;
|
||||
frameworkFile = settingsFormat.generate "framework.properties" cfg.frameworkSettings;
|
||||
|
||||
realmFile = pkgs.writeText "realm.properties" ''
|
||||
${cfg.adminUser}:@ADMIN_PASSWORD@,user,admin
|
||||
'';
|
||||
|
||||
replaceSecret =
|
||||
placeholder: file: target:
|
||||
"replace-secret ${
|
||||
lib.escapeShellArgs [
|
||||
placeholder
|
||||
file
|
||||
target
|
||||
]
|
||||
}";
|
||||
|
||||
rundeckStartScript = pkgs.writeShellScript "start-rundeck" ''
|
||||
# Generate SSH
|
||||
if [ ! -f ${cfg.dataDir}/.ssh/id_${cfg.sshKeyType} ]; then
|
||||
umask 0077
|
||||
${lib.getExe' pkgs.openssh "ssh-keygen"} -t ${cfg.sshKeyType} ${
|
||||
lib.optionalString (cfg.sshKeyType == "rsa") "-b 4096"
|
||||
} -N "" -f ${cfg.dataDir}/.ssh/id_${cfg.sshKeyType}
|
||||
chmod 644 ${cfg.dataDir}/.ssh/id_${cfg.sshKeyType}.pub
|
||||
chown ${cfg.user}:${cfg.group} ${cfg.dataDir}/.ssh/id_${cfg.sshKeyType} ${cfg.dataDir}/.ssh/id_${cfg.sshKeyType}.pub
|
||||
fi
|
||||
|
||||
${lib.getExe cfg.package} \
|
||||
--skipinstall \
|
||||
-b ${cfg.dataDir} \
|
||||
-c ${cfg.configDir} \
|
||||
-p ${cfg.dataDir}/projects
|
||||
'';
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.rundeck = {
|
||||
enable = lib.mkEnableOption "Rundeck service";
|
||||
|
||||
package = lib.mkPackageOption pkgs "rundeck" { };
|
||||
|
||||
adminUser = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "admin";
|
||||
description = "Username for the Rundeck admin user";
|
||||
example = "rundeck-admin";
|
||||
};
|
||||
|
||||
adminPasswordFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = "Path to a file containing the admin password";
|
||||
example = "/run/secrets/rundeck-admin-password";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "rundeck";
|
||||
description = "User account under which Rundeck runs";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "rundeck";
|
||||
description = "Group account under which Rundeck runs";
|
||||
};
|
||||
|
||||
serverHostname = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "localhost";
|
||||
description = "Hostname for the Rundeck server";
|
||||
};
|
||||
|
||||
serverURL = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "${scheme}://${cfg.serverHostname}:${toString effectivePort}";
|
||||
defaultText = lib.literalMD ''
|
||||
`<scheme>://<serverHostname>:<port>`, where scheme is `https` when
|
||||
`ssl.enable` else `http`, and port is `ssl.port` when `ssl.enable`
|
||||
else `serverPort`.
|
||||
'';
|
||||
description = "Complete Grails server URL";
|
||||
example = "https://myhost:4443/rundeck";
|
||||
};
|
||||
|
||||
serverPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 4440;
|
||||
description = "Port on which Rundeck will listen";
|
||||
};
|
||||
|
||||
serverUUID = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
description = "UUID for the Rundeck server (automatically generated if not specified)";
|
||||
};
|
||||
|
||||
dataDir = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = "/var/lib/rundeck";
|
||||
description = "Directory for Rundeck runtime data (RDECK_BASE)";
|
||||
};
|
||||
|
||||
configDir = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = "/etc/rundeck";
|
||||
description = "Directory for Rundeck configuration files";
|
||||
};
|
||||
|
||||
javaOpts = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [
|
||||
"-Xmx1024m"
|
||||
"-Xms256m"
|
||||
"-XX:MaxMetaspaceSize=256m"
|
||||
"-server"
|
||||
];
|
||||
description = "Additional Java options for Rundeck";
|
||||
};
|
||||
|
||||
aclPolicies = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
description = "ACL policies for Rundeck, where the attribute name is the filename and the value is the policy content";
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"admin.aclpolicy" = '''
|
||||
description: Admin access for administrators
|
||||
context:
|
||||
project: '.*'
|
||||
for:
|
||||
resource:
|
||||
- allow: '*'
|
||||
job:
|
||||
- allow: '*'
|
||||
node:
|
||||
- allow: '*'
|
||||
by:
|
||||
group: admin
|
||||
---
|
||||
description: Admin access in application scope
|
||||
context:
|
||||
application: 'rundeck'
|
||||
for:
|
||||
resource:
|
||||
- allow: '*'
|
||||
project:
|
||||
- allow: '*'
|
||||
by:
|
||||
group: admin
|
||||
''';
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
sshKeyType = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"rsa"
|
||||
"ed25519"
|
||||
];
|
||||
default = "rsa";
|
||||
description = "Type of SSH key to generate (rsa for compatibility, ed25519 for better security)";
|
||||
};
|
||||
|
||||
openFirewall = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Whether to open the Rundeck port in the firewall";
|
||||
};
|
||||
|
||||
startTimeout = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 180;
|
||||
description = "Timeout in seconds before systemd considers the service startup as failed";
|
||||
example = 120;
|
||||
};
|
||||
|
||||
database = {
|
||||
type = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"h2"
|
||||
"postgresql"
|
||||
"mysql"
|
||||
];
|
||||
default = "h2";
|
||||
description = "Database type to use (h2, postgresql, or mysql)";
|
||||
};
|
||||
|
||||
host = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "localhost";
|
||||
description = "Database host";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.port;
|
||||
default =
|
||||
if cfg.database.type == "postgresql" then
|
||||
5432
|
||||
else if cfg.database.type == "mysql" then
|
||||
3306
|
||||
else
|
||||
null;
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.services.rundeck.database.type == "postgresql" then
|
||||
5432
|
||||
else if config.services.rundeck.database.type == "mysql" then
|
||||
3306
|
||||
else
|
||||
null
|
||||
'';
|
||||
description = "Database port (defaults: PostgreSQL: 5432, MySQL: 3306)";
|
||||
};
|
||||
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "rundeck";
|
||||
description = "Database name";
|
||||
};
|
||||
|
||||
username = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "rundeck";
|
||||
description = "Database username";
|
||||
};
|
||||
|
||||
passwordFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = "Path to a file containing the database password";
|
||||
example = "/run/secrets/rundeck-db-password";
|
||||
};
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.submodule {
|
||||
freeformType = settingsFormat.type;
|
||||
};
|
||||
default = { };
|
||||
description = ''
|
||||
Configuration written to `rundeck-config.properties`.
|
||||
See <https://docs.rundeck.com/docs/administration/configuration/config-file-reference.html>
|
||||
for available options.
|
||||
|
||||
Secrets must not be set here, as this ends up world-readable in the Nix
|
||||
store. Use the dedicated `*File` options instead.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"rundeck.feature.repository.enabled" = "true";
|
||||
"rundeck.projectsStorageType" = "db";
|
||||
"rundeck.gui.title" = "My Rundeck Instance";
|
||||
"rdeck.security.useHMacRequestTokens" = "true";
|
||||
"rundeck.web.jetty.servlet.MaxFormKeys" = "2000";
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
frameworkSettings = lib.mkOption {
|
||||
type = lib.types.submodule {
|
||||
freeformType = settingsFormat.type;
|
||||
};
|
||||
default = { };
|
||||
description = ''
|
||||
Configuration written to `framework.properties`.
|
||||
See <https://docs.rundeck.com/docs/administration/configuration/config-file-reference.html>
|
||||
for available options.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"framework.ssh.timeout" = "120";
|
||||
"framework.ssh.user" = "deploy";
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
ssl = {
|
||||
enable = lib.mkEnableOption "SSL support";
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 4443;
|
||||
description = "Port on which Rundeck will listen for HTTPS when ssl.enable is true";
|
||||
};
|
||||
|
||||
keyStore = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
example = "/etc/rundeck/ssl/keystore";
|
||||
description = "Path to the keystore containing the SSL certificate";
|
||||
};
|
||||
|
||||
keyStorePasswordFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = "Path to a file containing the SSL keystore password";
|
||||
example = "/run/secrets/rundeck-keystore-password";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion =
|
||||
cfg.database.type == "h2"
|
||||
|| (cfg.database.host != "" && cfg.database.username != "" && cfg.database.passwordFile != null);
|
||||
message = "When using external database (PostgreSQL/MySQL), host, username, and passwordFile must be provided";
|
||||
}
|
||||
{
|
||||
assertion = cfg.database.type == "h2" || cfg.database.port != null;
|
||||
message = "Database port must be set when using an external database";
|
||||
}
|
||||
];
|
||||
|
||||
services.rundeck.settings = {
|
||||
"server.address" = lib.mkDefault "0.0.0.0";
|
||||
"server.port" = lib.mkDefault (toString cfg.serverPort);
|
||||
"grails.serverURL" = lib.mkDefault cfg.serverURL;
|
||||
"logging.config" = lib.mkDefault "${cfg.configDir}/log4j2.properties";
|
||||
"dataSource.url" = lib.mkDefault (
|
||||
if cfg.database.type == "h2" then
|
||||
"jdbc:h2:file:${cfg.dataDir}/data/rundeckdb;DB_CLOSE_ON_EXIT=FALSE;NON_KEYWORDS=MONTH,HOUR,MINUTE,YEAR,SECONDS"
|
||||
else if cfg.database.type == "postgresql" then
|
||||
"jdbc:postgresql://${cfg.database.host}:${toString cfg.database.port}/${cfg.database.name}"
|
||||
else
|
||||
"jdbc:mysql://${cfg.database.host}:${toString cfg.database.port}/${cfg.database.name}?autoReconnect=true&useSSL=false"
|
||||
);
|
||||
"dataSource.driverClassName" = lib.mkDefault (
|
||||
if cfg.database.type == "h2" then
|
||||
"org.h2.Driver"
|
||||
else if cfg.database.type == "postgresql" then
|
||||
"org.postgresql.Driver"
|
||||
else
|
||||
"org.mariadb.jdbc.Driver"
|
||||
);
|
||||
"dataSource.username" = lib.mkDefault cfg.database.username;
|
||||
}
|
||||
// lib.optionalAttrs (cfg.database.passwordFile != null) {
|
||||
"dataSource.password" = lib.mkDefault "@DB_PASSWORD@";
|
||||
}
|
||||
// lib.optionalAttrs (cfg.database.type == "h2") {
|
||||
"dataSource.dialect" = lib.mkDefault "org.hibernate.dialect.H2Dialect";
|
||||
}
|
||||
// lib.optionalAttrs (cfg.database.type == "mysql") {
|
||||
"dataSource.dialect" = lib.mkDefault "org.hibernate.dialect.MariaDB103Dialect";
|
||||
}
|
||||
// lib.optionalAttrs cfg.ssl.enable {
|
||||
"server.https.port" = lib.mkDefault (toString cfg.ssl.port);
|
||||
"server.ssl.keyStore" = lib.mkDefault (toString cfg.ssl.keyStore);
|
||||
"server.ssl.keyStorePassword" = lib.mkDefault "@KEYSTORE_PASSWORD@";
|
||||
};
|
||||
|
||||
services.rundeck.frameworkSettings = {
|
||||
"framework.server.name" = lib.mkDefault cfg.serverHostname;
|
||||
"framework.server.hostname" = lib.mkDefault cfg.serverHostname;
|
||||
"framework.server.port" = lib.mkDefault (toString effectivePort);
|
||||
"framework.server.url" = lib.mkDefault cfg.serverURL;
|
||||
"framework.ssh.keypath" = lib.mkDefault "${cfg.dataDir}/.ssh/id_${cfg.sshKeyType}";
|
||||
"framework.ssh.user" = lib.mkDefault cfg.user;
|
||||
"framework.ssh.timeout" = lib.mkDefault "60";
|
||||
"rdeck.base" = cfg.dataDir;
|
||||
"framework.projects.dir" = "${cfg.dataDir}/projects";
|
||||
"framework.etc.dir" = toString cfg.configDir;
|
||||
"framework.var.dir" = "${cfg.dataDir}/var";
|
||||
"framework.tmp.dir" = "${cfg.dataDir}/var/tmp";
|
||||
"framework.logs.dir" = "${cfg.dataDir}/var/logs";
|
||||
"framework.libext.dir" = "${cfg.dataDir}/libext";
|
||||
"rundeck.server.uuid" = if cfg.serverUUID != "" then cfg.serverUUID else "@SERVER_UUID@";
|
||||
};
|
||||
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = cfg.dataDir;
|
||||
createHome = true;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = { };
|
||||
|
||||
systemd.tmpfiles.settings."10-rundeck" = {
|
||||
"${cfg.dataDir}" = {
|
||||
d = {
|
||||
mode = "0750";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.dataDir}/etc" = {
|
||||
d = {
|
||||
mode = "0750";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.dataDir}/data" = {
|
||||
d = {
|
||||
mode = "0750";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.dataDir}/projects" = {
|
||||
d = {
|
||||
mode = "0750";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.dataDir}/libext" = {
|
||||
d = {
|
||||
mode = "0750";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.dataDir}/var" = {
|
||||
d = {
|
||||
mode = "0750";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.dataDir}/var/logs" = {
|
||||
d = {
|
||||
mode = "0750";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.dataDir}/var/tmp" = {
|
||||
d = {
|
||||
mode = "0750";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.dataDir}/.ssh" = {
|
||||
d = {
|
||||
mode = "0700";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.configDir}" = {
|
||||
d = {
|
||||
mode = "0750";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"${cfg.configDir}/ssl" = {
|
||||
d = {
|
||||
mode = "0750";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
"/var/log/rundeck" = {
|
||||
L = {
|
||||
argument = "${cfg.dataDir}/var/logs";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
environment.etc."rundeck/jaas-loginmodule.conf" = {
|
||||
mode = "0640";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
text = ''
|
||||
RDpropertyfilelogin {
|
||||
org.eclipse.jetty.jaas.spi.PropertyFileLoginModule required
|
||||
debug="true"
|
||||
file="/etc/rundeck/realm.properties";
|
||||
};
|
||||
'';
|
||||
};
|
||||
|
||||
environment.etc."rundeck/log4j2.properties" = {
|
||||
mode = "0640";
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
text = ''
|
||||
status = info
|
||||
name = RundeckPro
|
||||
|
||||
appender.console.type = Console
|
||||
appender.console.name = STDOUT
|
||||
appender.console.layout.type = PatternLayout
|
||||
appender.console.layout.pattern = %d{DEFAULT} %-5p %c{1} - %m%n
|
||||
|
||||
appender.file.type = RollingFile
|
||||
appender.file.name = FILE
|
||||
appender.file.fileName = ${cfg.dataDir}/var/logs/rundeck.log
|
||||
appender.file.filePattern = ${cfg.dataDir}/var/logs/rundeck.%d{yyyy-MM-dd}.log
|
||||
appender.file.layout.type = PatternLayout
|
||||
appender.file.layout.pattern = %d{DEFAULT} [%t] %-5p %c{1} - %m%n
|
||||
appender.file.policies.type = Policies
|
||||
appender.file.policies.time.type = TimeBasedTriggeringPolicy
|
||||
appender.file.policies.time.interval = 1
|
||||
appender.file.policies.time.modulate = true
|
||||
|
||||
rootLogger.level = info
|
||||
rootLogger.appenderRef.stdout.ref = STDOUT
|
||||
rootLogger.appenderRef.file.ref = FILE
|
||||
|
||||
logger.hibernate.name = org.hibernate
|
||||
logger.hibernate.level = ERROR
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.rundeck = {
|
||||
description = "Rundeck Service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [
|
||||
"network.target"
|
||||
]
|
||||
++ lib.optional (cfg.database.type == "mysql") "mysql.service"
|
||||
++ lib.optional (cfg.database.type == "postgresql") "postgresql.service";
|
||||
wants =
|
||||
lib.optional (cfg.database.type == "mysql") "mysql.service"
|
||||
++ lib.optional (cfg.database.type == "postgresql") "postgresql.service";
|
||||
|
||||
environment = {
|
||||
RDECK_BASE = cfg.dataDir;
|
||||
RUNDECK_CONFIG_DIR = cfg.configDir;
|
||||
JAVA_OPTS = lib.concatStringsSep " " cfg.javaOpts;
|
||||
};
|
||||
|
||||
path = [ pkgs.replace-secret ];
|
||||
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = rundeckStartScript;
|
||||
WorkingDirectory = cfg.dataDir;
|
||||
RuntimeDirectory = "rundeck";
|
||||
RuntimeDirectoryMode = "0750";
|
||||
UMask = "0027";
|
||||
|
||||
LimitNOFILE = 65536;
|
||||
ReadWritePaths = [
|
||||
cfg.dataDir
|
||||
cfg.configDir
|
||||
];
|
||||
RestartSec = "10s";
|
||||
Restart = "always";
|
||||
TimeoutStartSec = "${toString cfg.startTimeout}s";
|
||||
|
||||
CapabilityBoundingSet = [ "CAP_SYS_ADMIN" ];
|
||||
AmbientCapabilities = [ "CAP_SYS_ADMIN" ];
|
||||
|
||||
LimitCORE = 0;
|
||||
LockPersonality = true;
|
||||
MemorySwapMax = 0;
|
||||
MemoryZSwapMax = 0;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [
|
||||
"AF_UNIX"
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
};
|
||||
|
||||
preStart = ''
|
||||
${lib.optionalString (cfg.serverUUID == "") ''
|
||||
# Generate UUID
|
||||
UUID_FILE="${cfg.dataDir}/.uuid"
|
||||
if [ ! -f "$UUID_FILE" ]; then
|
||||
umask 0137
|
||||
${lib.getExe' pkgs.util-linux "uuidgen"} > "$UUID_FILE"
|
||||
fi
|
||||
''}
|
||||
|
||||
|
||||
install -m 0640 ${configFile} ${cfg.configDir}/rundeck-config.properties
|
||||
install -m 0640 ${frameworkFile} ${cfg.configDir}/framework.properties
|
||||
install -m 0600 ${realmFile} ${cfg.configDir}/realm.properties
|
||||
|
||||
${replaceSecret "@ADMIN_PASSWORD@" cfg.adminPasswordFile "${cfg.configDir}/realm.properties"}
|
||||
|
||||
${lib.optionalString (cfg.database.passwordFile != null) (
|
||||
replaceSecret "@DB_PASSWORD@" cfg.database.passwordFile "${cfg.configDir}/rundeck-config.properties"
|
||||
)}
|
||||
|
||||
${lib.optionalString cfg.ssl.enable (
|
||||
replaceSecret "@KEYSTORE_PASSWORD@" cfg.ssl.keyStorePasswordFile
|
||||
"${cfg.configDir}/rundeck-config.properties"
|
||||
)}
|
||||
|
||||
${lib.optionalString (cfg.serverUUID == "") (
|
||||
replaceSecret "@SERVER_UUID@" "${cfg.dataDir}/.uuid" "${cfg.configDir}/framework.properties"
|
||||
)}
|
||||
|
||||
if [ -f ${cfg.dataDir}/etc/framework.properties ]; then
|
||||
install -m 0640 ${cfg.configDir}/framework.properties ${cfg.dataDir}/etc/framework.properties
|
||||
fi
|
||||
|
||||
${lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (
|
||||
name: content:
|
||||
"install -m 0640 ${pkgs.writeText "rundeck-${name}" content} ${cfg.dataDir}/etc/${name}"
|
||||
) cfg.aclPolicies
|
||||
)}
|
||||
'';
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
|
||||
(if cfg.ssl.enable then cfg.ssl.port else cfg.serverPort)
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1554,6 +1554,7 @@ in
|
||||
rsyslogd = handleTest ./rsyslogd.nix { };
|
||||
rtkit = runTest ./rtkit.nix;
|
||||
rtorrent = runTest ./rtorrent.nix;
|
||||
rundeck = runTest ./rundeck.nix;
|
||||
rush = runTest ./rush.nix;
|
||||
rustfs = runTest ./rustfs.nix;
|
||||
rustical = runTest ./web-apps/rustical.nix;
|
||||
|
||||
111
nixos/tests/rundeck.nix
Normal file
111
nixos/tests/rundeck.nix
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
name = "rundeck";
|
||||
|
||||
nodes = {
|
||||
rundeck =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.etc."rundeck-admin-password" = {
|
||||
text = "testpassword";
|
||||
mode = "0400";
|
||||
user = "rundeck";
|
||||
group = "rundeck";
|
||||
};
|
||||
|
||||
services.rundeck = {
|
||||
enable = true;
|
||||
serverHostname = "rundeck";
|
||||
adminUser = "testadmin";
|
||||
adminPasswordFile = "/etc/rundeck-admin-password";
|
||||
serverPort = 4441;
|
||||
database.type = "h2";
|
||||
openFirewall = true;
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
curl
|
||||
jq
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
def login_and_verify_api(machine, host, port, user, password, timeout=300):
|
||||
"""Authenticate via Rundeck form login and verify API access."""
|
||||
machine.wait_until_succeeds(
|
||||
f"curl -s -c /tmp/cookies -L"
|
||||
f" -d 'j_username={user}&j_password={password}'"
|
||||
f" http://{host}:{port}/j_security_check -o /dev/null"
|
||||
f" && curl -s -b /tmp/cookies -H 'Accept: application/json'"
|
||||
f" http://{host}:{port}/api/26/system/info"
|
||||
f" | jq -e '.system.rundeck.version'",
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
def properties(machine, path):
|
||||
return machine.succeed(f"cat {path}").replace("\\", "").replace(" = ", "=")
|
||||
|
||||
with subtest("Rundeck starts and serves homepage"):
|
||||
rundeck.wait_for_unit("rundeck.service")
|
||||
rundeck.wait_for_open_port(4441)
|
||||
rundeck.wait_until_succeeds(
|
||||
"curl -sL http://rundeck:4441 | grep -qi Rundeck"
|
||||
)
|
||||
|
||||
with subtest("Regression - port is included in generated URLs"):
|
||||
config = properties(rundeck, "/etc/rundeck/rundeck-config.properties")
|
||||
framework = properties(rundeck, "/etc/rundeck/framework.properties")
|
||||
|
||||
assert "grails.serverURL=http://rundeck:4441" in config, config
|
||||
assert "server.port=4441" in config, config
|
||||
assert "framework.server.url=http://rundeck:4441" in framework, framework
|
||||
|
||||
with subtest("Structural settings end up in the generated config"):
|
||||
config = properties(rundeck, "/etc/rundeck/rundeck-config.properties")
|
||||
|
||||
assert "dataSource.driverClassName=org.h2.Driver" in config, config
|
||||
assert "dataSource.dialect=org.hibernate.dialect.H2Dialect" in config, config
|
||||
|
||||
with subtest("Server UUID is generated and substituted"):
|
||||
framework = properties(rundeck, "/etc/rundeck/framework.properties")
|
||||
|
||||
assert "@SERVER_UUID@" not in framework, framework
|
||||
rundeck.succeed(
|
||||
"grep -qE '^rundeck\\.server\\.uuid = [0-9a-f-]{36}$'"
|
||||
" /etc/rundeck/framework.properties"
|
||||
)
|
||||
|
||||
with subtest("Secrets are substituted and not world-readable"):
|
||||
rundeck.succeed(
|
||||
"grep -qxF 'testadmin:testpassword,user,admin'"
|
||||
" /etc/rundeck/realm.properties"
|
||||
)
|
||||
rundeck.fail("grep -q '@ADMIN_PASSWORD@' /etc/rundeck/realm.properties")
|
||||
rundeck.succeed(
|
||||
"[ \"$(stat -c %a /etc/rundeck/realm.properties)\" = 600 ]"
|
||||
)
|
||||
rundeck.succeed(
|
||||
"[ \"$(stat -c %U /etc/rundeck/realm.properties)\" = rundeck ]"
|
||||
)
|
||||
|
||||
with subtest("API authentication via form login"):
|
||||
login_and_verify_api(rundeck, "rundeck", 4441, "testadmin", "testpassword")
|
||||
|
||||
rundeck.succeed(
|
||||
"curl -s -b /tmp/cookies -X POST"
|
||||
" -H 'Accept: application/json'"
|
||||
" -H 'Content-Type: application/json'"
|
||||
" -d '{\"name\":\"test-project\",\"config\":{}}'"
|
||||
" http://rundeck:4441/api/26/projects"
|
||||
" | jq -e '.name == \"test-project\"'"
|
||||
)
|
||||
|
||||
rundeck.succeed(
|
||||
"curl -s -b /tmp/cookies -H 'Accept: application/json'"
|
||||
" http://rundeck:4441/api/26/projects"
|
||||
" | jq -e 'any(.[]; .name == \"test-project\")'"
|
||||
)
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user