feat: postgres, calibre, radicale, z2m (#133)
* hax * hax * shell monitoring * hax radicale! * hacking * haxor * hax * hack * feat: refactor paths etc for impermance * fix: restic * hax * more hax * feat: migrate z2m * fix: websockets i guess * cleanup * hacks * hax * feat: miniflux + postgres * feat: add calibre * feat: calibre-web * Auto lint/format --------- Co-authored-by: Truxnell <9149206+truxnell@users.noreply.github.com> Co-authored-by: truxnell <truxnell@users.noreply.github.com>
This commit is contained in:
parent
5a39b54ae8
commit
49621dec0e
39 changed files with 1293 additions and 243 deletions
|
@ -10,7 +10,7 @@
|
|||
|
||||
|
||||
];
|
||||
|
||||
config = {
|
||||
mySystem.purpose = "Network Attached Storage";
|
||||
mySystem.system.impermanence.enable = true;
|
||||
mySystem.services = {
|
||||
|
@ -43,7 +43,6 @@
|
|||
mySystem.system.motd.networkInterfaces = [ "eno1" ];
|
||||
|
||||
|
||||
|
||||
boot = {
|
||||
|
||||
initrd.availableKernelModules = [ "xhci_pci" "ahci" "mpt3sas" "nvme" "usbhid" "usb_storage" "sd_mod" ];
|
||||
|
@ -95,6 +94,44 @@
|
|||
swapDevices =
|
||||
[{ device = "/dev/disk/by-uuid/c2f716ef-9e8c-466b-bcb0-699397cb2dc0"; }];
|
||||
|
||||
# TODO does this live somewhere else?
|
||||
# it is very machine-specific...
|
||||
# add user with `sudo smbpasswd -a my_user`
|
||||
services.samba = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
extraConfig = ''
|
||||
workgroup = WORKGROUP
|
||||
server string = daedalus
|
||||
netbios name = daedalus
|
||||
security = user
|
||||
#use sendfile = yes
|
||||
#max protocol = smb2
|
||||
# note: localhost is the ipv6 localhost ::1
|
||||
hosts allow = 10.8.10. 127.0.0.1 localhost
|
||||
hosts deny = 0.0.0.0/0
|
||||
guest account = nobody
|
||||
map to guest = bad user
|
||||
'';
|
||||
shares = {
|
||||
backup = {
|
||||
path = "/tank/backup";
|
||||
"read only" = "no";
|
||||
};
|
||||
documents = {
|
||||
path = "/tank/documents";
|
||||
"read only" = "no";
|
||||
};
|
||||
natflix = {
|
||||
path = "/tank/natflix";
|
||||
"read only" = "no";
|
||||
};
|
||||
# paperless = {
|
||||
# path = "/tank/Apps/paperless/incoming";
|
||||
# "read only" = "no";
|
||||
# };
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,10 +11,13 @@
|
|||
openssh.enable = true;
|
||||
podman.enable = true;
|
||||
nginx.enable = true;
|
||||
|
||||
openvscode-server.enable = true;
|
||||
|
||||
postgresql =
|
||||
{ enable = true; backup = false; };
|
||||
calibre-web = { enable = true; backup = false; dev = true; };
|
||||
};
|
||||
# mySystem.containers.calibre = { enable = true; backup = false; dev = true; };
|
||||
|
||||
mySystem.system.systemd.pushover-alerts.enable = false;
|
||||
|
||||
mySystem.nfs.nas.enable = true;
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
mySystem.services = {
|
||||
openssh.enable = true;
|
||||
podman.enable = true;
|
||||
postgresql.enable = true;
|
||||
|
||||
nginx.enable = true;
|
||||
|
||||
|
@ -33,7 +34,14 @@
|
|||
openvscode-server.enable = true;
|
||||
|
||||
radicale.enable = true;
|
||||
miniflux.enable = true;
|
||||
|
||||
};
|
||||
mySystem.containers = {
|
||||
calibre.enable = true;
|
||||
ecowitt2mqtt.enable = true;
|
||||
};
|
||||
|
||||
|
||||
mySystem.security.acme.enable = true;
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
};
|
||||
|
|
157
nixos/modules/nixos/containers/calibre/default.nix
Normal file
157
nixos/modules/nixos/containers/calibre/default.nix
Normal file
|
@ -0,0 +1,157 @@
|
|||
{ lib
|
||||
, config
|
||||
, pkgs
|
||||
, ...
|
||||
}:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.mySystem.${category}.${app};
|
||||
app = "calibre";
|
||||
category = "containers";
|
||||
description = "eBook managment";
|
||||
image = "ghcr.io/linuxserver/calibre:version-v7.10.0";
|
||||
user = "0"; #string
|
||||
group = "0"; #string
|
||||
port = 8091; #int
|
||||
appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
url = "${host}.${config.networking.domain}";
|
||||
in
|
||||
{
|
||||
options.mySystem.${category}.${app} =
|
||||
{
|
||||
enable = mkEnableOption "${app}";
|
||||
addToHomepage = mkEnableOption "Add ${app} to homepage" // { default = true; };
|
||||
monitor = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable gatus monitoring";
|
||||
default = true;
|
||||
};
|
||||
prometheus = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable prometheus scraping";
|
||||
default = true;
|
||||
};
|
||||
addToDNS = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Add to DNS list";
|
||||
default = true;
|
||||
};
|
||||
dev = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Development instance";
|
||||
default = false;
|
||||
};
|
||||
backup = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable backups";
|
||||
default = true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
## Secrets
|
||||
# sops.secrets."${category}/${app}/env" = {
|
||||
# sopsFile = ./secrets.sops.yaml;
|
||||
# owner = user;
|
||||
# group = group;
|
||||
# restartUnits = [ "${app}.service" ];
|
||||
# };
|
||||
|
||||
users.users.truxnell.extraGroups = [ group ];
|
||||
|
||||
|
||||
# Folder perms - only for containers
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${appFolder}/ 0750 ${user} ${group} -"
|
||||
];
|
||||
|
||||
## service
|
||||
virtualisation.oci-containers.containers = config.lib.mySystem.mkContainer {
|
||||
inherit app image user group;
|
||||
|
||||
env = {
|
||||
PUID = "568";
|
||||
PGID = "568";
|
||||
};
|
||||
volumes = [
|
||||
"${appFolder}:/config:rw"
|
||||
"${config.mySystem.nasFolder}/natflix/:/media:rw"
|
||||
];
|
||||
ports = [ "${builtins.toString port}:8080" ];
|
||||
caps = {
|
||||
noNewPrivileges = true;
|
||||
};
|
||||
};
|
||||
|
||||
# homepage integration
|
||||
mySystem.services.homepage.infrastructure = mkIf cfg.addToHomepage [
|
||||
{
|
||||
${app} = {
|
||||
icon = "${app}.svg";
|
||||
href = "https://${url}";
|
||||
inherit description;
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
### gatus integration
|
||||
mySystem.services.gatus.monitors = mkIf cfg.monitor [
|
||||
{
|
||||
name = app;
|
||||
group = "${category}";
|
||||
url = "https://${url}";
|
||||
interval = "1m";
|
||||
conditions = [ "[CONNECTED] == true" "[STATUS] == 200" "[RESPONSE_TIME] < 50" ];
|
||||
}
|
||||
];
|
||||
|
||||
### Ingress
|
||||
services.nginx.virtualHosts.${url} = {
|
||||
forceSSL = true;
|
||||
useACMEHost = config.networking.domain;
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://127.0.0.1:${builtins.toString port}";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
|
||||
### firewall config
|
||||
|
||||
# networking.firewall = mkIf cfg.openFirewall {
|
||||
# allowedTCPPorts = [ port ];
|
||||
# allowedUDPPorts = [ port ];
|
||||
# };
|
||||
|
||||
### backups
|
||||
warnings = [
|
||||
(mkIf (!cfg.backup && config.mySystem.purpose != "Development")
|
||||
"WARNING: Backups for ${app} are disabled!")
|
||||
];
|
||||
|
||||
services.restic.backups = mkIf cfg.backup (config.lib.mySystem.mkRestic
|
||||
{
|
||||
inherit app user;
|
||||
paths = [ appFolder ];
|
||||
inherit appFolder;
|
||||
});
|
||||
|
||||
|
||||
# services.postgresqlBackup = {
|
||||
# databases = [ app ];
|
||||
# };
|
||||
|
||||
|
||||
|
||||
};
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
./whoogle
|
||||
./redlib
|
||||
./home-assistant
|
||||
# ./calibre
|
||||
./calibre
|
||||
./ecowitt2mqtt
|
||||
];
|
||||
}
|
||||
|
|
158
nixos/modules/nixos/containers/ecowitt2mqtt/default.nix
Normal file
158
nixos/modules/nixos/containers/ecowitt2mqtt/default.nix
Normal file
|
@ -0,0 +1,158 @@
|
|||
{ lib
|
||||
, config
|
||||
, pkgs
|
||||
, ...
|
||||
}:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.mySystem.${category}.${app};
|
||||
app = "ecowitt2mqtt";
|
||||
category = "containers";
|
||||
description = "Weather station to MQTT";
|
||||
image = "ghcr.io/bachya/ecowitt2mqtt:latest@sha256:3c6e9be9332e1b04db836ec721208c753a01faa236c7cf76f966f3ec4d64621d";
|
||||
user = "nobody"; #string
|
||||
group = "nobody"; #string
|
||||
port = 8080; #int
|
||||
# appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
url = "${host}.${config.networking.domain}";
|
||||
in
|
||||
{
|
||||
options.mySystem.${category}.${app} =
|
||||
{
|
||||
enable = mkEnableOption "${app}";
|
||||
addToHomepage = mkEnableOption "Add ${app} to homepage" // { default = true; };
|
||||
monitor = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable gatus monitoring";
|
||||
default = true;
|
||||
};
|
||||
prometheus = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable prometheus scraping";
|
||||
default = true;
|
||||
};
|
||||
addToDNS = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Add to DNS list";
|
||||
default = true;
|
||||
};
|
||||
dev = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Development instance";
|
||||
default = false;
|
||||
};
|
||||
backupLocal = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable local backups";
|
||||
default = true;
|
||||
};
|
||||
backupRemote = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable remote backups";
|
||||
default = true;
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
## Secrets
|
||||
sops.secrets."${category}/${app}/env" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = user;
|
||||
inherit group;
|
||||
restartUnits = [ "podman-${app}.service" ];
|
||||
};
|
||||
|
||||
users.users.truxnell.extraGroups = [ group ];
|
||||
|
||||
|
||||
# Folder perms - only for containers
|
||||
# systemd.tmpfiles.rules = [
|
||||
# "d ${persistentFolder}/ 0750 ${user} ${group} -"
|
||||
# ];
|
||||
|
||||
## service
|
||||
virtualisation.oci-containers.containers = config.lib.mySystem.mkContainer {
|
||||
inherit app image user group;
|
||||
env = {
|
||||
ECOWITT2MQTT_MQTT_BROKER = "mqtt.trux.dev";
|
||||
ECOWITT2MQTT_MQTT_PORT = "1883";
|
||||
ECOWITT2MQTT_MQTT_TOPIC = "ecowitt2mqtt/pws";
|
||||
ECOWITT2MQTT_PORT = "8080";
|
||||
ECOWITT2MQTT_HASS_DISCOVERY = "true";
|
||||
ECOWITT2MQTT_OUTPUT_UNIT_SYSTEM = "metric"; # Come on guys nobody want to use freedum units"
|
||||
};
|
||||
envFiles = [ config.sops.secrets."${category}/${app}/env".path ];
|
||||
};
|
||||
|
||||
# homepage integration
|
||||
# mySystem.services.homepage.infrastructure = mkIf cfg.addToHomepage [
|
||||
# {
|
||||
# ${app} = {
|
||||
# icon = "${app}.svg";
|
||||
# href = "https://${url}";
|
||||
# description = description;
|
||||
# };
|
||||
# }
|
||||
# ];
|
||||
|
||||
### gatus integration
|
||||
mySystem.services.gatus.monitors = mkIf cfg.monitor [
|
||||
{
|
||||
name = app;
|
||||
group = "${category}";
|
||||
url = "https://${url}/data/report"; # check the reporting URL for 405 'method not allowed's
|
||||
interval = "1m";
|
||||
conditions = [ "[CONNECTED] == true" "[STATUS] == 405" "[RESPONSE_TIME] < 50" ];
|
||||
}
|
||||
];
|
||||
|
||||
### Ingress
|
||||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
# I dont need/want ssl for this one, weather station expets http
|
||||
# useACMEHost = config.networking.domain;
|
||||
# forceSSL = true;
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
proxyWebsockets = true;
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
};
|
||||
};
|
||||
|
||||
### firewall config
|
||||
|
||||
# networking.firewall = mkIf cfg.openFirewall {
|
||||
# allowedTCPPorts = [ port ];
|
||||
# allowedUDPPorts = [ port ];
|
||||
# };
|
||||
|
||||
### backups
|
||||
# warnings = [
|
||||
# (mkIf (!cfg.backupLocal && config.mySystem.purpose != "Development")
|
||||
# "WARNING: Local backups for ${app} are disabled!")
|
||||
# (mkIf (!cfg.backupRemote && config.mySystem.purpose != "Development")
|
||||
# "WARNING: Remote backups for ${app} are disabled!")
|
||||
# ];
|
||||
|
||||
# services.restic.backups = mkIf cfg.backups config.lib.mySystem.mkRestic
|
||||
# {
|
||||
# inherit app user;
|
||||
# paths = [ appFolder ];
|
||||
# inherit appFolder;
|
||||
# local = cfg.backupLocal;
|
||||
# remote = cfg.backupRemote;
|
||||
# };
|
||||
|
||||
|
||||
};
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
containers:
|
||||
ecowitt2mqtt:
|
||||
env: ENC[AES256_GCM,data:VP1/jz+SAK+ilGAlTt51bDS9QjzV+KXxN+c++/ZdYl4pFZrideghqCkGdknn83Jv4DcnIb30AtUdGR0QfTXY4AfFUUwJfgL8EcOe2+0=,iv:GSQ3/4q/cdOWZxYcGuFBzzyhDxf9wyRustiZgqB8PbI=,tag:/kLE6z5s5DDKGRxix7EnLw==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1lj5vmr02qkudvv2xedfj5tq8x93gllgpr6tzylwdlt7lud4tfv5qfqsd5u
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjRENkdkgycktRYnRLbHh3
|
||||
MFNXSXZRU1Bnc0hpK1ZsOERQcG5Qd1BpRUJRCk00WkFKQWhZcmx4SFozd0JRN09r
|
||||
bHpaODh5UGNOUFErbVdsK1VFYmdqT3cKLS0tIHdzQ2hxaVl1M1U2VDgrcHBOdlFz
|
||||
eUhsRzJtNjQ1Mm9kdlI2OTRZVUpjY2sKDt5anrouamcYEaQDjiZEuZpdqe3uO1Ee
|
||||
0BidTQuN9jfWOlIljftZ2CiedlCoYzXlnGkjIOMoD3uvRTpmRPKBdw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age17edew3aahg3t5nte5g0a505sn96vnj8g8gqse8q06ccrrn2n3uysyshu2c
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxczQyV3JRNDVpZkZzSjVk
|
||||
UWhzL28rZmtmSEI0N3pvWDlGUHhIQllySlM4CktXZlJCam1CUU9BNUpKTUgrRGJ4
|
||||
NnE0c0x6WTRZMWsyVVBIVWw3Rmh6REUKLS0tIGJ4SllRa0p2c2p4UHkvMHBaUnhD
|
||||
SU40RnZ4dXo1MXU2SjN2Q3ZHZXVjVUUKjeJxmnY397EK9MBe4uFpBQg0XE1p4y/0
|
||||
cfck88D+LGvMrfBIsMcmMd2EmHsIzZt4SKMIr1ZQ+WdE+Mns6drHcQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1u4tht685sqg6dkmjyer96r93pl425u6353md6fphpd84jh3jwcusvm7mgk
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwWWxFcWhWOERzdVVmdE83
|
||||
bmRyRXU1Z3RUU3MydWVJNlBUdlQ2NFVWSGxVCjRFY1BKOER6ejRIVWhLTVp4eSt4
|
||||
c0Q5Rld1RFZKWEtGcThtUlJVMnpEcVEKLS0tIDhWMW45d1ZqZ25RY0NvVzQ0NTBK
|
||||
RGttN1JNbXhmS2NnTFpOQlNKUC9vbm8KILk4bxt2b58+igJ4c4PrjgJY9AyGrvsO
|
||||
zBfxhFvlGuYjR8W1r6VcH7+JsQhKZrw07gIU5ornHvE34oxgxdYXrA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1cp6vegrmqfkuj8nmt2u3z0sur7n0f7e9x9zmdv4zygp8j2pnucpsdkgagc
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1b0x5QmJQbHA0STliVEM3
|
||||
N0RZaFFud1VTbm4xUm5EMVFDMVRncGpDNWc0CndJYzVNMG8xZlo5dUlRb3BKVStF
|
||||
OFdKYXdRL2ZwWlk1Uzd2WUthRTRzOEkKLS0tIFRVRnFpYWdTeGhNc1VpdCsrTFcv
|
||||
bW5uVGNsQjFmUjNvQnYzc2pSMUliUjQKBzI9IGdKfEXNRVh3s7fon/6VS9ZUSpG/
|
||||
hOHQ93wX0VAy6wO+iugDfM/1Zuc2V66+T4wYPfUtWapj009L31eufw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1ekt5xz7u2xgdzgsrffhd9x22n80cn4thxd8zxjy2ey5vq3ca7gnqz25g5r
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBabEN5eXd2Q1A2R0ZsalYy
|
||||
MW43TUdqaG80MmtzVit1QVdvcExQRjZYT1NjCnA5L2NxRks5UDZVYVVsRGJJZ3cz
|
||||
bGhtVDFTVXFCTGtuUFRaNmtzVUdRZUEKLS0tIC9qYnRxZ2FqWllybnpMMU9NNDFx
|
||||
VGNTdnhiMW9KcE9ST3RPUitaMVdsRVkKLaABu7ZJ1Nc4ROkrzoRiMe4vdLKMmzMW
|
||||
O0ylhZRqqHx4nWGSBgx680zKm3WAx3o97nH3IFCaLlHuNGm46iOYkg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1jpeh4s553taxkyxhzlshzqjfrtvmmp5lw0hmpgn3mdnmgzku332qe082dl
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDM2IvQU1IRnVVRVo0bmUx
|
||||
QjMyZVRiN3pSUkNYUVYzSXVUSVBPTXdIWUZzCjZaOHo4bllaTklTb29XdVAySHJY
|
||||
bmMwT3RxS0p5MjFHdXhIL0tIRERNTTAKLS0tIHBlQ2RJYTNDQm1oRGN3b2JIOXFi
|
||||
YUdHa1RVVTRBbmJkdkdDa2JCbG40NlEKta9VW9hc2saA/nzWoER35S7Yl7M9CHQ7
|
||||
YmZ4LIz8IqB6Sihk1pcmX2RAeMA8uMPfEMEBejwiuM57NjEFMgllbw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1j2r8mypw44uvqhfs53424h6fu2rkr5m7asl7rl3zn3xzva9m3dcqpa97gw
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZSHYrejZ3RExjek1FR21Y
|
||||
VlNvbmdnSjNrcHBkNFByNXpGTGkwMnVQMWlRCjg5U2l6NHVCakprVS9kUTFRa1lk
|
||||
SXpEYU9WZHVlcGRSS1UyaDhkak5jMFEKLS0tIC8reGg4RWNDb1AzUkx5TGg1WVN1
|
||||
ZzBpamhpVFRBZHZ0MTdJRTl5d2RqK3MKQuNJCLdNBa9DG8V4sRVQ6GKhWjcKK9eC
|
||||
FcTxyUBPNDMVYlHR5DxONIfzT9ymlSReFUkwjXAgVTwhqLOgpTUGAw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2024-05-03T09:52:12Z"
|
||||
mac: ENC[AES256_GCM,data:Z4u5T4kJ+dRCE9qZENBstOMMrSSw503PRS5qwNmrx1kTbaNN5VPd/mrkxWM8pZudLxo3pBnCZSPYju1tpzQisRBcu/BwMAiBLx4xSJsEJhG//fWbv93XUGJ7i7nlW2Kl1V0QUrG1WSz5RXx1PE2oTEQCbRSGZyYo5FtHZsOhHEA=,iv:HidiueXjLNwBV1vY/7vNqWCDr+rfwN00NpfC9RuPZms=,tag:qkfNjPX6Ti2EnnWnqjtvNw==,type:str]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.8.1
|
|
@ -69,7 +69,7 @@ in
|
|||
RCON_PORT = "27019";
|
||||
};
|
||||
environmentFiles = [ config.sops.secrets."services/${app}/env".path ];
|
||||
ports = [ (builtins.toString port) ]; # expose port
|
||||
ports = [ "${builtins.toString port}:${builtins.toString port}/UDP" ]; # expose port
|
||||
};
|
||||
networking.firewall = mkIf cfg.openFirewall {
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
};
|
||||
|
|
|
@ -52,7 +52,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
proxyWebsockets = true;
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
@ -61,7 +61,7 @@ in
|
|||
|
||||
|
||||
|
||||
mySystem.services.homepage.media = mkIf cfg.addToHomepage [
|
||||
mySystem.services.homepage.home = mkIf cfg.addToHomepage [
|
||||
{
|
||||
${app} = {
|
||||
icon = "${app}.svg";
|
||||
|
|
|
@ -300,7 +300,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ let
|
|||
port = 8080; #int
|
||||
appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
host = "${app}" + (if cfg.development then "-dev" else "");
|
||||
host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
url = "${host}.${config.networking.domain}";
|
||||
in
|
||||
{
|
||||
|
@ -41,7 +41,7 @@ in
|
|||
description = "Add to DNS list";
|
||||
default = true;
|
||||
};
|
||||
development = mkOption
|
||||
dev = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Development instance";
|
||||
|
@ -83,11 +83,6 @@ in
|
|||
## container
|
||||
virtualisation.oci-containers.containers = config.lib.mySystem.mkContainer {
|
||||
inherit app image user group;
|
||||
env = {
|
||||
test = "derp";
|
||||
};
|
||||
envFiles = [ ];
|
||||
volumes = [ ];
|
||||
};
|
||||
|
||||
# homepage integration
|
||||
|
|
|
@ -43,7 +43,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ in
|
|||
services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
useACMEHost = config.networking.domain;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://${app}:${builtins.toString port}";
|
||||
extraConfig = "resolver 10.88.0.1;";
|
||||
|
||||
|
|
|
@ -22,11 +22,11 @@ with lib;
|
|||
user = "${options.user}:${options.group}";
|
||||
environment = {
|
||||
TZ = config.time.timeZone;
|
||||
} // options.env;
|
||||
} // lib.attrsets.attrByPath [ "env" ] { } options;
|
||||
environmentFiles = lib.attrsets.attrByPath [ "envFiles" ] [ ] options;
|
||||
volumes = [ "/etc/localtime:/etc/localtime:ro" ]
|
||||
++ lib.attrsets.attrByPath [ "volumes" ] [ ] options;
|
||||
|
||||
ports = lib.attrsets.attrByPath [ "ports" ] [ ] options;
|
||||
extraOptions = containerExtraOptions;
|
||||
};
|
||||
}
|
||||
|
|
138
nixos/modules/nixos/services/calibre-web/default.nix
Normal file
138
nixos/modules/nixos/services/calibre-web/default.nix
Normal file
|
@ -0,0 +1,138 @@
|
|||
{ lib
|
||||
, config
|
||||
, pkgs
|
||||
, ...
|
||||
}:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.mySystem.${category}.${app};
|
||||
app = "calibre-web";
|
||||
category = "services";
|
||||
description = "Calibre web-server";
|
||||
# image = "%{image}";
|
||||
inherit (config.services.calibre-web) user;#string
|
||||
inherit (config.services.calibre-web) group;#string
|
||||
port = 8083; #int
|
||||
appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
url = "${host}.${config.networking.domain}";
|
||||
in
|
||||
{
|
||||
options.mySystem.${category}.${app} =
|
||||
{
|
||||
enable = mkEnableOption "${app}";
|
||||
addToHomepage = mkEnableOption "Add ${app} to homepage" // { default = true; };
|
||||
monitor = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable gatus monitoring";
|
||||
default = true;
|
||||
};
|
||||
prometheus = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable prometheus scraping";
|
||||
default = true;
|
||||
};
|
||||
addToDNS = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Add to DNS list";
|
||||
default = true;
|
||||
};
|
||||
dev = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Development instance";
|
||||
default = false;
|
||||
};
|
||||
backup = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable backups";
|
||||
default = true;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
## Secrets
|
||||
# sops.secrets."${category}/${app}/env" = {
|
||||
# sopsFile = ./secrets.sops.yaml;
|
||||
# owner = user;
|
||||
# group = group;
|
||||
# restartUnits = [ "${app}.service" ];
|
||||
# };
|
||||
|
||||
users.users.truxnell.extraGroups = [ group ];
|
||||
|
||||
|
||||
# Folder perms - only for containers
|
||||
# systemd.tmpfiles.rules = [
|
||||
# "d ${persistentFolder}/ 0750 ${user} ${group} -"
|
||||
# ];
|
||||
|
||||
## service
|
||||
services.calibre-web = {
|
||||
enable = true;
|
||||
listen.port = port;
|
||||
options = {
|
||||
calibreLibrary = "${config.mySystem.nasFolder}/natflix/books/";
|
||||
};
|
||||
};
|
||||
|
||||
# homepage integration
|
||||
mySystem.services.homepage.infrastructure = mkIf cfg.addToHomepage [
|
||||
{
|
||||
${app} = {
|
||||
icon = "${app}.svg";
|
||||
href = "https://${url}";
|
||||
inherit description;
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
### gatus integration
|
||||
mySystem.services.gatus.monitors = mkIf cfg.monitor [
|
||||
{
|
||||
name = app;
|
||||
group = "${category}";
|
||||
url = "https://${url}";
|
||||
interval = "1m";
|
||||
conditions = [ "[CONNECTED] == true" "[STATUS] == 200" "[RESPONSE_TIME] < 50" ];
|
||||
}
|
||||
];
|
||||
|
||||
### Ingress
|
||||
services.nginx.virtualHosts.${url} = {
|
||||
forceSSL = true;
|
||||
useACMEHost = config.networking.domain;
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://127.0.0.1:${builtins.toString port}";
|
||||
};
|
||||
};
|
||||
|
||||
### firewall config
|
||||
|
||||
# networking.firewall = mkIf cfg.openFirewall {
|
||||
# allowedTCPPorts = [ port ];
|
||||
# allowedUDPPorts = [ port ];
|
||||
# };
|
||||
|
||||
### backups
|
||||
warnings = [
|
||||
(mkIf (!cfg.backup && config.mySystem.purpose != "Development")
|
||||
"WARNING: Backups for ${app} are disabled!")
|
||||
];
|
||||
|
||||
services.restic.backups = mkIf cfg.backup (config.lib.mySystem.mkRestic
|
||||
{
|
||||
inherit app user;
|
||||
paths = [ appFolder ];
|
||||
inherit appFolder;
|
||||
});
|
||||
|
||||
};
|
||||
}
|
|
@ -25,5 +25,7 @@
|
|||
./radicale
|
||||
./node-red
|
||||
./nginx
|
||||
./miniflux
|
||||
./calibre-web
|
||||
];
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ let
|
|||
port = 2342; #int
|
||||
appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
host = "${app}" + (if cfg.development then "-dev" else "");
|
||||
host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
url = "${host}.${config.networking.domain}";
|
||||
in
|
||||
{
|
||||
|
@ -40,7 +40,7 @@ in
|
|||
description = "Add to DNS list";
|
||||
default = true;
|
||||
};
|
||||
development = mkOption
|
||||
dev = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Development instance";
|
||||
|
|
|
@ -12,13 +12,13 @@ let
|
|||
image = "%{image}";
|
||||
user = "%{user kah}"; #string
|
||||
group = "%{group kah}"; #string
|
||||
port = %{ port }; #int
|
||||
port = 1234; #int
|
||||
appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
host="${app}" ++ mkIf cfg.development "-dev";
|
||||
host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
url = "${host}.${config.networking.domain}";
|
||||
in
|
||||
{
|
||||
in
|
||||
{
|
||||
options.mySystem.${category}.${app} =
|
||||
{
|
||||
enable = mkEnableOption "${app}";
|
||||
|
@ -41,26 +41,21 @@ let
|
|||
description = "Add to DNS list";
|
||||
default = true;
|
||||
};
|
||||
development = mkOption
|
||||
dev = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Development instance";
|
||||
default = false;
|
||||
};
|
||||
backupLocal = mkOption
|
||||
backup = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable local backups";
|
||||
default = true;
|
||||
};
|
||||
backupRemote = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable remote backups";
|
||||
description = "Enable backups";
|
||||
default = true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
@ -92,7 +87,7 @@ let
|
|||
${app} = {
|
||||
icon = "${app}.svg";
|
||||
href = "https://${url}";
|
||||
description = description;
|
||||
inherit description;
|
||||
};
|
||||
}
|
||||
];
|
||||
|
@ -110,8 +105,8 @@ let
|
|||
|
||||
### Ingress
|
||||
services.nginx.virtualHosts.${url} = {
|
||||
useACMEHost = host;
|
||||
forceSSL = true;
|
||||
useACMEHost = config.networking.domain;
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://127.0.0.1:${builtins.toString port}";
|
||||
};
|
||||
|
@ -126,21 +121,23 @@ let
|
|||
|
||||
### backups
|
||||
warnings = [
|
||||
(mkIf (!cfg.backupLocal && config.mySystem.purpose != "Development")
|
||||
"WARNING: Local backups for ${app} are disabled!")
|
||||
(mkIf (!cfg.backupRemote && config.mySystem.purpose != "Development")
|
||||
"WARNING: Remote backups for ${app} are disabled!")
|
||||
(mkIf (!cfg.backup && config.mySystem.purpose != "Development")
|
||||
"WARNING: Backups for ${app} are disabled!")
|
||||
];
|
||||
|
||||
services.restic.backups = mkIf cfg.backups config.lib.mySystem.mkRestic
|
||||
services.restic.backups = mkIf cfg.backup (config.lib.mySystem.mkRestic
|
||||
{
|
||||
inherit app user;
|
||||
paths = [ appFolder ];
|
||||
inherit appFolder;
|
||||
local=cfg.backupLocal;
|
||||
remote=cfg.backupRemote;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
# services.postgresqlBackup = {
|
||||
# databases = [ app ];
|
||||
# };
|
||||
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
180
nixos/modules/nixos/services/miniflux/default.nix
Normal file
180
nixos/modules/nixos/services/miniflux/default.nix
Normal file
|
@ -0,0 +1,180 @@
|
|||
{ lib
|
||||
, config
|
||||
, pkgs
|
||||
, ...
|
||||
}:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.mySystem.${category}.${app};
|
||||
app = "miniflux";
|
||||
category = "services";
|
||||
description = "Minimalist feed reader";
|
||||
# image = "%{image}";
|
||||
user = app; #string
|
||||
group = app; #string
|
||||
port = 8072; #int
|
||||
appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
url = "${host}.${config.networking.domain}";
|
||||
databaseUrl = "user=miniflux host=/run/postgresql dbname=miniflux";
|
||||
in
|
||||
{
|
||||
options.mySystem.${category}.${app} =
|
||||
{
|
||||
enable = mkEnableOption "${app}";
|
||||
addToHomepage = mkEnableOption "Add ${app} to homepage" // { default = true; };
|
||||
monitor = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable gatus monitoring";
|
||||
default = true;
|
||||
};
|
||||
prometheus = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable prometheus scraping";
|
||||
default = true;
|
||||
};
|
||||
addToDNS = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Add to DNS list";
|
||||
default = true;
|
||||
};
|
||||
dev = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Development instance";
|
||||
default = false;
|
||||
};
|
||||
backup = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable backups";
|
||||
default = true;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
## Secrets
|
||||
sops.secrets."${category}/${app}/env" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = user;
|
||||
inherit group;
|
||||
restartUnits = [ "${app}.service" ];
|
||||
};
|
||||
|
||||
users.users.truxnell.extraGroups = [ group ];
|
||||
users.users.miniflux = {
|
||||
isSystemUser = true;
|
||||
group = "miniflux";
|
||||
};
|
||||
|
||||
users.groups.miniflux = { };
|
||||
|
||||
environment.persistence."${config.mySystem.system.impermanence.persistPath}" = lib.mkIf config.mySystem.system.impermanence.enable {
|
||||
directories = [{ directory = appFolder; inherit user; inherit group; mode = "750"; }];
|
||||
};
|
||||
|
||||
## service
|
||||
services.miniflux = {
|
||||
enable = true;
|
||||
adminCredentialsFile = config.sops.secrets."${category}/${app}/env".path;
|
||||
config = {
|
||||
LISTEN_ADDR = "localhost:${builtins.toString port}";
|
||||
DATABASE_URL = databaseUrl;
|
||||
RUN_MIGRATIONS = "1";
|
||||
CREATE_ADMIN = "1";
|
||||
};
|
||||
};
|
||||
|
||||
# automatically reset feed errors regular
|
||||
# systemd.services.miniflux-reset-feed-errors = {
|
||||
# description = "Miniflux reset feed errors";
|
||||
# wantedBy = [ "multi-user.target" ];
|
||||
# after = [ "network.target" "${app}.service" ];
|
||||
# environment.DATABASE_URL = databaseUrl;
|
||||
# startAt = "00/4:00"; # Every four hours.
|
||||
# serviceConfig = {
|
||||
# Type = "oneshot";
|
||||
# DynamicUser = true;
|
||||
# RuntimeDirectory = "miniflux"; # Creates /run/miniflux.
|
||||
## EnvironmentFile = cfg.envFilePath;
|
||||
# ExecStart = pkgs.writeShellScriptBin "miniflux-reset-feed-errors" ''
|
||||
# ${cfg.package}/bin/miniflux -reset-feed-errors
|
||||
# '';
|
||||
# };
|
||||
# };
|
||||
|
||||
# homepage integration
|
||||
mySystem.services.homepage.infrastructure = mkIf cfg.addToHomepage [
|
||||
{
|
||||
${app} = {
|
||||
icon = "${app}.svg";
|
||||
href = "https://${url}";
|
||||
inherit description;
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
# ensure postgresql setup
|
||||
|
||||
services.postgresql = {
|
||||
ensureDatabases = [ app ];
|
||||
ensureUsers = [{
|
||||
name = app;
|
||||
ensureDBOwnership = true;
|
||||
}];
|
||||
};
|
||||
|
||||
### gatus integration
|
||||
mySystem.services.gatus.monitors = mkIf cfg.monitor [
|
||||
{
|
||||
name = app;
|
||||
group = "${category}";
|
||||
url = "https://${url}";
|
||||
interval = "1m";
|
||||
conditions = [ "[CONNECTED] == true" "[STATUS] == 200" "[RESPONSE_TIME] < 50" ];
|
||||
}
|
||||
];
|
||||
|
||||
### Ingress
|
||||
services.nginx.virtualHosts.${url} = {
|
||||
forceSSL = true;
|
||||
useACMEHost = config.networking.domain;
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://127.0.0.1:${builtins.toString port}";
|
||||
};
|
||||
};
|
||||
|
||||
### firewall config
|
||||
|
||||
# networking.firewall = mkIf cfg.openFirewall {
|
||||
# allowedTCPPorts = [ port ];
|
||||
# allowedUDPPorts = [ port ];
|
||||
# };
|
||||
|
||||
### backups
|
||||
warnings = [
|
||||
(mkIf (!cfg.backup && config.mySystem.purpose != "Development")
|
||||
"WARNING: Backups for ${app} are disabled!")
|
||||
];
|
||||
|
||||
# services.restic.backups = mkIf cfg.backup (config.lib.mySystem.mkRestic
|
||||
# {
|
||||
# inherit app user;
|
||||
# paths = [ appFolder ];
|
||||
# inherit appFolder;
|
||||
# });
|
||||
|
||||
services.postgresqlBackup = mkIf cfg.backup {
|
||||
databases = [ app ];
|
||||
};
|
||||
|
||||
|
||||
|
||||
};
|
||||
}
|
77
nixos/modules/nixos/services/miniflux/secrets.sops.yaml
Normal file
77
nixos/modules/nixos/services/miniflux/secrets.sops.yaml
Normal file
|
@ -0,0 +1,77 @@
|
|||
services:
|
||||
miniflux:
|
||||
env: ENC[AES256_GCM,data:ZJeYF8xK2WhiIINfpnSQvqhKMywyI7sln/SMe7TN9RhPtO2bninS3+Fa50qWvEDlg3c=,iv:2B1LG2t3VbjSNLIIQrcvF9FtT+9ca+2+5Ox7hUsehFA=,tag:ydHlhoMqJkCVy+MYqblpUQ==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1lj5vmr02qkudvv2xedfj5tq8x93gllgpr6tzylwdlt7lud4tfv5qfqsd5u
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSNGIxTVpnYVJhaUNsOWtW
|
||||
RUMrbzJkUWZvYkZPcUU5c2ZLNkFHeU9PSm44CnFHTFB4dlBzOVFUQldxNzlxRkNG
|
||||
MkFncUQ3QlVpYmgrUHBkYk1nZm5hTEEKLS0tICtMeWx6bnAxMDBmWVhsakdNcUNP
|
||||
MXdTTTJia2Jyb2hTMExtOEdJMkZJN2cKO8qhHDYVpSg3KhrGizcErDIg6ejYyBNE
|
||||
V6w7jKHl5aaNLTaP4nBt7jMM3DQzc1pnFJBCPwj8uYQcG9Bd5+j7yw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age17edew3aahg3t5nte5g0a505sn96vnj8g8gqse8q06ccrrn2n3uysyshu2c
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWR2JzQWFFQ29uL0FvZGNT
|
||||
dU9pMklGaVRnWlRpZ3pKM3ZZTTVQQkxZUHhrCkxBTzMrR09zcW04WDlPRjZVbjNI
|
||||
TVVxWVRaS2pKaGtmdVc4WUE5SEI2MXcKLS0tIE9ZcjQzM1prSm1yRXFVNWxZeWVv
|
||||
a0x2cDhiUDlBZ2ZZVGZRbytBUEJqN2sK/+tVAbPLLBil/sHSBj0tKacvsEHGbXto
|
||||
V1InpgnmZG3vmrFJM/i2ApjwpBC1diNzrd25A7jQZyriUw6CZdKZlw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1u4tht685sqg6dkmjyer96r93pl425u6353md6fphpd84jh3jwcusvm7mgk
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJZEhscHdsWk5nek1BUEhJ
|
||||
OWI1ekNZNEYvVXhrN0Y2U1FYRThZUlhsWVdNCnFERkNqTUkwY1FzRDRZS3ljWXpm
|
||||
VFM1ejVjZVRkVnZyTlM5eldKMm15bkEKLS0tIHlvbXA0ZFU4R2FUSXlKU3A0S1lH
|
||||
N0ExUFFxd3lFK3pvQzRJS1FzUy80ZE0KlCXPhxh5DHsL+QueUYpyfsr9/bjYoO+J
|
||||
bNo45Dm0WruG+KUGYpk3+lK5ASAnSsEfL+q7NkyBVLT/ag+DSvL9IA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1cp6vegrmqfkuj8nmt2u3z0sur7n0f7e9x9zmdv4zygp8j2pnucpsdkgagc
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1T2duOFNQQlRKMGhLZDA2
|
||||
dG0zek9DelIveDZ3ZUNIWWx3M2hmZEdva2dFClRndEkzVXpRU1REbXY4ZEJhSElO
|
||||
Z1RXdkZ3UGxrcjNXbExCYml4cEpCSjgKLS0tIGlObjkyT1JZV3VYYitqbk5NNkdC
|
||||
dEI0azRtU0lCT25GSTRtaldNdzJzQVUKlkmuvYVDI9qkQpVhHMtJTOGSYXDQQoeo
|
||||
/zUI1mrmplCoWhipSMmxg7pxEorK8VUxhX7iBg5360xRBbXWqhGh3w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1ekt5xz7u2xgdzgsrffhd9x22n80cn4thxd8zxjy2ey5vq3ca7gnqz25g5r
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjQzc2OEQxOGp6Yk01eWE4
|
||||
ZjVlYjhMMzQrdG5UdmdENEMrVVMvTnhURTJvCkZ5T2JmeXc3eTBKYy9EbGYzM0s5
|
||||
cmlRc3dvYWMzeUtxMEt1QjBtVXVtSTQKLS0tIFJtQjIwbUxyWjl6MTlRUEluNlVu
|
||||
RGlHc1Jobnl5bkxaSnhHWWlScjIxbWsKxSnmobPuqg4aQcEGw2LcaBtpAMb1mkUX
|
||||
rj/UniXbYnZkIibIj/qaWpFuUbo1iCavl8cWzTrm/cjLSVjoIVYGAQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1jpeh4s553taxkyxhzlshzqjfrtvmmp5lw0hmpgn3mdnmgzku332qe082dl
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnYVVrL0RIdkV4bmpjN2ZN
|
||||
SlZaRllGcU1KanY5ZnhZcFNMelRmSEQvSUI0CmFqRUNQMXRlcjNPOUg0UmUzdlVp
|
||||
ZThjMFlnZ3pXdkJNZEZrbk1zUGJHOUUKLS0tIHFOdzhUT1VRdlFzZEplcU4zaHdR
|
||||
R01xaEt1bkgvNElXUlY3WWx5RUF4Sm8KX9E/YojwOmaulW2j80jR3zGH0WmWzXZq
|
||||
5TBWE0M4+cQPoEpA48deAV1y+oKQovFjSR08ytvZxFCK7RbKkz6h4g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1j2r8mypw44uvqhfs53424h6fu2rkr5m7asl7rl3zn3xzva9m3dcqpa97gw
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyVk9ZMGFmK1FUeG53N2lZ
|
||||
V25BdDMvT01TSjZ1MEx6NEVaU2dESnRRb0RFCklHZ05KYUNtVFV0QmFGTDJDejEv
|
||||
VjNlU293MjQ5ZDhLYUVmRTg3c3FwWTgKLS0tIEx0dDdZY3czekl1b1FaQWp0d092
|
||||
V1JHaFgxUHNCWjNteWNpckVSNGdLQW8KAYTC47LvPauQiDjylAegfl5zAdJMtzZV
|
||||
BP1aNMPDIidKKiK3wonpslnNkT83sHDpdDf+IpvnBuWL9oj36fY+yA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2024-05-05T01:01:40Z"
|
||||
mac: ENC[AES256_GCM,data:bazV7Inpog/BX/rPAQEbDqpZuOtCVQUzTXjPpnwHrAbWobYYV9QeBCOhRxXQj8jU37lsnN9vNxNeAtJMQbLQu1KVYt6qs9tFVm/kGNeaAQooXQqaUXCHnQoVNuJbZmKjkiWgpepWfkoKyP4NAuBUG8jT1DMYIp6QvuzjxRwyiI8=,iv:Nu38xj+oYkEruqZ/PvKQ6/7aHYzOxQ6gv1jPU39fEOg=,tag:+doKpf6O7iUyBnIZaV2PMQ==,type:str]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.8.1
|
|
@ -8,7 +8,17 @@ let
|
|||
cfg = config.mySystem.nfs.nas;
|
||||
in
|
||||
{
|
||||
options.mySystem.nfs.nas.enable = mkEnableOption "Mount NAS";
|
||||
options.mySystem.nfs.nas = {
|
||||
enable = mkEnableOption "Mount NAS";
|
||||
lazy = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable lazymount";
|
||||
default = false;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
config = mkIf cfg.enable
|
||||
{
|
||||
|
@ -17,7 +27,7 @@ in
|
|||
|
||||
environment.systemPackages = with pkgs; [ nfs-utils ];
|
||||
|
||||
systemd.mounts = [{
|
||||
systemd.mounts = lib.mkIf cfg.lazy [{
|
||||
type = "nfs";
|
||||
mountConfig = {
|
||||
Options = "noatime";
|
||||
|
@ -26,7 +36,7 @@ in
|
|||
where = "/mnt/nas";
|
||||
}];
|
||||
|
||||
systemd.automounts = [{
|
||||
systemd.automounts = lib.mkIf cfg.lazy [{
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
automountConfig = {
|
||||
TimeoutIdleSec = "600";
|
||||
|
@ -34,5 +44,10 @@ in
|
|||
where = "/mnt/nas";
|
||||
}];
|
||||
|
||||
fileSystems."${config.mySystem.nasFolder}" = lib.mkIf (!cfg.lazy) {
|
||||
device = "daedalus.${config.mySystem.internalDomain}:/tank";
|
||||
fsType = "nfs";
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ in
|
|||
forceSSL = true;
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:${builtins.toString config.services.node-red.port}";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -39,7 +40,7 @@ in
|
|||
directories = [{ directory = appFolder; inherit user; inherit group; mode = "750"; }];
|
||||
};
|
||||
|
||||
mySystem.services.homepage.media = mkIf cfg.addToHomepage [
|
||||
mySystem.services.homepage.home = mkIf cfg.addToHomepage [
|
||||
{
|
||||
${app} = {
|
||||
icon = "${app}.svg";
|
||||
|
|
|
@ -38,9 +38,9 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
mySystem.services.homepage.media = mkIf cfg.addToHomepage [
|
||||
mySystem.services.homepage.infrastructure = mkIf cfg.addToHomepage [
|
||||
{
|
||||
code-shodan = {
|
||||
"code-${config.networking.hostName}" = {
|
||||
icon = "vscode.svg";
|
||||
href = "https://${url}";
|
||||
|
||||
|
|
|
@ -5,26 +5,84 @@
|
|||
}:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.mySystem.services.postgresql;
|
||||
cfg = config.mySystem.${category}.${app};
|
||||
app = "postgresql";
|
||||
category = "services";
|
||||
description = "Postgres RDMS";
|
||||
# user = "%{user kah}"; #string
|
||||
# group = "%{group kah}"; #string
|
||||
# port = 1234; #int
|
||||
appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
# host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
# url = "${host}.${config.networking.domain}";
|
||||
in
|
||||
{
|
||||
options.mySystem.services.postgresql.enable = mkEnableOption "postgresql";
|
||||
options.mySystem.${category}.${app} =
|
||||
{
|
||||
enable = mkEnableOption "${app}";
|
||||
addToHomepage = mkEnableOption "Add ${app} to homepage" // { default = true; };
|
||||
prometheus = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable prometheus scraping";
|
||||
default = true;
|
||||
};
|
||||
backup = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable backups";
|
||||
default = true;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
## Secrets
|
||||
# sops.secrets."${category}/${app}/env" = {
|
||||
# sopsFile = ./secrets.sops.yaml;
|
||||
# owner = user;
|
||||
# group = group;
|
||||
# restartUnits = [ "${app}.service" ];
|
||||
# };
|
||||
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
authentication = ''
|
||||
local homeassistant homeassistant ident map=ha
|
||||
'';
|
||||
identMap = ''
|
||||
ha root homeassistant
|
||||
# ArbitraryMapName systemUser DBUser
|
||||
superuser_map root postgres
|
||||
superuser_map postgres postgres
|
||||
# Let other names login as themselves
|
||||
superuser_map /^(.*)$ \1
|
||||
'';
|
||||
ensureDatabases = [ "homeassistant" ];
|
||||
ensureUsers = [
|
||||
{ name = "homeassistant"; ensureDBOwnership = true; }
|
||||
];
|
||||
authentication = ''
|
||||
#type database DBuser auth-method optional_ident_map
|
||||
local sameuser all peer map=superuser_map
|
||||
'';
|
||||
settings = {
|
||||
max_connections = 200;
|
||||
random_page_cost = 1.1;
|
||||
};
|
||||
};
|
||||
|
||||
# enable backups
|
||||
services.postgresqlBackup = mkIf cfg.backup {
|
||||
enable = lib.mkForce true;
|
||||
location = "${config.mySystem.nasFolder}/backup/nixos/postgresql";
|
||||
};
|
||||
|
||||
|
||||
|
||||
### firewall config
|
||||
|
||||
# networking.firewall = mkIf cfg.openFirewall {
|
||||
# allowedTCPPorts = [ port ];
|
||||
# allowedUDPPorts = [ port ];
|
||||
# };
|
||||
|
||||
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ let
|
|||
port = 9001; #int
|
||||
appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
host = "${app}" + (if cfg.development then "-dev" else "");
|
||||
host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
url = "${host}.${config.networking.domain}";
|
||||
in
|
||||
{
|
||||
|
@ -34,7 +34,7 @@ in
|
|||
description = "Add to DNS list";
|
||||
default = true;
|
||||
};
|
||||
development = mkOption
|
||||
dev = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Development instance";
|
||||
|
|
|
@ -15,7 +15,7 @@ let
|
|||
port = 5232; #int
|
||||
appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
host = "${app}" + (if cfg.development then "-dev" else "");
|
||||
host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
url = "${host}.${config.networking.domain}";
|
||||
in
|
||||
{
|
||||
|
@ -41,7 +41,7 @@ in
|
|||
description = "Add to DNS list";
|
||||
default = true;
|
||||
};
|
||||
development = mkOption
|
||||
dev = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Development instance";
|
||||
|
|
143
nixos/modules/nixos/services/rss-bridge/default.nix
Normal file
143
nixos/modules/nixos/services/rss-bridge/default.nix
Normal file
|
@ -0,0 +1,143 @@
|
|||
{ lib
|
||||
, config
|
||||
, pkgs
|
||||
, ...
|
||||
}:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.mySystem.${category}.${app};
|
||||
app = "rss-bridge";
|
||||
category = "services";
|
||||
description = "rss feed for sites without";
|
||||
# image = "%{image}";
|
||||
inherit (services.rss-bridge) user;#string
|
||||
inherit (services.rss-bridge) group;#string
|
||||
port = 1234; #int
|
||||
appFolder = "/var/lib/${app}";
|
||||
# persistentFolder = "${config.mySystem.persistentFolder}/var/lib/${appFolder}";
|
||||
host = "${app}" + (if cfg.dev then "-dev" else "");
|
||||
url = "${host}.${config.networking.domain}";
|
||||
in
|
||||
{
|
||||
options.mySystem.${category}.${app} =
|
||||
{
|
||||
enable = mkEnableOption "${app}";
|
||||
addToHomepage = mkEnableOption "Add ${app} to homepage" // { default = true; };
|
||||
monitor = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable gatus monitoring";
|
||||
default = true;
|
||||
};
|
||||
prometheus = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable prometheus scraping";
|
||||
default = true;
|
||||
};
|
||||
addToDNS = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Add to DNS list";
|
||||
default = true;
|
||||
};
|
||||
dev = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Development instance";
|
||||
default = false;
|
||||
};
|
||||
backup = mkOption
|
||||
{
|
||||
type = lib.types.bool;
|
||||
description = "Enable backups";
|
||||
default = true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
## Secrets
|
||||
# sops.secrets."${category}/${app}/env" = {
|
||||
# sopsFile = ./secrets.sops.yaml;
|
||||
# owner = user;
|
||||
# group = group;
|
||||
# restartUnits = [ "${app}.service" ];
|
||||
# };
|
||||
|
||||
users.users.truxnell.extraGroups = [ group ];
|
||||
|
||||
|
||||
# Folder perms - only for containers
|
||||
# systemd.tmpfiles.rules = [
|
||||
# "d ${persistentFolder}/ 0750 ${user} ${group} -"
|
||||
# ];
|
||||
|
||||
## service
|
||||
# services.test= {
|
||||
# enable = true;
|
||||
# };
|
||||
|
||||
# homepage integration
|
||||
mySystem.services.homepage.infrastructure = mkIf cfg.addToHomepage [
|
||||
{
|
||||
${app} = {
|
||||
icon = "${app}.svg";
|
||||
href = "https://${url}";
|
||||
inherit description;
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
### gatus integration
|
||||
mySystem.services.gatus.monitors = mkIf cfg.monitor [
|
||||
{
|
||||
name = app;
|
||||
group = "${category}";
|
||||
url = "https://${url}";
|
||||
interval = "1m";
|
||||
conditions = [ "[CONNECTED] == true" "[STATUS] == 200" "[RESPONSE_TIME] < 50" ];
|
||||
}
|
||||
];
|
||||
|
||||
### Ingress
|
||||
services.nginx.virtualHosts.${url} = {
|
||||
forceSSL = true;
|
||||
useACMEHost = config.networking.domain;
|
||||
locations."^~ /" = {
|
||||
proxyPass = "http://127.0.0.1:${builtins.toString port}";
|
||||
};
|
||||
};
|
||||
|
||||
### firewall config
|
||||
|
||||
# networking.firewall = mkIf cfg.openFirewall {
|
||||
# allowedTCPPorts = [ port ];
|
||||
# allowedUDPPorts = [ port ];
|
||||
# };
|
||||
|
||||
### backups
|
||||
warnings = [
|
||||
(mkIf (!cfg.backup && config.mySystem.purpose != "Development")
|
||||
"WARNING: Backups for ${app} are disabled!")
|
||||
];
|
||||
|
||||
services.restic.backups = config.lib.mySystem.mkRestic
|
||||
{
|
||||
inherit app user;
|
||||
paths = [ appFolder ];
|
||||
inherit appFolder;
|
||||
};
|
||||
|
||||
|
||||
# services.postgresqlBackup = {
|
||||
# databases = [ app ];
|
||||
# };
|
||||
|
||||
|
||||
|
||||
};
|
||||
}
|
|
@ -16,7 +16,10 @@ with config;
|
|||
# TODO decide if i drop to bash on pis?
|
||||
shell.fish.enable = true;
|
||||
|
||||
nfs.nas.enable = true;
|
||||
nfs.nas = {
|
||||
enable = true;
|
||||
lazy = true;
|
||||
};
|
||||
system.resticBackup.local.enable = false;
|
||||
system.resticBackup.remote.enable = false;
|
||||
|
||||
|
|
Reference in a new issue