Compare commits
11 commits
d3f5308eaf
...
dec2d76d2a
Author | SHA1 | Date | |
---|---|---|---|
b12f | dec2d76d2a | ||
b12f | 6efc884353 | ||
b12f | 38a34f4345 | ||
b12f | 3bc699fccf | ||
teutat3s | fb8ee1278a | ||
teutat3s | 66ed87e666 | ||
teutat3s | 88b76beb5c | ||
teutat3s | e857c6198b | ||
teutat3s | 998cf4c63d | ||
teutat3s | a0b52d51e5 | ||
teutat3s | 701c62dd69 |
|
@ -18,14 +18,20 @@ jobs:
|
||||||
# Prevent cache garbage collection by creating GC roots
|
# Prevent cache garbage collection by creating GC roots
|
||||||
mkdir -p /var/lib/gitea-runner/tankstelle/.local/state/nix/results
|
mkdir -p /var/lib/gitea-runner/tankstelle/.local/state/nix/results
|
||||||
|
|
||||||
for target in $(nix flake show --json --all-systems | jq '
|
for target in $(nix flake show --json --all-systems | jq --raw-output '
|
||||||
.["nixosConfigurations"] |
|
.["nixosConfigurations"] |
|
||||||
to_entries[] |
|
to_entries[] |
|
||||||
.key
|
.key'
|
||||||
' | tr -d '"'
|
|
||||||
); do
|
); do
|
||||||
nix --print-build-logs --verbose --accept-flake-config --access-tokens '' \
|
nix --print-build-logs --verbose --accept-flake-config --access-tokens '' \
|
||||||
build --out-link /var/lib/gitea-runner/tankstelle/.local/state/nix/results/"$target" ".#nixosConfigurations.${target}.config.system.build.toplevel"
|
build --out-link /var/lib/gitea-runner/tankstelle/.local/state/nix/results/"$target" ".#nixosConfigurations.${target}.config.system.build.toplevel"
|
||||||
done
|
done
|
||||||
|
|
||||||
nix --print-build-logs --verbose --accept-flake-config --access-tokens '' flake check
|
for check in $(nix flake show --json --all-systems | jq --raw-output '
|
||||||
|
.checks."x86_64-linux" |
|
||||||
|
to_entries[] |
|
||||||
|
.key'
|
||||||
|
); do
|
||||||
|
nix --print-build-logs --verbose --accept-flake-config --access-tokens '' \
|
||||||
|
build --out-link /var/lib/gitea-runner/tankstelle/.local/state/nix/results/"$check" ".#checks.x86_64-linux.${check}"
|
||||||
|
done
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
master = import inputs.master { inherit system; };
|
master = import inputs.master { inherit system; };
|
||||||
};
|
};
|
||||||
|
|
||||||
packages = import ./tests ({ inherit inputs self; } // args);
|
checks = import ./tests ({ inherit inputs self; } // args);
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
|
@ -111,7 +111,8 @@
|
||||||
devShells.ci = pkgs.mkShell { buildInputs = with pkgs; [ nodejs ]; };
|
devShells.ci = pkgs.mkShell { buildInputs = with pkgs; [ nodejs ]; };
|
||||||
};
|
};
|
||||||
|
|
||||||
flake = let
|
flake =
|
||||||
|
let
|
||||||
username = "barkeeper";
|
username = "barkeeper";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
|
@ -43,17 +43,6 @@
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nachtigall-test = {
|
|
||||||
imports = [
|
|
||||||
self.inputs.agenix.nixosModules.default
|
|
||||||
self.nixosModules.home-manager
|
|
||||||
./nachtigall/test-vm.nix
|
|
||||||
self.nixosModules.overlays
|
|
||||||
self.nixosModules.core
|
|
||||||
self.nixosModules.docker
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
flora-6 = self.nixos-flake.lib.mkLinuxSystem {
|
flora-6 = self.nixos-flake.lib.mkLinuxSystem {
|
||||||
imports = [
|
imports = [
|
||||||
self.inputs.agenix.nixosModules.default
|
self.inputs.agenix.nixosModules.default
|
||||||
|
|
|
@ -59,11 +59,16 @@
|
||||||
database-password-file = config.age.secrets.keycloak-database-password.path;
|
database-password-file = config.age.secrets.keycloak-database-password.path;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub-solar-os.backups.stores.storagebox = {
|
pub-solar-os.backups.repos.storagebox = {
|
||||||
passwordFile = config.age.secrets."restic-repo-storagebox".path;
|
passwordFile = config.age.secrets."restic-repo-storagebox".path;
|
||||||
repository = "sftp:u377325@u377325.your-storagebox.de:/backups";
|
repository = "sftp:u377325@u377325.your-storagebox.de:/backups";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd.services.postgresql = {
|
||||||
|
after = [ "var-lib-postgresql.mount" ];
|
||||||
|
requisite = [ "var-lib-postgresql.mount" ];
|
||||||
|
};
|
||||||
|
|
||||||
# This value determines the NixOS release with which your system is to be
|
# This value determines the NixOS release with which your system is to be
|
||||||
# compatible, in order to avoid breaking some software such as database
|
# compatible, in order to avoid breaking some software such as database
|
||||||
# servers. You should change this only after NixOS release notes say you
|
# servers. You should change this only after NixOS release notes say you
|
||||||
|
|
|
@ -1,40 +1,39 @@
|
||||||
{ flake, lib, ... }:
|
{ flake, lib, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
imports =
|
imports = [
|
||||||
[
|
./backups.nix
|
||||||
./backups.nix
|
./apps/nginx.nix
|
||||||
./apps/nginx.nix
|
|
||||||
|
|
||||||
./apps/collabora.nix
|
./apps/collabora.nix
|
||||||
./apps/coturn.nix
|
./apps/coturn.nix
|
||||||
./apps/forgejo.nix
|
./apps/forgejo.nix
|
||||||
./apps/keycloak.nix
|
./apps/keycloak.nix
|
||||||
./apps/mailman.nix
|
./apps/mailman.nix
|
||||||
./apps/mastodon.nix
|
./apps/mastodon.nix
|
||||||
./apps/mediawiki.nix
|
./apps/mediawiki.nix
|
||||||
./apps/nextcloud.nix
|
./apps/nextcloud.nix
|
||||||
./apps/nginx-mastodon.nix
|
./apps/nginx-mastodon.nix
|
||||||
./apps/nginx-mastodon-files.nix
|
./apps/nginx-mastodon-files.nix
|
||||||
./apps/nginx-prometheus-exporters.nix
|
./apps/nginx-prometheus-exporters.nix
|
||||||
./apps/nginx-website.nix
|
./apps/nginx-website.nix
|
||||||
./apps/nginx-website-miom.nix
|
./apps/nginx-website-miom.nix
|
||||||
./apps/opensearch.nix
|
./apps/opensearch.nix
|
||||||
./apps/owncast.nix
|
./apps/owncast.nix
|
||||||
./apps/postgresql.nix
|
./apps/postgresql.nix
|
||||||
./apps/prometheus-exporters.nix
|
./apps/prometheus-exporters.nix
|
||||||
./apps/promtail.nix
|
./apps/promtail.nix
|
||||||
./apps/searx.nix
|
./apps/searx.nix
|
||||||
./apps/tmate.nix
|
./apps/tmate.nix
|
||||||
|
|
||||||
./apps/matrix/irc.nix
|
./apps/matrix/irc.nix
|
||||||
./apps/matrix/mautrix-telegram.nix
|
./apps/matrix/mautrix-telegram.nix
|
||||||
./apps/matrix/synapse.nix
|
./apps/matrix/synapse.nix
|
||||||
./apps/nginx-matrix.nix
|
./apps/nginx-matrix.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
|
||||||
security.acme.defaults.server = "https://acme-staging-v02.api.letsencrypt.org/directory";
|
security.acme.defaults.server = "https://acme-staging-v02.api.letsencrypt.org/directory";
|
||||||
security.acme.preliminarySelfsigned = true;
|
security.acme.preliminarySelfsigned = true;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
{
|
{
|
||||||
# Configuration common to all Linux systems
|
# Configuration common to all Linux systems
|
||||||
flake = {
|
flake = {
|
||||||
lib = let
|
lib =
|
||||||
|
let
|
||||||
callLibs = file: import file { inherit lib; };
|
callLibs = file: import file { inherit lib; };
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
|
|
|
@ -4,260 +4,268 @@
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}: let
|
}:
|
||||||
utils = import "${flake.inputs.nixpkgs}/nixos/lib/utils.nix" { inherit lib; inherit config; inherit pkgs; };
|
let
|
||||||
|
utils = import "${flake.inputs.nixpkgs}/nixos/lib/utils.nix" {
|
||||||
|
inherit lib;
|
||||||
|
inherit config;
|
||||||
|
inherit pkgs;
|
||||||
|
};
|
||||||
# Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
|
# Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
|
||||||
inherit (utils.systemdUtils.unitOptions) unitOption;
|
inherit (utils.systemdUtils.unitOptions) unitOption;
|
||||||
in {
|
inherit (lib)
|
||||||
|
literalExpression
|
||||||
|
mkOption
|
||||||
|
mkPackageOption
|
||||||
|
types
|
||||||
|
;
|
||||||
|
in
|
||||||
|
{
|
||||||
options.pub-solar-os.backups = {
|
options.pub-solar-os.backups = {
|
||||||
stores =
|
repos = mkOption {
|
||||||
with lib;
|
description = ''
|
||||||
mkOption {
|
Configuration of Restic repositories.
|
||||||
description = ''
|
'';
|
||||||
Periodic backups to create with Restic.
|
type = types.attrsOf (
|
||||||
'';
|
types.submodule (
|
||||||
type = types.attrsOf (
|
{ name, ... }:
|
||||||
types.submodule (
|
{
|
||||||
{ name, ... }:
|
options = {
|
||||||
{
|
passwordFile = mkOption {
|
||||||
options = {
|
type = types.str;
|
||||||
passwordFile = mkOption {
|
description = ''
|
||||||
type = types.str;
|
Read the repository password from a file.
|
||||||
description = ''
|
'';
|
||||||
Read the repository password from a file.
|
example = "/etc/nixos/restic-password";
|
||||||
'';
|
|
||||||
example = "/etc/nixos/restic-password";
|
|
||||||
};
|
|
||||||
|
|
||||||
repository = mkOption {
|
|
||||||
type = with types; nullOr str;
|
|
||||||
default = null;
|
|
||||||
description = ''
|
|
||||||
repository to backup to.
|
|
||||||
'';
|
|
||||||
example = "sftp:backup@192.168.1.100:/backups/${name}";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
default = { };
|
repository = mkOption {
|
||||||
example = {
|
type = with types; nullOr str;
|
||||||
remotebackup = {
|
default = null;
|
||||||
repository = "sftp:backup@host:/backups/home";
|
description = ''
|
||||||
passwordFile = "/etc/nixos/secrets/restic-password";
|
repository to backup to.
|
||||||
};
|
'';
|
||||||
};
|
example = "sftp:backup@192.168.1.100:/backups/${name}";
|
||||||
};
|
|
||||||
|
|
||||||
backups =
|
|
||||||
with lib;
|
|
||||||
mkOption {
|
|
||||||
description = ''
|
|
||||||
Periodic backups to create with Restic.
|
|
||||||
'';
|
|
||||||
type = types.attrsOf (
|
|
||||||
types.submodule (
|
|
||||||
{ name, ... }:
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
paths = mkOption {
|
|
||||||
# This is nullable for legacy reasons only. We should consider making it a pure listOf
|
|
||||||
# after some time has passed since this comment was added.
|
|
||||||
type = types.nullOr (types.listOf types.str);
|
|
||||||
default = [ ];
|
|
||||||
description = ''
|
|
||||||
Which paths to backup, in addition to ones specified via
|
|
||||||
`dynamicFilesFrom`. If null or an empty array and
|
|
||||||
`dynamicFilesFrom` is also null, no backup command will be run.
|
|
||||||
This can be used to create a prune-only job.
|
|
||||||
'';
|
|
||||||
example = [
|
|
||||||
"/var/lib/postgresql"
|
|
||||||
"/home/user/backup"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
exclude = mkOption {
|
|
||||||
type = types.listOf types.str;
|
|
||||||
default = [ ];
|
|
||||||
description = ''
|
|
||||||
Patterns to exclude when backing up. See
|
|
||||||
https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for
|
|
||||||
details on syntax.
|
|
||||||
'';
|
|
||||||
example = [
|
|
||||||
"/var/cache"
|
|
||||||
"/home/*/.cache"
|
|
||||||
".git"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
timerConfig = mkOption {
|
|
||||||
type = types.nullOr (types.attrsOf unitOption);
|
|
||||||
default = {
|
|
||||||
OnCalendar = "daily";
|
|
||||||
Persistent = true;
|
|
||||||
};
|
|
||||||
description = ''
|
|
||||||
When to run the backup. See {manpage}`systemd.timer(5)` for
|
|
||||||
details. If null no timer is created and the backup will only
|
|
||||||
run when explicitly started.
|
|
||||||
'';
|
|
||||||
example = {
|
|
||||||
OnCalendar = "00:05";
|
|
||||||
RandomizedDelaySec = "5h";
|
|
||||||
Persistent = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
user = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "root";
|
|
||||||
description = ''
|
|
||||||
As which user the backup should run.
|
|
||||||
'';
|
|
||||||
example = "postgresql";
|
|
||||||
};
|
|
||||||
|
|
||||||
extraBackupArgs = mkOption {
|
|
||||||
type = types.listOf types.str;
|
|
||||||
default = [ ];
|
|
||||||
description = ''
|
|
||||||
Extra arguments passed to restic backup.
|
|
||||||
'';
|
|
||||||
example = [ "--exclude-file=/etc/nixos/restic-ignore" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
extraOptions = mkOption {
|
|
||||||
type = types.listOf types.str;
|
|
||||||
default = [ ];
|
|
||||||
description = ''
|
|
||||||
Extra extended options to be passed to the restic --option flag.
|
|
||||||
'';
|
|
||||||
example = [ "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
initialize = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = false;
|
|
||||||
description = ''
|
|
||||||
Create the repository if it doesn't exist.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
pruneOpts = mkOption {
|
|
||||||
type = types.listOf types.str;
|
|
||||||
default = [ ];
|
|
||||||
description = ''
|
|
||||||
A list of options (--keep-\* et al.) for 'restic forget
|
|
||||||
--prune', to automatically prune old snapshots. The
|
|
||||||
'forget' command is run *after* the 'backup' command, so
|
|
||||||
keep that in mind when constructing the --keep-\* options.
|
|
||||||
'';
|
|
||||||
example = [
|
|
||||||
"--keep-daily 7"
|
|
||||||
"--keep-weekly 5"
|
|
||||||
"--keep-monthly 12"
|
|
||||||
"--keep-yearly 75"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
runCheck = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = (builtins.length config.pub-solar-os.backups.backups.${name}.checkOpts > 0);
|
|
||||||
defaultText = literalExpression ''builtins.length config.services.backups.${name}.checkOpts > 0'';
|
|
||||||
description = "Whether to run the `check` command with the provided `checkOpts` options.";
|
|
||||||
example = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
checkOpts = mkOption {
|
|
||||||
type = types.listOf types.str;
|
|
||||||
default = [ ];
|
|
||||||
description = ''
|
|
||||||
A list of options for 'restic check'.
|
|
||||||
'';
|
|
||||||
example = [ "--with-cache" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
dynamicFilesFrom = mkOption {
|
|
||||||
type = with types; nullOr str;
|
|
||||||
default = null;
|
|
||||||
description = ''
|
|
||||||
A script that produces a list of files to back up. The
|
|
||||||
results of this command are given to the '--files-from'
|
|
||||||
option. The result is merged with paths specified via `paths`.
|
|
||||||
'';
|
|
||||||
example = "find /home/matt/git -type d -name .git";
|
|
||||||
};
|
|
||||||
|
|
||||||
backupPrepareCommand = mkOption {
|
|
||||||
type = with types; nullOr str;
|
|
||||||
default = null;
|
|
||||||
description = ''
|
|
||||||
A script that must run before starting the backup process.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
backupCleanupCommand = mkOption {
|
|
||||||
type = with types; nullOr str;
|
|
||||||
default = null;
|
|
||||||
description = ''
|
|
||||||
A script that must run after finishing the backup process.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
package = mkPackageOption pkgs "restic" { };
|
|
||||||
|
|
||||||
createWrapper = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = ''
|
|
||||||
Whether to generate and add a script to the system path, that has the same environment variables set
|
|
||||||
as the systemd service. This can be used to e.g. mount snapshots or perform other opterations, without
|
|
||||||
having to manually specify most options.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
localbackup = {
|
|
||||||
paths = [ "/home" ];
|
|
||||||
exclude = [ "/home/*/.cache" ];
|
|
||||||
initialize = true;
|
|
||||||
};
|
|
||||||
remotebackup = {
|
|
||||||
paths = [ "/home" ];
|
|
||||||
extraOptions = [
|
|
||||||
"sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
|
|
||||||
];
|
|
||||||
timerConfig = {
|
|
||||||
OnCalendar = "00:05";
|
|
||||||
RandomizedDelaySec = "5h";
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
remotebackup = {
|
||||||
|
repository = "sftp:backup@host:/backups/home";
|
||||||
|
passwordFile = "/etc/nixos/secrets/restic-password";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
backups = mkOption {
|
||||||
|
description = ''
|
||||||
|
Periodic backups to create with Restic.
|
||||||
|
'';
|
||||||
|
type = types.attrsOf (
|
||||||
|
types.submodule (
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
paths = mkOption {
|
||||||
|
# This is nullable for legacy reasons only. We should consider making it a pure listOf
|
||||||
|
# after some time has passed since this comment was added.
|
||||||
|
type = types.nullOr (types.listOf types.str);
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
Which paths to backup, in addition to ones specified via
|
||||||
|
`dynamicFilesFrom`. If null or an empty array and
|
||||||
|
`dynamicFilesFrom` is also null, no backup command will be run.
|
||||||
|
This can be used to create a prune-only job.
|
||||||
|
'';
|
||||||
|
example = [
|
||||||
|
"/var/lib/postgresql"
|
||||||
|
"/home/user/backup"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
exclude = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
Patterns to exclude when backing up. See
|
||||||
|
https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for
|
||||||
|
details on syntax.
|
||||||
|
'';
|
||||||
|
example = [
|
||||||
|
"/var/cache"
|
||||||
|
"/home/*/.cache"
|
||||||
|
".git"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
timerConfig = mkOption {
|
||||||
|
type = types.nullOr (types.attrsOf unitOption);
|
||||||
|
default = {
|
||||||
|
OnCalendar = "daily";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
When to run the backup. See {manpage}`systemd.timer(5)` for
|
||||||
|
details. If null no timer is created and the backup will only
|
||||||
|
run when explicitly started.
|
||||||
|
'';
|
||||||
|
example = {
|
||||||
|
OnCalendar = "00:05";
|
||||||
|
RandomizedDelaySec = "5h";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "root";
|
||||||
|
description = ''
|
||||||
|
As which user the backup should run.
|
||||||
|
'';
|
||||||
|
example = "postgresql";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraBackupArgs = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
Extra arguments passed to restic backup.
|
||||||
|
'';
|
||||||
|
example = [ "--exclude-file=/etc/nixos/restic-ignore" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
extraOptions = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
Extra extended options to be passed to the restic --option flag.
|
||||||
|
'';
|
||||||
|
example = [ "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
initialize = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Create the repository if it doesn't exist.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
pruneOpts = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
A list of options (--keep-\* et al.) for 'restic forget
|
||||||
|
--prune', to automatically prune old snapshots. The
|
||||||
|
'forget' command is run *after* the 'backup' command, so
|
||||||
|
keep that in mind when constructing the --keep-\* options.
|
||||||
|
'';
|
||||||
|
example = [
|
||||||
|
"--keep-daily 7"
|
||||||
|
"--keep-weekly 5"
|
||||||
|
"--keep-monthly 12"
|
||||||
|
"--keep-yearly 75"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
runCheck = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = (builtins.length config.pub-solar-os.backups.backups.${name}.checkOpts > 0);
|
||||||
|
defaultText = literalExpression ''builtins.length config.services.backups.${name}.checkOpts > 0'';
|
||||||
|
description = "Whether to run the `check` command with the provided `checkOpts` options.";
|
||||||
|
example = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
checkOpts = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
A list of options for 'restic check'.
|
||||||
|
'';
|
||||||
|
example = [ "--with-cache" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
dynamicFilesFrom = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
A script that produces a list of files to back up. The
|
||||||
|
results of this command are given to the '--files-from'
|
||||||
|
option. The result is merged with paths specified via `paths`.
|
||||||
|
'';
|
||||||
|
example = "find /home/matt/git -type d -name .git";
|
||||||
|
};
|
||||||
|
|
||||||
|
backupPrepareCommand = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
A script that must run before starting the backup process.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
backupCleanupCommand = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
A script that must run after finishing the backup process.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
package = mkPackageOption pkgs "restic" { };
|
||||||
|
|
||||||
|
createWrapper = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Whether to generate and add a script to the system path, that has the same environment variables set
|
||||||
|
as the systemd service. This can be used to e.g. mount snapshots or perform other opterations, without
|
||||||
|
having to manually specify most options.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
localbackup = {
|
||||||
|
paths = [ "/home" ];
|
||||||
|
exclude = [ "/home/*/.cache" ];
|
||||||
|
initialize = true;
|
||||||
|
};
|
||||||
|
remotebackup = {
|
||||||
|
paths = [ "/home" ];
|
||||||
|
extraOptions = [
|
||||||
|
"sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
|
||||||
|
];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "00:05";
|
||||||
|
RandomizedDelaySec = "5h";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
services.restic.backups =
|
services.restic.backups =
|
||||||
let
|
let
|
||||||
stores = config.pub-solar-os.backups.stores;
|
repos = config.pub-solar-os.backups.repos;
|
||||||
backups = config.pub-solar-os.backups.backups;
|
backups = config.pub-solar-os.backups.backups;
|
||||||
|
|
||||||
storeNames = builtins.attrNames stores;
|
storeNames = builtins.attrNames repos;
|
||||||
backupNames = builtins.attrNames backups;
|
backupNames = builtins.attrNames backups;
|
||||||
|
|
||||||
createBackups =
|
createBackups =
|
||||||
backupName:
|
backupName:
|
||||||
map (storeName: {
|
map (storeName: {
|
||||||
name = "${backupName}-${storeName}";
|
name = "${backupName}-${storeName}";
|
||||||
value = stores."${storeName}" // backups."${backupName}";
|
value = repos."${storeName}" // backups."${backupName}";
|
||||||
}) storeNames;
|
}) storeNames;
|
||||||
|
|
||||||
in
|
in
|
||||||
|
|
|
@ -6,9 +6,6 @@
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
disabledModules = [ "services/web-apps/keycloak.nix" ];
|
|
||||||
imports = [ ./keycloak.nix ];
|
|
||||||
|
|
||||||
options.pub-solar-os.auth = with lib; {
|
options.pub-solar-os.auth = with lib; {
|
||||||
enable = mkEnableOption "Enable keycloak to run on the node";
|
enable = mkEnableOption "Enable keycloak to run on the node";
|
||||||
|
|
||||||
|
@ -59,9 +56,7 @@
|
||||||
"pub.solar" =
|
"pub.solar" =
|
||||||
flake.inputs.keycloak-theme-pub-solar.legacyPackages.${pkgs.system}.keycloak-theme-pub-solar;
|
flake.inputs.keycloak-theme-pub-solar.legacyPackages.${pkgs.system}.keycloak-theme-pub-solar;
|
||||||
};
|
};
|
||||||
plugins = [
|
plugins = [ flake.inputs.keycloak-event-listener.packages.${pkgs.system}.keycloak-event-listener ];
|
||||||
flake.inputs.keycloak-event-listener.packages.${pkgs.system}.keycloak-event-listener
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub-solar-os.backups.backups.keycloak = {
|
pub-solar-os.backups.backups.keycloak = {
|
||||||
|
|
|
@ -1,705 +0,0 @@
|
||||||
{ config, options, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.services.keycloak;
|
|
||||||
opt = options.services.keycloak;
|
|
||||||
|
|
||||||
inherit (lib)
|
|
||||||
types
|
|
||||||
mkMerge
|
|
||||||
mkOption
|
|
||||||
mkChangedOptionModule
|
|
||||||
mkRenamedOptionModule
|
|
||||||
mkRemovedOptionModule
|
|
||||||
mkPackageOption
|
|
||||||
concatStringsSep
|
|
||||||
mapAttrsToList
|
|
||||||
escapeShellArg
|
|
||||||
mkIf
|
|
||||||
optionalString
|
|
||||||
optionals
|
|
||||||
mkDefault
|
|
||||||
literalExpression
|
|
||||||
isAttrs
|
|
||||||
literalMD
|
|
||||||
maintainers
|
|
||||||
catAttrs
|
|
||||||
collect
|
|
||||||
hasPrefix
|
|
||||||
;
|
|
||||||
|
|
||||||
inherit (builtins)
|
|
||||||
elem
|
|
||||||
typeOf
|
|
||||||
isInt
|
|
||||||
isString
|
|
||||||
hashString
|
|
||||||
isPath
|
|
||||||
;
|
|
||||||
|
|
||||||
prefixUnlessEmpty = prefix: string: optionalString (string != "") "${prefix}${string}";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
imports =
|
|
||||||
[
|
|
||||||
(mkRenamedOptionModule
|
|
||||||
[ "services" "keycloak" "bindAddress" ]
|
|
||||||
[ "services" "keycloak" "settings" "http-host" ])
|
|
||||||
(mkRenamedOptionModule
|
|
||||||
[ "services" "keycloak" "forceBackendUrlToFrontendUrl"]
|
|
||||||
[ "services" "keycloak" "settings" "hostname-strict-backchannel"])
|
|
||||||
(mkChangedOptionModule
|
|
||||||
[ "services" "keycloak" "httpPort" ]
|
|
||||||
[ "services" "keycloak" "settings" "http-port" ]
|
|
||||||
(config:
|
|
||||||
builtins.fromJSON config.services.keycloak.httpPort))
|
|
||||||
(mkChangedOptionModule
|
|
||||||
[ "services" "keycloak" "httpsPort" ]
|
|
||||||
[ "services" "keycloak" "settings" "https-port" ]
|
|
||||||
(config:
|
|
||||||
builtins.fromJSON config.services.keycloak.httpsPort))
|
|
||||||
(mkRemovedOptionModule
|
|
||||||
[ "services" "keycloak" "frontendUrl" ]
|
|
||||||
''
|
|
||||||
Set `services.keycloak.settings.hostname' and `services.keycloak.settings.http-relative-path' instead.
|
|
||||||
NOTE: You likely want to set 'http-relative-path' to '/auth' to keep compatibility with your clients.
|
|
||||||
See its description for more information.
|
|
||||||
'')
|
|
||||||
(mkRemovedOptionModule
|
|
||||||
[ "services" "keycloak" "extraConfig" ]
|
|
||||||
"Use `services.keycloak.settings' instead.")
|
|
||||||
];
|
|
||||||
|
|
||||||
options.services.keycloak =
|
|
||||||
let
|
|
||||||
inherit (types)
|
|
||||||
bool
|
|
||||||
str
|
|
||||||
int
|
|
||||||
nullOr
|
|
||||||
attrsOf
|
|
||||||
oneOf
|
|
||||||
path
|
|
||||||
enum
|
|
||||||
package
|
|
||||||
port;
|
|
||||||
|
|
||||||
assertStringPath = optionName: value:
|
|
||||||
if isPath value then
|
|
||||||
throw ''
|
|
||||||
services.keycloak.${optionName}:
|
|
||||||
${toString value}
|
|
||||||
is a Nix path, but should be a string, since Nix
|
|
||||||
paths are copied into the world-readable Nix store.
|
|
||||||
''
|
|
||||||
else value;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
enable = mkOption {
|
|
||||||
type = bool;
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
description = ''
|
|
||||||
Whether to enable the Keycloak identity and access management
|
|
||||||
server.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
sslCertificate = mkOption {
|
|
||||||
type = nullOr path;
|
|
||||||
default = null;
|
|
||||||
example = "/run/keys/ssl_cert";
|
|
||||||
apply = assertStringPath "sslCertificate";
|
|
||||||
description = ''
|
|
||||||
The path to a PEM formatted certificate to use for TLS/SSL
|
|
||||||
connections.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
sslCertificateKey = mkOption {
|
|
||||||
type = nullOr path;
|
|
||||||
default = null;
|
|
||||||
example = "/run/keys/ssl_key";
|
|
||||||
apply = assertStringPath "sslCertificateKey";
|
|
||||||
description = ''
|
|
||||||
The path to a PEM formatted private key to use for TLS/SSL
|
|
||||||
connections.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
plugins = lib.mkOption {
|
|
||||||
type = lib.types.listOf lib.types.path;
|
|
||||||
default = [ ];
|
|
||||||
description = ''
|
|
||||||
Keycloak plugin jar, ear files or derivations containing
|
|
||||||
them. Packaged plugins are available through
|
|
||||||
`pkgs.keycloak.plugins`.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
database = {
|
|
||||||
type = mkOption {
|
|
||||||
type = enum [ "mysql" "mariadb" "postgresql" ];
|
|
||||||
default = "postgresql";
|
|
||||||
example = "mariadb";
|
|
||||||
description = ''
|
|
||||||
The type of database Keycloak should connect to.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
host = mkOption {
|
|
||||||
type = str;
|
|
||||||
default = "localhost";
|
|
||||||
description = ''
|
|
||||||
Hostname of the database to connect to.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
port =
|
|
||||||
let
|
|
||||||
dbPorts = {
|
|
||||||
postgresql = 5432;
|
|
||||||
mariadb = 3306;
|
|
||||||
mysql = 3306;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
mkOption {
|
|
||||||
type = port;
|
|
||||||
default = dbPorts.${cfg.database.type};
|
|
||||||
defaultText = literalMD "default port of selected database";
|
|
||||||
description = ''
|
|
||||||
Port of the database to connect to.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
useSSL = mkOption {
|
|
||||||
type = bool;
|
|
||||||
default = cfg.database.host != "localhost";
|
|
||||||
defaultText = literalExpression ''config.${opt.database.host} != "localhost"'';
|
|
||||||
description = ''
|
|
||||||
Whether the database connection should be secured by SSL /
|
|
||||||
TLS.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
caCert = mkOption {
|
|
||||||
type = nullOr path;
|
|
||||||
default = null;
|
|
||||||
description = ''
|
|
||||||
The SSL / TLS CA certificate that verifies the identity of the
|
|
||||||
database server.
|
|
||||||
|
|
||||||
Required when PostgreSQL is used and SSL is turned on.
|
|
||||||
|
|
||||||
For MySQL, if left at `null`, the default
|
|
||||||
Java keystore is used, which should suffice if the server
|
|
||||||
certificate is issued by an official CA.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
createLocally = mkOption {
|
|
||||||
type = bool;
|
|
||||||
default = true;
|
|
||||||
description = ''
|
|
||||||
Whether a database should be automatically created on the
|
|
||||||
local host. Set this to false if you plan on provisioning a
|
|
||||||
local database yourself. This has no effect if
|
|
||||||
services.keycloak.database.host is customized.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
name = mkOption {
|
|
||||||
type = str;
|
|
||||||
default = "keycloak";
|
|
||||||
description = ''
|
|
||||||
Database name to use when connecting to an external or
|
|
||||||
manually provisioned database; has no effect when a local
|
|
||||||
database is automatically provisioned.
|
|
||||||
|
|
||||||
To use this with a local database, set [](#opt-services.keycloak.database.createLocally) to
|
|
||||||
`false` and create the database and user
|
|
||||||
manually.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
username = mkOption {
|
|
||||||
type = str;
|
|
||||||
default = "keycloak";
|
|
||||||
description = ''
|
|
||||||
Username to use when connecting to an external or manually
|
|
||||||
provisioned database; has no effect when a local database is
|
|
||||||
automatically provisioned.
|
|
||||||
|
|
||||||
To use this with a local database, set [](#opt-services.keycloak.database.createLocally) to
|
|
||||||
`false` and create the database and user
|
|
||||||
manually.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
passwordFile = mkOption {
|
|
||||||
type = path;
|
|
||||||
example = "/run/keys/db_password";
|
|
||||||
apply = assertStringPath "passwordFile";
|
|
||||||
description = ''
|
|
||||||
The path to a file containing the database password.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
package = mkPackageOption pkgs "keycloak" { };
|
|
||||||
|
|
||||||
initialAdminPassword = mkOption {
|
|
||||||
type = str;
|
|
||||||
default = "changeme";
|
|
||||||
description = ''
|
|
||||||
Initial password set for the `admin`
|
|
||||||
user. The password is not stored safely and should be changed
|
|
||||||
immediately in the admin panel.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
themes = mkOption {
|
|
||||||
type = attrsOf package;
|
|
||||||
default = { };
|
|
||||||
description = ''
|
|
||||||
Additional theme packages for Keycloak. Each theme is linked into
|
|
||||||
subdirectory with a corresponding attribute name.
|
|
||||||
|
|
||||||
Theme packages consist of several subdirectories which provide
|
|
||||||
different theme types: for example, `account`,
|
|
||||||
`login` etc. After adding a theme to this option you
|
|
||||||
can select it by its name in Keycloak administration console.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
extraStartupFlags = lib.mkOption {
|
|
||||||
type = lib.types.listOf str;
|
|
||||||
default = [ ];
|
|
||||||
description = ''
|
|
||||||
Extra flags to be added to the startup command kc.sh.
|
|
||||||
This can be used to import a realm during startup or to
|
|
||||||
set configuration variables, see <https://www.keycloak.org/server/configuration>.
|
|
||||||
|
|
||||||
--verbose and --optimized are always added.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = mkOption {
|
|
||||||
type = lib.types.submodule {
|
|
||||||
freeformType = attrsOf (nullOr (oneOf [ str int bool (attrsOf path) ]));
|
|
||||||
|
|
||||||
options = {
|
|
||||||
http-host = mkOption {
|
|
||||||
type = str;
|
|
||||||
default = "0.0.0.0";
|
|
||||||
example = "127.0.0.1";
|
|
||||||
description = ''
|
|
||||||
On which address Keycloak should accept new connections.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
http-port = mkOption {
|
|
||||||
type = port;
|
|
||||||
default = 80;
|
|
||||||
example = 8080;
|
|
||||||
description = ''
|
|
||||||
On which port Keycloak should listen for new HTTP connections.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
https-port = mkOption {
|
|
||||||
type = port;
|
|
||||||
default = 443;
|
|
||||||
example = 8443;
|
|
||||||
description = ''
|
|
||||||
On which port Keycloak should listen for new HTTPS connections.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
http-relative-path = mkOption {
|
|
||||||
type = str;
|
|
||||||
default = "/";
|
|
||||||
example = "/auth";
|
|
||||||
apply = x: if !(hasPrefix "/") x then "/" + x else x;
|
|
||||||
description = ''
|
|
||||||
The path relative to `/` for serving
|
|
||||||
resources.
|
|
||||||
|
|
||||||
::: {.note}
|
|
||||||
In versions of Keycloak using Wildfly (<17),
|
|
||||||
this defaulted to `/auth`. If
|
|
||||||
upgrading from the Wildfly version of Keycloak,
|
|
||||||
i.e. a NixOS version before 22.05, you'll likely
|
|
||||||
want to set this to `/auth` to
|
|
||||||
keep compatibility with your clients.
|
|
||||||
|
|
||||||
See <https://www.keycloak.org/migration/migrating-to-quarkus>
|
|
||||||
for more information on migrating from Wildfly to Quarkus.
|
|
||||||
:::
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
hostname = mkOption {
|
|
||||||
type = nullOr str;
|
|
||||||
example = "keycloak.example.com";
|
|
||||||
description = ''
|
|
||||||
The hostname part of the public URL used as base for
|
|
||||||
all frontend requests.
|
|
||||||
|
|
||||||
See <https://www.keycloak.org/server/hostname>
|
|
||||||
for more information about hostname configuration.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
hostname-backchannel-dynamic = mkOption {
|
|
||||||
type = bool;
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
description = ''
|
|
||||||
Enables dynamic resolving of backchannel URLs,
|
|
||||||
including hostname, scheme, port and context path.
|
|
||||||
|
|
||||||
See <https://www.keycloak.org/server/hostname>
|
|
||||||
for more information about hostname configuration.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
proxy = mkOption {
|
|
||||||
type = enum [ "edge" "reencrypt" "passthrough" "none" ];
|
|
||||||
default = "none";
|
|
||||||
example = "edge";
|
|
||||||
description = ''
|
|
||||||
The proxy address forwarding mode if the server is
|
|
||||||
behind a reverse proxy.
|
|
||||||
|
|
||||||
- `edge`:
|
|
||||||
Enables communication through HTTP between the
|
|
||||||
proxy and Keycloak.
|
|
||||||
- `reencrypt`:
|
|
||||||
Requires communication through HTTPS between the
|
|
||||||
proxy and Keycloak.
|
|
||||||
- `passthrough`:
|
|
||||||
Enables communication through HTTP or HTTPS between
|
|
||||||
the proxy and Keycloak.
|
|
||||||
|
|
||||||
See <https://www.keycloak.org/server/reverseproxy> for more information.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
example = literalExpression ''
|
|
||||||
{
|
|
||||||
hostname = "keycloak.example.com";
|
|
||||||
proxy = "reencrypt";
|
|
||||||
https-key-store-file = "/path/to/file";
|
|
||||||
https-key-store-password = { _secret = "/run/keys/store_password"; };
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
|
|
||||||
description = ''
|
|
||||||
Configuration options corresponding to parameters set in
|
|
||||||
{file}`conf/keycloak.conf`.
|
|
||||||
|
|
||||||
Most available options are documented at <https://www.keycloak.org/server/all-config>.
|
|
||||||
|
|
||||||
Options containing secret data should be set to an attribute
|
|
||||||
set containing the attribute `_secret` - a
|
|
||||||
string pointing to a file containing the value the option
|
|
||||||
should be set to. See the example to get a better picture of
|
|
||||||
this: in the resulting
|
|
||||||
{file}`conf/keycloak.conf` file, the
|
|
||||||
`https-key-store-password` key will be set
|
|
||||||
to the contents of the
|
|
||||||
{file}`/run/keys/store_password` file.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config =
|
|
||||||
let
|
|
||||||
# We only want to create a database if we're actually going to
|
|
||||||
# connect to it.
|
|
||||||
databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost";
|
|
||||||
createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql";
|
|
||||||
createLocalMySQL = databaseActuallyCreateLocally && elem cfg.database.type [ "mysql" "mariadb" ];
|
|
||||||
|
|
||||||
mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } ''
|
|
||||||
${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Both theme and theme type directories need to be actual
|
|
||||||
# directories in one hierarchy to pass Keycloak checks.
|
|
||||||
themesBundle = pkgs.runCommand "keycloak-themes" { } ''
|
|
||||||
linkTheme() {
|
|
||||||
theme="$1"
|
|
||||||
name="$2"
|
|
||||||
|
|
||||||
mkdir "$out/$name"
|
|
||||||
for typeDir in "$theme"/*; do
|
|
||||||
if [ -d "$typeDir" ]; then
|
|
||||||
type="$(basename "$typeDir")"
|
|
||||||
mkdir "$out/$name/$type"
|
|
||||||
for file in "$typeDir"/*; do
|
|
||||||
ln -sn "$file" "$out/$name/$type/$(basename "$file")"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdir -p "$out"
|
|
||||||
for theme in ${keycloakBuild}/themes/*; do
|
|
||||||
if [ -d "$theme" ]; then
|
|
||||||
linkTheme "$theme" "$(basename "$theme")"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
${concatStringsSep "\n" (mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes)}
|
|
||||||
'';
|
|
||||||
|
|
||||||
keycloakConfig = lib.generators.toKeyValue {
|
|
||||||
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
|
|
||||||
mkValueString = v:
|
|
||||||
if isInt v then toString v
|
|
||||||
else if isString v then v
|
|
||||||
else if true == v then "true"
|
|
||||||
else if false == v then "false"
|
|
||||||
else if isSecret v then hashString "sha256" v._secret
|
|
||||||
else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
isSecret = v: isAttrs v && v ? _secret && isString v._secret;
|
|
||||||
filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{ } null])) cfg.settings;
|
|
||||||
confFile = pkgs.writeText "keycloak.conf" (keycloakConfig filteredConfig);
|
|
||||||
keycloakBuild = cfg.package.override {
|
|
||||||
inherit confFile;
|
|
||||||
plugins = cfg.package.enabledPlugins ++ cfg.plugins ++
|
|
||||||
(with cfg.package.plugins; [quarkus-systemd-notify quarkus-systemd-notify-deployment]);
|
|
||||||
};
|
|
||||||
in
|
|
||||||
mkIf cfg.enable
|
|
||||||
{
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null);
|
|
||||||
message = "A CA certificate must be specified (in 'services.keycloak.database.caCert') when PostgreSQL is used with SSL";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = createLocalPostgreSQL -> config.services.postgresql.settings.standard_conforming_strings or true;
|
|
||||||
message = "Setting up a local PostgreSQL db for Keycloak requires `standard_conforming_strings` turned on to work reliably";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = cfg.settings.hostname != null || ! cfg.settings.hostname-strict or true;
|
|
||||||
message = "Setting the Keycloak hostname is required, see `services.keycloak.settings.hostname`";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = cfg.settings.hostname-url or null == null;
|
|
||||||
message = ''
|
|
||||||
The option `services.keycloak.settings.hostname-url' has been removed.
|
|
||||||
Set `services.keycloak.settings.hostname' instead.
|
|
||||||
See [New Hostname options](https://www.keycloak.org/docs/25.0.0/upgrading/#new-hostname-options) for details.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = cfg.settings.hostname-strict-backchannel or null == null;
|
|
||||||
message = ''
|
|
||||||
The option `services.keycloak.settings.hostname-strict-backchannel' has been removed.
|
|
||||||
Set `services.keycloak.settings.hostname-backchannel-dynamic' instead.
|
|
||||||
See [New Hostname options](https://www.keycloak.org/docs/25.0.0/upgrading/#new-hostname-options) for details.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = [ keycloakBuild ];
|
|
||||||
|
|
||||||
services.keycloak.settings =
|
|
||||||
let
|
|
||||||
postgresParams = concatStringsSep "&" (
|
|
||||||
optionals cfg.database.useSSL [
|
|
||||||
"ssl=true"
|
|
||||||
] ++ optionals (cfg.database.caCert != null) [
|
|
||||||
"sslrootcert=${cfg.database.caCert}"
|
|
||||||
"sslmode=verify-ca"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
mariadbParams = concatStringsSep "&" ([
|
|
||||||
"characterEncoding=UTF-8"
|
|
||||||
] ++ optionals cfg.database.useSSL [
|
|
||||||
"useSSL=true"
|
|
||||||
"requireSSL=true"
|
|
||||||
"verifyServerCertificate=true"
|
|
||||||
] ++ optionals (cfg.database.caCert != null) [
|
|
||||||
"trustCertificateKeyStoreUrl=file:${mySqlCaKeystore}"
|
|
||||||
"trustCertificateKeyStorePassword=notsosecretpassword"
|
|
||||||
]);
|
|
||||||
dbProps = if cfg.database.type == "postgresql" then postgresParams else mariadbParams;
|
|
||||||
in
|
|
||||||
mkMerge [
|
|
||||||
{
|
|
||||||
db = if cfg.database.type == "postgresql" then "postgres" else cfg.database.type;
|
|
||||||
db-username = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
|
|
||||||
db-password._secret = cfg.database.passwordFile;
|
|
||||||
db-url-host = cfg.database.host;
|
|
||||||
db-url-port = toString cfg.database.port;
|
|
||||||
db-url-database = if databaseActuallyCreateLocally then "keycloak" else cfg.database.name;
|
|
||||||
db-url-properties = prefixUnlessEmpty "?" dbProps;
|
|
||||||
db-url = null;
|
|
||||||
}
|
|
||||||
(mkIf (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
|
|
||||||
https-certificate-file = "/run/keycloak/ssl/ssl_cert";
|
|
||||||
https-certificate-key-file = "/run/keycloak/ssl/ssl_key";
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.services.keycloakPostgreSQLInit = mkIf createLocalPostgreSQL {
|
|
||||||
after = [ "postgresql.service" ];
|
|
||||||
before = [ "keycloak.service" ];
|
|
||||||
bindsTo = [ "postgresql.service" ];
|
|
||||||
path = [ config.services.postgresql.package ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
RemainAfterExit = true;
|
|
||||||
User = "postgres";
|
|
||||||
Group = "postgres";
|
|
||||||
LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
set -o errexit -o pipefail -o nounset -o errtrace
|
|
||||||
shopt -s inherit_errexit
|
|
||||||
|
|
||||||
create_role="$(mktemp)"
|
|
||||||
trap 'rm -f "$create_role"' EXIT
|
|
||||||
|
|
||||||
# Read the password from the credentials directory and
|
|
||||||
# escape any single quotes by adding additional single
|
|
||||||
# quotes after them, following the rules laid out here:
|
|
||||||
# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS
|
|
||||||
db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
|
|
||||||
db_password="''${db_password//\'/\'\'}"
|
|
||||||
|
|
||||||
echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" > "$create_role"
|
|
||||||
psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role"
|
|
||||||
psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"'
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.keycloakMySQLInit = mkIf createLocalMySQL {
|
|
||||||
after = [ "mysql.service" ];
|
|
||||||
before = [ "keycloak.service" ];
|
|
||||||
bindsTo = [ "mysql.service" ];
|
|
||||||
path = [ config.services.mysql.package ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
RemainAfterExit = true;
|
|
||||||
User = config.services.mysql.user;
|
|
||||||
Group = config.services.mysql.group;
|
|
||||||
LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
set -o errexit -o pipefail -o nounset -o errtrace
|
|
||||||
shopt -s inherit_errexit
|
|
||||||
|
|
||||||
# Read the password from the credentials directory and
|
|
||||||
# escape any single quotes by adding additional single
|
|
||||||
# quotes after them, following the rules laid out here:
|
|
||||||
# https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
|
|
||||||
db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
|
|
||||||
db_password="''${db_password//\'/\'\'}"
|
|
||||||
|
|
||||||
( echo "SET sql_mode = 'NO_BACKSLASH_ESCAPES';"
|
|
||||||
echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';"
|
|
||||||
echo "CREATE DATABASE IF NOT EXISTS keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
|
|
||||||
echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';"
|
|
||||||
) | mysql -N
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.keycloak =
|
|
||||||
let
|
|
||||||
databaseServices =
|
|
||||||
if createLocalPostgreSQL then [
|
|
||||||
"keycloakPostgreSQLInit.service"
|
|
||||||
"postgresql.service"
|
|
||||||
]
|
|
||||||
else if createLocalMySQL then [
|
|
||||||
"keycloakMySQLInit.service"
|
|
||||||
"mysql.service"
|
|
||||||
]
|
|
||||||
else [ ];
|
|
||||||
secretPaths = catAttrs "_secret" (collect isSecret cfg.settings);
|
|
||||||
mkSecretReplacement = file: ''
|
|
||||||
replace-secret ${hashString "sha256" file} $CREDENTIALS_DIRECTORY/${baseNameOf file} /run/keycloak/conf/keycloak.conf
|
|
||||||
'';
|
|
||||||
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
|
|
||||||
extraStartupFlags = lib.concatStringsSep " " cfg.extraStartupFlags;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
after = databaseServices;
|
|
||||||
bindsTo = databaseServices;
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
path = with pkgs; [
|
|
||||||
keycloakBuild
|
|
||||||
openssl
|
|
||||||
replace-secret
|
|
||||||
];
|
|
||||||
environment = {
|
|
||||||
KC_HOME_DIR = "/run/keycloak";
|
|
||||||
KC_CONF_DIR = "/run/keycloak/conf";
|
|
||||||
};
|
|
||||||
serviceConfig = {
|
|
||||||
LoadCredential =
|
|
||||||
map (p: "${baseNameOf p}:${p}") secretPaths
|
|
||||||
++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [
|
|
||||||
"ssl_cert:${cfg.sslCertificate}"
|
|
||||||
"ssl_key:${cfg.sslCertificateKey}"
|
|
||||||
];
|
|
||||||
User = "keycloak";
|
|
||||||
Group = "keycloak";
|
|
||||||
DynamicUser = true;
|
|
||||||
RuntimeDirectory = "keycloak";
|
|
||||||
RuntimeDirectoryMode = "0700";
|
|
||||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
|
||||||
Type = "notify"; # Requires quarkus-systemd-notify plugin
|
|
||||||
NotifyAccess = "all";
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
set -o errexit -o pipefail -o nounset -o errtrace
|
|
||||||
shopt -s inherit_errexit
|
|
||||||
|
|
||||||
umask u=rwx,g=,o=
|
|
||||||
|
|
||||||
ln -s ${themesBundle} /run/keycloak/themes
|
|
||||||
ln -s ${keycloakBuild}/providers /run/keycloak/
|
|
||||||
|
|
||||||
install -D -m 0600 ${confFile} /run/keycloak/conf/keycloak.conf
|
|
||||||
|
|
||||||
${secretReplacements}
|
|
||||||
|
|
||||||
# Escape any backslashes in the db parameters, since
|
|
||||||
# they're otherwise unexpectedly read as escape
|
|
||||||
# sequences.
|
|
||||||
sed -i '/db-/ s|\\|\\\\|g' /run/keycloak/conf/keycloak.conf
|
|
||||||
|
|
||||||
'' + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
|
|
||||||
mkdir -p /run/keycloak/ssl
|
|
||||||
cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/
|
|
||||||
'' + ''
|
|
||||||
export KEYCLOAK_ADMIN=admin
|
|
||||||
export KEYCLOAK_ADMIN_PASSWORD=${escapeShellArg cfg.initialAdminPassword}
|
|
||||||
kc.sh --verbose start --optimized ${extraStartupFlags}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
services.postgresql.enable = mkDefault createLocalPostgreSQL;
|
|
||||||
services.mysql.enable = mkDefault createLocalMySQL;
|
|
||||||
services.mysql.package =
|
|
||||||
let
|
|
||||||
dbPkg = if cfg.database.type == "mariadb" then pkgs.mariadb else pkgs.mysql80;
|
|
||||||
in
|
|
||||||
mkIf createLocalMySQL (mkDefault dbPkg);
|
|
||||||
};
|
|
||||||
|
|
||||||
meta.doc = ./keycloak.md;
|
|
||||||
meta.maintainers = [ maintainers.talyz ];
|
|
||||||
}
|
|
|
@ -5,15 +5,22 @@ args@{
|
||||||
pkgs,
|
pkgs,
|
||||||
inputs,
|
inputs,
|
||||||
...
|
...
|
||||||
}: let
|
}:
|
||||||
|
let
|
||||||
nixos-lib = import (inputs.nixpkgs + "/nixos/lib") { };
|
nixos-lib = import (inputs.nixpkgs + "/nixos/lib") { };
|
||||||
|
|
||||||
loadTestFiles = with lib; dir: mapAttrs' (name: _: let
|
loadTestFiles =
|
||||||
test = ((import (dir + "/${name}")) args);
|
with lib;
|
||||||
in {
|
dir:
|
||||||
name = "test-" + (lib.strings.removeSuffix ".nix" name);
|
mapAttrs' (
|
||||||
value = nixos-lib.runTest test;
|
name: _:
|
||||||
})
|
let
|
||||||
(filterAttrs (name: _: (hasSuffix ".nix" name) && name != "default.nix")
|
test = ((import (dir + "/${name}")) args);
|
||||||
(builtins.readDir dir));
|
in
|
||||||
in loadTestFiles ./.
|
{
|
||||||
|
name = "test-" + (lib.strings.removeSuffix ".nix" name);
|
||||||
|
value = nixos-lib.runTest test;
|
||||||
|
}
|
||||||
|
) (filterAttrs (name: _: (hasSuffix ".nix" name) && name != "default.nix") (builtins.readDir dir));
|
||||||
|
in
|
||||||
|
loadTestFiles ./.
|
||||||
|
|
|
@ -72,17 +72,7 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript =
|
testScript =
|
||||||
{ nodes, ... }:
|
{ ... }: ''
|
||||||
let
|
|
||||||
user = nodes.client.users.users.${nodes.client.pub-solar-os.authentication.username};
|
|
||||||
#uid = toString user.uid;
|
|
||||||
bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u ${user.name})/bus";
|
|
||||||
gdbus = "${bus} gdbus";
|
|
||||||
su = command: "su - ${user.name} -c '${command}'";
|
|
||||||
gseval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval";
|
|
||||||
wmClass = su "${gdbus} ${gseval} global.display.focus_window.wm_class";
|
|
||||||
in
|
|
||||||
''
|
|
||||||
def puppeteer_run(cmd):
|
def puppeteer_run(cmd):
|
||||||
client.succeed(f'puppeteer-run \'{cmd}\' ')
|
client.succeed(f'puppeteer-run \'{cmd}\' ')
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,12 @@
|
||||||
lib,
|
lib,
|
||||||
config,
|
config,
|
||||||
...
|
...
|
||||||
}: let
|
}:
|
||||||
puppeteer-socket = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-socket.nix) {});
|
let
|
||||||
puppeteer-run = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-run.nix) {});
|
puppeteer-socket = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-socket.nix) { });
|
||||||
in {
|
puppeteer-run = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-run.nix) { });
|
||||||
|
in
|
||||||
|
{
|
||||||
imports = [ ./global.nix ];
|
imports = [ ./global.nix ];
|
||||||
|
|
||||||
security.polkit.enable = true;
|
security.polkit.enable = true;
|
||||||
|
@ -18,9 +20,7 @@ in {
|
||||||
|
|
||||||
services.getty.autologinUser = config.pub-solar-os.authentication.username;
|
services.getty.autologinUser = config.pub-solar-os.authentication.username;
|
||||||
|
|
||||||
virtualisation.qemu.options = [
|
virtualisation.qemu.options = [ "-vga std" ];
|
||||||
"-vga std"
|
|
||||||
];
|
|
||||||
|
|
||||||
home-manager.users.${config.pub-solar-os.authentication.username} = {
|
home-manager.users.${config.pub-solar-os.authentication.username} = {
|
||||||
programs.bash.profileExtra = ''
|
programs.bash.profileExtra = ''
|
||||||
|
@ -34,9 +34,9 @@ in {
|
||||||
'';
|
'';
|
||||||
config = {
|
config = {
|
||||||
modifier = "Mod4";
|
modifier = "Mod4";
|
||||||
terminal = "${pkgs.alacritty}/bin/alacritty";
|
terminal = "${pkgs.alacritty}/bin/alacritty";
|
||||||
startup = [
|
startup = [
|
||||||
{command = "EXECUTABLE=${pkgs.firefox}/bin/firefox ${puppeteer-socket}/bin/puppeteer-socket";}
|
{ command = "EXECUTABLE=${pkgs.firefox}/bin/firefox ${puppeteer-socket}/bin/puppeteer-socket"; }
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{ writeShellScriptBin, curl }:
|
||||||
writeShellScriptBin,
|
writeShellScriptBin "puppeteer-run" ''
|
||||||
curl
|
set -e
|
||||||
}: writeShellScriptBin "puppeteer-run" ''
|
|
||||||
set -e
|
|
||||||
|
|
||||||
exec ${curl}/bin/curl --fail-with-body -X POST -d "$@" --unix-socket "/tmp/puppeteer.sock" http://puppeteer-socket
|
exec ${curl}/bin/curl --fail-with-body -X POST -d "$@" --unix-socket "/tmp/puppeteer.sock" http://puppeteer-socket
|
||||||
''
|
''
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
{
|
{ buildNpmPackage, nodejs }:
|
||||||
buildNpmPackage,
|
|
||||||
nodejs,
|
|
||||||
}:
|
|
||||||
buildNpmPackage rec {
|
buildNpmPackage rec {
|
||||||
src = ./.;
|
src = ./.;
|
||||||
name = "puppeteer-socket";
|
name = "puppeteer-socket";
|
||||||
|
|
Loading…
Reference in a new issue