diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e62b828 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 diff --git a/.gitattributes b/.gitattributes index 780e15a..6313b56 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1 @@ -* text=auto -core.autocrlf=false -core.eol=lf -core.filemode=false +* text=auto eol=lf diff --git a/.just/machine.just b/.just/machine.just new file mode 100644 index 0000000..3cb4587 --- /dev/null +++ b/.just/machine.just @@ -0,0 +1,20 @@ +@_default: list + +[doc('List machines')] +@list: + ls -1 ../systems/x86_64-linux/ + +[doc('Update target machine')] +[no-exit-message] +@update machine: + echo "Checking vars" + cd .. && just vars _check {{ machine }} + echo "" + just assert '-d "../systems/x86_64-linux/{{ machine }}"' "Machine {{ machine }} does not exist, must be one of: $(ls ../systems/x86_64-linux/ | sed ':a;N;$!ba;s/\n/, /g')" + nixos-rebuild switch -L --sudo --target-host {{ machine }} --flake ..#{{ machine }} --log-format internal-json -v |& nom --json + +[doc('Check if target machine builds')] +[no-exit-message] +@check machine: + just assert '-d "../systems/x86_64-linux/{{ machine }}"' "Machine {{ machine }} does not exist, must be one of: $(ls ../systems/x86_64-linux/ | sed ':a;N;$!ba;s/\n/, /g')" + nix build ..#nixosConfigurations.{{ machine }}.config.system.build.toplevel diff --git a/.just/users.just b/.just/users.just new file mode 100644 index 0000000..e798cc3 --- /dev/null +++ b/.just/users.just @@ -0,0 +1,101 @@ +set unstable := true +set quiet := true + +_default: + just --list users + +[doc('List available users')] +[script] +list: + cd .. && just vars get ulmo zitadel/users | jq -r -C ' + import ".jq/table" as table; + import ".jq/format" as f; + + fromjson + | to_entries + | sort_by(.key) + | map( + (.key|f::to_title) + ":\n" + + table::create( + .value + | to_entries + | sort_by(.key) + | map({username:.key} + .value) + ) + ) + | join("\n\n┄┄┄\n\n") + '; + +[doc('Add a new user')] +[script] +add: + exec 5>&1 + + pad () { [ "$#" -gt 1 ] && [ -n "$2" ] && printf "%$2.${2#-}s" "$1"; } + + input() { + local label=$1 + local value=$2 + + local res=$(gum input --header "$label" --value "$value") + echo -e "\e[2m$(pad "$label" -11)\e[0m$res" >&5 + echo $res + } + + data=`cd .. && just vars get ulmo zitadel/users | jq 'fromjson'` + + # Gather inputs + org=` + jq -r 'to_entries | map(.key)[]' <<< "$data" \ + | gum choose --header 'Which organisation to save to?' --select-if-one + ` + username=`input 'user name' ''` + email=`input 'email' ''` + first_name=`input 'first name' ''` + last_name=`input 'last name' ''` + + user_exists=`jq --arg 'org' "$org" --arg 'username' "$username" '.[$org][$username]? | . != null' <<< "$data"` + + if [ "$user_exists" == "true" ]; then + gum confirm 'User already exists, overwrite it?' --padding="1 1" || exit 0 + fi + + next=` + jq \ + --arg 'org' "$org" \ + --arg 'username' "$username" \ + --arg 'email' "$email" \ + --arg 'first_name' "$first_name" \ + --arg 'last_name' "$last_name" \ + --compact-output \ + '.[$org] += { $username: { email: $email, firstName: $first_name, lastName: $last_name } }' \ + <<< $data + ` + + gum spin --title "saving..." -- echo "$(cd .. && just vars set ulmo 'zitadel/users' "$next")" + +[doc('Remove a new user')] +[script] +remove: + data=`cd .. && just vars get ulmo zitadel/users | jq fromjson` + + # Gather inputs + org=` + jq -r 'to_entries | map(.key)[]' <<< "$data" \ + | gum choose --header 'Which organisation?' --select-if-one + ` + user=` + jq -r --arg org "$org" '.[$org] | to_entries | map(.key)[]' <<< "$data" \ + | gum choose --header 'Which user?' --select-if-one + ` + + next=` + jq \ + --arg 'org' "$org" \ + --arg 'user' "$user" \ + --compact-output \ + 'del(.[$org][$user])' \ + <<< $data + ` + + gum spin --title "saving..." -- echo "$(cd .. && just vars set ulmo 'zitadel/users' "$next")" diff --git a/.just/vars.just b/.just/vars.just index 79c1bda..2ae9a44 100644 --- a/.just/vars.just +++ b/.just/vars.just @@ -1,39 +1,38 @@ set unstable := true set quiet := true -machine_base_path := justfile_directory() + "/machines" -secret_base_path := justfile_directory() + "/systems/x86_64-linux" +base_path := justfile_directory() + "/systems/x86_64-linux" _default: just --list vars [doc('List all vars of {machine}')] list machine: - sops decrypt {{ secret_base_path }}/{{ machine }}/secrets.yml + sops decrypt {{ base_path }}/{{ machine }}/secrets.yml [doc('Edit all vars of {machine} in your editor')] edit machine: - sops edit {{ secret_base_path }}/{{ machine }}/secrets.yml + sops edit {{ base_path }}/{{ machine }}/secrets.yml [doc('Set var {value} by {key} for {machine}')] @set machine key value: - sops set {{ secret_base_path }}/{{ machine }}/secrets.yml "$(printf '%s\n' '["{{ key }}"]' | sed -E 's#/#"]["#g; s/\["([0-9]+)"\]/[\1]/g')" "\"$(echo '{{ value }}' | sed 's/\"/\\\"/g')\"" + sops set {{ base_path }}/{{ machine }}/secrets.yml "$(printf '%s\n' '["{{ key }}"]' | sed -E 's#/#"]["#g; s/\["([0-9]+)"\]/[\1]/g')" "\"$(echo '{{ value }}' | sed 's/\"/\\\"/g')\"" - git add {{ secret_base_path }}/{{ machine }}/secrets.yml - git commit -m 'chore(secrets): set secret "{{ key }}" for machine "{{ machine }}"' -- {{ secret_base_path }}/{{ machine }}/secrets.yml > /dev/null + git add {{ base_path }}/{{ machine }}/secrets.yml + git commit -m 'chore(secrets): set secret "{{ key }}" for machine "{{ machine }}"' -- {{ base_path }}/{{ machine }}/secrets.yml > /dev/null echo "Done" [doc('Get var by {key} from {machine}')] get machine key: - sops decrypt {{ secret_base_path }}/{{ machine }}/secrets.yml | yq ".$(echo "{{ key }}" | sed -E 's/\//./g') // \"\"" + sops decrypt {{ base_path }}/{{ machine }}/secrets.yml | yq ".$(echo "{{ key }}" | sed -E 's/\//./g') // \"\"" [doc('Remove var by {key} for {machine}')] remove machine key: - sops unset {{ secret_base_path }}/{{ machine }}/secrets.yml "$(printf '%s\n' '["{{ key }}"]' | sed -E 's#/#"]["#g; s/\["([0-9]+)"\]/[\1]/g')" + sops unset {{ base_path }}/{{ machine }}/secrets.yml "$(printf '%s\n' '["{{ key }}"]' | sed -E 's#/#"]["#g; s/\["([0-9]+)"\]/[\1]/g')" - git add {{ secret_base_path }}/{{ machine }}/secrets.yml - git commit -m 'chore(secrets): removed secret "{{ key }}" from machine "{{ machine }}"' -- {{ secret_base_path }}/{{ machine }}/secrets.yml > /dev/null + git add {{ base_path }}/{{ machine }}/secrets.yml + git commit -m 'chore(secrets): removed secret "{{ key }}" from machine "{{ machine }}"' -- {{ base_path }}/{{ machine }}/secrets.yml > /dev/null echo "Done" @@ -44,14 +43,14 @@ generate machine: # Skip if we already have a value [ $(just vars get "{{ machine }}" "$key" | jq -r) ] && continue - just _rotate "{{ machine }}" "$key" + just vars _rotate "{{ machine }}" "$key" done [doc('Regenerate var values for {machine}')] [script] _rotate machine key: # Exit if there's no script - [ -f "{{ justfile_directory() }}/script/{{ key }}" ] || exit + [ -f "{{ justfile_directory() }}/script/{{ key }}" ] || exit 0 echo "Executing script for {{ key }}" just vars set "{{ machine }}" "{{ key }}" "$(cd -- "$(dirname "{{ justfile_directory() }}/script/{{ key }}")" && source "./$(basename "{{ key }}")")" @@ -60,7 +59,7 @@ _rotate machine key: check: cd .. - for machine in $(ls {{ machine_base_path }}); do + for machine in $(ls {{ base_path }}); do just vars _check "$machine" done @@ -71,14 +70,14 @@ _check machine: # we can skip this folder as we are # missing the files used to compare # the defined vs the configured secrets - if [ ! -f "{{ machine_base_path }}/{{ machine }}/default.nix" ]; then + if [ ! -f "{{ base_path }}/{{ machine }}/default.nix" ]; then printf "\r• %-8sskipped\n" "{{ machine }}" exit 0 fi exec 3< <(jq -nr \ --rawfile defined <(nix eval --json ..#nixosConfigurations.{{ machine }}.config.sops.secrets 2>/dev/null) \ - --rawfile configured <([ -f "{{ secret_base_path }}/{{ machine }}/secrets.yml" ] && sops decrypt {{ secret_base_path }}/{{ machine }}/secrets.yml | yq '.' || echo "{}") \ + --rawfile configured <([ -f "{{ base_path }}/{{ machine }}/secrets.yml" ] && sops decrypt {{ base_path }}/{{ machine }}/secrets.yml | yq '.' || echo "{}") \ ' [ $configured | fromjson | paths(scalars) | join("/") ] as $conf | $defined diff --git a/.justfile b/.justfile index 7f91bca..cee0db9 100644 --- a/.justfile +++ b/.justfile @@ -3,3 +3,34 @@ [doc('Manage vars')] mod vars '.just/vars.just' + +[doc('Manage users')] +mod users '.just/users.just' + +[doc('Manage machines')] +mod machine '.just/machine.just' + +[doc('Show information about project')] +@show: + echo "show" + +[doc('update the flake dependencies')] +@update: + nix flake update + git commit -m 'chore: update dependencies' -- ./flake.lock > /dev/null + echo "Done" + +[doc('Introspection on flake output')] +@select key: + nix eval --show-trace --json .#{{ key }} | jq . + + + +#=============================================================================================== +# Utils +#=============================================================================================== +[no-exit-message] +[no-cd] +[private] +@assert condition message: + [ {{ condition }} ] || { echo -e 1>&2 "\n\x1b[1;41m Error \x1b[0m {{ message }}\n"; exit 1; } diff --git a/clan/flake-module.nix b/clan/flake-module.nix deleted file mode 100644 index 16a10f4..0000000 --- a/clan/flake-module.nix +++ /dev/null @@ -1,43 +0,0 @@ -{ - lib, - inputs, - ... -}: { - imports = [ - ./machines.nix - ./tags.nix - ./instances.nix - ]; - - clan = { - meta = { - name = "arda"; - domain = "arda"; - description = "My personal machines at home"; - }; - - directory = ../.; - - specialArgs = { - ardaLib = { - types = - ./types - |> (inputs.import-tree.withLib lib).leafs - |> lib.map (mod: { - name = mod |> lib.baseNameOf |> lib.splitString "." |> lib.head; - value = lib.types.submoduleWith {modules = [mod];}; - }) - |> lib.listToAttrs; - }; - }; - - exportInterfaces = - ./interfaces - |> (inputs.import-tree.withLib lib).leafs - |> lib.map (mod: { - name = mod |> lib.baseNameOf |> lib.splitString "." |> lib.head; - value = import mod; - }) - |> lib.listToAttrs; - }; -} diff --git a/clan/instances.nix b/clan/instances.nix deleted file mode 100644 index 57d06f6..0000000 --- a/clan/instances.nix +++ /dev/null @@ -1,253 +0,0 @@ -{ - self, - inputs, - ... -}: let - db = - self.clan.exports - |> inputs.clan-core.lib.getExport { - serviceName = "arda/persistence"; - roleName = "default"; - machineName = "ulmo"; - instanceName = "persistence"; - } - |> (v: v.persistence.driver.${v.persistence.main}); -in { - clan.inventory.instances = { - users-chris = { - module = { - name = "users"; - input = "clan-core"; - }; - - roles.default.machines.mandos.settings = {}; - roles.default.machines.manwe.settings = {}; - roles.default.machines.orome.settings = {}; - roles.default.machines.tulkas.settings = {}; - - roles.default.settings = { - user = "chris"; - groups = ["wheel"]; - prompt = true; - share = true; - }; - }; - - clanDns = { - module = { - name = "dm-dns"; - input = "clan-core"; - }; - - roles.default.tags = ["all"]; - }; - - gateway = { - module = { - name = "gateway"; - input = "self"; - }; - - roles.default = { - tags = ["operational:role:gateway"]; - - settings = { - driver = "caddy"; - - hosts = { - "auth.kruining.eu" = '' - reverse_proxy h2c://[::1]:9092 - ''; - }; - }; - }; - }; - - persistence = { - module = { - name = "persistence"; - input = "self"; - }; - - roles.default.tags = ["operational:availability:always-on" "operational:storage:large"]; - }; - - identity = { - module = { - name = "identity"; - input = "self"; - }; - - roles.default = { - tags = ["operational:availability:always-on"]; - - settings = { - database = db; - - organization = { - nix = { - user = { - chris = { - email = "chris@kruining.eu"; - firstName = "Chris"; - lastName = "Kruining"; - - roles = ["ORG_OWNER"]; - instanceRoles = ["IAM_OWNER"]; - }; - - kaas = { - email = "chris+kaas@kruining.eu"; - firstName = "Kaas"; - lastName = "Kruining"; - }; - }; - - project = { - ulmo = { - projectRoleCheck = true; - projectRoleAssertion = true; - hasProjectCheck = true; - - role = { - jellyfin = { - group = "jellyfin"; - }; - jellyfin_admin = { - group = "jellyfin"; - }; - }; - - assign = { - chris = ["jellyfin" "jellyfin_admin"]; - kaas = ["jellyfin"]; - }; - - application = { - jellyfin = { - redirectUris = ["https://jellyfin.kruining.eu/sso/OID/redirect/zitadel"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - - forgejo = { - redirectUris = ["https://git.amarth.cloud/user/oauth2/zitadel/callback"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - - vaultwarden = { - redirectUris = ["https://vault.kruining.eu/identity/connect/oidc-signin"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - exportMap = { - client_id = "SSO_CLIENT_ID"; - client_secret = "SSO_CLIENT_SECRET"; - }; - }; - - matrix = { - redirectUris = ["https://matrix.kruining.eu/_synapse/client/oidc/callback"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - - mydia = { - redirectUris = ["http://localhost:2010/auth/oidc/callback"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - - grafana = { - redirectUris = ["http://localhost:9001/login/generic_oauth"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - }; - }; - - convex = { - projectRoleCheck = true; - projectRoleAssertion = true; - hasProjectCheck = true; - - application = { - scry = { - redirectUris = ["https://nautical-salamander-320.eu-west-1.convex.cloud/api/auth/callback/zitadel"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - }; - }; - }; - - action = { - flattenRoles = { - script = '' - (ctx, api) => { - if (ctx.v1.user.grants == undefined || ctx.v1.user.grants.count == 0) { - return; - } - - const roles = ctx.v1.user.grants.grants.flatMap(({ roles, projectId }) => roles.map(role => projectId + ':' + role)); - - api.v1.claims.setClaim('nix:zitadel:custom', JSON.stringify({ roles })); - }; - ''; - }; - }; - - triggers = [ - { - flowType = "customiseToken"; - triggerType = "preUserinfoCreation"; - actions = ["flattenRoles"]; - } - { - flowType = "customiseToken"; - triggerType = "preAccessTokenCreation"; - actions = ["flattenRoles"]; - } - ]; - }; - }; - }; - }; - }; - - servarr = { - module = { - name = "servarr"; - input = "self"; - }; - - roles.default = { - tags = ["operational:availability:always-on"]; - - settings = { - enable = true; - database = db; - - services = { - sonarr = { - rootFolders = [ - "/var/media/series" - ]; - }; - radarr = { - rootFolders = [ - "/var/media/movies" - ]; - }; - lidarr = { - rootFolders = [ - "/var/media/music" - ]; - }; - prowlarr = {}; - }; - }; - }; - }; - }; -} diff --git a/clan/interfaces/gateway.nix b/clan/interfaces/gateway.nix deleted file mode 100644 index c8faf04..0000000 --- a/clan/interfaces/gateway.nix +++ /dev/null @@ -1,94 +0,0 @@ -{lib, ...}: let - inherit (lib) mkOption types; -in { - options = { - services = mkOption { - type = types.attrsOf (types.submodule ({name, ...}: { - options = { - name = mkOption { - type = types.str; - default = name; - }; - - endpoint = mkOption { - type = types.submoduleWith { - modules = [../types/endpoint.nix]; - }; - default = {}; - apply = attrs: - attrs - // { - __toString = self: let - protocol = - if self.protocol != null - then "${self.protocol}://" - else ""; - - port = - if self.port != null - then ":${toString self.port}" - else ""; - - path = - if self.path != null - then "/${self.path}" - else ""; - - query = - if self.query != null - then "?${toString self.query - |> lib.attrsToList - |> lib.map ({ - name, - value, - }: "${name}=${value}")}" - else ""; - - hash = - if self.hash != null - then "#${toString self.hash - |> lib.attrsToList - |> lib.map ({ - name, - value, - }: "${name}=${value}")}" - else ""; - in "${protocol}${self.host}${port}${path}${query}${hash}"; - }; - }; - - # protocol = mkOption { - # type = types.str; - # default = "http"; - # }; - - # host = mkOption { - # type = types.str; - # default = "[::1]"; - # }; - - # port = mkOption { - # type = types.port; - # }; - }; - })); - default = {}; - }; - - functions = mkOption { - type = types.attrsOf (types.submodule ({name, ...}: { - options = { - name = mkOption { - type = types.str; - default = name; - }; - - body = mkOption { - type = types.str; - }; - }; - })); - default = {}; - }; - }; -} diff --git a/clan/interfaces/persistence.nix b/clan/interfaces/persistence.nix deleted file mode 100644 index 878d4c5..0000000 --- a/clan/interfaces/persistence.nix +++ /dev/null @@ -1,24 +0,0 @@ -{lib, ...}: let - inherit (lib) mkOption types; -in { - options = { - main = mkOption { - type = types.nullOr types.str; - default = null; - }; - - driver = mkOption { - type = types.attrsOf (types.submoduleWith { - modules = [ - ../types/endpoint.nix - ]; - }); - default = {}; - }; - - databases = mkOption { - type = types.listOf types.str; - default = []; - }; - }; -} diff --git a/clan/machines.nix b/clan/machines.nix deleted file mode 100644 index f838aeb..0000000 --- a/clan/machines.nix +++ /dev/null @@ -1,75 +0,0 @@ -{...}: { - clan.inventory.machines = { - aule = { - name = "aule"; - description = "Planned build server."; - machineClass = "nixos"; - tags = []; - }; - mandos = { - name = "mandos"; - description = "Living room Steam box."; - machineClass = "nixos"; - tags = [ - "capability:mobility:stationary" - "operational:availability:wake-on-demand" - ]; - }; - manwe = { - name = "manwe"; - description = "Main desktop."; - machineClass = "nixos"; - tags = [ - "capability:mobility:stationary" - "operational:availability:manual" - ]; - }; - melkor = { - name = "melkor"; - description = "Planned machine with no defined role yet."; - machineClass = "nixos"; - tags = []; - }; - orome = { - name = "orome"; - description = "Work laptop."; - machineClass = "nixos"; - tags = [ - "capability:mobility:portable" - "operational:availability:manual" - ]; - }; - tulkas = { - name = "tulkas"; - description = "Steam Deck."; - machineClass = "nixos"; - tags = [ - "capability:mobility:portable" - "operational:availability:manual" - ]; - }; - ulmo = { - name = "ulmo"; - description = "Primary self-hosted services machine."; - machineClass = "nixos"; - tags = [ - "capability:mobility:stationary" - "operational:availability:always-on" - "operational:storage:large" - "operational:role:gateway" - ]; - }; - varda = { - name = "varda"; - description = "Planned machine with no defined role yet."; - machineClass = "nixos"; - tags = []; - }; - yavanna = { - name = "yavanna"; - description = "Planned machine with no defined role yet."; - machineClass = "nixos"; - tags = []; - }; - }; -} diff --git a/clan/tags.nix b/clan/tags.nix deleted file mode 100644 index 1c5256c..0000000 --- a/clan/tags.nix +++ /dev/null @@ -1,12 +0,0 @@ -{...}: { - clan.inventory.tags = { - config, - machines, - ... - }: { - # tag_name = [ "list" "of" "machines" ] - "capability:hardware:gpu" = [""]; - "capability:hardware:audio" = [""]; - "capability:hardware:bluetooth" = [""]; - }; -} diff --git a/clan/types/endpoint.nix b/clan/types/endpoint.nix deleted file mode 100644 index fab5a86..0000000 --- a/clan/types/endpoint.nix +++ /dev/null @@ -1,45 +0,0 @@ -{lib, ...}: let - inherit (lib) mkOption types; -in { - options = { - protocol = mkOption { - type = types.str; - default = "http"; - }; - - host = mkOption { - type = types.str; - default = "localhost"; - }; - - port = mkOption { - type = types.nullOr types.port; - default = null; - }; - - user = mkOption { - type = types.nullOr types.str; - default = null; - }; - - password = mkOption { - type = types.nullOr types.str; - default = null; - }; - - path = mkOption { - type = types.nullOr types.str; - default = null; - }; - - query = mkOption { - type = types.nullOr (types.attrsOf types.str); - default = null; - }; - - hash = mkOption { - type = types.nullOr (types.attrsOf types.str); - default = null; - }; - }; -} diff --git a/clanServices/flake-module.nix b/clanServices/flake-module.nix deleted file mode 100644 index 141d07b..0000000 --- a/clanServices/flake-module.nix +++ /dev/null @@ -1,19 +0,0 @@ -{lib, ...}: { - imports = - ./. - |> builtins.readDir - |> lib.attrsToList - |> builtins.map ({ - name, - value, - }: { - type = value; - path = ./. + "/${name}/flake-module.nix"; - }) - |> builtins.filter ({ - type, - path, - }: - type == "directory" && (builtins.pathExists path)) - |> builtins.map ({path, ...}: path); -} diff --git a/clanServices/gateway/README.md b/clanServices/gateway/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/clanServices/gateway/default.nix b/clanServices/gateway/default.nix deleted file mode 100644 index 2c6a311..0000000 --- a/clanServices/gateway/default.nix +++ /dev/null @@ -1,92 +0,0 @@ -{ - lib, - clanLib, - exports, - ... -}: let - inherit (builtins) toString; -in { - _class = "clan.service"; - manifest = { - name = "arda/gateway"; - description = '' - ''; - readme = builtins.readFile ./README.md; - exports = { - inputs = []; - out = []; - }; - }; - - roles.default = { - description = ''''; - - interface = {lib, ...}: let - inherit (lib) mkOption types; - in { - options = { - driver = mkOption { - type = types.enum ["caddy" "nginx"]; - }; - - hosts = mkOption { - type = types.attrsOf types.str; - default = {}; - }; - }; - }; - - perInstance = { - mkExports, - machine, - settings, - ... - }: let - reverse_proxies = - exports - |> clanLib.selectExports (_scope: true) - |> lib.mapAttrsToList (_: value: (value.gateway.services or {}) |> lib.attrValues) - |> lib.concatLists - |> lib.map ({ - name, - endpoint, - }: { - name = "${name}.${machine.name}.arda"; - value = { - extraConfig = '' - reverse_proxy ${toString endpoint} - ''; - }; - }) - |> lib.listToAttrs; - in { - # exports = - # mkExports { - # }; - - nixosModule = { - lib, - pkgs, - ... - }: let - inherit (lib) mkMerge mkIf; - - caddyPackage = pkgs.caddy.withPlugins { - plugins = ["github.com/corazawaf/coraza-caddy/v2@v2.1.0"]; - hash = "sha256-pSXjLaZoRtKV3eFl2ySRSjl3yxi514G1Cb7pfrpxxtE="; - }; - in { - config = mkMerge [ - (lib.mkIf (settings.driver == "caddy") { - services.caddy = { - enable = true; - package = caddyPackage; - - virtualHosts = reverse_proxies // {}; - }; - }) - ]; - }; - }; - }; -} diff --git a/clanServices/gateway/flake-module.nix b/clanServices/gateway/flake-module.nix deleted file mode 100644 index a53d5d7..0000000 --- a/clanServices/gateway/flake-module.nix +++ /dev/null @@ -1,13 +0,0 @@ -{...}: let - module = ./default.nix; -in { - clan.modules.gateway = module; - - # perSystem = {...}: { - # clan.nixosTests.gateway = { - # imports = []; - - # clan.modules."@arda/gateway" = module; - # }; - # }; -} diff --git a/clanServices/identity/README.md b/clanServices/identity/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/clanServices/identity/default.nix b/clanServices/identity/default.nix deleted file mode 100644 index 1030ebb..0000000 --- a/clanServices/identity/default.nix +++ /dev/null @@ -1,518 +0,0 @@ -{ - lib, - clanLib, - exports, - ... -}: let - inherit (builtins) toString readFile; - inherit (lib) mkMerge mkIf; -in { - _class = "clan.service"; - manifest = { - name = "arda/identity"; - description = '' - ''; - readme = readFile ./README.md; - exports = { - inputs = ["persistence"]; - out = ["gateway" "persistence"]; - }; - }; - - roles.default = { - description = ''''; - - interface = {lib, ...}: let - inherit (lib) mkOption types toSentenceCase literalExpression; - in { - options = { - driver = mkOption { - type = types.enum ["zitadel"]; - default = "zitadel"; - }; - - database = mkOption { - type = types.anything; - }; - - port = mkOption { - type = types.port; - default = 9092; - }; - - organization = mkOption { - type = types.attrsOf (types.submodule ({ name, ... }: { - options = - let - org = name; - in - { - isDefault = mkOption { - type = types.bool; - default = false; - example = "true"; - description = '' - True sets the '${org}' org as default org for the instance. Only one org can be default org. - Nothing happens if you set it to false until you set another org as default org. - ''; - }; - - project = mkOption { - default = {}; - type = types.attrsOf (types.submodule { - options = { - hasProjectCheck = mkOption { - type = types.bool; - default = false; - example = "true"; - description = '' - ZITADEL checks if the org of the user has permission to this project. - ''; - }; - - privateLabelingSetting = mkOption { - type = types.nullOr (types.enum [ "unspecified" "enforceProjectResourceOwnerPolicy" "allowLoginUserResourceOwnerPolicy" ]); - default = null; - example = "enforceProjectResourceOwnerPolicy"; - description = '' - Defines from where the private labeling should be triggered, - - supported values: - - unspecified - - enforceProjectResourceOwnerPolicy - - allowLoginUserResourceOwnerPolicy - ''; - }; - - projectRoleAssertion = mkOption { - type = types.bool; - default = false; - example = "true"; - description = '' - Describes if roles of user should be added in token. - ''; - }; - - projectRoleCheck = mkOption { - type = types.bool; - default = false; - example = "true"; - description = '' - ZITADEL checks if the user has at least one on this project. - ''; - }; - - role = mkOption { - default = {}; - type = types.attrsOf (types.submodule ({ name, ... }: { - options = - let - roleName = name; - in - { - displayName = mkOption { - type = types.str; - default = toSentenceCase name; - example = "RoleName"; - description = '' - Name used for project role. - ''; - }; - - group = mkOption { - type = types.nullOr types.str; - default = null; - example = "some_group"; - description = '' - Group used for project role. - ''; - }; - }; - })); - }; - - assign = mkOption { - default = {}; - type = types.attrsOf (types.listOf types.str); - }; - - application = mkOption { - default = {}; - type = types.attrsOf (types.submodule { - options = { - redirectUris = mkOption { - type = types.nonEmptyListOf types.str; - example = '' - [ "https://example.com/redirect/url" ] - ''; - description = '' - . - ''; - }; - - grantTypes = mkOption { - type = types.nonEmptyListOf (types.enum [ "authorizationCode" "implicit" "refreshToken" "deviceCode" "tokenExchange" ]); - example = '' - [ "authorizationCode" ] - ''; - description = '' - . - ''; - }; - - responseTypes = mkOption { - type = types.nonEmptyListOf (types.enum [ "code" "idToken" "idTokenToken" ]); - example = '' - [ "code" ] - ''; - description = '' - . - ''; - }; - - exportMap = - let - strOpt = mkOption { type = types.nullOr types.str; default = null; }; - in - mkOption { - type = types.submodule { options = { client_id = strOpt; client_secret = strOpt; }; }; - default = {}; - example = literalExpression '' - { - client_id = "SSO_CLIENT_ID"; - client_secret = "SSO_CLIENT_SECRET"; - } - ''; - description = '' - Remap the outputted variables to another key. - ''; - }; - }; - }); - }; - }; - }); - }; - - user = mkOption { - default = {}; - type = types.attrsOf (types.submodule ({ name, ... }: { - options = - let - username = name; - in - { - email = mkOption { - type = types.str; - example = "someone@some.domain"; - description = '' - Username. - ''; - }; - - userName = mkOption { - type = types.nullOr types.str; - default = username; - example = "some_user_name"; - description = '' - Username. Default value is the key of the config object you created, you can overwrite that by setting this option - ''; - }; - - firstName = mkOption { - type = types.str; - example = "John"; - description = '' - First name of the user. - ''; - }; - - lastName = mkOption { - type = types.str; - example = "Doe"; - description = '' - Last name of the user. - ''; - }; - - roles = mkOption { - type = types.listOf types.str; - default = []; - example = "[ \"ORG_OWNER\" ]"; - description = '' - List of roles granted to organisation. - ''; - }; - - instanceRoles = mkOption { - type = types.listOf types.str; - default = []; - example = "[ \"IAM_OWNER\" ]"; - description = '' - List of roles granted to instance. - ''; - }; - }; - })); - }; - - action = mkOption { - default = {}; - type = types.attrsOf (types.submodule ({ name, ... }: { - options = { - script = mkOption { - type = types.str; - example = '' - (ctx, api) => { - api.v1.claims.setClaim('some_claim', 'some_value'); - }; - ''; - description = '' - The script to run. This must be a function that receives 2 parameters, and returns void. During the creation of the action's script this module simly does `const {{name}} = {{script}}`. - ''; - }; - - timeout = mkOption { - type = (types.ints.between 0 20); - default = 10; - example = "10"; - description = '' - After which time the action will be terminated if not finished. - ''; - }; - - allowedToFail = mkOption { - type = types.bool; - default = true; - example = "true"; - description = '' - Allowed to fail. - ''; - }; - }; - })); - }; - - triggers = mkOption { - default = []; - type = types.listOf (types.submodule { - options = { - flowType = mkOption { - type = types.enum [ "authentication" "customiseToken" "internalAuthentication" "samlResponse" ]; - example = "customiseToken"; - description = '' - Type of the flow to which the action triggers belong. - ''; - }; - - triggerType = mkOption { - type = types.enum [ "postAuthentication" "preCreation" "postCreation" "preUserinfoCreation" "preAccessTokenCreation" "preSamlResponse" ]; - example = "postAuthentication"; - description = '' - Trigger type on when the actions get triggered. - ''; - }; - - actions = mkOption { - type = types.nonEmptyListOf types.str; - example = ''[ "action_name" ]''; - description = '' - Names of actions to trigger - ''; - }; - }; - }); - }; - }; - })); - }; - }; - }; - - perInstance = { - mkExports, - settings, - machine, - instanceName, - ... - }: { - exports = mkExports (mkMerge [ - { - gateway.services.identity = {endpoint.port = settings.port;}; - } - (mkIf (settings.driver == "zitadel") { - gateway.functions.auth = { - body = '' - forward_auth h2c://[::1]:${toString settings.port} { - uri /api/authz/forward-auth - copy_headers Remote-User Remote-Groups Remote-Email Remote-Name - } - ''; - }; - - persistence.databases = ["zitadel"]; - }) - ]); - - nixosModule = args@{ - lib, - pkgs, - config, - ... - }: let - vars = config.clan.core.vars.generators.zitadel.files; - users = config.clan.core.vars.generators.zitadel_users.files.users.path; - email_password = config.clan.core.vars.generators.zitadel_email_password.files.password.path; - - ardaLib = import ../../lib/strings.nix args; - zLib = import ./lib.nix (args // {inherit settings ardaLib;}); - in { - config = mkMerge [ - (mkIf (settings.driver == "zitadel") ({ - clan.core.vars.generators.zitadel = { - dependencies = ["persistence"]; - - files = { - masterKey = { - deploy = true; - owner = "zitadel"; - group = "zitadel"; - restartUnits = ["zitadel.service"]; - }; - - settings = { - deploy = true; - owner = "zitadel"; - group = "zitadel"; - restartUnits = ["zitadel.service"]; - }; - - infraPrivateKey = { - deploy = true; - owner = "zitadel"; - group = "zitadel"; - restartUnits = ["zitadel.service"]; - }; - - infraPublicKey = { - deploy = true; - owner = "zitadel"; - group = "zitadel"; - restartUnits = ["zitadel.service"]; - }; - }; - - runtimeInputs = with pkgs; [pwgen openssl_3_5]; - script = '' - pwgen -s 32 1 > $out/masterKey - - openssl genrsa -traditional -out $out/infraPrivateKey 2048 - openssl rsa -pubout -in $out/infraPrivateKey -out $out/infraPublicKey - - cat << EOL > $out/settings - Database: - postgres: - User: - Password: $(cat $in/persistence/zitadel_password) - Admin: - Password: $(cat $in/persistence/zitadel_password) - EOL - ''; - }; - - clan.core.vars.generators.zitadel_users = { - files = { - users = { - deploy = true; - owner = "zitadel"; - group = "zitadel"; - restartUnits = ["infra-zitadel.service"]; - }; - }; - - script = '' - echo "{}" > $out/users - ''; - }; - - clan.core.vars.generators.zitadel_email_password = { - prompts = { - password = { - description = "password to email for zitadel's smpt connection"; - type = "hidden"; - persist = true; - }; - }; - - files = { - password = { - deploy = true; - owner = "zitadel"; - group = "zitadel"; - restartUnits = ["infra-zitadel.service"]; - }; - }; - - script = '' - cat $prompts/password > $out/password - ''; - }; - - environment.systemPackages = with pkgs; [ - zitadel - ]; - - services.zitadel = { - enable = true; - masterKeyFile = vars.masterKey.path; - - tlsMode = "external"; - - extraSettingsPaths = [ - vars.settings.path - ]; - - settings = { - Port = settings.port; - - ExternalDomain = "auth.kruining.eu"; - ExternalPort = 443; - ExternalSecure = true; - - Metrics.Type = "otel"; - Tracing.Type = "otel"; - Telemetry.Enabled = true; - - SystemDefaults = { - PasswordHasher.Hasher.Algorithm = "argon2id"; - SecretHasher.Hasher.Algorithm = "argon2id"; - }; - - Database.postgres = { - Host = settings.database.host; - Port = settings.database.port; - Database = "zitadel"; - User = { - Username = "zitadel"; - }; - Admin = { - Username = "zitadel"; - }; - }; - - SystemAPIUsers = { - infra = { - Path = vars.infraPublicKey.path; - Memberships = [ - { MemberType = "System"; Roles = [ "SYSTEM_OWNER" "IAM_OWNER" "ORG_OWNER" ]; } - ]; - }; - }; - }; - }; - } // (zLib.createInfra { inherit users email_password; key_file = vars.infraPrivateKey.path; }))) - ]; - }; - }; - }; -} diff --git a/clanServices/identity/flake-module.nix b/clanServices/identity/flake-module.nix deleted file mode 100644 index 1dd8972..0000000 --- a/clanServices/identity/flake-module.nix +++ /dev/null @@ -1,13 +0,0 @@ -{...}: let - module = ./default.nix; -in { - clan.modules.identity = module; - - # perSystem = {...}: { - # clan.nixosTests.identity = { - # imports = []; - - # clan.modules."@arda/identity" = module; - # }; - # }; -} diff --git a/clanServices/identity/lib.nix b/clanServices/identity/lib.nix deleted file mode 100644 index 1783529..0000000 --- a/clanServices/identity/lib.nix +++ /dev/null @@ -1,372 +0,0 @@ -{ - lib, - ardaLib, - self, - pkgs, - settings, - ... -}: let - createTerranixModule = { - users, - email_password, - key_file, - ... - }: terra: let - inherit (lib) toUpper toSentenceCase nameValuePair mapAttrs mapAttrs' concatMapAttrs concatMapStringsSep filterAttrsRecursive listToAttrs imap0 head drop length literalExpression attrNames; - inherit (ardaLib) toSnakeCase; - inherit (terra.lib) tfRef; - - _refTypeMap = { - org = {type = "org";}; - project = {type = "project";}; - user = { - type = "user"; - tfType = "human_user"; - }; - }; - - mapRef' = { - type, - tfType ? type, - }: name: {"${type}Id" = "\${ resource.zitadel_${tfType}.${toSnakeCase name}.id }";}; - mapRef = type: name: mapRef' (_refTypeMap.${type}) name; - mapEnum = prefix: value: "${prefix}_${value |> toSnakeCase |> toUpper}"; - - mapValue = type: value: ({ - appType = mapEnum "OIDC_APP_TYPE" value; - grantTypes = map (t: mapEnum "OIDC_GRANT_TYPE" t) value; - responseTypes = map (t: mapEnum "OIDC_RESPONSE_TYPE" t) value; - authMethodType = mapEnum "OIDC_AUTH_METHOD_TYPE" value; - - flowType = mapEnum "FLOW_TYPE" value; - triggerType = mapEnum "TRIGGER_TYPE" value; - accessTokenType = mapEnum "OIDC_TOKEN_TYPE" value; - }."${type}" or value); - - toResource = name: value: - nameValuePair - (toSnakeCase name) - (lib.mapAttrs' (k: v: nameValuePair (toSnakeCase k) (mapValue k v)) value); - - withRef = type: name: attrs: attrs // (mapRef type name); - - select = keys: callback: set: - if (length keys) == 0 - then mapAttrs' callback set - else let - key = head keys; - in - concatMapAttrs (k: v: select (drop 1 keys) (callback k) (v.${key} or {})) set; - - append = attrList: set: set // (listToAttrs attrList); - - forEach = src: key: set: let - _key = concatMapStringsSep "_" (k: "\${item.${k}}") key; - in - { - forEach = tfRef '' { - for item in ${src} : - "''${item.org}_''${item.name}" => item - }''; - } - // set; - in { - terraform.required_providers.zitadel = { - source = "zitadel/zitadel"; - version = "2.2.0"; - }; - - provider.zitadel = { - domain = "auth.kruining.eu"; - insecure = false; - - system_api = { - user = "infra"; - inherit key_file; - }; - }; - - locals = { - extra_users = tfRef " - flatten([ for org, users in jsondecode(file(\"${users}\")): [ - for name, details in users: { - org = org - name = name - email = details.email - firstName = details.firstName - lastName = details.lastName - } - ] ]) - "; - orgs = settings.organization |> mapAttrs (org: _: tfRef "resource.zitadel_org.${org}.id"); - }; - - resource = { - # Organizations - zitadel_org = - settings.organization - |> select [] ( - name: {isDefault, ...}: - {inherit name isDefault;} - |> toResource name - ); - - # Projects per organization - zitadel_project = - settings.organization - |> select ["project"] ( - org: name: { - hasProjectCheck, - privateLabelingSetting, - projectRoleAssertion, - projectRoleCheck, - ... - }: - { - inherit name hasProjectCheck privateLabelingSetting projectRoleAssertion projectRoleCheck; - } - |> withRef "org" org - |> toResource "${org}_${name}" - ); - - # Each OIDC app per project - zitadel_application_oidc = - settings.organization - |> select ["project" "application"] ( - org: project: name: { - redirectUris, - grantTypes, - responseTypes, - ... - }: - { - inherit name redirectUris grantTypes responseTypes; - - accessTokenRoleAssertion = true; - idTokenRoleAssertion = true; - accessTokenType = "JWT"; - } - |> withRef "org" org - |> withRef "project" "${org}_${project}" - |> toResource "${org}_${project}_${name}" - ); - - # Each project role - zitadel_project_role = - settings.organization - |> select ["project" "role"] ( - org: project: name: value: - { - inherit (value) displayName group; - roleKey = name; - } - |> withRef "org" org - |> withRef "project" "${org}_${project}" - |> toResource "${org}_${project}_${name}" - ); - - # Each project role assignment - zitadel_user_grant = - settings.organization - |> select ["project" "assign"] ( - org: project: user: roles: - {roleKeys = roles;} - |> withRef "org" org - |> withRef "project" "${org}_${project}" - |> withRef "user" "${org}_${user}" - |> toResource "${org}_${project}_${user}" - ); - - # Users - zitadel_human_user = - settings.organization - |> select ["user"] ( - org: name: { - email, - userName, - firstName, - lastName, - ... - }: - { - inherit email userName firstName lastName; - - isEmailVerified = true; - lifecycle = { - ignore_changes = ["first_name" "last_name" "user_name"]; - }; - } - |> withRef "org" org - |> toResource "${org}_${name}" - ) - |> append [ - (forEach "local.extra_users" ["org" "name"] { - orgId = tfRef "local.orgs[each.value.org]"; - userName = tfRef "each.value.name"; - email = tfRef "each.value.email"; - firstName = tfRef "each.value.firstName"; - lastName = tfRef "each.value.lastName"; - - isEmailVerified = true; - } - |> toResource "extraUsers") - ]; - - # Global user roles - zitadel_instance_member = - settings.organization - |> filterAttrsRecursive (n: v: !(v ? "instanceRoles" && (length v.instanceRoles) == 0)) - |> select ["user"] ( - org: name: {instanceRoles, ...}: - {roles = instanceRoles;} - |> withRef "user" "${org}_${name}" - |> toResource "${org}_${name}" - ); - - # Organazation specific roles - zitadel_org_member = - settings.organization - |> filterAttrsRecursive (n: v: !(v ? "roles" && (length v.roles) == 0)) - |> select ["user"] ( - org: name: {roles, ...}: - {inherit roles;} - |> withRef "org" org - |> withRef "user" "${org}_${name}" - |> toResource "${org}_${name}" - ); - - # Organazation's actions - zitadel_action = - settings.organization - |> select ["action"] ( - org: name: { - timeout, - allowedToFail, - script, - ... - }: - { - inherit allowedToFail name; - timeout = "${toString timeout}s"; - script = "const ${name} = ${script}"; - } - |> withRef "org" org - |> toResource "${org}_${name}" - ); - - # Organazation's action assignments - zitadel_trigger_actions = - settings.organization - |> concatMapAttrs ( - org: {triggers, ...}: - triggers - |> imap0 (i: { - flowType, - triggerType, - actions, - ... - }: ( - let - name = "trigger_${toString i}"; - in - { - inherit flowType triggerType; - - actionIds = - actions - |> map (action: (tfRef "zitadel_action.${org}_${toSnakeCase action}.id")); - } - |> withRef "org" org - |> toResource "${org}_${name}" - )) - |> listToAttrs - ); - - # SMTP config - zitadel_smtp_config.default = { - sender_address = "chris@kruining.eu"; - sender_name = "no-reply (Zitadel)"; - tls = true; - host = "black-mail.nl:587"; - user = "chris@kruining.eu"; - password = tfRef "file(\"${email_password}\")"; - set_active = true; - }; - - # Client credentials per app - local_sensitive_file = - settings.organization - |> select ["project" "application"] ( - org: project: name: {exportMap, ...}: - nameValuePair "${org}_${project}_${name}" { - content = '' - ${ - if exportMap.client_id != null - then exportMap.client_id - else "CLIENT_ID" - }=${tfRef "resource.zitadel_application_oidc.${org}_${project}_${name}.client_id"} - ${ - if exportMap.client_secret != null - then exportMap.client_secret - else "CLIENT_SECRET" - }=${tfRef "resource.zitadel_application_oidc.${org}_${project}_${name}.client_secret"} - ''; - filename = "/var/lib/zitadel/clients/${org}_${project}_${name}"; - } - ); - }; - }; -in { - createInfra = args @ {...}: let - tofu = "${lib.getExe pkgs.opentofu} -input=false"; - terraformConfiguration = self.inputs.terranix.lib.terranixConfiguration { - system = pkgs.stdenv.hostPlatform.system; - modules = [ - (createTerranixModule args) - ]; - }; - in { - systemd.services."infra-zitadel" = { - description = "Infra for Zitadel"; - - wantedBy = ["multi-user.target"]; - wants = ["zitadel.service"]; - after = ["zitadel.service"]; - - preStart = '' - install -d -m 0770 -o zitadel -g media /var/lib/infra-zitadel - ''; - - script = '' - # Sleep for a bit to give the service a chance to start up - sleep 5s - - if [ "$(systemctl is-active zitadel)" != "active" ]; then - echo "zitadel is not running" - exit 1 - fi - - # Print the path to the source for easier debugging - echo "config location: ${terraformConfiguration}" - - # Copy infra code into workspace - cp -f ${terraformConfiguration} config.tf.json - - # Initialize OpenTofu - ${tofu} init - - # Run the infrastructure code - ${tofu} plan -out=tfplan - ${tofu} apply -json -auto-approve tfplan - ''; - - serviceConfig = { - Type = "oneshot"; - User = "zitadel"; - Group = "zitadel"; - - StateDirectory = "/var/lib/infra-zitadel"; - }; - }; - }; -} diff --git a/clanServices/peristence/README.md b/clanServices/peristence/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/clanServices/peristence/default.nix b/clanServices/peristence/default.nix deleted file mode 100644 index a06e73a..0000000 --- a/clanServices/peristence/default.nix +++ /dev/null @@ -1,169 +0,0 @@ -{ - lib, - clanLib, - exports, - ... -}: let - inherit (builtins) toString; -in { - _class = "clan.service"; - manifest = { - name = "arda/persistence"; - description = '' - Configuration of persistence resrouce(s) - (for now this means a database. and specifically it means postgres) - ''; - readme = builtins.readFile ./README.md; - exports = { - inputs = ["persistence"]; - out = ["persistence"]; - }; - }; - - roles.default = { - description = ''''; - - interface = {lib, ...}: let - inherit (lib) mkOption types; - in { - options = { - port = mkOption { - type = types.port; - default = 5432; - }; - }; - }; - - perInstance = { - mkExports, - machine, - settings, - ... - }: let - requested_databases = - exports - |> clanLib.selectExports (_scope: true) - |> lib.mapAttrsToList (_: value: value.persistence.databases or []) - |> lib.concatLists; - in { - exports = mkExports { - persistence = { - main = "postgresql"; - driver.postgresql = { - host = "localhost"; - port = settings.port; - }; - }; - }; - - nixosModule = { - lib, - pkgs, - config, - ... - }: { - clan.core.vars.generators.postgresql = let - password_files = - requested_databases - |> lib.map (db: [ - { - name = "${db}_password"; - value = { - secret = true; - deploy = false; - }; - } - ]) - |> lib.concatLists - |> lib.listToAttrs; - in { - files = - { - "server.crt" = { - secret = true; - deploy = true; - }; - "server.key" = { - secret = true; - deploy = true; - }; - ".pgpass" = { - secret = true; - deploy = true; - - owner = "postgres"; - group = "postgres"; - mode = "0600"; - restartUnits = ["postgresql.service"]; - }; - } - // password_files; - - runtimeInputs = with pkgs; [openssl_3_5 pwgen]; - script = '' - openssl req \ - -new -x509 -days 365 -nodes -text \ - -out $out/server.crt \ - -keyout $out/server.key \ - -subj "/CN=db.${config.networking.fqdn}" - - ${requested_databases - |> lib.map (db: "pwgen -s 128 1 > $out/${db}_password") - |> lib.join "\n"} - - cat << EOL > $out/.pgpass - #host:port:database:user:password - ${requested_databases - |> lib.map (db: "*:${toString settings.port}:${db}:${db}:$(cat $out/${db}_password)") - |> lib.join "\n"} - EOL - ''; - }; - - systemd.services.postgresql.environment.PGPASSFILE = config.clan.core.vars.generators.postgresql.files.".pgpass".path; - - services = { - postgresql = { - enable = true; - # enableTCPIP = true; - - settings = { - port = settings.port; - ssl = true; - }; - - ensureDatabases = requested_databases; - ensureUsers = - requested_databases - |> lib.map (db: { - name = db; - ensureDBOwnership = true; - ensureClauses = { - login = true; - connection_limit = 5; - }; - }); - - identMap = '' - #map sys user db user - superuser_map root postgres - superuser_map postgres postgres - superuser_map /^(.+)$ \1 - ''; - - authentication = '' - # Generated file, do not edit! - # type database user auth-method optional_ident_map - local sameuser all peer map=superuser_map - - # TYPE DATABASE USER ADDRESS METHOD - # local all all trust - host all all 127.0.0.1/32 scram-sha-256 - host all all ::1/128 scram-sha-256 - ''; - }; - }; - }; - }; - }; -} diff --git a/clanServices/peristence/flake-module.nix b/clanServices/peristence/flake-module.nix deleted file mode 100644 index 56801d9..0000000 --- a/clanServices/peristence/flake-module.nix +++ /dev/null @@ -1,13 +0,0 @@ -{...}: let - module = ./default.nix; -in { - clan.modules.persistence = module; - - # perSystem = {...}: { - # clan.nixosTests.persistence = { - # imports = []; - - # clan.modules."@arda/persistence" = module; - # }; - # }; -} diff --git a/clanServices/servarr/README.md b/clanServices/servarr/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/clanServices/servarr/default.nix b/clanServices/servarr/default.nix deleted file mode 100644 index c06a89b..0000000 --- a/clanServices/servarr/default.nix +++ /dev/null @@ -1,150 +0,0 @@ -{ - exports, - clanLib, - lib, - ... -}: let - inherit (lib) toString; -in { - _class = "clan.service"; - manifest = { - name = "arda/servarr"; - description = ''''; - categories = ["Service" "Media"]; - readme = builtins.readFile ./README.md; - exports = { - inputs = ["persistence"]; - out = ["gateway" "persistence"]; - }; - }; - - roles.default = { - description = ''''; - - interface = {lib, ...}: let - inherit (lib) mkOption mkEnableOption types; - in { - options = { - enable = mkEnableOption "Enable configured *arr services"; - - database = mkOption { - type = types.anything; #ardaLib.types.endpoint; - }; - - services = mkOption { - type = types.attrsOf (types.submodule ({name, ...}: { - options = { - enable = mkEnableOption "Enable ${name}" // {default = true;}; - debug = mkEnableOption "Use tofu plan instead of tofu apply for ${name} "; - - rootFolders = mkOption { - type = types.listOf types.str; - default = []; - }; - }; - })); - default = {}; - description = '' - Settings foreach *arr service - ''; - }; - }; - }; - - perInstance = { - instanceName, - settings, - machine, - roles, - mkExports, - ... - }: { - exports = mkExports { - # endpoints.hosts = - # settings.services - # |> lib.attrNames - # |> (s: lib.concat s ["sabnzbd" "qbittorrent" "flaresolverr"]) - # |> lib.map (service: "${service}.${machine.name}.arda"); - - persistence.databases = - settings.services - |> lib.attrNames; - - gateway.services = - settings.services - |> lib.attrNames - # |> (s: lib.concat s ["sabnzbd" "qbittorrent" "flaresolverr"]) - |> lib.imap1 (i: name: { - inherit name; - value = { - endpoint.port = 2000 + i; - }; - }) - |> lib.listToAttrs; - }; - - nixosModule = args @ { - config, - lib, - pkgs, - ... - }: let - services = settings.services |> lib.attrNames; - service_count = services |> lib.length; - - servarr = import ./lib.nix (args // {inherit settings;}); - in { - imports = [ - (import ./sabnzbd.nix (args - // { - inherit settings; - port = 2000 + service_count + 1; - })) - (import ./qbittorrent.nix (args - // { - inherit settings; - port = 2000 + service_count + 2; - })) - (servarr.createModule settings.services) - ]; - - config = { - clan.core.vars.generators.servarr = rec { - dependencies = - services ++ ["sabnzbd" "qbittorrent"]; - - files."config.tfvars" = { - owner = "media"; - group = "media"; - mode = "0440"; - restartUnits = services |> lib.map (s: "${s}.service"); - }; - - script = '' - cat << EOL > $out/config.tfvars - ${ - services - |> lib.map (s: "${s}_api_key = \"$(cat $in/${s}/api_key)\"") - |> lib.join "\n" - } - qbittorrent_api_key = "$(cat $in/qbittorrent/password)" - sabnzbd_api_key = "$(cat $in/sabnzbd/api_key)" - EOL - ''; - }; - - services = { - flaresolverr = { - enable = true; - openFirewall = true; - port = 2000 + service_count + 3; - }; - }; - }; - }; - }; - }; - - perMachine = {...}: { - }; -} diff --git a/clanServices/servarr/flake-module.nix b/clanServices/servarr/flake-module.nix deleted file mode 100644 index 4a63342..0000000 --- a/clanServices/servarr/flake-module.nix +++ /dev/null @@ -1,13 +0,0 @@ -{...}: let - module = ./default.nix; -in { - clan.modules.servarr = module; - - # perSystem = {...}: { - # clan.nixosTests.servarr = { - # imports = []; - - # clan.modules."@arda/servarr" = module; - # }; - # }; -} diff --git a/clanServices/servarr/lib.nix b/clanServices/servarr/lib.nix deleted file mode 100644 index 4a15ca7..0000000 --- a/clanServices/servarr/lib.nix +++ /dev/null @@ -1,329 +0,0 @@ -{ - self, - config, - lib, - pkgs, - settings, - ... -}: let - inherit (lib) mkIf; - - createGenerator = { - service, - options, - ... - }: { - dependencies = ["postgresql"]; - - files = { - api_key = { - secret = true; - deploy = true; - owner = service; - group = "media"; - restartUnits = ["${service}.service"]; - }; - "config.env" = { - secret = true; - deploy = true; - owner = service; - group = "media"; - restartUnits = ["${service}.service"]; - }; - }; - - runtimeInputs = with pkgs; [pwgen]; - script = '' - pwgen -s 128 1 > $out/api_key - cat << EOL > $out/config.env - ${lib.toUpper service}__AUTH__APIKEY="$(cat $out/api_key)" - ${lib.toUpper service}__POSTGRES_PASSWORD="$(cat $in/postgresql/${service}_password)" - EOL - ''; - }; - - createService = { - service, - options, - ... - }: let - inherit (builtins) toString; - in - { - enable = true; - # openFirewall = true; - - environmentFiles = [ - config.clan.core.vars.generators.${service}.files."config.env".path - ]; - - settings = { - auth.authenticationMethod = "External"; - - server = { - bindaddress = "[::1]"; - port = options.port; - }; - - # Password provided via environment file - postgres = { - host = settings.database.host; - port = toString settings.database.port; - user = service; - maindb = service; - logdb = service; - }; - }; - } - // (lib.optionalAttrs (lib.elem service ["radarr" "sonarr" "lidarr" "whisparr"]) { - user = service; - group = "media"; - }); - - createSystemdService = args @ { - service, - options, - ... - }: let - tofu = lib.getExe pkgs.opentofu; - terraformConfiguration = self.inputs.terranix.lib.terranixConfiguration { - system = pkgs.stdenv.hostPlatform.system; - modules = [ - (createInfra args) - ]; - }; - in { - description = "${service} apply infra"; - - wantedBy = ["multi-user.target"]; - wants = ["${service}.service"]; - - preStart = '' - install -d -m 0770 -o ${service} -g media /var/lib/infra-${service} - ${ - options.rootFolders - |> lib.map (folder: "install -d -m 0770 -o media -g media ${folder}") - |> lib.join "\n" - } - ''; - - script = '' - # Sleep for a bit to give the service a chance to start up - sleep 5s - - if [ "$(systemctl is-active ${lib.escapeShellArg service})" != "active" ]; then - echo "${service} is not running" - exit 1 - fi - - # Print the path to the source for easier debugging - echo "config location: ${terraformConfiguration}" - - # Copy infra code into workspace - cp -f ${terraformConfiguration} config.tf.json - - # Initialize OpenTofu - ${tofu} init - - # Run the infrastructure code - ${tofu} \ - ${ - if options.debug - then "plan" - else "apply -auto-approve" - } \ - -var-file='${config.clan.core.vars.generators.servarr.files."config.tfvars".path}' - ''; - - serviceConfig = { - Type = "oneshot"; - User = service; - Group = "media"; - - WorkingDirectory = "/var/lib/${service}-apply-infra"; - - EnvironmentFile = [ - config.clan.core.vars.generators.${service}.files."config.env".path - ]; - }; - }; - - # Returns a module to be used in a modules list of terranix - createInfra = { - service, - options, - ... - }: terra: let - inherit (terra.lib) tfRef; - in { - variable = { - "${service}_api_key" = { - type = "string"; - description = "${service} API key"; - }; - - qbittorrent_api_key = { - type = "string"; - description = "qbittorrent api key"; - }; - - sabnzbd_api_key = { - type = "string"; - description = "sabnzbd api key"; - }; - }; - - terraform.required_providers.${service} = { - source = "devopsarr/${service}"; - version = - { - radarr = "2.3.5"; - sonarr = "3.4.2"; - prowlarr = "3.2.1"; - lidarr = "1.13.0"; - readarr = "2.1.0"; - whisparr = "1.2.0"; - }.${ - service - }; - }; - - provider.${service} = { - url = "http://[::1]:${toString options.port}"; - api_key = tfRef "var.${service}_api_key"; - }; - - resource = - { - "${service}_root_folder" = mkIf (lib.elem service ["radarr" "sonarr" "whisparr" "readarr"]) ( - options.rootFolders - |> lib.imap (i: f: lib.nameValuePair "local${toString i}" {path = f;}) - |> lib.listToAttrs - ); - - "${service}_download_client_qbittorrent" = mkIf (lib.elem service ["radarr" "sonarr" "lidarr" "whisparr"]) { - "main" = { - name = "qBittorrent"; - enable = true; - priority = 1; - host = "localhost"; - username = "admin"; - password = tfRef "var.qbittorrent_api_key"; - url_base = "/"; - port = config.services.qbittorrent.webuiPort; - }; - }; - - "${service}_download_client_sabnzbd" = mkIf (lib.elem service ["radarr" "sonarr" "lidarr" "whisparr"]) { - "main" = { - name = "SABnzbd"; - enable = true; - priority = 1; - host = "localhost"; - api_key = tfRef "var.sabnzbd_api_key"; - url_base = "/"; - port = config.services.sabnzbd.settings.misc.port; - }; - }; - } - // (lib.optionalAttrs (service == "prowlarr") ( - settings.services - |> lib.filterAttrs (s: _: lib.elem s ["radarr" "sonarr" "lidarr" "whisparr"]) - |> lib.mapAttrsToList (s: {port, ...}: { - "prowlarr_application_${s}"."main" = let - p = config.services.prowlarr.settings.server.port or 9696; - in { - name = s; - sync_level = "addOnly"; - base_url = "http://localhost:${toString port}"; - prowlarr_url = "http://localhost:${toString p}"; - api_key = tfRef "var.${s}_api_key"; - }; - }) - |> lib.concat [ - { - "prowlarr_indexer" = { - "nyaa" = { - enable = true; - - app_profile_id = 1; - priority = 1; - - name = "Nyaa"; - implementation = "Cardigann"; - config_contract = "CardigannSettings"; - protocol = "torrent"; - - fields = [ - { - name = "definitionFile"; - text_value = "nyaasi"; - } - { - name = "baseSettings.limitsUnit"; - number_value = 0; - } - { - name = "torrentBaseSettings.preferMagnetUrl"; - bool_value = false; - } - { - name = "prefer_magnet_links"; - bool_value = true; - } - { - name = "sonarr_compatibility"; - bool_value = false; - } - { - name = "strip_s01"; - bool_value = false; - } - { - name = "radarr_compatibility"; - bool_value = false; - } - { - name = "filter-id"; - number_value = 0; - } - { - name = "cat-id"; - number_value = 0; - } - { - name = "sort"; - number_value = 0; - } - { - name = "type"; - number_value = 1; - } - ]; - }; - }; - } - ] - |> lib.mkMerge - )); - }; -in { - createModule = services: args: { - config = - services - |> lib.attrsToList - |> lib.imap1 (i: { - name, - value, - }: let - service = name; - options = value // {port = 2000 + i;}; - in { - clan.core.vars.generators.${service} = createGenerator (args // {inherit service options;}); - services.${service} = createService (args // {inherit service options;}); - - systemd.services."infra-${service}" = lib.mkIf settings.enable (createSystemdService (args // {inherit service options;})); - }) - |> lib.mkMerge; - }; -} diff --git a/clanServices/servarr/qbittorrent.nix b/clanServices/servarr/qbittorrent.nix deleted file mode 100644 index dee52fd..0000000 --- a/clanServices/servarr/qbittorrent.nix +++ /dev/null @@ -1,96 +0,0 @@ -{ - config, - pkgs, - lib, - settings, - port, - ... -}: { - clan.core.vars.generators.qbittorrent = let - hash_password = pkgs.writers.writePython3 "hashPassword" {} '' - import base64 - import hashlib - import sys - import uuid - - password = sys.argv[1] - salt = uuid.uuid4() - salt_bytes = salt.bytes - - password = str.encode(password) - hashed_password = hashlib.pbkdf2_hmac( - "sha512", - password, - salt_bytes, - 100000, - dklen=64 - ) - b64_salt = base64.b64encode(salt_bytes).decode("utf-8") - b64_password = base64.b64encode(hashed_password).decode("utf-8") - password_string = "@ByteArray({salt}:{password})".format( - salt=b64_salt, password=b64_password - ) - print(password_string) - ''; - in { - files = { - "password" = { - secret = true; - deploy = true; - }; - "password_hash" = { - secret = true; - deploy = true; - }; - "qBittorrent.conf" = { - secret = true; - deploy = true; - owner = "qbittorrent"; - group = "media"; - mode = "0660"; - restartUnits = ["qbittorrent.service"]; - }; - }; - - runtimeInputs = with pkgs; [pwgen hash_password]; - - script = '' - pwgen -s 128 1 > $out/password - - ${hash_password} $(cat $out/password) > $out/password_hash - - cat << EOF > $out/qBittorrent.conf - [LegalNotice] - Accepted=true - - [Preferences] - WebUI\AlternativeUIEnabled=true - WebUI\RootFolder=${pkgs.vuetorrent}/share/vuetorrent - WebUI\Username=admin - WebUI\Password_PBKDF2=$(cat $out/password_hash) - EOF - ''; - }; - - system.activationScripts.qbittorrent-config = { - deps = lib.optional (!config.sops.useSystemdActivation) "setupSecrets"; - # TODO: If sops-nix is switched to systemd activation, add a systemd unit - # for this install step that runs after sops-install-secrets.service, - # because this activation-script dependency only orders against setupSecrets. - text = '' - install -Dm0600 -o ${config.services.qbittorrent.user} -g ${config.services.qbittorrent.group} \ - ${config.clan.core.vars.generators.qbittorrent.files."qBittorrent.conf".path} \ - ${config.services.qbittorrent.profileDir}/qBittorrent/config/qBittorrent.conf - ''; - }; - - services.qbittorrent = { - enable = true; - openFirewall = true; - webuiPort = port; - serverConfig = lib.mkForce {}; - - user = "qbittorrent"; - group = "media"; - }; -} diff --git a/clanServices/servarr/sabnzbd.nix b/clanServices/servarr/sabnzbd.nix deleted file mode 100644 index 49ae9a2..0000000 --- a/clanServices/servarr/sabnzbd.nix +++ /dev/null @@ -1,95 +0,0 @@ -{ - config, - lib, - pkgs, - settings, - port, - ... -}: { - clan.core.vars.generators.sabnzbd = { - files = { - "api_key" = { - secret = true; - deploy = true; - }; - "nzb_key" = { - secret = true; - deploy = true; - }; - "config.ini" = { - secret = true; - deploy = true; - owner = "sabnzbd"; - group = "media"; - mode = "0660"; - }; - }; - - prompts = { - username = { - description = "usenet username"; - type = "hidden"; - persist = true; - }; - password = { - description = "usenet password"; - type = "hidden"; - persist = true; - }; - }; - - runtimeInputs = with pkgs; [pwgen]; - - script = '' - pwgen -s 128 1 > $out/api_key - pwgen -s 128 1 > $out/nzb_key - - cat << EOF > $out/config.ini - [misc] - api_key = $(cat $out/api_key) - nzb_key = $(cat $out/nzb_key) - - [servers] - [[news.sunnyusenet.com]] - username = $(cat $prompts/username) - password = $(cat $prompts/password) - EOF - ''; - }; - - services.sabnzbd = { - enable = true; - openFirewall = true; - - allowConfigWrite = false; - configFile = lib.mkForce null; - - secretFiles = [ - config.clan.core.vars.generators.sabnzbd.files."config.ini".path - ]; - - settings = { - misc = { - host = "0.0.0.0"; - port = port; - host_whitelist = "${config.networking.hostName}"; - - download_dir = "/var/media/downloads/incomplete"; - complete_dir = "/var/media/downloads/done"; - }; - - servers = { - "news.sunnyusenet.com" = { - name = "news.sunnyusenet.com"; - displayname = "news.sunnyusenet.com"; - host = "news.sunnyusenet.com"; - port = 563; - timeout = 60; - }; - }; - }; - - user = "sabnzbd"; - group = "media"; - }; -} diff --git a/devShell.nix b/devShell.nix deleted file mode 100644 index 8be0232..0000000 --- a/devShell.nix +++ /dev/null @@ -1,22 +0,0 @@ -{ - inputs, - ... -}: { - perSystem = {pkgs, system, ...}: { - devShells.default = pkgs.mkShell { - packages = with pkgs; [ - bash - sops - just - yq - pwgen - alejandra - nil - nixd - openssl - inputs.clan-core.packages.${system}.clan-cli - nix-output-monitor - ]; - }; - }; -} diff --git a/docs/plans/mandos-wake-on-demand-build-host.md b/docs/plans/mandos-wake-on-demand-build-host.md deleted file mode 100644 index 0775828..0000000 --- a/docs/plans/mandos-wake-on-demand-build-host.md +++ /dev/null @@ -1,125 +0,0 @@ -# Mandos as a wake-on-demand build host - -## Goal - -Mandos is primarily an interactive living-room machine, but it is also a strong candidate for handling remote Nix builds when it is idle. The goal is to make that dual use practical without keeping the machine powered all the time. - -## Current context - -On `main`, Mandos is configured as an interactive gaming machine: - -- `systems/x86_64-linux/mandos/default.nix` - - `sneeuwvlok.hardware.has.gpu.nvidia = true` - - `sneeuwvlok.hardware.has.audio = true` - - `sneeuwvlok.desktop.use = "gamescope"` - - `sneeuwvlok.application.steam.enable = true` -- `homes/x86_64-linux/chris@mandos/default.nix` - - user-facing application set for an interactive machine - -This makes Mandos a poor fit for "always running random infrastructure", but a reasonable fit for "available for work when needed". - -## Desired behavior - -- Mandos remains an interactive machine first. -- Mandos can be used as a remote build worker when no one is actively using it. -- Mandos should not need to stay fully on all day just to be eligible for builds. -- Waking and idling down should be automatic enough that the machine can participate in builds without turning into a maintenance burden. - -## Recommended model - -### 1. Use wake-on-LAN as the activation mechanism - -Mandos should support being awakened by another machine on the same LAN. - -Requirements: - -- BIOS or UEFI wake-on-LAN support enabled -- NixOS interface configuration enabling wake-on-LAN -- one low-power machine that is effectively always available to send wake requests - -In this repo, `ulmo` is the obvious candidate to act as the coordinator, but the pattern should stay generic: one machine is always reachable, and one or more stronger machines can be woken on demand. - -### 2. Prefer suspend-first over shutdown-first - -There are two main power states worth considering: - -- **Suspend on idle** - - faster resume - - generally better user experience - - often easier to make reliable for wake-on-LAN -- **Shutdown on idle** - - lowest power draw - - more fragile in practice because firmware support for wake from soft-off varies - - longer time to become available again - -Recommended rollout order: - -1. Prove the concept with suspend on idle. -2. Only consider full power-off later if the hardware and firmware behave reliably. - -## 3. Add an explicit availability policy - -The interesting lesson for tagging is not "Mandos should have a build tag". The interesting lesson is that some machines have a deliberate availability policy that affects how safely they can participate in automation. - -A future host-level setting could encode this policy directly, for example: - -- `always-on` -- `wake-on-demand` -- `manual` - -That setting would be a better source for any computed operational tag than current workload or ad hoc tags. - -## 4. Idle detection should be policy-driven - -If Mandos becomes a build worker, idle shutdown or suspend should depend on signals such as: - -- no local interactive session activity -- no active build job -- no long-running system task that should keep the machine awake - -This should not be a blind timer that powers the machine down every X minutes regardless of context. - -## 5. Build orchestration needs a coordinator - -Wake-on-demand only works well if something else can wake the machine and wait for it to become reachable. In practice, this means: - -- a coordinator sends the wake signal -- the build client retries until the machine is reachable -- the remote builder participates only after it is actually ready - -The exact implementation can vary, but the architectural point is the same: a wakeable build worker is not self-sufficient. - -## Risks and caveats - -- Firmware wake support may be unreliable, especially from full shutdown. -- Build latency increases because wake and readiness checks take time. -- A machine that users expect to be immediately available should not surprise them with power-state transitions at awkward moments. -- Interactive workload detection matters; otherwise the machine will feel hostile as a living-room device. - -## Recommendation - -Treat the Mandos idea as a good pattern, but generalize it: - -- some machines are **interactive** -- some machines are **wakeable on demand** -- some machines are suitable for **interruptible background work** - -Those are more reusable concepts than "Mandos is the build server". - -## Implications for the tag strategy - -This investigation strengthens a small part of the `operational:*` space: - -- `operational:availability:always-on` -- `operational:availability:wake-on-demand` -- `operational:workload:interruptible` - -These should not be assigned by hand if they can instead be computed from explicit machine settings that describe availability policy. - -## References - -- Clan inventory tags and dynamic tags docs: `https://clan.lol/docs/25.11/reference/options/clan_inventory` -- NixOS Wake-on-LAN wiki: `https://wiki.nixos.org/wiki/Wake_on_LAN` -- Home-lab wake-on-demand discussion and patterns: - - `https://dgross.ca/blog/linux-home-server-auto-sleep` - - `https://danielpgross.github.io/friendly_neighbor/howto-sleep-wake-on-demand.html` diff --git a/docs/plans/tagging-strategy.md b/docs/plans/tagging-strategy.md deleted file mode 100644 index cb217f9..0000000 --- a/docs/plans/tagging-strategy.md +++ /dev/null @@ -1,235 +0,0 @@ -# Clan machine tagging strategy - -## Goal - -Replace machine-name targeting with stable tags that survive machine renames, hardware reshuffles, and service moves. - -The strategy should fit how this repo is evolving: - -- machine tags should describe the machine -- service roles should describe service topology -- computed tags should be derived from machine settings or other explicit metadata, not from other tags - -## Source material - -This plan is based on: - -- current Clan inventory in `clan.nix` -- current machine configs under `machines/*/configuration.nix` -- workload and module usage on `main` under: - - `systems/x86_64-linux/*/default.nix` - - `homes/x86_64-linux/chris@*/default.nix` -- Clan inventory tag and dynamic-tag documentation - -## Guiding principles - -### 1. Prefer capabilities over roles - -A machine rarely has one permanent role. In this repo especially, a machine may be interactive, portable, build-capable, and temporarily host some service at the same time. - -Because of that, tags should describe durable traits and capabilities rather than trying to answer "what is this machine?" - -### 2. Do not encode current workload as a machine tag - -A machine currently running Grafana, Jellyfin, or PostgreSQL does not mean that those should become machine tags. Those are current placements, not stable identity. - -If a service can move, its current presence is weak evidence for tagging. - -### 3. Use service roles for topology - -Some relationships belong in service definitions rather than host tags. - -Examples: - -- NFS producer and consumer -- persistence provider and client -- reverse proxy frontend and backend - -These are not machine identity tags; they are service-topology relationships. - -### 4. Derive tags from settings when possible - -If a machine setting already captures a fact, derive the tag from that setting instead of duplicating it by hand. - -Good examples in this repo: - -- `desktop.use` can imply whether a machine is interactive -- `hardware.has.gpu.*` can imply GPU availability -- `hardware.has.audio` can imply audio capability -- `hardware.has.bluetooth` can imply Bluetooth capability - -### 5. Avoid deriving tags from other tags - -Clan supports dynamic tags, but tag-from-tag derivation can become fragile and can even recurse. If tags need computation, compute them from machine settings or an explicit metadata source instead. - -## Proposed namespaces - -Use full words: - -- `capability:*` -- `operational:*` - -The intention is: - -- `capability:*` describes stable machine traits -- `operational:*` describes automation-relevant policy or availability behavior - -## Tag catalog - -This is the current list of tags discussed so far, grouped by status. - -### Agreed capability tags - -- `capability:runtime:interactive` -- `capability:runtime:headless` -- `capability:hardware:gpu` -- `capability:hardware:audio` -- `capability:hardware:bluetooth` -- `capability:mobility:portable` -- `capability:mobility:stationary` - -### Agreed operational tags - -- `operational:availability:always-on` -- `operational:availability:wake-on-demand` -- `operational:availability:manual` -- `operational:workload:interruptible` - -### Explicitly rejected or deferred - -- GPU vendor-specific tags such as AMD- or NVIDIA-specific variants -- service-presence tags such as Jellyfin, Grafana, Forgejo, or PostgreSQL -- service-topology tags such as NFS producer or consumer -- application-presence tags such as Discord or TeamSpeak -- desktop-environment tags such as Plasma or Gamescope -- location tags such as "living room" unless location later becomes a deliberate scheduling dimension - -## Current static tags in `clan.nix` - -These are the manually assigned tags currently present in the inventory. Settings-derived tags are intentionally not listed here because they are meant to be computed rather than maintained by hand. - -- `mandos` - - `capability:mobility:stationary` - - `operational:availability:wake-on-demand` -- `manwe` - - `capability:mobility:stationary` - - `operational:availability:manual` -- `orome` - - `capability:mobility:portable` - - `operational:availability:manual` -- `tulkas` - - `capability:mobility:portable` - - `operational:availability:manual` -- `ulmo` - - `capability:mobility:stationary` - - `operational:availability:always-on` - -## Capability tags - -These are the strongest candidates for machine tags. - -### Runtime - -- `capability:runtime:interactive` -- `capability:runtime:headless` - -These are directly useful for deciding where a service with a user-facing local experience does or does not belong. - -### Hardware - -- `capability:hardware:gpu` -- `capability:hardware:audio` -- `capability:hardware:bluetooth` - -At the moment, the repo provides enough configuration structure to derive these from machine settings. - -GPU vendor-specific tags are intentionally excluded for now. The current conclusion is that the presence of GPU hardware may matter, but the vendor usually does not unless there is a specific workload that depends on CUDA, ROCm, or a similar stack. - -### Mobility - -- `capability:mobility:portable` -- `capability:mobility:stationary` - -These are useful concepts, but they are not currently obvious from one uniform machine setting in the repo. If they become desirable, they likely need either: - -- an explicit machine setting, or -- a stronger convention around machine form factor - -For now they are candidates, not automatic defaults. - -## Operational tags - -Operational tags are weaker than capability tags and should stay small in number. - -They should only exist when they capture real automation constraints that are not already represented elsewhere. - -### Availability - -- `operational:availability:always-on` -- `operational:availability:wake-on-demand` -- `operational:availability:manual` - -This dimension became clearer while thinking through the Mandos build-host idea. A machine may be technically capable of a workload, while its availability policy determines whether it is a sensible target. - -These tags should not be guessed from existing workloads. They should come from an explicit machine setting that states the intended availability policy. - -### Interruptibility - -- `operational:workload:interruptible` - -This is not about the machine by itself. It is a useful policy boundary for selecting machines that may host work that can be delayed, retried, paused, or moved. - -If introduced, it should again come from explicit machine policy rather than being inferred from current services. - -## What should not become machine tags - -- current service assignments, such as Jellyfin, Grafana, Forgejo, or PostgreSQL -- service topology, such as NFS producer or consumer -- user application presence, such as Discord or TeamSpeak -- detailed desktop-environment choice, such as Plasma or Gamescope -- one-off descriptions like "living room" unless location becomes a deliberate scheduling dimension - -## What is derivable today - -The repo already contains enough structure to derive several useful capability tags. - -Examples from the current configuration style: - -- if a machine enables a desktop session, derive `capability:runtime:interactive` -- if a machine does not, derive `capability:runtime:headless` -- if a machine enables `hardware.has.audio`, derive `capability:hardware:audio` -- if a machine enables `hardware.has.bluetooth`, derive `capability:hardware:bluetooth` -- if a machine enables any `hardware.has.gpu.*`, derive `capability:hardware:gpu` - -## What probably needs explicit policy - -These should not be inferred from current services or tag combinations: - -- `operational:availability:*` -- `operational:workload:interruptible` -- mobility-related tags if there is no explicit machine setting to derive them from - -The clean way to support these is to introduce one or more explicit machine settings whose purpose is to describe machine policy rather than workload. - -## Mandos update - -The Mandos wake-on-demand build-host idea adds an important refinement: - -- some machines should be eligible for background work only when they are available through a specific policy, such as wake-on-demand - -This does **not** mean Mandos should get a hand-maintained "build server" tag. - -It instead suggests a more generic pattern: - -- a machine may be interactive -- a machine may be available on demand rather than always on -- that availability policy may influence whether certain classes of automation should target it - -That strengthens the case for a very small `operational:*` namespace derived from explicit machine policy. - -## Recommended next steps - -1. Start with `capability:*` tags that are clearly derivable from machine settings. -2. Keep service topology in service roles instead of machine tags. -3. If availability policy becomes important, add an explicit machine setting for it and derive `operational:*` tags from that setting. -4. Avoid expanding the tag vocabulary until there is a clear service-selection use case for each added tag. diff --git a/flake.lock b/flake.lock index a888110..757ab1e 100644 --- a/flake.lock +++ b/flake.lock @@ -71,31 +71,23 @@ "clan-core": { "inputs": { "data-mesher": "data-mesher", - "disko": [ - "disko" - ], - "flake-parts": [ - "flake-parts" - ], + "disko": "disko", + "flake-parts": "flake-parts", "nix-darwin": "nix-darwin", "nix-select": "nix-select", "nixpkgs": [ "nixpkgs" ], - "sops-nix": [ - "sops-nix" - ], - "systems": [ - "systems" - ], + "sops-nix": "sops-nix", + "systems": "systems", "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1774258552, - "narHash": "sha256-wTJJxhLPr3OHXQ23H9+Ch1YjdlaoMf3605ezfRYLaC4=", - "rev": "28bb98f5aec0ea70b623ab4953eb8186acdb7bba", + "lastModified": 1775389026, + "narHash": "sha256-cHYF7eGiVqgEnIQKs105eV0P5/zOvxl443qO1f5/Bps=", + "rev": "d53f3c0b42400ff608dd468ac33359881baf969e", "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/28bb98f5aec0ea70b623ab4953eb8186acdb7bba.tar.gz" + "url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/d53f3c0b42400ff608dd468ac33359881baf969e.tar.gz" }, "original": { "type": "tarball", @@ -133,11 +125,11 @@ ] }, "locked": { - "lastModified": 1774087718, - "narHash": "sha256-UU4KzRMTFJttIoSnRm1SWheFcfAVAsNqG+4JauKib3g=", - "rev": "734047b2dd1e67c3a803999777cdf749f3199342", + "lastModified": 1774796937, + "narHash": "sha256-uDcgnNHK1D2oTHOQKsqQUPdDGMuG94dp3Nv8LsnqkEM=", + "rev": "04e10e10c7b4bbf2930f24d139326707a43cbb54", "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/734047b2dd1e67c3a803999777cdf749f3199342.tar.gz" + "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/04e10e10c7b4bbf2930f24d139326707a43cbb54.tar.gz" }, "original": { "type": "tarball", @@ -147,6 +139,7 @@ "disko": { "inputs": { "nixpkgs": [ + "clan-core", "nixpkgs" ] }, @@ -170,11 +163,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1773767380, - "narHash": "sha256-fHrKh0/EQlEJe6czXPo9/bw1lki7w0RAGKRqYv/445s=", + "lastModified": 1775241072, + "narHash": "sha256-YpXDFEkd+JjxZOgTnvt5GHvEhORxkAda9Lc1e8e8Ox8=", "owner": "emmanuelrosa", "repo": "erosanix", - "rev": "ada69cf31f7649f8e59fe5376c94f3b0ea38bf37", + "rev": "14ac50e5ddefdb1c5ed66c11d2c6fa68959d690a", "type": "github" }, "original": { @@ -191,11 +184,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1774423251, - "narHash": "sha256-g/PP8G9WcP4vtZVOBNYwfGxLnwLQoTERHnef8irAMeQ=", + "lastModified": 1775373929, + "narHash": "sha256-Elx3es3UvLova3YBdJTc9rju9ULl9+5XF4K5t5Ejsa8=", "owner": "nix-community", "repo": "fenix", - "rev": "b70d7535088cd8a9e4322c372a475f66ffa18adf", + "rev": "221468471f762f355db24ce728012544561650f5", "type": "github" }, "original": { @@ -204,14 +197,34 @@ "type": "github" } }, + "firefox": { + "inputs": { + "flake-compat": "flake-compat_2", + "lib-aggregate": "lib-aggregate", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1775388520, + "narHash": "sha256-WUnKn7L/yBo7a5xH2UmPvBfYUr3d4Q8EPCz5r09C8Eo=", + "owner": "nix-community", + "repo": "flake-firefox-nightly", + "rev": "00070174d7a635f5238aee06e4feb481ccc7d9f9", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "flake-firefox-nightly", + "type": "github" + } + }, "firefox-gnome-theme": { "flake": false, "locked": { - "lastModified": 1764873433, - "narHash": "sha256-1XPewtGMi+9wN9Ispoluxunw/RwozuTRVuuQOmxzt+A=", + "lastModified": 1775176642, + "narHash": "sha256-2veEED0Fg7Fsh81tvVDNYR6SzjqQxa7hbi18Jv4LWpM=", "owner": "rafaelmardojai", "repo": "firefox-gnome-theme", - "rev": "f7ffd917ac0d253dbd6a3bf3da06888f57c69f92", + "rev": "179704030c5286c729b5b0522037d1d51341022c", "type": "github" }, "original": { @@ -237,6 +250,21 @@ } }, "flake-compat_2": { + "locked": { + "lastModified": 1761640442, + "narHash": "sha256-AtrEP6Jmdvrqiv4x2xa5mrtaIp3OEe8uBYCDZDS+hu8=", + "owner": "nix-community", + "repo": "flake-compat", + "rev": "4a56054d8ffc173222d09dad23adf4ba946c8884", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_3": { "flake": false, "locked": { "lastModified": 1747046372, @@ -252,7 +280,7 @@ "type": "github" } }, - "flake-compat_3": { + "flake-compat_4": { "flake": false, "locked": { "lastModified": 1751685974, @@ -268,18 +296,35 @@ "url": "https://git.lix.systems/lix-project/flake-compat.git" } }, + "flake-compat_5": { + "flake": false, + "locked": { + "lastModified": 1650374568, + "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "b4a34015c698c7793d592d66adbab377907a2be8", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": [ + "clan-core", "nixpkgs" ] }, "locked": { - "lastModified": 1772408722, - "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=", + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", "type": "github" }, "original": { @@ -338,11 +383,11 @@ ] }, "locked": { - "lastModified": 1767609335, - "narHash": "sha256-feveD98mQpptwrAEggBQKJTYbvwwglSbOv53uCfH9PY=", + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "250481aafeb741edfe23d29195671c19b36b6dca", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", "type": "github" }, "original": { @@ -351,10 +396,86 @@ "type": "github" } }, + "flake-parts_5": { + "inputs": { + "nixpkgs-lib": [ + "terranix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1736143030, + "narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils-plus": { + "inputs": { + "flake-utils": "flake-utils_2" + }, + "locked": { + "lastModified": 1715533576, + "narHash": "sha256-fT4ppWeCJ0uR300EH3i7kmgRZnAVxrH+XtK09jQWihk=", + "owner": "gytis-ivaskevicius", + "repo": "flake-utils-plus", + "rev": "3542fe9126dc492e53ddd252bb0260fe035f2c0f", + "type": "github" + }, + "original": { + "owner": "gytis-ivaskevicius", + "repo": "flake-utils-plus", + "rev": "3542fe9126dc492e53ddd252bb0260fe035f2c0f", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_5" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "flux": { "inputs": { "mcman": "mcman", - "nixpkgs": "nixpkgs_3" + "nixpkgs": "nixpkgs_4" }, "locked": { "lastModified": 1767316901, @@ -389,25 +510,23 @@ "gnome-shell": { "flake": false, "locked": { - "host": "gitlab.gnome.org", "lastModified": 1767737596, "narHash": "sha256-eFujfIUQDgWnSJBablOuG+32hCai192yRdrNHTv0a+s=", "owner": "GNOME", "repo": "gnome-shell", "rev": "ef02db02bf0ff342734d525b5767814770d85b49", - "type": "gitlab" + "type": "github" }, "original": { - "host": "gitlab.gnome.org", "owner": "GNOME", - "ref": "gnome-49", "repo": "gnome-shell", - "type": "gitlab" + "rev": "ef02db02bf0ff342734d525b5767814770d85b49", + "type": "github" } }, "grub2-themes": { "inputs": { - "nixpkgs": "nixpkgs_4" + "nixpkgs": "nixpkgs_5" }, "locked": { "lastModified": 1757136219, @@ -430,11 +549,11 @@ ] }, "locked": { - "lastModified": 1774387289, - "narHash": "sha256-Z/0IfVHrb0lEdv1WcHEe/ni4utBMR2GXZIktzYcTDSU=", + "lastModified": 1775230022, + "narHash": "sha256-FBhkbsqDTULYB1nS92y1CT7qSAM9rUMZR9hS8AvIw24=", "owner": "himmelblau-idm", "repo": "himmelblau", - "rev": "b2eccc7cb188253e49bffdddd743d01f52ab9625", + "rev": "d700f39281354c0b08cfb9640011a381bed29136", "type": "github" }, "original": { @@ -450,11 +569,11 @@ ] }, "locked": { - "lastModified": 1774379316, - "narHash": "sha256-0nGNxWDUH2Hzlj/R3Zf4FEK6fsFNB/dvewuboSRZqiI=", + "lastModified": 1775360939, + "narHash": "sha256-XUBlSgUFdvTh6+K5LcI5mJu5F5L8scmJDMRiZM484TM=", "owner": "nix-community", "repo": "home-manager", - "rev": "1eb0549a1ab3fe3f5acf86668249be15fa0e64f7", + "rev": "2097a5c82bdc099c6135eae4b111b78124604554", "type": "github" }, "original": { @@ -471,11 +590,11 @@ ] }, "locked": { - "lastModified": 1773422513, - "narHash": "sha256-MPjR48roW7CUMU6lu0+qQGqj92Kuh3paIulMWFZy+NQ=", + "lastModified": 1774991950, + "narHash": "sha256-kScKj3qJDIWuN9/6PMmgy5esrTUkYinrO5VvILik/zw=", "owner": "nix-community", "repo": "home-manager", - "rev": "ef12a9a2b0f77c8fa3dda1e7e494fca668909056", + "rev": "f2d3e04e278422c7379e067e323734f3e8c585a7", "type": "github" }, "original": { @@ -484,21 +603,6 @@ "type": "github" } }, - "import-tree": { - "locked": { - "lastModified": 1773693634, - "narHash": "sha256-BtZ2dtkBdSUnFPPFc+n0kcMbgaTxzFNPv2iaO326Ffg=", - "owner": "vic", - "repo": "import-tree", - "rev": "c41e7d58045f9057880b0d85e1152d6a4430dbf1", - "type": "github" - }, - "original": { - "owner": "vic", - "repo": "import-tree", - "type": "github" - } - }, "jovian": { "inputs": { "nix-github-actions": "nix-github-actions", @@ -507,11 +611,11 @@ ] }, "locked": { - "lastModified": 1774333446, - "narHash": "sha256-jeAUd4mfLle7Zw8F3lDdXvw2cmeP3FgVphHq2XuEKbs=", + "lastModified": 1775287496, + "narHash": "sha256-tCBlt+RP85MLrMYntro/YvG7NWktbmFiyItGBo85Tf8=", "owner": "Jovian-Experiments", "repo": "Jovian-NixOS", - "rev": "79b45622eff2ae0437d7a712610044bbc7b87fa2", + "rev": "0a7a3feb77606db451aa10287ad4c4c8f85922f8", "type": "github" }, "original": { @@ -520,10 +624,29 @@ "type": "github" } }, + "lib-aggregate": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1774789463, + "narHash": "sha256-MFraiT8o6manIcEloazGYafji1ua3HJ7Re/A/uauqYA=", + "owner": "nix-community", + "repo": "lib-aggregate", + "rev": "dc3bd444a2ea0834374b7d759c532f232e144128", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "lib-aggregate", + "type": "github" + } + }, "mcman": { "inputs": { "crane": "crane", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_3" }, "locked": { "lastModified": 1766962671, @@ -557,7 +680,7 @@ "mydia": { "inputs": { "flake-parts": "flake-parts_2", - "nixpkgs": "nixpkgs_5" + "nixpkgs": "nixpkgs_6" }, "locked": { "lastModified": 1764866402, @@ -603,11 +726,11 @@ ] }, "locked": { - "lastModified": 1773000227, - "narHash": "sha256-zm3ftUQw0MPumYi91HovoGhgyZBlM4o3Zy0LhPNwzXE=", + "lastModified": 1775037210, + "narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=", "owner": "nix-darwin", "repo": "nix-darwin", - "rev": "da529ac9e46f25ed5616fd634079a5f3c579135f", + "rev": "06648f4902343228ce2de79f291dd5a58ee12146", "type": "github" }, "original": { @@ -640,16 +763,16 @@ }, "nix-minecraft": { "inputs": { - "flake-compat": "flake-compat_2", - "nixpkgs": "nixpkgs_6", - "systems": "systems" + "flake-compat": "flake-compat_3", + "nixpkgs": "nixpkgs_7", + "systems": "systems_3" }, "locked": { - "lastModified": 1774407052, - "narHash": "sha256-rUkn7Bo3PAlpcZl8+0FDsTwFyDwvS4xwMT9+RJ+XJoE=", + "lastModified": 1775359538, + "narHash": "sha256-PbX+bT49p9c7cmT03ufao8tDDEn0Qi7R82R1yXDyk5k=", "owner": "Infinidoge", "repo": "nix-minecraft", - "rev": "70daf1f48885f0b4a70797076cd2ff5d9139b46e", + "rev": "bdf703935b0aa47d9de1c6a7536fc76756b044ef", "type": "github" }, "original": { @@ -671,13 +794,85 @@ "url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz" } }, + "nixlib": { + "locked": { + "lastModified": 1736643958, + "narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixos-boot": { + "locked": { + "lastModified": 1722927293, + "narHash": "sha256-8oCsiFyAuidAdhSz60Lu8+TwCPHxaeWixyv0xT0mLt4=", + "owner": "Melkor333", + "repo": "nixos-boot", + "rev": "afaed735149d0a06f234e54dd2d9db2e18dc64ae", + "type": "github" + }, + "original": { + "owner": "Melkor333", + "repo": "nixos-boot", + "type": "github" + } + }, + "nixos-generators": { + "inputs": { + "nixlib": "nixlib", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769813415, + "narHash": "sha256-nnVmNNKBi1YiBNPhKclNYDORoHkuKipoz7EtVnXO50A=", + "owner": "nix-community", + "repo": "nixos-generators", + "rev": "8946737ff703382fda7623b9fab071d037e897d5", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixos-generators", + "type": "github" + } + }, + "nixos-wsl": { + "inputs": { + "flake-compat": [], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1774972752, + "narHash": "sha256-DnLIpFxznohpLkIFs390uZ0gxwkVyhtknhKNu+lQJK8=", + "owner": "nix-community", + "repo": "nixos-wsl", + "rev": "d97e078f4788cddb8d11c3c99f72a4bb9ddec221", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixos-wsl", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1772380631, - "narHash": "sha256-FhW0uxeXjefINP0vUD4yRBB52Us7fXZPk9RiPAopfiY=", + "lastModified": 1775054576, + "narHash": "sha256-iiIr1hlTMu2LLARsUYtiqlE90tqocqIMVLK2fIzB/UY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6d3b61b190a899042ce82a5355111976ba76d698", + "rev": "fc4b9b74d4b0bdbf3c97fef4bd34c05225172912", "type": "github" }, "original": { @@ -687,13 +882,44 @@ "type": "github" } }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1774748309, + "narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "333c4e0545a6da976206c74db8773a1645b5870a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, "nixpkgs_10": { "locked": { - "lastModified": 1767767207, - "narHash": "sha256-Mj3d3PfwltLmukFal5i3fFt27L6NiKXdBezC1EBuZs4=", + "lastModified": 1775126147, + "narHash": "sha256-J0dZU4atgcfo4QvM9D92uQ0Oe1eLTxBVXjJzdEMQpD0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5912c1772a44e31bf1c63c0390b90501e5026886", + "rev": "8d8c1fa5b412c223ffa47410867813290cdedfef", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_11": { + "locked": { + "lastModified": 1775036866, + "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", "type": "github" }, "original": { @@ -704,6 +930,22 @@ } }, "nixpkgs_2": { + "locked": { + "lastModified": 1775371993, + "narHash": "sha256-shlcgEOzW6rl7zmZeYBMP9EpF3O/cTL7/HpWlyqearw=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "ff2af6f7ebc6c123603d5689aeea6461290f46b5", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable-small", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { "locked": { "lastModified": 1757347588, "narHash": "sha256-tLdkkC6XnsY9EOZW9TlpesTclELy8W7lL2ClL+nma8o=", @@ -718,7 +960,7 @@ "type": "indirect" } }, - "nixpkgs_3": { + "nixpkgs_4": { "locked": { "lastModified": 1766902085, "narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=", @@ -734,13 +976,13 @@ "type": "github" } }, - "nixpkgs_4": { + "nixpkgs_5": { "locked": { - "lastModified": 1774449288, - "narHash": "sha256-ukB6NS45Oi62fQM4RpZfx3dpqxIu66ADCCFl6h72Fjo=", + "lastModified": 1775391773, + "narHash": "sha256-8h0YBzKR6kf+68qnZtZnC6GhTf2XAilTQ9F/tm5JDWs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cd0256cd8c537170cf24827fa821efb57aed9f40", + "rev": "728629d3d4797ab52406df91b319c07a7d2ce479", "type": "github" }, "original": { @@ -750,7 +992,7 @@ "type": "github" } }, - "nixpkgs_5": { + "nixpkgs_6": { "locked": { "lastModified": 1764242076, "narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=", @@ -766,7 +1008,7 @@ "type": "github" } }, - "nixpkgs_6": { + "nixpkgs_7": { "locked": { "lastModified": 1769461804, "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", @@ -782,29 +1024,13 @@ "type": "github" } }, - "nixpkgs_7": { - "locked": { - "lastModified": 1774386573, - "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "nixpkgs_8": { "locked": { - "lastModified": 1771008912, - "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "lastModified": 1775036866, + "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", "owner": "nixos", "repo": "nixpkgs", - "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", "type": "github" }, "original": { @@ -816,16 +1042,16 @@ }, "nixpkgs_9": { "locked": { - "lastModified": 1773840656, - "narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=", - "owner": "NixOS", + "lastModified": 1774386573, + "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512", + "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", + "owner": "nixos", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } @@ -842,11 +1068,11 @@ ] }, "locked": { - "lastModified": 1767810917, - "narHash": "sha256-ZKqhk772+v/bujjhla9VABwcvz+hB2IaRyeLT6CFnT0=", + "lastModified": 1775228139, + "narHash": "sha256-ebbeHmg+V7w8050bwQOuhmQHoLOEOfqKzM1KgCTexK4=", "owner": "nix-community", "repo": "NUR", - "rev": "dead29c804adc928d3a69dfe7f9f12d0eec1f1a4", + "rev": "601971b9c89e0304561977f2c28fa25e73aa7132", "type": "github" }, "original": { @@ -857,19 +1083,19 @@ }, "nvf": { "inputs": { - "flake-compat": "flake-compat_3", + "flake-compat": "flake-compat_4", "flake-parts": "flake-parts_3", "mnw": "mnw", "ndg": "ndg", - "nixpkgs": "nixpkgs_8", - "systems": "systems_2" + "nixpkgs": "nixpkgs_9", + "systems": "systems_4" }, "locked": { - "lastModified": 1774375131, - "narHash": "sha256-d22VIgsDXagQQWnAnebYeQWGHlmF81YRwuGCzAgNZAQ=", + "lastModified": 1775122065, + "narHash": "sha256-ZlowJNkQOhpsXDuWbHgB1xY6W8kyzYn9coK9nJsqqNg=", "owner": "notashelf", "repo": "nvf", - "rev": "d847d401bea4dcb1478d02a61a3209fa8512f71d", + "rev": "d3304af3d5771e8d5bac6ee9bbdbce56086d54f7", "type": "github" }, "original": { @@ -888,11 +1114,11 @@ ] }, "locked": { - "lastModified": 1772361940, - "narHash": "sha256-B1Cz+ydL1iaOnGlwOFld/C8lBECPtzhiy/pP93/CuyY=", + "lastModified": 1774915545, + "narHash": "sha256-COT4l/+ZddGBvrDVfPf7MEOJxV8EDKame6/aRnNIKcY=", "owner": "nix-community", "repo": "plasma-manager", - "rev": "a4b33606111c9c5dcd10009042bb710307174f51", + "rev": "f3177b3c69fb3f03201098d7fe8ab6422cce7fc1", "type": "github" }, "original": { @@ -904,24 +1130,25 @@ "root": { "inputs": { "clan-core": "clan-core", - "disko": "disko", "erosanix": "erosanix", "fenix": "fenix", - "flake-parts": "flake-parts", + "firefox": "firefox", "flux": "flux", "grub2-themes": "grub2-themes", "himmelblau": "himmelblau", "home-manager": "home-manager", - "import-tree": "import-tree", "jovian": "jovian", "mydia": "mydia", "nix-minecraft": "nix-minecraft", - "nixpkgs": "nixpkgs_7", + "nixos-boot": "nixos-boot", + "nixos-generators": "nixos-generators", + "nixos-wsl": "nixos-wsl", + "nixpkgs": "nixpkgs_8", "nvf": "nvf", "plasma-manager": "plasma-manager", - "sops-nix": "sops-nix", + "snowfall-lib": "snowfall-lib", + "sops-nix": "sops-nix_2", "stylix": "stylix", - "systems": "systems_4", "terranix": "terranix", "zen-browser": "zen-browser" } @@ -929,11 +1156,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1774376228, - "narHash": "sha256-7oA0u4aghFjjIcIDKZ26NUpXH7hVXGPC0sI1OfK7NUk=", + "lastModified": 1775228522, + "narHash": "sha256-+6eTD6EAabjow5gdjWRP6aI2UUwOZJEjzzsvvbVu8f8=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "eabb84b771420b8396ab4bb4747694302d9be277", + "rev": "f4b77dc99d9925667246e2887783b79bdc46a50d", "type": "github" }, "original": { @@ -943,16 +1170,59 @@ "type": "github" } }, - "sops-nix": { + "snowfall-lib": { "inputs": { - "nixpkgs": "nixpkgs_9" + "flake-compat": "flake-compat_5", + "flake-utils-plus": "flake-utils-plus", + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1774303811, - "narHash": "sha256-fhG4JAcLgjKwt+XHbjs8brpWnyKUfU4LikLm3s0Q/ic=", + "lastModified": 1765361626, + "narHash": "sha256-kX0Dp/kYSRbQ+yd9e3lmmUWdNbipufvKfL2IzbrSpnY=", + "owner": "snowfallorg", + "repo": "lib", + "rev": "c566ad8b7352c30ec3763435de7c8f1c46ebb357", + "type": "github" + }, + "original": { + "owner": "snowfallorg", + "repo": "lib", + "type": "github" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "clan-core", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775365543, + "narHash": "sha256-f50qrK0WwZ9z5EdaMGWOTtALgSF7yb7XwuE7LjCuDmw=", "owner": "Mic92", "repo": "sops-nix", - "rev": "614e256310e0a4f8a9ccae3fa80c11844fba7042", + "rev": "a4ee2de76efb759fe8d4868c33dec9937897916f", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + }, + "sops-nix_2": { + "inputs": { + "nixpkgs": "nixpkgs_10" + }, + "locked": { + "lastModified": 1775365543, + "narHash": "sha256-f50qrK0WwZ9z5EdaMGWOTtALgSF7yb7XwuE7LjCuDmw=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "a4ee2de76efb759fe8d4868c33dec9937897916f", "type": "github" }, "original": { @@ -970,21 +1240,20 @@ "firefox-gnome-theme": "firefox-gnome-theme", "flake-parts": "flake-parts_4", "gnome-shell": "gnome-shell", - "nixpkgs": "nixpkgs_10", + "nixpkgs": "nixpkgs_11", "nur": "nur", - "systems": "systems_3", - "tinted-foot": "tinted-foot", + "systems": "systems_6", "tinted-kitty": "tinted-kitty", "tinted-schemes": "tinted-schemes", "tinted-tmux": "tinted-tmux", "tinted-zed": "tinted-zed" }, "locked": { - "lastModified": 1774124764, - "narHash": "sha256-Poz9WTjiRlqZIf197CrMMJfTifZhrZpbHFv0eU1Nhtg=", + "lastModified": 1775247334, + "narHash": "sha256-eVKt8wpQqg6Hq/UdHQkV1izXGloGQxdlE4SSk9/X27s=", "owner": "nix-community", "repo": "stylix", - "rev": "e31c79f571c5595a155f84b9d77ce53a84745494", + "rev": "6d0502ef7447090abf8b00362b5cda8ac64595b4", "type": "github" }, "original": { @@ -1068,15 +1337,43 @@ "type": "github" } }, + "systems_6": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_7": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "terranix": { "inputs": { - "flake-parts": [ - "flake-parts" - ], + "flake-parts": "flake-parts_5", "nixpkgs": [ "nixpkgs" ], - "systems": "systems_5" + "systems": "systems_7" }, "locked": { "lastModified": 1773700838, @@ -1092,23 +1389,6 @@ "type": "github" } }, - "tinted-foot": { - "flake": false, - "locked": { - "lastModified": 1726913040, - "narHash": "sha256-+eDZPkw7efMNUf3/Pv0EmsidqdwNJ1TaOum6k7lngDQ=", - "owner": "tinted-theming", - "repo": "tinted-foot", - "rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4", - "type": "github" - }, - "original": { - "owner": "tinted-theming", - "repo": "tinted-foot", - "rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4", - "type": "github" - } - }, "tinted-kitty": { "flake": false, "locked": { @@ -1128,11 +1408,11 @@ "tinted-schemes": { "flake": false, "locked": { - "lastModified": 1767710407, - "narHash": "sha256-+W1EB79Jl0/gm4JqmO0Nuc5C7hRdp4vfsV/VdzI+des=", + "lastModified": 1772661346, + "narHash": "sha256-4eu3LqB9tPqe0Vaqxd4wkZiBbthLbpb7llcoE/p5HT0=", "owner": "tinted-theming", "repo": "schemes", - "rev": "2800e2b8ac90f678d7e4acebe4fa253f602e05b2", + "rev": "13b5b0c299982bb361039601e2d72587d6846294", "type": "github" }, "original": { @@ -1144,11 +1424,11 @@ "tinted-tmux": { "flake": false, "locked": { - "lastModified": 1767489635, - "narHash": "sha256-e6nnFnWXKBCJjCv4QG4bbcouJ6y3yeT70V9MofL32lU=", + "lastModified": 1772934010, + "narHash": "sha256-x+6+4UvaG+RBRQ6UaX+o6DjEg28u4eqhVRM9kpgJGjQ=", "owner": "tinted-theming", "repo": "tinted-tmux", - "rev": "3c32729ccae99be44fe8a125d20be06f8d7d8184", + "rev": "c3529673a5ab6e1b6830f618c45d9ce1bcdd829d", "type": "github" }, "original": { @@ -1160,11 +1440,11 @@ "tinted-zed": { "flake": false, "locked": { - "lastModified": 1767488740, - "narHash": "sha256-wVOj0qyil8m+ouSsVZcNjl5ZR+1GdOOAooAatQXHbuU=", + "lastModified": 1772909925, + "narHash": "sha256-jx/5+pgYR0noHa3hk2esin18VMbnPSvWPL5bBjfTIAU=", "owner": "tinted-theming", "repo": "base16-zed", - "rev": "11abb0b282ad3786a2aae088d3a01c60916f2e40", + "rev": "b4d3a1b3bcbd090937ef609a0a3b37237af974df", "type": "github" }, "original": { @@ -1181,11 +1461,11 @@ ] }, "locked": { - "lastModified": 1773297127, - "narHash": "sha256-6E/yhXP7Oy/NbXtf1ktzmU8SdVqJQ09HC/48ebEGBpk=", + "lastModified": 1775125835, + "narHash": "sha256-2qYcPgzFhnQWchHo0SlqLHrXpux5i6ay6UHA+v2iH4U=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "71b125cd05fbfd78cab3e070b73544abe24c5016", + "rev": "75925962939880974e3ab417879daffcba36c4a3", "type": "github" }, "original": { @@ -1202,11 +1482,11 @@ ] }, "locked": { - "lastModified": 1774352774, - "narHash": "sha256-gibUM0pSnLxEeuFrYA8T1oEaixk+fjQpqXbYaxcEX/4=", + "lastModified": 1775367672, + "narHash": "sha256-nGC6qrRsWysfR7/8wsSooq0X71rfJjhq1b+dFI6oQtY=", "owner": "0xc000022070", "repo": "zen-browser-flake", - "rev": "a0f3d47dbd8f8618a1920d5a5ca09b7993415895", + "rev": "33cd729244914f1e121477c5de148639c5e73c4a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 272314a..692afe1 100644 --- a/flake.nix +++ b/flake.nix @@ -3,22 +3,13 @@ nixConfig = { warn-dirty = false; - extra-experimental-features = ["nix-command" "flakes" "pipe-operators"]; }; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - flake-parts = { - url = "github:hercules-ci/flake-parts"; - inputs.nixpkgs-lib.follows = "nixpkgs"; - }; - import-tree.url = "github:vic/import-tree"; - systems.url = "github:nix-systems/default"; - sops-nix.url = "github:Mic92/sops-nix"; - - disko = { - url = "github:nix-community/disko"; + snowfall-lib = { + url = "github:snowfallorg/lib"; inputs.nixpkgs.follows = "nixpkgs"; }; @@ -27,32 +18,25 @@ inputs.nixpkgs.follows = "nixpkgs"; }; - terranix = { - url = "github:terranix/terranix"; - inputs.nixpkgs.follows = "nixpkgs"; - inputs.flake-parts.follows = "flake-parts"; - }; - - clan-core = { - url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz"; - inputs = { - flake-parts.follows = "flake-parts"; - nixpkgs.follows = "nixpkgs"; - sops-nix.follows = "sops-nix"; - disko.follows = "disko"; - systems.follows = "systems"; - }; - }; - plasma-manager = { url = "github:nix-community/plasma-manager"; inputs.nixpkgs.follows = "nixpkgs"; inputs.home-manager.follows = "home-manager"; }; + nixos-generators = { + url = "github:nix-community/nixos-generators"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + # neovim nvf.url = "github:notashelf/nvf"; + # plymouth theme + nixos-boot.url = "github:Melkor333/nixos-boot"; + + firefox.url = "github:nix-community/flake-firefox-nightly"; + stylix.url = "github:nix-community/stylix"; # Rust toolchain @@ -70,6 +54,8 @@ flux.url = "github:IogaMaster/flux"; + sops-nix.url = "github:Mic92/sops-nix"; + # Azure AD for linux himmelblau = { url = "github:himmelblau-idm/himmelblau"; @@ -89,54 +75,72 @@ url = "github:vinceliuice/grub2-themes"; }; + nixos-wsl = { + url = "github:nix-community/nixos-wsl"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-compat.follows = ""; + }; + }; + + terranix = { + url = "github:terranix/terranix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + clan-core = { + url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + mydia = { url = "github:chris-kruining/mydia"; # url = "github:getmydia/mydia"; }; }; - outputs = inputs @ { - flake-parts, - nixpkgs, - systems, - ... - }: - flake-parts.lib.mkFlake {inherit inputs;} { - systems = import systems; + outputs = inputs: + inputs.snowfall-lib.mkFlake { + inherit inputs; + src = ./.; - imports = with inputs; [ - flake-parts.flakeModules.modules - clan-core.flakeModules.default - home-manager.flakeModules.default - ./clan/flake-module.nix - ./packages/flake-module.nix - ./clanServices/flake-module.nix - ]; + snowfall = { + namespace = "sneeuwvlok"; - perSystem = {system, ...}: { - _module.args = { - pkgs = import nixpkgs { - inherit system; - - overlays = with inputs; [ - fenix.overlays.default - nix-minecraft.overlay - flux.overlays.default - ]; - - config = { - allowUnfree = true; - - permittedInsecurePackages = [ - # I think this is because of zen - "qtwebengine-5.15.19" - - # For mautrix-signal, the matrix to signal bridge - "olm-3.2.16" - ]; - }; - }; + meta = { + name = "sneeuwvlok"; + title = "Sneeuwvlok"; }; }; + + channels-config = { + allowUnfree = true; + permittedInsecurePackages = [ + # Due to *arr stack + "dotnet-sdk-6.0.428" + "aspnetcore-runtime-6.0.36" + + # I think this is because of zen + "qtwebengine-5.15.19" + + # For Nheko, the matrix client + "olm-3.2.16" + ]; + }; + + overlays = with inputs; [ + fenix.overlays.default + nix-minecraft.overlay + flux.overlays.default + ]; + + systems.modules = with inputs; [ + clan-core.nixosModules.default + ]; + + homes.modules = with inputs; [ + stylix.homeModules.stylix + plasma-manager.homeModules.plasma-manager + ]; }; } diff --git a/homes/x86_64-linux/chris@mandos/default.nix b/homes/x86_64-linux/chris@mandos/default.nix new file mode 100644 index 0000000..ba87e73 --- /dev/null +++ b/homes/x86_64-linux/chris@mandos/default.nix @@ -0,0 +1,36 @@ +{osConfig, ...}: { + home.stateVersion = osConfig.system.stateVersion; + + programs.git = { + settings.user = { + name = "Chris Kruining"; + email = "chris@kruining.eu"; + }; + }; + + sneeuwvlok = { + defaults = { + shell = "zsh"; + terminal = "ghostty"; + browser = "zen"; + editor = "zed"; + }; + + shell = { + corePkgs.enable = true; + }; + + themes = { + enable = true; + theme = "everforest"; + polarity = "dark"; + }; + + application = { + bitwarden.enable = true; + teamspeak.enable = true; + steam.enable = true; + zen.enable = true; + }; + }; +} diff --git a/homes/x86_64-linux/chris@manwe/default.nix b/homes/x86_64-linux/chris@manwe/default.nix new file mode 100644 index 0000000..0aced9b --- /dev/null +++ b/homes/x86_64-linux/chris@manwe/default.nix @@ -0,0 +1,59 @@ +{osConfig, ...}: { + home.stateVersion = osConfig.system.stateVersion; + + programs.git = { + settings.user = { + name = "Chris Kruining"; + email = "chris@kruining.eu"; + }; + }; + + sneeuwvlok = { + defaults = { + shell = "zsh"; + terminal = "ghostty"; + browser = "zen"; + editor = "zed"; + }; + + shell = { + corePkgs.enable = true; + }; + + themes = { + enable = true; + theme = "everforest"; + polarity = "dark"; + }; + + development = { + rust.enable = true; + javascript.enable = true; + dotnet.enable = true; + }; + + application = { + bitwarden.enable = true; + discord.enable = true; + ladybird.enable = true; + matrix.enable = true; + obs.enable = true; + onlyoffice.enable = true; + signal.enable = true; + steam.enable = true; + studio.enable = true; + teamspeak.enable = true; + thunderbird.enable = true; + zen.enable = true; + }; + + shell.zsh.enable = true; + terminal.ghostty.enable = true; + + editor = { + zed.enable = true; + nvim.enable = true; + nano.enable = true; + }; + }; +} diff --git a/homes/x86_64-linux/chris@orome/default.nix b/homes/x86_64-linux/chris@orome/default.nix new file mode 100644 index 0000000..7a1dc43 --- /dev/null +++ b/homes/x86_64-linux/chris@orome/default.nix @@ -0,0 +1,49 @@ +{osConfig, ...}: { + home.stateVersion = osConfig.system.stateVersion; + + programs.git = { + settings.user = { + name = "Chris Kruining"; + email = "chris@kruining.eu"; + }; + }; + + sneeuwvlok = { + defaults = { + shell = "zsh"; + terminal = "ghostty"; + browser = "zen"; + editor = "zed"; + }; + + shell = { + corePkgs.enable = true; + }; + + themes = { + enable = true; + theme = "everforest"; + polarity = "dark"; + }; + + development = { + javascript.enable = true; + dotnet.enable = true; + }; + + application = { + bitwarden.enable = true; + onlyoffice.enable = true; + signal.enable = true; + zen.enable = true; + }; + + shell.zsh.enable = true; + terminal.ghostty.enable = true; + + editor = { + zed.enable = true; + nano.enable = true; + }; + }; +} diff --git a/homes/x86_64-linux/chris@tulkas/default.nix b/homes/x86_64-linux/chris@tulkas/default.nix new file mode 100644 index 0000000..ba87e73 --- /dev/null +++ b/homes/x86_64-linux/chris@tulkas/default.nix @@ -0,0 +1,36 @@ +{osConfig, ...}: { + home.stateVersion = osConfig.system.stateVersion; + + programs.git = { + settings.user = { + name = "Chris Kruining"; + email = "chris@kruining.eu"; + }; + }; + + sneeuwvlok = { + defaults = { + shell = "zsh"; + terminal = "ghostty"; + browser = "zen"; + editor = "zed"; + }; + + shell = { + corePkgs.enable = true; + }; + + themes = { + enable = true; + theme = "everforest"; + polarity = "dark"; + }; + + application = { + bitwarden.enable = true; + teamspeak.enable = true; + steam.enable = true; + zen.enable = true; + }; + }; +} diff --git a/lib/options.nix b/lib/options.nix deleted file mode 100644 index 683b812..0000000 --- a/lib/options.nix +++ /dev/null @@ -1,37 +0,0 @@ -{lib, ...}: let - inherit (lib) mkOption types; -in { - mkUrlOptions = defaults: { - host = - mkOption { - type = types.str; - example = "host.tld"; - description = '' - Hostname - ''; - } - // (defaults.host or {}); - - port = - mkOption { - type = types.port; - default = 1234; - example = "1234"; - description = '' - Port - ''; - } - // (defaults.port or {}); - - protocol = - mkOption { - type = types.str; - default = "https"; - example = "https"; - description = '' - Which protocol to use when creating a url string - ''; - } - // (defaults.protocol or {}); - }; -} diff --git a/lib/options/default.nix b/lib/options/default.nix new file mode 100644 index 0000000..72e8621 --- /dev/null +++ b/lib/options/default.nix @@ -0,0 +1,38 @@ +{ lib, ...}: +let + inherit (builtins) isString typeOf; + inherit (lib) mkOption types throwIfNot concatStringsSep splitStringBy toLower map; +in +{ + options = { + mkUrlOptions = + defaults: + { + host = mkOption { + type = types.str; + example = "host.tld"; + description = '' + Hostname + ''; + } // (defaults.host or {}); + + port = mkOption { + type = types.port; + default = 1234; + example = "1234"; + description = '' + Port + ''; + } // (defaults.port or {}); + + protocol = mkOption { + type = types.str; + default = "https"; + example = "https"; + description = '' + Which protocol to use when creating a url string + ''; + } // (defaults.protocol or {}); + }; + }; +} \ No newline at end of file diff --git a/lib/strings.nix b/lib/strings.nix deleted file mode 100644 index 5a163c2..0000000 --- a/lib/strings.nix +++ /dev/null @@ -1,53 +0,0 @@ -{lib, ...}: let - inherit (builtins) isString typeOf match toString head; - inherit (lib) throwIfNot concatStringsSep splitStringBy toLower map concatMapAttrsStringSep; -in { - #======================================================================================== - # Converts a string to snake case - # - # simply replaces any uppercase letter to its lowercase variant preceeded by an underscore - #======================================================================================== - toSnakeCase = str: - throwIfNot (isString str) "toSnakeCase only accepts string values, but got ${typeOf str}" ( - str - |> splitStringBy (prev: curr: builtins.match "[a-z]" prev != null && builtins.match "[A-Z]" curr != null) true - |> map (p: toLower p) - |> concatStringsSep "_" - ); - - #======================================================================================== - # Converts a set of url parts to a string - #======================================================================================== - toUrl = { - protocol ? null, - host, - port ? null, - path ? null, - query ? null, - hash ? null, - }: let - trim_slashes = str: str |> match "^\/*(.+?)\/*$" |> head; - encode_to_str = set: concatMapAttrsStringSep "&" (n: v: "${n}=${v}") set; - - _protocol = - if protocol != null - then "${protocol}://" - else ""; - _port = - if port != null - then ":${toString port}" - else ""; - _path = - if path != null - then "/${path |> trim_slashes}" - else ""; - _query = - if query != null - then "?${query |> encode_to_str}" - else ""; - _hash = - if hash != null - then "#${hash |> encode_to_str}" - else ""; - in "${_protocol}${host}${_port}${_path}${_query}${_hash}"; -} diff --git a/lib/strings/default.nix b/lib/strings/default.nix new file mode 100644 index 0000000..0c15699 --- /dev/null +++ b/lib/strings/default.nix @@ -0,0 +1,39 @@ +{ lib, ...}: +let + inherit (builtins) isString typeOf match toString head; + inherit (lib) throwIfNot concatStringsSep splitStringBy toLower map concatMapAttrsStringSep; +in +{ + strings = { + #======================================================================================== + # Converts a string to snake case + # + # simply replaces any uppercase letter to its lowercase variant preceeded by an underscore + #======================================================================================== + toSnakeCase = + str: + throwIfNot (isString str) "toSnakeCase only accepts string values, but got ${typeOf str}" ( + str + |> splitStringBy (prev: curr: builtins.match "[a-z]" prev != null && builtins.match "[A-Z]" curr != null) true + |> map (p: toLower p) + |> concatStringsSep "_" + ); + + #======================================================================================== + # Converts a set of url parts to a string + #======================================================================================== + toUrl = + { protocol ? null, host, port ? null, path ? null, query ? null, hash ? null }: + let + trim_slashes = str: str |> match "^\/*(.+?)\/*$" |> head; + encode_to_str = set: concatMapAttrsStringSep "&" (n: v: "${n}=${v}") set; + + _protocol = if protocol != null then "${protocol}://" else ""; + _port = if port != null then ":${toString port}" else ""; + _path = if path != null then "/${path |> trim_slashes}" else ""; + _query = if query != null then "?${query |> encode_to_str}" else ""; + _hash = if hash != null then "#${hash |> encode_to_str}" else ""; + in + "${_protocol}${host}${_port}${_path}${_query}${_hash}"; + }; +} \ No newline at end of file diff --git a/logs/bridge-2026-04-15T09-11-43.612.log b/logs/bridge-2026-04-15T09-11-43.612.log new file mode 100644 index 0000000..df81d78 --- /dev/null +++ b/logs/bridge-2026-04-15T09-11-43.612.log @@ -0,0 +1,2 @@ +{"level":"fatal","error":"homeserver.address not configured","time":"2026-04-15T09:10:06.949460064Z","message":"Configuration error"} +{"level":"info","time":"2026-04-15T09:10:06.949840013Z","message":"See https://docs.mau.fi/faq/field-unconfigured for more info"} diff --git a/logs/bridge.log b/logs/bridge.log new file mode 100644 index 0000000..63567e0 --- /dev/null +++ b/logs/bridge.log @@ -0,0 +1,2 @@ +{"level":"fatal","error":"appservice.as_token not configured. Did you forget to generate the registration? ","time":"2026-04-15T09:11:43.617908298Z","message":"Configuration error"} +{"level":"info","time":"2026-04-15T09:11:43.618232253Z","message":"See https://docs.mau.fi/faq/field-unconfigured for more info"} diff --git a/machines/aule/configuration.nix b/machines/aule/configuration.nix deleted file mode 100644 index e75bc1c..0000000 --- a/machines/aule/configuration.nix +++ /dev/null @@ -1,3 +0,0 @@ -{ ... }: { - nixpkgs.hostPlatform = "x86_64-linux"; -} diff --git a/machines/mandos/configuration.nix b/machines/mandos/configuration.nix deleted file mode 100644 index 131b987..0000000 --- a/machines/mandos/configuration.nix +++ /dev/null @@ -1,40 +0,0 @@ -{self, ...}: { - imports = [ - ./disks.nix - ./hardware.nix - self.inputs.home-manager.nixosModules.home-manager - self.inputs.himmelblau.nixosModules.himmelblau - self.inputs.jovian.nixosModules.default - self.inputs.mydia.nixosModules.default - self.inputs.nix-minecraft.nixosModules.minecraft-servers - self.inputs.nvf.nixosModules.default - self.inputs.sops-nix.nixosModules.sops - (self.inputs.import-tree ../../modules/nixos) - ]; - - nixpkgs.hostPlatform = "x86_64-linux"; - - sneeuwvlok = { - hardware.has = { - gpu.nvidia = true; - audio = true; - }; - - boot = { - quiet = true; - animated = true; - }; - - desktop.use = "gamescope"; - - application = { - steam.enable = true; - }; - - editor = { - nano.enable = true; - }; - }; - - system.stateVersion = "23.11"; -} diff --git a/machines/manwe/configuration.nix b/machines/manwe/configuration.nix deleted file mode 100644 index 98bc5ed..0000000 --- a/machines/manwe/configuration.nix +++ /dev/null @@ -1,79 +0,0 @@ -{ - self, - lib, - pkgs, - ... -}: { - _module.args = { - pkgs = lib.mkForce (import self.inputs.nixpkgs { - system = "x86_64-linux"; - - overlays = with self.inputs; [ - fenix.overlays.default - nix-minecraft.overlay - flux.overlays.default - ]; - - config = { - allowUnfree = true; - - permittedInsecurePackages = [ - # I think this is because of zen - "qtwebengine-5.15.19" - - # For mautrix-signal, the matrix to signal bridge - "olm-3.2.16" - ]; - }; - }); - }; - - imports = [ - ./disks.nix - ./hardware.nix - self.inputs.home-manager.nixosModules.home-manager - self.inputs.himmelblau.nixosModules.himmelblau - self.inputs.jovian.nixosModules.default - self.inputs.mydia.nixosModules.default - self.inputs.nix-minecraft.nixosModules.minecraft-servers - self.inputs.nvf.nixosModules.default - self.inputs.sops-nix.nixosModules.sops - (self.inputs.import-tree ../../modules/nixos) - ]; - - system.activationScripts.remove-gtkrc.text = "rm -f /home/chris/.gtkrc-2.0"; - - services.logrotate.checkConfig = false; - - environment.systemPackages = with pkgs; [beyond-all-reason openrct2]; - - sneeuwvlok = { - hardware.has = { - gpu.amd = true; - bluetooth = true; - audio = true; - }; - - boot = { - quiet = true; - animated = true; - }; - - desktop.use = "plasma"; - - application = { - steam.enable = true; - }; - - editor = { - nano.enable = true; - }; - }; - - services.displayManager.autoLogin = { - enable = true; - user = "chris"; - }; - - system.stateVersion = "23.11"; -} diff --git a/machines/manwe/hardware.nix b/machines/manwe/hardware.nix deleted file mode 100644 index 8c48c1c..0000000 --- a/machines/manwe/hardware.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ - config, - lib, - ... -}: let - inherit (lib.modules) mkDefault; -in { - boot = { - initrd.availableKernelModules = ["xhci_pci" "ahci" "usb_storage" "usbhid" "sd_mod"]; - initrd.kernelModules = []; - kernelModules = ["kvm-amd"]; - kernelParams = []; - extraModulePackages = []; - }; - - nixpkgs.hostPlatform = "x86_64-linux"; - hardware.cpu.amd.updateMicrocode = mkDefault config.hardware.enableRedistributableFirmware; -} diff --git a/machines/melkor/configuration.nix b/machines/melkor/configuration.nix deleted file mode 100644 index e75bc1c..0000000 --- a/machines/melkor/configuration.nix +++ /dev/null @@ -1,3 +0,0 @@ -{ ... }: { - nixpkgs.hostPlatform = "x86_64-linux"; -} diff --git a/machines/orome/configuration.nix b/machines/orome/configuration.nix deleted file mode 100644 index 2c94238..0000000 --- a/machines/orome/configuration.nix +++ /dev/null @@ -1,44 +0,0 @@ -{ - self, - pkgs, - ... -}: { - imports = [ - ./disks.nix - ./hardware.nix - self.inputs.home-manager.nixosModules.home-manager - self.inputs.himmelblau.nixosModules.himmelblau - self.inputs.jovian.nixosModules.default - self.inputs.mydia.nixosModules.default - self.inputs.nix-minecraft.nixosModules.minecraft-servers - self.inputs.nvf.nixosModules.default - self.inputs.sops-nix.nixosModules.sops - (self.inputs.import-tree ../../modules/nixos) - ]; - - nixpkgs.hostPlatform = "x86_64-linux"; - - environment.systemPackages = with pkgs; [ - azure-cli - github-copilot-cli - ]; - - sneeuwvlok = { - hardware.has = { - bluetooth = true; - audio = true; - }; - - services.authentication.himmelblau.enable = true; - - application = { - steam.enable = true; - }; - - editor = { - nano.enable = true; - }; - }; - - system.stateVersion = "23.11"; -} diff --git a/machines/tulkas/configuration.nix b/machines/tulkas/configuration.nix deleted file mode 100644 index bbfe3d8..0000000 --- a/machines/tulkas/configuration.nix +++ /dev/null @@ -1,41 +0,0 @@ -{self, ...}: { - imports = [ - ./disks.nix - ./hardware.nix - self.inputs.home-manager.nixosModules.home-manager - self.inputs.himmelblau.nixosModules.himmelblau - self.inputs.jovian.nixosModules.default - self.inputs.mydia.nixosModules.default - self.inputs.nix-minecraft.nixosModules.minecraft-servers - self.inputs.nvf.nixosModules.default - self.inputs.sops-nix.nixosModules.sops - (self.inputs.import-tree ../../modules/nixos) - ]; - - nixpkgs.hostPlatform = "x86_64-linux"; - - sneeuwvlok = { - hardware.has = { - gpu.amd = true; - bluetooth = true; - audio = true; - }; - - boot = { - quiet = true; - animated = true; - }; - - desktop.use = "gamescope"; - - application = { - steam.enable = true; - }; - - editor = { - nano.enable = true; - }; - }; - - system.stateVersion = "23.11"; -} diff --git a/machines/ulmo/configuration.nix b/machines/ulmo/configuration.nix deleted file mode 100644 index ad2ab71..0000000 --- a/machines/ulmo/configuration.nix +++ /dev/null @@ -1,286 +0,0 @@ -{ - pkgs, - lib, - self, - ... -}: { - _module.args = { - pkgs = lib.mkForce (import self.inputs.nixpkgs { - system = "x86_64-linux"; - - overlays = with self.inputs; [ - fenix.overlays.default - nix-minecraft.overlay - flux.overlays.default - ]; - - config = { - allowUnfree = true; - - permittedInsecurePackages = [ - # I think this is because of zen - "qtwebengine-5.15.19" - - # For mautrix-signal, the matrix to signal bridge - "olm-3.2.16" - ]; - }; - }); - }; - - imports = [ - ./disks.nix - ./hardware.nix - self.inputs.home-manager.nixosModules.home-manager - self.inputs.himmelblau.nixosModules.himmelblau - self.inputs.jovian.nixosModules.default - self.inputs.mydia.nixosModules.default - self.inputs.nix-minecraft.nixosModules.minecraft-servers - self.inputs.nvf.nixosModules.default - self.inputs.sops-nix.nixosModules.sops - (self.inputs.import-tree ../../modules/nixos) - ]; - - system.stateVersion = "23.11"; - - networking = { - interfaces.enp2s0 = { - ipv6.addresses = [ - { - address = "2a0d:6e00:1dc9:0::dead:beef"; - prefixLength = 64; - } - ]; - - useDHCP = true; - }; - - defaultGateway = { - address = "192.168.1.1"; - interface = "enp2s0"; - }; - - defaultGateway6 = { - address = "fe80::1"; - interface = "enp2s0"; - }; - }; - - # sneeuwvlok = { - # services = { - # backup.borg.enable = true; - - # authentication.zitadel = { - # enable = true; - - # organization = { - # nix = { - # user = { - # chris = { - # email = "chris@kruining.eu"; - # firstName = "Chris"; - # lastName = "Kruining"; - - # roles = ["ORG_OWNER"]; - # instanceRoles = ["IAM_OWNER"]; - # }; - - # kaas = { - # email = "chris+kaas@kruining.eu"; - # firstName = "Kaas"; - # lastName = "Kruining"; - # }; - # }; - - # project = { - # ulmo = { - # projectRoleCheck = true; - # projectRoleAssertion = true; - # hasProjectCheck = true; - - # role = { - # jellyfin = { - # group = "jellyfin"; - # }; - # jellyfin_admin = { - # group = "jellyfin"; - # }; - # }; - - # assign = { - # chris = ["jellyfin" "jellyfin_admin"]; - # kaas = ["jellyfin"]; - # }; - - # application = { - # jellyfin = { - # redirectUris = ["https://jellyfin.kruining.eu/sso/OID/redirect/zitadel"]; - # grantTypes = ["authorizationCode"]; - # responseTypes = ["code"]; - # }; - - # forgejo = { - # redirectUris = ["https://git.amarth.cloud/user/oauth2/zitadel/callback"]; - # grantTypes = ["authorizationCode"]; - # responseTypes = ["code"]; - # }; - - # vaultwarden = { - # redirectUris = ["https://vault.kruining.eu/identity/connect/oidc-signin"]; - # grantTypes = ["authorizationCode"]; - # responseTypes = ["code"]; - # exportMap = { - # client_id = "SSO_CLIENT_ID"; - # client_secret = "SSO_CLIENT_SECRET"; - # }; - # }; - - # matrix = { - # redirectUris = ["https://matrix.kruining.eu/_synapse/client/oidc/callback"]; - # grantTypes = ["authorizationCode"]; - # responseTypes = ["code"]; - # }; - - # mydia = { - # redirectUris = ["http://localhost:2010/auth/oidc/callback"]; - # grantTypes = ["authorizationCode"]; - # responseTypes = ["code"]; - # }; - - # grafana = { - # redirectUris = ["http://localhost:9001/login/generic_oauth"]; - # grantTypes = ["authorizationCode"]; - # responseTypes = ["code"]; - # }; - # }; - # }; - - # convex = { - # projectRoleCheck = true; - # projectRoleAssertion = true; - # hasProjectCheck = true; - - # application = { - # scry = { - # redirectUris = ["https://nautical-salamander-320.eu-west-1.convex.cloud/api/auth/callback/zitadel"]; - # grantTypes = ["authorizationCode"]; - # responseTypes = ["code"]; - # }; - # }; - # }; - # }; - - # action = { - # flattenRoles = { - # script = '' - # (ctx, api) => { - # if (ctx.v1.user.grants == undefined || ctx.v1.user.grants.count == 0) { - # return; - # } - - # const roles = ctx.v1.user.grants.grants.flatMap(({ roles, projectId }) => roles.map(role => projectId + ':' + role)); - - # api.v1.claims.setClaim('nix:zitadel:custom', JSON.stringify({ roles })); - # }; - # ''; - # }; - # }; - - # triggers = [ - # { - # flowType = "customiseToken"; - # triggerType = "preUserinfoCreation"; - # actions = ["flattenRoles"]; - # } - # { - # flowType = "customiseToken"; - # triggerType = "preAccessTokenCreation"; - # actions = ["flattenRoles"]; - # } - # ]; - # }; - # }; - # }; - - # communication.matrix.enable = true; - - # development.forgejo.enable = true; - - # networking.ssh.enable = true; - # networking.caddy.hosts = { - # # Expose amarht cloud stuff like this until I have a proper solution - # "auth.amarth.cloud" = '' - # reverse_proxy http://192.168.1.223:9092 - # ''; - - # "amarth.cloud" = '' - # reverse_proxy http://192.168.1.223:8080 - # ''; - # }; - - # media.enable = true; - # media.glance.enable = true; - # media.mydia.enable = true; - # media.nfs.enable = true; - # media.jellyfin.enable = true; - # # media.servarr = { - # # radarr = { - # # enable = true; - # # port = 2001; - # # rootFolders = [ - # # "/var/media/movies" - # # ]; - # # }; - - # # sonarr = { - # # enable = true; - # # # debug = true; - # # port = 2002; - # # rootFolders = [ - # # "/var/media/series" - # # ]; - # # }; - - # # lidarr = { - # # enable = true; - # # debug = true; - # # port = 2003; - # # rootFolders = [ - # # "/var/media/music" - # # ]; - # # }; - - # # prowlarr = { - # # enable = true; - # # # debug = true; - # # port = 2004; - # # }; - # # }; - - # observability = { - # grafana.enable = true; - # prometheus.enable = true; - # loki.enable = true; - # promtail.enable = true; - # # uptime-kuma.enable = true; - # }; - - # security.vaultwarden = { - # enable = true; - # database = { - # # type = "sqlite"; - # # file = "/var/lib/vaultwarden/state.db"; - - # type = "postgresql"; - # host = "localhost"; - # port = 5432; - # sslMode = "disabled"; - # }; - # }; - # }; - - # editor = { - # nano.enable = true; - # }; - # }; -} diff --git a/machines/ulmo/hardware.nix b/machines/ulmo/hardware.nix deleted file mode 100644 index 77439d0..0000000 --- a/machines/ulmo/hardware.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ - config, - pkgs, - lib, - modulesPath, - ... -}: let - inherit (lib.modules) mkDefault; -in { - boot = { - initrd.availableKernelModules = ["xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod"]; - initrd.kernelModules = []; - kernelModules = ["kvm-intel"]; - kernelParams = []; - extraModulePackages = []; - }; - - nixpkgs.hostPlatform = "x86_64-linux"; - hardware.cpu.intel.updateMicrocode = mkDefault config.hardware.enableRedistributableFirmware; -} diff --git a/machines/varda/configuration.nix b/machines/varda/configuration.nix deleted file mode 100644 index e75bc1c..0000000 --- a/machines/varda/configuration.nix +++ /dev/null @@ -1,3 +0,0 @@ -{ ... }: { - nixpkgs.hostPlatform = "x86_64-linux"; -} diff --git a/machines/yavanna/configuration.nix b/machines/yavanna/configuration.nix deleted file mode 100644 index e75bc1c..0000000 --- a/machines/yavanna/configuration.nix +++ /dev/null @@ -1,3 +0,0 @@ -{ ... }: { - nixpkgs.hostPlatform = "x86_64-linux"; -} diff --git a/modules/home/application/bitwarden.nix b/modules/home/application/bitwarden.nix deleted file mode 100644 index bd9c02d..0000000 --- a/modules/home/application/bitwarden.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.application.bitwarden; -in { - options.sneeuwvlok.application.bitwarden = { - enable = mkEnableOption "enable bitwarden"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [bitwarden-desktop]; - }; -} diff --git a/modules/home/application/bitwarden/default.nix b/modules/home/application/bitwarden/default.nix new file mode 100644 index 0000000..f2cd869 --- /dev/null +++ b/modules/home/application/bitwarden/default.nix @@ -0,0 +1,15 @@ +{ inputs, config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.application.bitwarden; +in +{ + options.${namespace}.application.bitwarden = { + enable = mkEnableOption "enable bitwarden"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ bitwarden-desktop ]; + }; +} diff --git a/modules/home/application/chrome.nix b/modules/home/application/chrome/default.nix similarity index 94% rename from modules/home/application/chrome.nix rename to modules/home/application/chrome/default.nix index 142abaa..ac9f5ef 100644 --- a/modules/home/application/chrome.nix +++ b/modules/home/application/chrome/default.nix @@ -1,15 +1,11 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let +{ inputs, config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.application.chrome; -in { - options.sneeuwvlok.application.chrome = { + cfg = config.${namespace}.application.chrome; +in +{ + options.${namespace}.application.chrome = { enable = mkEnableOption "enable chrome"; }; diff --git a/modules/home/application/discord.nix b/modules/home/application/discord.nix deleted file mode 100644 index f459cae..0000000 --- a/modules/home/application/discord.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.application.discord; -in { - options.sneeuwvlok.application.discord = { - enable = mkEnableOption "enable discord (vesktop)"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [vesktop]; - }; -} diff --git a/modules/home/application/discord/default.nix b/modules/home/application/discord/default.nix new file mode 100644 index 0000000..a736b37 --- /dev/null +++ b/modules/home/application/discord/default.nix @@ -0,0 +1,15 @@ +{ inputs, config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.application.discord; +in +{ + options.${namespace}.application.discord = { + enable = mkEnableOption "enable discord (vesktop)"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ vesktop ]; + }; +} diff --git a/modules/home/application/ladybird.nix b/modules/home/application/ladybird.nix deleted file mode 100644 index 9d4d3d3..0000000 --- a/modules/home/application/ladybird.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.application.ladybird; -in { - options.sneeuwvlok.application.ladybird = { - enable = mkEnableOption "enable ladybird"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [ladybird]; - }; -} diff --git a/modules/home/application/ladybird/default.nix b/modules/home/application/ladybird/default.nix new file mode 100644 index 0000000..31d7c17 --- /dev/null +++ b/modules/home/application/ladybird/default.nix @@ -0,0 +1,15 @@ +{ inputs, config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.application.ladybird; +in +{ + options.${namespace}.application.ladybird = { + enable = mkEnableOption "enable ladybird"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ ladybird ]; + }; +} diff --git a/modules/home/application/matix.nix b/modules/home/application/matix.nix deleted file mode 100644 index 1164ad6..0000000 --- a/modules/home/application/matix.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - config, - lib, - pkgs, - osConfig ? {}, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.application.matrix; -in { - options.sneeuwvlok.application.matrix = { - enable = mkEnableOption "enable Matrix client (Fractal)"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [fractal element-desktop]; - - programs.element-desktop = { - enable = true; - }; - }; -} diff --git a/modules/home/application/matrix/default.nix b/modules/home/application/matrix/default.nix new file mode 100644 index 0000000..867a94f --- /dev/null +++ b/modules/home/application/matrix/default.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, namespace, osConfig ? {}, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.application.matrix; +in +{ + options.${namespace}.application.matrix = { + enable = mkEnableOption "enable Matrix client (Fractal)"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ fractal element-desktop ]; + + programs.element-desktop = { + enable = true; + }; + }; +} diff --git a/modules/home/application/obs.nix b/modules/home/application/obs/default.nix similarity index 79% rename from modules/home/application/obs.nix rename to modules/home/application/obs/default.nix index 40a3c54..a2be203 100644 --- a/modules/home/application/obs.nix +++ b/modules/home/application/obs/default.nix @@ -1,15 +1,11 @@ -{ - config, - lib, - pkgs, - osConfig ? {}, - ... -}: let +{ config, lib, pkgs, namespace, osConfig ? {}, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.application.obs; -in { - options.sneeuwvlok.application.obs = { + cfg = config.${namespace}.application.obs; +in +{ + options.${namespace}.application.obs = { enable = mkEnableOption "enable obs"; }; diff --git a/modules/home/application/onlyoffice.nix b/modules/home/application/onlyoffice/default.nix similarity index 67% rename from modules/home/application/onlyoffice.nix rename to modules/home/application/onlyoffice/default.nix index 33706ee..0479539 100644 --- a/modules/home/application/onlyoffice.nix +++ b/modules/home/application/onlyoffice/default.nix @@ -3,13 +3,14 @@ config, lib, pkgs, + namespace, ... }: let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.application.onlyoffice; + cfg = config.${namespace}.application.onlyoffice; in { - options.sneeuwvlok.application.onlyoffice = { + options.${namespace}.application.onlyoffice = { enable = mkEnableOption "enable onlyoffice"; }; diff --git a/modules/home/application/signal.nix b/modules/home/application/signal.nix deleted file mode 100644 index 5377795..0000000 --- a/modules/home/application/signal.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.application.signal; -in { - options.sneeuwvlok.application.signal = { - enable = mkEnableOption "enable signal"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [signal-desktop]; - }; -} diff --git a/modules/home/application/signal/default.nix b/modules/home/application/signal/default.nix new file mode 100644 index 0000000..f4eb1d0 --- /dev/null +++ b/modules/home/application/signal/default.nix @@ -0,0 +1,15 @@ +{ inputs, config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.application.signal; +in +{ + options.${namespace}.application.signal = { + enable = mkEnableOption "enable signal"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ signal-desktop ]; + }; +} diff --git a/modules/home/application/steam.nix b/modules/home/application/steam/default.nix similarity index 84% rename from modules/home/application/steam.nix rename to modules/home/application/steam/default.nix index 80b6321..8c87b40 100644 --- a/modules/home/application/steam.nix +++ b/modules/home/application/steam/default.nix @@ -1,20 +1,16 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let +{ inputs, config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.application.steam; -in { - options.sneeuwvlok.application.steam = { + cfg = config.${namespace}.application.steam; +in +{ + options.${namespace}.application.steam = { enable = mkEnableOption "enable steam"; }; config = mkIf cfg.enable { - home.packages = with pkgs; [protonup-ng]; + home.packages = with pkgs; [ protonup-ng ]; home.sessionVariables = { STEAM_EXTRA_COMPAT_TOOLS_PATHS = "\${HOME}/.steam/root/compatibilitytools.d"; diff --git a/modules/home/application/studio.nix b/modules/home/application/studio.nix deleted file mode 100644 index 1b4dc27..0000000 --- a/modules/home/application/studio.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ - config, - lib, - self, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.application.studio; -in { - options.sneeuwvlok.application.studio = { - enable = mkEnableOption "enable Bricklink Studio"; - }; - - config = mkIf cfg.enable { - home.packages = [self.packages.studio]; - }; -} diff --git a/modules/home/application/studio/default.nix b/modules/home/application/studio/default.nix new file mode 100644 index 0000000..7f8173a --- /dev/null +++ b/modules/home/application/studio/default.nix @@ -0,0 +1,15 @@ +{ config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.application.studio; +in +{ + options.${namespace}.application.studio = { + enable = mkEnableOption "enable Bricklink Studio"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs.${namespace}; [ studio ]; + }; +} diff --git a/modules/home/application/teamspeak.nix b/modules/home/application/teamspeak/default.nix similarity index 58% rename from modules/home/application/teamspeak.nix rename to modules/home/application/teamspeak/default.nix index 7ff7bf2..3e5e530 100644 --- a/modules/home/application/teamspeak.nix +++ b/modules/home/application/teamspeak/default.nix @@ -1,15 +1,11 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let +{ inputs, config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.application.teamspeak; -in { - options.sneeuwvlok.application.teamspeak = { + cfg = config.${namespace}.application.teamspeak; +in +{ + options.${namespace}.application.teamspeak = { enable = mkEnableOption "enable teamspeak"; }; diff --git a/modules/home/application/thunderbird.nix b/modules/home/application/thunderbird/default.nix similarity index 74% rename from modules/home/application/thunderbird.nix rename to modules/home/application/thunderbird/default.nix index f21cb4a..c05f57b 100644 --- a/modules/home/application/thunderbird.nix +++ b/modules/home/application/thunderbird/default.nix @@ -1,15 +1,11 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let +{ inputs, config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.application.thunderbird; -in { - options.sneeuwvlok.application.thunderbird = { + cfg = config.${namespace}.application.thunderbird; +in +{ + options.${namespace}.application.thunderbird = { enable = mkEnableOption "enable thunderbird"; }; @@ -18,7 +14,7 @@ in { enable = true; package = pkgs.thunderbird-latest; - profiles.chris = { + profiles.${config.snowfallorg.user.name} = { isDefault = true; }; }; @@ -34,7 +30,7 @@ in { }; thunderbird = { enable = true; - profiles = ["chris"]; + profiles = [ config.snowfallorg.user.name ]; }; }; diff --git a/modules/home/application/zen.nix b/modules/home/application/zen/default.nix similarity index 85% rename from modules/home/application/zen.nix rename to modules/home/application/zen/default.nix index e018ea6..b7cec03 100644 --- a/modules/home/application/zen.nix +++ b/modules/home/application/zen/default.nix @@ -1,14 +1,15 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ inputs, config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.application.zen; -in { - options.sneeuwvlok.application.zen = { + cfg = config.${namespace}.application.zen; +in +{ + imports = [ + inputs.zen-browser.homeModules.default + ]; + + options.${namespace}.application.zen = { enable = mkEnableOption "enable zen"; }; @@ -53,7 +54,8 @@ in { install_url = "https://addons.mozilla.org/firefox/downloads/latest/${builtins.toString id}/latest.xpi"; installation_mode = "force_installed"; }; - in { + in + { ublock_origin = 4531307; ghostry = 4562168; bitwarden = 4562769; diff --git a/modules/home/default.nix b/modules/home/default.nix index 8140c1b..6dc81b5 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -1,38 +1,34 @@ -{ - pkgs, - config, - lib, - ... -}: let +{ pkgs, config, lib, namespace, ... }: +let inherit (lib) mkOption; inherit (lib.types) enum; - cfg = config.sneeuwvlok.defaults; + cfg = config.${namespace}.defaults; in { - options.sneeuwvlok.defaults = { + options.${namespace}.defaults = { editor = mkOption { - type = enum ["nano" "nvim" "zed"]; + type = enum [ "nano" "nvim" "zed" ]; default = "nano"; description = "Default editor for text manipulation"; example = "nvim"; }; shell = mkOption { - type = enum ["fish" "zsh" "bash"]; + type = enum [ "fish" "zsh" "bash" ]; default = "zsh"; description = "Default shell"; example = "zsh"; }; terminal = mkOption { - type = enum ["ghostty" "alacritty"]; + type = enum [ "ghostty" "alacritty" ]; default = "ghostty"; description = "Default terminal"; example = "ghostty"; }; browser = mkOption { - type = enum ["chrome" "ladybird" "zen"]; + type = enum [ "chrome" "ladybird" "zen" ]; default = "zen"; description = "Default terminal"; example = "zen"; diff --git a/modules/home/desktop/plasma/default.nix b/modules/home/desktop/plasma/default.nix index 368dc41..0b679a0 100644 --- a/modules/home/desktop/plasma/default.nix +++ b/modules/home/desktop/plasma/default.nix @@ -1,15 +1,13 @@ -{ - config, - lib, - osConfig ? {}, - ... -}: let +{ config, lib, namespace, osConfig ? {}, ... }: +let inherit (lib) mkIf; - cfg = config.sneeuwvlok.desktop.plasma; - osCfg = osConfig.sneeuwvlok.desktop.plasma or {enable = false;}; -in { - options.sneeuwvlok.desktop.plasma = { + cfg = config.${namespace}.desktop.plasma; + osCfg = osConfig.${namespace}.desktop.plasma or { enable = false; }; +in +{ + options.${namespace}.desktop.plasma = { + }; config = mkIf osCfg.enable { diff --git a/modules/home/development/dotnet.nix b/modules/home/development/dotnet.nix deleted file mode 100644 index 0f2d0b9..0000000 --- a/modules/home/development/dotnet.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkEnableOption mkIf; - - cfg = config.sneeuwvlok.development.dotnet; -in { - options.sneeuwvlok.development.dotnet = { - enable = mkEnableOption "Enable dotnet development tools"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [dotnet-sdk_8]; - }; -} diff --git a/modules/home/development/dotnet/default.nix b/modules/home/development/dotnet/default.nix new file mode 100644 index 0000000..7ed848e --- /dev/null +++ b/modules/home/development/dotnet/default.nix @@ -0,0 +1,15 @@ +{ config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkEnableOption mkIf; + + cfg = config.${namespace}.development.dotnet; +in +{ + options.${namespace}.development.dotnet = { + enable = mkEnableOption "Enable dotnet development tools"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ dotnet-sdk_8 ]; + }; +} diff --git a/modules/home/development/javascript.nix b/modules/home/development/javascript.nix deleted file mode 100644 index 9dfc3be..0000000 --- a/modules/home/development/javascript.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkEnableOption mkIf; - - cfg = config.sneeuwvlok.development.javascript; -in { - options.sneeuwvlok.development.javascript = { - enable = mkEnableOption "Enable javascript development tools"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [bun nodejs nodePackages_latest.typescript-language-server]; - }; -} diff --git a/modules/home/development/javascript/default.nix b/modules/home/development/javascript/default.nix new file mode 100644 index 0000000..e649c86 --- /dev/null +++ b/modules/home/development/javascript/default.nix @@ -0,0 +1,15 @@ +{ config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkEnableOption mkIf; + + cfg = config.${namespace}.development.javascript; +in +{ + options.${namespace}.development.javascript = { + enable = mkEnableOption "Enable javascript development tools"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ bun nodejs nodePackages_latest.typescript-language-server ]; + }; +} diff --git a/modules/home/development/rust.nix b/modules/home/development/rust.nix deleted file mode 100644 index 3a56f2a..0000000 --- a/modules/home/development/rust.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkEnableOption mkIf; - - cfg = config.sneeuwvlok.development.rust; -in { - options.sneeuwvlok.development.rust = { - enable = mkEnableOption "Enable rust development tools"; - }; - - config = - mkIf cfg.enable { - }; -} diff --git a/modules/home/development/rust/default.nix b/modules/home/development/rust/default.nix new file mode 100644 index 0000000..4208c68 --- /dev/null +++ b/modules/home/development/rust/default.nix @@ -0,0 +1,15 @@ +{ config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkEnableOption mkIf; + + cfg = config.${namespace}.development.rust; +in +{ + options.${namespace}.development.rust = { + enable = mkEnableOption "Enable rust development tools"; + }; + + config = mkIf cfg.enable { + + }; +} diff --git a/modules/home/editor/nano.nix b/modules/home/editor/nano/default.nix similarity index 71% rename from modules/home/editor/nano.nix rename to modules/home/editor/nano/default.nix index 270549b..870db8e 100644 --- a/modules/home/editor/nano.nix +++ b/modules/home/editor/nano/default.nix @@ -1,20 +1,16 @@ -{ - config, - options, - lib, - pkgs, - ... -}: let +{ config, options, lib, pkgs, namespace, ... }: +let inherit (lib) mkEnableOption mkIf; - cfg = config.sneeuwvlok.editor.nano; -in { - options.sneeuwvlok.editor.nano = { + cfg = config.${namespace}.editor.nano; +in +{ + options.${namespace}.editor.nano = { enable = mkEnableOption "nano"; }; config = mkIf cfg.enable { - home.packages = with pkgs; [nano]; + home.packages = with pkgs; [ nano ]; # programs.nano = { # enable = true; diff --git a/modules/home/editor/nvim.nix b/modules/home/editor/nvim/default.nix similarity index 85% rename from modules/home/editor/nvim.nix rename to modules/home/editor/nvim/default.nix index 9b3e523..162772f 100644 --- a/modules/home/editor/nvim.nix +++ b/modules/home/editor/nvim/default.nix @@ -1,19 +1,15 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let +{ inputs, config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.editor.nvim; -in { + cfg = config.${namespace}.editor.nvim; +in +{ # imports = [ # inputs.nvf.nixosModules.default # ]; - options.sneeuwvlok.editor.nvim = { + options.${namespace}.editor.nvim = { enable = mkEnableOption "enable nvim via nvf on user level"; }; diff --git a/modules/home/editor/zed.nix b/modules/home/editor/zed/default.nix similarity index 86% rename from modules/home/editor/zed.nix rename to modules/home/editor/zed/default.nix index 7bc2ad7..f0fe7fa 100644 --- a/modules/home/editor/zed.nix +++ b/modules/home/editor/zed/default.nix @@ -1,29 +1,21 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.editor.zed; + cfg = config.${namespace}.editor.zed; in { - options.sneeuwvlok.editor.zed = { + options.${namespace}.editor.zed = { enable = mkEnableOption "zed"; }; config = mkIf cfg.enable { home.packages = with pkgs; [ - zed-editor - nixd - nil - alejandra + zed-editor nixd nil alejandra ]; programs.zed-editor = { enable = true; - extensions = ["nix" "toml" "html" "just-ls"]; + extensions = [ "nix" "toml" "html" "just-ls" ]; userSettings = { assistant.enabled = false; diff --git a/modules/home/game/minecraft/default.nix b/modules/home/game/minecraft/default.nix new file mode 100644 index 0000000..e5dedc6 --- /dev/null +++ b/modules/home/game/minecraft/default.nix @@ -0,0 +1,15 @@ +{ inputs, config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.game.minecraft; +in +{ + options.${namespace}.game.minecraft = { + enable = mkEnableOption "enable minecraft"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ prismlauncher ]; + }; +} diff --git a/modules/home/game/minescraft.nix b/modules/home/game/minescraft.nix deleted file mode 100644 index 384142e..0000000 --- a/modules/home/game/minescraft.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.game.minecraft; -in { - options.sneeuwvlok.game.minecraft = { - enable = mkEnableOption "enable minecraft"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [prismlauncher]; - }; -} diff --git a/modules/home/shell/default.nix b/modules/home/shell/default.nix index 98a2054..9968e54 100644 --- a/modules/home/shell/default.nix +++ b/modules/home/shell/default.nix @@ -1,20 +1,17 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkMerge mkEnableOption mkDefault; - cfg = config.sneeuwvlok.shell; -in { - options.sneeuwvlok.shell = { + cfg = config.${namespace}.shell; +in +{ + options.${namespace}.shell = { corePkgs.enable = mkEnableOption "core shell packages"; }; config = mkMerge [ (mkIf (cfg.corePkgs.enable) { - sneeuwvlok.shell.toolset = mkDefault { + ${namespace}.shell.toolset = mkDefault { bat.enable = true; btop.enable = true; eza.enable = true; @@ -28,8 +25,8 @@ in { }; }) - { - home.packages = with pkgs; [any-nix-shell pwgen yt-dlp ripdrag fd (ripgrep.override {withPCRE2 = true;})]; + ({ + home.packages = with pkgs; [ any-nix-shell pwgen yt-dlp ripdrag fd (ripgrep.override {withPCRE2 = true;}) ]; programs = { direnv = { @@ -43,6 +40,6 @@ in { config.whitelist.prefix = ["/home"]; }; }; - } + }) ]; } diff --git a/modules/home/shell/toolset/bat.nix b/modules/home/shell/toolset/bat/default.nix similarity index 50% rename from modules/home/shell/toolset/bat.nix rename to modules/home/shell/toolset/bat/default.nix index 101e3d8..78899df 100644 --- a/modules/home/shell/toolset/bat.nix +++ b/modules/home/shell/toolset/bat/default.nix @@ -1,19 +1,16 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.shell.toolset.bat; -in { - options.sneeuwvlok.shell.toolset.bat = { + cfg = config.${namespace}.shell.toolset.bat; +in +{ + options.${namespace}.shell.toolset.bat = { enable = mkEnableOption "cat replacement"; }; config = mkIf cfg.enable { - home.packages = with pkgs; [bat]; + home.packages = with pkgs; [ bat ]; programs.bat = { enable = true; diff --git a/modules/home/shell/toolset/btop.nix b/modules/home/shell/toolset/btop/default.nix similarity index 92% rename from modules/home/shell/toolset/btop.nix rename to modules/home/shell/toolset/btop/default.nix index cbcddde..b490acc 100644 --- a/modules/home/shell/toolset/btop.nix +++ b/modules/home/shell/toolset/btop/default.nix @@ -1,20 +1,17 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; inherit (lib.strings) concatStringsSep; - cfg = config.sneeuwvlok.shell.toolset.btop; -in { - options.sneeuwvlok.shell.toolset.btop = { + cfg = config.${namespace}.shell.toolset.btop; +in +{ + options.${namespace}.shell.toolset.btop = { enable = mkEnableOption "system-monitor"; }; config = mkIf cfg.enable { - home.packages = with pkgs; [btop]; + home.packages = with pkgs; [ btop ]; programs.btop = { enable = true; diff --git a/modules/home/shell/toolset/eza.nix b/modules/home/shell/toolset/eza/default.nix similarity index 64% rename from modules/home/shell/toolset/eza.nix rename to modules/home/shell/toolset/eza/default.nix index f0d7b94..00026cf 100644 --- a/modules/home/shell/toolset/eza.nix +++ b/modules/home/shell/toolset/eza/default.nix @@ -1,19 +1,16 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.shell.toolset.eza; -in { - options.sneeuwvlok.shell.toolset.eza = { + cfg = config.${namespace}.shell.toolset.eza; +in +{ + options.${namespace}.shell.toolset.eza = { enable = mkEnableOption "system-monitor"; }; config = mkIf cfg.enable { - home.packages = with pkgs; [eza]; + home.packages = with pkgs; [ eza ]; programs.eza = { enable = true; diff --git a/modules/home/shell/toolset/fzf.nix b/modules/home/shell/toolset/fzf/default.nix similarity index 80% rename from modules/home/shell/toolset/fzf.nix rename to modules/home/shell/toolset/fzf/default.nix index 7054e4a..e2f0313 100644 --- a/modules/home/shell/toolset/fzf.nix +++ b/modules/home/shell/toolset/fzf/default.nix @@ -1,19 +1,16 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.shell.toolset.fzf; -in { - options.sneeuwvlok.shell.toolset.fzf = { + cfg = config.${namespace}.shell.toolset.fzf; +in +{ + options.${namespace}.shell.toolset.fzf = { enable = mkEnableOption "TUI Fuzzy Finder."; }; config = mkIf cfg.enable { - home.packages = with pkgs; [fzf]; + home.packages = with pkgs; [ fzf ]; programs.fzf = { enable = true; diff --git a/modules/home/shell/toolset/git.nix b/modules/home/shell/toolset/git/default.nix similarity index 95% rename from modules/home/shell/toolset/git.nix rename to modules/home/shell/toolset/git/default.nix index 7412ce1..dd138c8 100644 --- a/modules/home/shell/toolset/git.nix +++ b/modules/home/shell/toolset/git/default.nix @@ -2,13 +2,14 @@ config, lib, pkgs, + namespace, ... }: let inherit (lib) mkEnableOption mkIf; - cfg = config.sneeuwvlok.shell.toolset.git; + cfg = config.${namespace}.shell.toolset.git; in { - options.sneeuwvlok.shell.toolset.git = { + options.${namespace}.shell.toolset.git = { enable = mkEnableOption "version-control system"; }; diff --git a/modules/home/shell/toolset/gnupgp.nix b/modules/home/shell/toolset/gnugpg/default.nix similarity index 83% rename from modules/home/shell/toolset/gnupgp.nix rename to modules/home/shell/toolset/gnugpg/default.nix index 1f4dc1e..8b6ae38 100644 --- a/modules/home/shell/toolset/gnupgp.nix +++ b/modules/home/shell/toolset/gnugpg/default.nix @@ -1,14 +1,11 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.shell.toolset.gnupg; -in { - options.sneeuwvlok.shell.toolset.gnupg = { + cfg = config.${namespace}.shell.toolset.gnupg; +in +{ + options.${namespace}.shell.toolset.gnupg = { enable = mkEnableOption "cryptographic suite"; }; diff --git a/modules/home/shell/toolset/just.nix b/modules/home/shell/toolset/just.nix deleted file mode 100644 index 6f7e67e..0000000 --- a/modules/home/shell/toolset/just.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkEnableOption mkIf; - - cfg = config.sneeuwvlok.shell.toolset.just; -in { - options.sneeuwvlok.shell.toolset.just = { - enable = mkEnableOption "version-control system"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [just gum]; - }; -} diff --git a/modules/home/shell/toolset/just/default.nix b/modules/home/shell/toolset/just/default.nix new file mode 100644 index 0000000..e956b2a --- /dev/null +++ b/modules/home/shell/toolset/just/default.nix @@ -0,0 +1,15 @@ +{ config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkEnableOption mkIf; + + cfg = config.${namespace}.shell.toolset.just; +in +{ + options.${namespace}.shell.toolset.just = { + enable = mkEnableOption "version-control system"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ just gum ]; + }; +} diff --git a/modules/home/shell/toolset/starship.nix b/modules/home/shell/toolset/starship/default.nix similarity index 91% rename from modules/home/shell/toolset/starship.nix rename to modules/home/shell/toolset/starship/default.nix index 3a99fdd..28d57f7 100644 --- a/modules/home/shell/toolset/starship.nix +++ b/modules/home/shell/toolset/starship/default.nix @@ -1,19 +1,16 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.shell.toolset.starship; -in { - options.sneeuwvlok.shell.toolset.starship = { + cfg = config.${namespace}.shell.toolset.starship; +in +{ + options.${namespace}.shell.toolset.starship = { enable = mkEnableOption "fancy pansy shell prompt"; }; config = mkIf cfg.enable { - home.packages = with pkgs; [starship]; + home.packages = with pkgs; [ starship ]; programs.starship = { enable = true; diff --git a/modules/home/shell/toolset/tmux.nix b/modules/home/shell/toolset/tmux/default.nix similarity index 92% rename from modules/home/shell/toolset/tmux.nix rename to modules/home/shell/toolset/tmux/default.nix index 34e20dd..ed14ba6 100644 --- a/modules/home/shell/toolset/tmux.nix +++ b/modules/home/shell/toolset/tmux/default.nix @@ -1,19 +1,16 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.shell.toolset.tmux; -in { - options.sneeuwvlok.shell.toolset.tmux = { - enable = mkEnableOption "terminal multiplexer"; + cfg = config.${namespace}.shell.toolset.tmux; +in +{ + options.${namespace}.shell.toolset.tmux = { + enable = mkEnableOption "terminal multiplexer"; }; config = mkIf cfg.enable { - home.packages = with pkgs; [tmux]; + home.packages = with pkgs; [ tmux ]; programs.tmux = { enable = true; diff --git a/modules/home/shell/toolset/yazi.nix b/modules/home/shell/toolset/yazi/default.nix similarity index 50% rename from modules/home/shell/toolset/yazi.nix rename to modules/home/shell/toolset/yazi/default.nix index 6ad6519..37138a0 100644 --- a/modules/home/shell/toolset/yazi.nix +++ b/modules/home/shell/toolset/yazi/default.nix @@ -1,19 +1,16 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.shell.toolset.yazi; -in { - options.sneeuwvlok.shell.toolset.yazi = { + cfg = config.${namespace}.shell.toolset.yazi; +in +{ + options.${namespace}.shell.toolset.yazi = { enable = mkEnableOption "cli file browser"; }; config = mkIf cfg.enable { - home.packages = with pkgs; [yazi]; + home.packages = with pkgs; [ yazi ]; programs.yazi = { enable = true; diff --git a/modules/home/shell/toolset/zellij.nix b/modules/home/shell/toolset/zellij/default.nix similarity index 56% rename from modules/home/shell/toolset/zellij.nix rename to modules/home/shell/toolset/zellij/default.nix index 52e69f4..db5b7bd 100644 --- a/modules/home/shell/toolset/zellij.nix +++ b/modules/home/shell/toolset/zellij/default.nix @@ -1,19 +1,16 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.shell.toolset.zellij; -in { - options.sneeuwvlok.shell.toolset.zellij = { + cfg = config.${namespace}.shell.toolset.zellij; +in +{ + options.${namespace}.shell.toolset.zellij = { enable = mkEnableOption "terminal multiplexer"; }; config = mkIf cfg.enable { - home.packages = with pkgs; [zellij]; + home.packages = with pkgs; [ zellij ]; programs.zellij = { enable = true; diff --git a/modules/home/shell/toolset/zoxide.nix b/modules/home/shell/toolset/zoxide.nix deleted file mode 100644 index 25f4508..0000000 --- a/modules/home/shell/toolset/zoxide.nix +++ /dev/null @@ -1,22 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.shell.toolset.zoxide; -in { - options.sneeuwvlok.shell.toolset.zoxide = { - enable = mkEnableOption "cd replacement"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [zoxide]; - - programs.zoxide = { - enable = true; - }; - }; -} diff --git a/modules/home/shell/toolset/zoxide/default.nix b/modules/home/shell/toolset/zoxide/default.nix new file mode 100644 index 0000000..5b8acb6 --- /dev/null +++ b/modules/home/shell/toolset/zoxide/default.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.shell.toolset.zoxide; +in +{ + options.${namespace}.shell.toolset.zoxide = { + enable = mkEnableOption "cd replacement"; + }; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ zoxide ]; + + programs.zoxide = { + enable = true; + }; + }; +} diff --git a/modules/home/shell/zsh.nix b/modules/home/shell/zsh/default.nix similarity index 93% rename from modules/home/shell/zsh.nix rename to modules/home/shell/zsh/default.nix index a202fa3..b37cc4c 100644 --- a/modules/home/shell/zsh.nix +++ b/modules/home/shell/zsh/default.nix @@ -1,19 +1,16 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.shell.zsh; -in { - options.sneeuwvlok.shell.zsh = { + cfg = config.${namespace}.shell.zsh; +in +{ + options.${namespace}.shell.zsh = { enable = mkEnableOption "enable ZSH"; }; config = mkIf cfg.enable { - # sneeuwvlok.shell = { + # ${namespace}.shell = { # zsh.enable = true; # }; diff --git a/modules/home/terminal/alacritty.nix b/modules/home/terminal/alacritty/default.nix similarity index 84% rename from modules/home/terminal/alacritty.nix rename to modules/home/terminal/alacritty/default.nix index 6b46514..b6e5822 100644 --- a/modules/home/terminal/alacritty.nix +++ b/modules/home/terminal/alacritty/default.nix @@ -1,13 +1,11 @@ -{ - config, - lib, - ... -}: let +{ config, lib, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.terminal.alacritty; -in { - options.sneeuwvlok.terminal.alacritty = { + cfg = config.${namespace}.terminal.alacritty; +in +{ + options.${namespace}.terminal.alacritty = { enable = mkEnableOption "enable alacritty"; }; diff --git a/modules/home/terminal/ghostty.nix b/modules/home/terminal/ghostty/default.nix similarity index 58% rename from modules/home/terminal/ghostty.nix rename to modules/home/terminal/ghostty/default.nix index 65487a9..4681b53 100644 --- a/modules/home/terminal/ghostty.nix +++ b/modules/home/terminal/ghostty/default.nix @@ -1,13 +1,11 @@ -{ - config, - lib, - ... -}: let +{ config, lib, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.terminal.ghostty; -in { - options.sneeuwvlok.terminal.ghostty = { + cfg = config.${namespace}.terminal.ghostty; +in +{ + options.${namespace}.terminal.ghostty = { enable = mkEnableOption "enable ghostty"; }; @@ -15,10 +13,10 @@ in { programs.ghostty = { enable = true; settings = { - command = config.sneeuwvlok.defaults.shell; + command = config.${namespace}.defaults.shell; background-blur-radius = 20; theme = "dark:stylix,light:stylix"; - window-theme = config.sneeuwvlok.themes.polarity or "dark"; + window-theme = (config.${namespace}.themes.polarity or "dark"); background-opacity = 0.8; minimum-contrast = 1.1; }; diff --git a/modules/home/themes/default.nix b/modules/home/themes/default.nix index 37dcb39..d338b88 100644 --- a/modules/home/themes/default.nix +++ b/modules/home/themes/default.nix @@ -1,29 +1,24 @@ -{ - config, - lib, - pkgs, - osConfig ? {}, - ... -}: let +{ config, lib, pkgs, namespace, osConfig ? {}, ... }: +let inherit (lib) mkIf mkDefault; inherit (lib.options) mkOption mkEnableOption; inherit (lib.types) nullOr enum; - cfg = config.sneeuwvlok.themes; - osCfg = osConfig.sneeuwvlok.theming; + cfg = config.${namespace}.themes; + osCfg = osConfig.${namespace}.theming; in { - options.sneeuwvlok.themes = { + options.${namespace}.themes = { enable = mkEnableOption "Theming (Stylix)"; theme = mkOption { - type = nullOr (enum ["everforest" "catppuccin-latte" "chalk"]); + type = nullOr (enum [ "everforest" "catppuccin-latte" "chalk" ]); default = "everforest"; description = "The theme to set the system to"; example = "everforest"; }; polarity = mkOption { - type = nullOr (enum ["dark" "light"]); + type = nullOr (enum [ "dark" "light" ]); default = "dark"; description = "determine if system is in dark or light mode"; }; @@ -38,7 +33,7 @@ in { polarity = cfg.polarity; targets.qt.platform = mkDefault "kde"; - targets.zen-browser.profileNames = ["Chris"]; + targets.zen-browser.profileNames = [ "Chris" ]; fonts = { serif = { diff --git a/modules/nixos/application/steam.nix b/modules/nixos/application/steam.nix deleted file mode 100644 index 6b6319b..0000000 --- a/modules/nixos/application/steam.nix +++ /dev/null @@ -1,29 +0,0 @@ -{ - lib, - pkgs, - config, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.application.steam; -in { - options.sneeuwvlok.application.steam = { - enable = mkEnableOption "enable steam"; - }; - config = mkIf cfg.enable { - # environment.systemPackages = with pkgs; [steam]; - - programs = { - steam = { - enable = true; - remotePlay.openFirewall = true; - dedicatedServer.openFirewall = true; - localNetworkGameTransfers.openFirewall = true; - extraCompatPackages = with pkgs; [ - proton-ge-bin - ]; - }; - }; - }; -} diff --git a/modules/nixos/application/steam/default.nix b/modules/nixos/application/steam/default.nix new file mode 100644 index 0000000..061765e --- /dev/null +++ b/modules/nixos/application/steam/default.nix @@ -0,0 +1,64 @@ +{ + inputs, + config, + lib, + pkgs, + namespace, + ... +}: let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.application.steam; +in { + options.${namespace}.application.steam = { + enable = mkEnableOption "enable steam"; + }; + + config = mkIf cfg.enable { + # environment.systemPackages = with pkgs; [ steam ]; + + programs = { + steam = { + enable = true; + remotePlay.openFirewall = true; + dedicatedServer.openFirewall = true; + localNetworkGameTransfers.openFirewall = true; + + extraCompatPackages = with pkgs; [ + proton-ge-bin + ]; + + # package = pkgs.steam.override { + # extraEnv = { + # DXVK_HUD = "compiler"; + # MANGOHUD = true; + # }; + # }; + + # gamescopeSession = { + # enable = true; + # args = ["--immediate-flips"]; + # }; + }; + + # https://github.com/FeralInteractive/gamemode + # gamemode = { + # enable = true; + # enableRenice = true; + # settings = {}; + # }; + + # gamescope = { + # enable = true; + # capSysNice = true; + # env = { + # DXVK_HDR = "1"; + # ENABLE_GAMESCOPE_WSI = "1"; + # WINE_FULLSCREEN_FSR = "1"; + # WLR_RENDERER = "vulkan"; + # }; + # args = ["--hdr-enabled"]; + # }; + }; + }; +} diff --git a/modules/nixos/boot.nix b/modules/nixos/boot/default.nix similarity index 80% rename from modules/nixos/boot.nix rename to modules/nixos/boot/default.nix index 8a8a204..1f844d1 100644 --- a/modules/nixos/boot.nix +++ b/modules/nixos/boot/default.nix @@ -1,18 +1,14 @@ -{ - inputs, - lib, - config, - pkgs, - ... -}: let +{ lib, namespace, config, pkgs, ... }: +let inherit (lib) mkIf mkMerge mkDefault mkOption; inherit (lib.types) enum bool; - cfg = config.sneeuwvlok.boot; -in { - options.sneeuwvlok.boot = { + cfg = config.${namespace}.boot; +in +{ + options.${namespace}.boot = { type = mkOption { - type = enum ["bios" "uefi"]; + type = enum [ "bios" "uefi" ]; default = "uefi"; }; @@ -28,7 +24,7 @@ in { }; config = mkMerge [ - { + ({ boot = { kernelPackages = pkgs.linuxPackages_latest; @@ -36,16 +32,16 @@ in { systemd-boot.enable = false; grub.enable = mkDefault true; - # grub2-theme = { - # enable = true; - # theme = "vimix"; - # footer = true; - # }; + grub2-theme = { + enable = true; + theme = "vimix"; + footer = true; + }; }; - supportedFilesystems = ["nfs"]; + supportedFilesystems = [ "nfs" ]; }; - } + }) (mkIf (cfg.type == "bios") { boot.loader.grub.efiSupport = false; @@ -91,7 +87,7 @@ in { theme = mkDefault "pixels"; themePackages = with pkgs; [ (adi1090x-plymouth-themes.override { - selected_themes = ["pixels"]; + selected_themes = [ "pixels" ]; }) ]; }; diff --git a/modules/nixos/desktop/cosmic.nix b/modules/nixos/desktop/cosmic/default.nix similarity index 66% rename from modules/nixos/desktop/cosmic.nix rename to modules/nixos/desktop/cosmic/default.nix index 78e0bc4..cba6955 100644 --- a/modules/nixos/desktop/cosmic.nix +++ b/modules/nixos/desktop/cosmic/default.nix @@ -1,18 +1,19 @@ { lib, config, + namespace, inputs, ... }: let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.desktop.cosmic; + cfg = config.${namespace}.desktop.cosmic; in { - options.sneeuwvlok.desktop.cosmic = { + options.${namespace}.desktop.cosmic = { enable = mkEnableOption "Enable Cosmic desktop" // { - default = config.sneeuwvlok.desktop.use == "cosmic"; + default = config.${namespace}.desktop.use == "cosmic"; }; }; diff --git a/modules/nixos/desktop/default.nix b/modules/nixos/desktop/default.nix index 7aa6b57..13ef881 100644 --- a/modules/nixos/desktop/default.nix +++ b/modules/nixos/desktop/default.nix @@ -1,14 +1,20 @@ { lib, config, + namespace, + inputs, ... }: let inherit (lib) mkIf mkOption mkEnableOption mkMerge; inherit (lib.types) nullOr enum; - cfg = config.sneeuwvlok.desktop; + cfg = config.${namespace}.desktop; in { - options.sneeuwvlok.desktop = { + imports = [ + inputs.grub2-themes.nixosModules.default + ]; + + options.${namespace}.desktop = { use = mkOption { type = nullOr (enum ["plasma" "gamescope" "gnome" "cosmic"]); default = null; @@ -25,7 +31,7 @@ in { } # (mkIf (cfg.use != null) { - # sneeuwvlok.desktop.${cfg.use}.enable = true; + # ${namespace}.desktop.${cfg.use}.enable = true; # }) ]; } diff --git a/modules/nixos/desktop/gamescope.nix b/modules/nixos/desktop/gamescope/default.nix similarity index 54% rename from modules/nixos/desktop/gamescope.nix rename to modules/nixos/desktop/gamescope/default.nix index 2ccd631..80e6099 100644 --- a/modules/nixos/desktop/gamescope.nix +++ b/modules/nixos/desktop/gamescope/default.nix @@ -1,22 +1,20 @@ -{ - lib, - config, - ... -}: let +{ lib, config, namespace, inputs, ... }: +let inherit (lib) mkIf mkEnableOption mkForce; - cfg = config.sneeuwvlok.desktop.gamescope; -in { - options.sneeuwvlok.desktop.gamescope = { - enable = - mkEnableOption "Enable Steamdeck ui" - // { - default = config.sneeuwvlok.desktop.use == "gamescope"; - }; + cfg = config.${namespace}.desktop.gamescope; +in +{ + imports = [ inputs.jovian.nixosModules.default ]; + + options.${namespace}.desktop.gamescope = { + enable = mkEnableOption "Enable Steamdeck ui" // { + default = (config.${namespace}.desktop.use == "gamescope"); + }; }; config = mkIf cfg.enable { - sneeuwvlok.desktop.plasma.enable = true; + ${namespace}.desktop.plasma.enable = true; services.displayManager.sddm.enable = mkForce false; services.displayManager.gdm.enable = mkForce false; diff --git a/modules/nixos/desktop/gnome.nix b/modules/nixos/desktop/gnome.nix deleted file mode 100644 index 3deb9be..0000000 --- a/modules/nixos/desktop/gnome.nix +++ /dev/null @@ -1,21 +0,0 @@ -{ - lib, - config, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.desktop.gnome; -in { - options.sneeuwvlok.desktop.gnome = { - enable = - mkEnableOption "Enable Gnome" - // { - default = config.sneeuwvlok.desktop.use == "gnome"; - }; - }; - - config = - mkIf cfg.enable { - }; -} diff --git a/modules/nixos/desktop/gnome/default.nix b/modules/nixos/desktop/gnome/default.nix new file mode 100644 index 0000000..979587f --- /dev/null +++ b/modules/nixos/desktop/gnome/default.nix @@ -0,0 +1,16 @@ +{ lib, config, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.desktop.gnome; +in +{ + options.${namespace}.desktop.gnome = { + enable = mkEnableOption "Enable Gnome" // { + default = (config.${namespace}.desktop.use == "gnome"); + }; + }; + + config = mkIf cfg.enable { + }; +} diff --git a/modules/nixos/desktop/plasma.nix b/modules/nixos/desktop/plasma/default.nix similarity index 70% rename from modules/nixos/desktop/plasma.nix rename to modules/nixos/desktop/plasma/default.nix index 06bc31d..aa1e497 100644 --- a/modules/nixos/desktop/plasma.nix +++ b/modules/nixos/desktop/plasma/default.nix @@ -1,19 +1,14 @@ -{ - pkgs, - lib, - config, - ... -}: let +{ pkgs, lib, config, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.desktop.plasma; -in { - options.sneeuwvlok.desktop.plasma = { - enable = - mkEnableOption "Enable KDE Plasma" - // { - default = config.sneeuwvlok.desktop.use == "plasma"; - }; + cfg = config.${namespace}.desktop.plasma; +in +{ + options.${namespace}.desktop.plasma = { + enable = mkEnableOption "Enable KDE Plasma" // { + default = (config.${namespace}.desktop.use == "plasma"); + }; }; config = mkIf cfg.enable { diff --git a/modules/nixos/editor/nano.nix b/modules/nixos/editor/nano/default.nix similarity index 75% rename from modules/nixos/editor/nano.nix rename to modules/nixos/editor/nano/default.nix index 26ec1db..1cb7ff1 100644 --- a/modules/nixos/editor/nano.nix +++ b/modules/nixos/editor/nano/default.nix @@ -1,15 +1,11 @@ -{ - config, - options, - lib, - pkgs, - ... -}: let +{ config, options, lib, pkgs, namespace, ... }: +let inherit (lib) mkEnableOption mkIf; - cfg = config.sneeuwvlok.editor.nano; -in { - options.sneeuwvlok.editor.nano = { + cfg = config.${namespace}.editor.nano; +in +{ + options.${namespace}.editor.nano = { enable = mkEnableOption "nano"; }; diff --git a/modules/nixos/editor/nvim.nix b/modules/nixos/editor/nvim.nix deleted file mode 100644 index 1179957..0000000 --- a/modules/nixos/editor/nvim.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.editor.nvim; -in { - options.sneeuwvlok.editor.nvim = { - enable = mkEnableOption "enable nvim via nvf on system level"; - }; - - config = - mkIf cfg.enable { - }; -} diff --git a/modules/nixos/editor/nvim/default.nix b/modules/nixos/editor/nvim/default.nix new file mode 100644 index 0000000..c29de0f --- /dev/null +++ b/modules/nixos/editor/nvim/default.nix @@ -0,0 +1,18 @@ +{ inputs, config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.editor.nvim; +in +{ + imports = [ + inputs.nvf.nixosModules.default + ]; + + options.${namespace}.editor.nvim = { + enable = mkEnableOption "enable nvim via nvf on system level"; + }; + + config = mkIf cfg.enable { + }; +} diff --git a/modules/nixos/hardware/audio.nix b/modules/nixos/hardware/audio/default.nix similarity index 72% rename from modules/nixos/hardware/audio.nix rename to modules/nixos/hardware/audio/default.nix index e507417..d3f340f 100644 --- a/modules/nixos/hardware/audio.nix +++ b/modules/nixos/hardware/audio/default.nix @@ -1,14 +1,11 @@ -{ - pkgs, - lib, - config, - ... -}: let +{ pkgs, lib, namespace, config, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.hardware.has.audio; -in { - options.sneeuwvlok.hardware.has.audio = mkEnableOption "Enable bluetooth"; + cfg = config.${namespace}.hardware.has.audio; +in +{ + options.${namespace}.hardware.has.audio = mkEnableOption "Enable bluetooth"; config = mkIf cfg { environment.systemPackages = with pkgs; [ diff --git a/modules/nixos/hardware/bluetooth.nix b/modules/nixos/hardware/bluetooth/default.nix similarity index 67% rename from modules/nixos/hardware/bluetooth.nix rename to modules/nixos/hardware/bluetooth/default.nix index 720d121..98fc678 100644 --- a/modules/nixos/hardware/bluetooth.nix +++ b/modules/nixos/hardware/bluetooth/default.nix @@ -1,13 +1,11 @@ -{ - lib, - config, - ... -}: let +{ lib, namespace, config, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.hardware.has.bluetooth; -in { - options.sneeuwvlok.hardware.has.bluetooth = mkEnableOption "Enable bluetooth"; + cfg = config.${namespace}.hardware.has.bluetooth; +in +{ + options.${namespace}.hardware.has.bluetooth = mkEnableOption "Enable bluetooth"; config = mkIf cfg { hardware.bluetooth = { @@ -23,7 +21,7 @@ in { "bluez5.enable-sbc-xq" = true; "bluez5.enable-msbc" = true; "bluez5.enable-hw-volume" = true; - "bluez5.roles" = ["hsp_hs" "hsp_ag" "hfp_hf" "hfp_ag"]; + "bluez5.roles" = [ "hsp_hs" "hsp_ag" "hfp_hf" "hfp_ag" ]; }; }; }; diff --git a/modules/nixos/hardware/gpu/amd.nix b/modules/nixos/hardware/gpu/amd/default.nix similarity index 50% rename from modules/nixos/hardware/gpu/amd.nix rename to modules/nixos/hardware/gpu/amd/default.nix index 58ddd6a..cdc9d1e 100644 --- a/modules/nixos/hardware/gpu/amd.nix +++ b/modules/nixos/hardware/gpu/amd/default.nix @@ -1,17 +1,14 @@ -{ - pkgs, - lib, - config, - ... -}: let +{ pkgs, lib, namespace, config, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.hardware.has.gpu; -in { - options.sneeuwvlok.hardware.has.gpu.amd = mkEnableOption "Enable AMD gpu configuration"; + cfg = config.${namespace}.hardware.has.gpu; +in +{ + options.${namespace}.hardware.has.gpu.amd = mkEnableOption "Enable AMD gpu configuration"; config = mkIf cfg.amd { - services.xserver.videoDrivers = ["amd"]; + services.xserver.videoDrivers = [ "amd" ]; hardware = { graphics = { diff --git a/modules/nixos/hardware/gpu/nvidia.nix b/modules/nixos/hardware/gpu/nvidia.nix index ec875be..e8ac542 100644 --- a/modules/nixos/hardware/gpu/nvidia.nix +++ b/modules/nixos/hardware/gpu/nvidia.nix @@ -1,17 +1,14 @@ -{ - pkgs, - lib, - config, - ... -}: let +{ pkgs, lib, namespace, config, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.hardware.has.gpu; -in { - options.sneeuwvlok.hardware.has.gpu.nvidia = mkEnableOption "Enable NVidia gpu configuration"; + cfg = config.${namespace}.hardware.has.gpu.nvidia; +in +{ + options.${namespace}.hardware.has.gpu.nvidia = mkEnableOption "Enable NVidia gpu configuration"; - config = mkIf cfg.nvidia { - services.xserver.videoDrivers = ["nvidia"]; + config = mkIf cfg { + services.xserver.videoDrivers = [ "nvidia" ]; hardware = { graphics = { diff --git a/modules/nixos/hardware/gpu/nvidia/default.nix b/modules/nixos/hardware/gpu/nvidia/default.nix new file mode 100644 index 0000000..ab7c087 --- /dev/null +++ b/modules/nixos/hardware/gpu/nvidia/default.nix @@ -0,0 +1,49 @@ +{ pkgs, lib, namespace, config, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.hardware.has.gpu; +in +{ + options.${namespace}.hardware.has.gpu.nvidia = mkEnableOption "Enable NVidia gpu configuration"; + + config = mkIf cfg.nvidia { + services.xserver.videoDrivers = [ "nvidia" ]; + + hardware = { + graphics = { + enable = true; + enable32Bit = true; + }; + + nvidia = { + modesetting.enable = true; + open = false; + nvidiaSettings = true; + + powerManagement = { + enable = true; + finegrained = false; + }; + + # package = config.boot.kernelPackages.nvidiaPackages.vulkan_beta; + + # package = let + # rcu_patch = pkgs.fetchpatch { + # url = "https://github.com/gentoo/gentoo/raw/c64caf53/x11-drivers/nvidia-drivers/files/nvidia-drivers-470.223.02-gpl-pfn_valid.patch"; + # hash = "sha256-eZiQQp2S/asE7MfGvfe6dA/kdCvek9SYa/FFGp24dVg="; + # }; + # in config.boot.kernelPackages.nvidiaPackages.mkDriver { + # version = "550.40.07"; + # sha256_64bit = "sha256-KYk2xye37v7ZW7h+uNJM/u8fNf7KyGTZjiaU03dJpK0="; + # sha256_aarch64 = "sha256-AV7KgRXYaQGBFl7zuRcfnTGr8rS5n13nGUIe3mJTXb4="; + # openSha256 = "sha256-mRUTEWVsbjq+psVe+kAT6MjyZuLkG2yRDxCMvDJRL1I="; + # settingsSha256 = "sha256-c30AQa4g4a1EHmaEu1yc05oqY01y+IusbBuq+P6rMCs="; + # persistencedSha256 = "sha256-11tLSY8uUIl4X/roNnxf5yS2PQvHvoNjnd2CB67e870="; + + # patches = [ rcu_patch ]; + # }; + }; + }; + }; +} diff --git a/modules/nixos/hardware/keyboard/voyager.nix b/modules/nixos/hardware/keyboard/voyager.nix index a7823f9..fd470f8 100644 --- a/modules/nixos/hardware/keyboard/voyager.nix +++ b/modules/nixos/hardware/keyboard/voyager.nix @@ -1,15 +1,12 @@ -{ - lib, - config, - pkgs, - ... -}: let +{ lib, config, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.hardware.keyboard.voyager; -in { - options.sneeuwvlok.hardware.keyboard.voyager = { - enable = mkEnableOption "Enable tools for ZSA Voyager"; + cfg = config.${namespace}.hardware.keyboard.voyager; +in +{ + options.${namespace}.hardware.keyboard.voyager = { + enble = mkEnableOption "Enable tools for ZSA Voyager"; }; config = mkIf cfg.enable { diff --git a/modules/nixos/home-manager.nix b/modules/nixos/home-manager/default.nix similarity index 100% rename from modules/nixos/home-manager.nix rename to modules/nixos/home-manager/default.nix diff --git a/modules/nixos/nix.nix b/modules/nixos/nix/default.nix similarity index 73% rename from modules/nixos/nix.nix rename to modules/nixos/nix/default.nix index 870dd24..bf96f59 100644 --- a/modules/nixos/nix.nix +++ b/modules/nixos/nix/default.nix @@ -1,12 +1,9 @@ +{ pkgs, lib, namespace, config, ... }: +let + cfg = config.${namespace}.nix; +in { - pkgs, - lib, - config, - ... -}: let - cfg = config.sneeuwvlok.nix; -in { - options.sneeuwvlok.nix = {}; + options.${namespace}.nix = {}; config = { programs.git.enable = true; @@ -17,9 +14,9 @@ in { extraOptions = "experimental-features = nix-command flakes pipe-operators"; settings = { - experimental-features = ["nix-command" "flakes" "pipe-operators"]; - allowed-users = ["@wheel"]; - trusted-users = ["@wheel"]; + experimental-features = [ "nix-command" "flakes" "pipe-operators" ]; + allowed-users = [ "@wheel" ]; + trusted-users = [ "@wheel" ]; auto-optimise-store = true; connect-timeout = 5; diff --git a/modules/nixos/services/authentication/authelia.nix b/modules/nixos/services/authentication/authelia/default.nix similarity index 97% rename from modules/nixos/services/authentication/authelia.nix rename to modules/nixos/services/authentication/authelia/default.nix index 1a1b8ff..7aea103 100644 --- a/modules/nixos/services/authentication/authelia.nix +++ b/modules/nixos/services/authentication/authelia/default.nix @@ -2,19 +2,20 @@ config, lib, pkgs, + namespace, ... }: let inherit (lib) mkIf mkEnableOption; user = "authelia-testing"; - cfg = config.sneeuwvlok.services.authentication.authelia; + cfg = config.${namespace}.services.authentication.authelia; in { - options.sneeuwvlok.services.authentication.authelia = { + options.${namespace}.services.authentication.authelia = { enable = mkEnableOption "Authelia"; }; config = mkIf cfg.enable { - sneeuwvlok.services.networking.caddy = { + ${namespace}.services.networking.caddy = { hosts = { "auth.kruining.eu".extraConfig = '' reverse_proxy http://127.0.0.1:9091 diff --git a/modules/nixos/services/authentication/himmelblau.nix b/modules/nixos/services/authentication/himmelblau/default.nix similarity index 62% rename from modules/nixos/services/authentication/himmelblau.nix rename to modules/nixos/services/authentication/himmelblau/default.nix index 4a52840..d39d4cf 100644 --- a/modules/nixos/services/authentication/himmelblau.nix +++ b/modules/nixos/services/authentication/himmelblau/default.nix @@ -1,13 +1,17 @@ { + inputs, lib, config, + namespace, ... }: let inherit (lib) mkEnableOption mkIf; - cfg = config.sneeuwvlok.services.authentication.himmelblau; + cfg = config.${namespace}.services.authentication.himmelblau; in { - options.sneeuwvlok.services.authentication.himmelblau = { + imports = [inputs.himmelblau.nixosModules.himmelblau]; + + options.${namespace}.services.authentication.himmelblau = { enable = mkEnableOption "enable azure entra ID authentication"; }; diff --git a/modules/nixos/services/authentication/zitadel.nix b/modules/nixos/services/authentication/zitadel/default.nix similarity index 96% rename from modules/nixos/services/authentication/zitadel.nix rename to modules/nixos/services/authentication/zitadel/default.nix index e120d32..6e42eeb 100644 --- a/modules/nixos/services/authentication/zitadel.nix +++ b/modules/nixos/services/authentication/zitadel/default.nix @@ -1,14 +1,15 @@ -{ config, lib, pkgs, self, ... }: +{ config, lib, pkgs, namespace, system, inputs, ... }: let - inherit (lib) mkIf mkEnableOption mkOption types toUpper toSentenceCase nameValuePair mapAttrs mapAttrs' concatMapAttrs concatMapStringsSep filterAttrsRecursive listToAttrs imap0 head drop length literalExpression attrNames; - inherit ((import ../../../../../lib/strings { inherit lib;}).strings) toSnakeCase; + inherit (lib) mkIf mkEnableOption mkOption toString types toUpper toSentenceCase nameValuePair mapAttrs mapAttrs' concatMapAttrs concatMapStringsSep filterAttrsRecursive listToAttrs imap0 head drop length literalExpression attrNames; + inherit (lib.${namespace}.strings) toSnakeCase; - cfg = config.sneeuwvlok.services.authentication.zitadel; + cfg = config.${namespace}.services.authentication.zitadel; + port = 3010; database = "zitadel"; in { - options.sneeuwvlok.services.authentication.zitadel = { + options.${namespace}.services.authentication.zitadel = { enable = mkEnableOption "Zitadel"; organization = mkOption { @@ -339,8 +340,8 @@ in config' = config; # this is a nix package, the generated json file to be exact - terraformConfiguration = self.inputs.terranix.lib.terranixConfiguration { - system = pkgs.stdenv.hostPlatform.system; + terraformConfiguration = inputs.terranix.lib.terranixConfiguration { + inherit system; modules = [ ({ config, lib, ... }: { @@ -355,7 +356,8 @@ in for item in ${src} : "''${item.org}_''${item.name}" => item }''; - } // set; + } + // set; in { terraform.required_providers.zitadel = { @@ -536,18 +538,18 @@ in }; in mkIf cfg.enable { - sneeuwvlok.services = { - persistence.postgresql.enable = true; + ${namespace}.services = { + persistance.postgresql.enable = true; networking.caddy = { hosts = { "auth.kruining.eu" = '' - reverse_proxy h2c://[::1]:9092 + reverse_proxy h2c://[::1]:${toString port} ''; }; extraConfig = '' (auth) { - forward_auth h2c://[::1]:9092 { + forward_auth h2c://[::1]:${toString port} { uri /api/authz/forward-auth copy_headers Remote-User Remote-Groups Remote-Email Remote-Name } @@ -565,16 +567,17 @@ in "d /var/lib/zitadel/clients 0755 zitadel zitadel -" ]; - systemd.services.zitadelApplyTerraform = - let - tofu = lib.getExe pkgs.opentofu; - in { + systemd.services.zitadelApplyTerraform = { description = "Zitadel terraform apply"; wantedBy = [ "multi-user.target" ]; wants = [ "zitadel.service" ]; - script = '' + script = + let + tofu = lib.getExe pkgs.opentofu; + in + '' if [ "$(systemctl is-active zitadel)" != "active" ]; then echo "Zitadel is not running" exit 1 @@ -610,7 +613,7 @@ in masterKeyFile = config.sops.secrets."zitadel/masterKey".path; tlsMode = "external"; settings = { - Port = 9092; + Port = port; ExternalDomain = "auth.kruining.eu"; ExternalPort = 443; @@ -696,8 +699,6 @@ in }; }; - networking.firewall.allowedTCPPorts = [ 80 443 ]; - # Secrets sops = { secrets = { diff --git a/modules/nixos/services/backup/borg.nix b/modules/nixos/services/backup/borg/default.nix similarity index 83% rename from modules/nixos/services/backup/borg.nix rename to modules/nixos/services/backup/borg/default.nix index 417c911..9cbbea0 100644 --- a/modules/nixos/services/backup/borg.nix +++ b/modules/nixos/services/backup/borg/default.nix @@ -1,14 +1,11 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.services.backup.borg; -in { - options.sneeuwvlok.services.backup.borg = { + cfg = config.${namespace}.services.backup.borg; +in +{ + options.${namespace}.services.backup.borg = { enable = mkEnableOption "Borg Backup"; }; diff --git a/modules/nixos/services/communication/matrix.nix b/modules/nixos/services/communication/matrix.nix deleted file mode 100644 index 1d1df2a..0000000 --- a/modules/nixos/services/communication/matrix.nix +++ /dev/null @@ -1,416 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - inherit (builtins) toString toJSON; - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.services.communication.matrix; - - domain = "kruining.eu"; - fqn = "matrix.${domain}"; - port = 4001; - - database = "synapse"; - keyFile = "/var/lib/element-call/key"; -in { - options.sneeuwvlok.services.communication.matrix = { - enable = mkEnableOption "Matrix server (Synapse)"; - }; - - config = mkIf cfg.enable { - sneeuwvlok.services = { - persistence.postgresql.enable = true; - # virtualisation.podman.enable = true; - - networking.caddy = { - # globalConfig = '' - # layer4 { - # 127.0.0.1:4004 - # route { - # proxy { - # upstream synapse:4004 - # } - # } - # } - # 127.0.0.1:4005 - # route { - # proxy { - # upstream synapse:4005 - # } - # } - # } - # } - # ''; - hosts = let - server = { - "m.server" = "${fqn}:443"; - }; - client = { - "m.homeserver".base_url = "https://${fqn}"; - "m.identity_server".base_url = "https://auth.${domain}"; - "org.matrix.msc3575.proxy".url = "https://${domain}"; - "org.matrix.msc4143.rtc_foci" = [ - { - type = "livekit"; - livekit_service_url = "https://${domain}/livekit/jwt"; - } - ]; - }; - in { - "${domain}, darkch.at" = '' - # Route for lk-jwt-service - handle /livekit/jwt* { - uri strip_prefix /livekit/jwt - reverse_proxy http://[::1]:${toString config.services.lk-jwt-service.port} { - header_up Host {host} - header_up X-Forwarded-Server {host} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - } - } - - handle_path /livekit/sfu* { - reverse_proxy http://[::1]:${toString config.services.livekit.settings.port} { - header_up Host {host} - header_up X-Forwarded-Server {host} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - } - } - - header /.well-known/matrix/* Content-Type application/json - header /.well-known/matrix/* Access-Control-Allow-Origin * - respond /.well-known/matrix/server `${toJSON server}` - respond /.well-known/matrix/client `${toJSON client}` - ''; - - "${fqn}" = '' - reverse_proxy /_matrix/* http://[::1]:${toString port} - reverse_proxy /_synapse/client/* http://[::1]:${toString port} - ''; - }; - }; - }; - - services = { - matrix-synapse = { - enable = true; - - extras = ["oidc"]; - - extraConfigFiles = [ - config.sops.templates."synapse-oidc.yaml".path - ]; - - settings = { - server_name = domain; - public_baseurl = "https://${fqn}"; - - enable_metrics = true; - - registration_shared_secret = "tZtBnlhEmLbMwF0lQ112VH1Rl5MkZzYH9suI4pEoPXzk6nWUB8FJF4eEnwLkbstz"; - - url_preview_enabled = true; - precence.enabled = true; - - # Since we'll be using OIDC for auth disable all local options - enable_registration = false; - enable_registration_without_verification = false; - password_config.enabled = true; - backchannel_logout_enabled = true; - - # Element Call options - max_event_delay_duration = "24h"; - rc_message = { - per_second = 0.5; - burst_count = 30; - }; - rc_delayed_event_mgmt = { - per_second = 1; - burst_count = 20; - }; - turn_uris = ["turn:turn.${domain}:4004?transport=udp" "turn:turn.${domain}:4004?transport=tcp"]; - - experimental_features = { - # MSC2965: OAuth 2.0 Authorization Server Metadata discovery - msc2965_enabled = true; - - # MSC3266: Room summary API. Used for knocking over federation - msc3266_enabled = true; - # MSC4222 needed for syncv2 state_after. This allow clients to - # correctly track the state of the room. - msc4222_enabled = true; - }; - - sso = { - client_whitelist = ["http://[::1]:9092/" "https://auth.kruining.eu/"]; - update_profile_information = true; - }; - - database = { - # this is postgresql (also the default, but I prefer to be explicit) - name = "psycopg2"; - args = { - database = database; - user = database; - }; - }; - - listeners = [ - { - bind_addresses = ["::"]; - port = port; - type = "http"; - tls = false; - x_forwarded = true; - - resources = [ - { - names = ["client" "federation" "openid" "metrics" "media" "health"]; - compress = true; - } - ]; - } - ]; - }; - }; - - mautrix-signal = { - enable = true; - registerToSynapse = true; - - settings = { - appservice = { - provisioning.enabled = false; - }; - - homeserver = { - address = "http://[::1]:${toString port}"; - domain = domain; - }; - - bridge = { - permissions = { - "@chris:${domain}" = "admin"; - }; - }; - }; - }; - - mautrix-telegram = { - enable = true; - registerToSynapse = true; - - settings = { - telegram = { - api_id = 32770816; - api_hash = "7b63778a976619c9d4ab62adc51cde79"; - bot_token = "disabled"; - - catch_up = true; - sequential_updates = true; - }; - - appservice = { - port = 40011; - provisioning.enabled = false; - }; - - homeserver = { - address = "http://[::1]:${toString port}"; - domain = domain; - }; - - bridge = { - permissions = { - "@chris:${domain}" = "admin"; - }; - }; - }; - }; - - mautrix-whatsapp = { - enable = true; - registerToSynapse = true; - - settings = { - appservice = { - provisioning.enabled = false; - }; - - homeserver = { - address = "http://[::1]:${toString port}"; - domain = domain; - }; - - bridge = { - permissions = { - "@chris:${domain}" = "admin"; - }; - }; - }; - }; - - postgresql = { - enable = true; - ensureDatabases = [database]; - ensureUsers = [ - { - name = database; - ensureDBOwnership = true; - } - ]; - }; - - livekit = { - enable = true; - openFirewall = true; - inherit keyFile; - - settings = { - port = 4002; - room.auto_create = false; - }; - }; - - lk-jwt-service = { - enable = true; - port = 4003; - # can be on the same virtualHost as synapse - livekitUrl = "wss://${domain}/livekit/sfu"; - inherit keyFile; - }; - - coturn = rec { - enable = true; - listening-port = 4004; - tls-listening-port = 40004; - no-cli = true; - no-tcp-relay = true; - min-port = 50000; - max-port = 50100; - use-auth-secret = true; - static-auth-secret-file = config.sops.secrets."coturn/secret".path; - realm = "turn.${domain}"; - # cert = "${config.security.acme.certs.${realm}.directory}/full.pem"; - # pkey = "${config.security.acme.certs.${realm}.directory}/key.pem"; - extraConfig = '' - # for debugging - verbose - # ban private IP ranges - no-multicast-peers - denied-peer-ip=0.0.0.0-0.255.255.255 - denied-peer-ip=10.0.0.0-10.255.255.255 - denied-peer-ip=100.64.0.0-100.127.255.255 - denied-peer-ip=127.0.0.0-127.255.255.255 - denied-peer-ip=169.254.0.0-169.254.255.255 - denied-peer-ip=172.16.0.0-172.31.255.255 - denied-peer-ip=192.0.0.0-192.0.0.255 - denied-peer-ip=192.0.2.0-192.0.2.255 - denied-peer-ip=192.88.99.0-192.88.99.255 - denied-peer-ip=192.168.0.0-192.168.255.255 - denied-peer-ip=198.18.0.0-198.19.255.255 - denied-peer-ip=198.51.100.0-198.51.100.255 - denied-peer-ip=203.0.113.0-203.0.113.255 - denied-peer-ip=240.0.0.0-255.255.255.255 - denied-peer-ip=::1 - denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff - denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 - denied-peer-ip=100::-100::ffff:ffff:ffff:ffff - denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff - denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff - denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff - denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff - ''; - }; - }; - - networking.firewall = { - allowedTCPPortRanges = []; - allowedTCPPorts = [ - # Synapse - port - - # coTURN ports - config.services.coturn.listening-port - config.services.coturn.alt-listening-port - config.services.coturn.tls-listening-port - config.services.coturn.alt-tls-listening-port - ]; - allowedUDPPortRanges = with config.services.coturn; - lib.singleton { - from = min-port; - to = max-port; - }; - allowedUDPPorts = [ - # coTURN ports - config.services.coturn.listening-port - config.services.coturn.alt-listening-port - ]; - }; - - systemd = { - services.livekit-key = { - before = ["lk-jwt-service.service" "livekit.service"]; - wantedBy = ["multi-user.target"]; - path = with pkgs; [livekit coreutils gawk]; - script = '' - echo "Key missing, generating key" - echo "lk-jwt-service: $(livekit-server generate-keys | tail -1 | awk '{print $3}')" > "${keyFile}" - ''; - serviceConfig.Type = "oneshot"; - unitConfig.ConditionPathExists = "!${keyFile}"; - }; - services.lk-jwt-service.environment.LIVEKIT_FULL_ACCESS_HOMESERVERS = "${domain}"; - }; - - sops = { - secrets = { - "synapse/oidc_id" = { - restartUnits = ["synapse-matrix.service"]; - }; - "synapse/oidc_secret" = { - restartUnits = ["synapse-matrix.service"]; - }; - "coturn/secret" = { - owner = config.systemd.services.coturn.serviceConfig.User; - group = config.systemd.services.coturn.serviceConfig.Group; - restartUnits = ["coturn.service"]; - }; - }; - - templates = { - "synapse-oidc.yaml" = { - owner = "matrix-synapse"; - content = '' - oidc_providers: - - discover: true - idp_id: zitadel - idp_name: Zitadel - issuer: "https://auth.kruining.eu" - scopes: - - openid - - profile - - email - - offline_access - client_id: '${config.sops.placeholder."synapse/oidc_id"}' - client_secret: '${config.sops.placeholder."synapse/oidc_secret"}' - backchannel_logout_enabled: true - user_profile_method: userinfo_endpoint - allow_existing_users: true - enable_registration: true - user_mapping_provider: - config: - localpart_template: "{{ user.preferred_username }}" - display_name_template: "{{ user.name }}" - email_template: "{{ user.email }}" - ''; - restartUnits = ["matrix-synapse.service"]; - }; - }; - }; - }; -} diff --git a/modules/nixos/services/communication/matrix/default.nix b/modules/nixos/services/communication/matrix/default.nix new file mode 100644 index 0000000..9a7d53c --- /dev/null +++ b/modules/nixos/services/communication/matrix/default.nix @@ -0,0 +1,399 @@ +{ + config, + lib, + pkgs, + namespace, + ... +}: let + inherit (builtins) toString toJSON; + inherit (lib) mkIf mkEnableOption mkMerge; + + cfg = config.${namespace}.services.communication.matrix; + + domain = "kruining.eu"; + fqn = "matrix.${domain}"; + port = 4001; + + database = "synapse"; + keyFile = "/var/lib/element-call/key"; + + mkMautrix = bridge: i: conf: { + ${bridge} = mkMerge [ + { + enable = true; + registerToSynapse = true; + + settings = { + appservice = { + # hostname = "[::]"; + # port = 40010 + i; + # address = "http://${config.services.${bridge}.settings.appservice.hostname}:${toString config.services.${bridge}.settings.appservice.port}"; + provisioning.enabled = false; + }; + + homeserver = { + inherit domain; + address = "http://[::1]:${toString port}"; + }; + + bridge = { + permissions = { + "@chris:${domain}" = "admin"; + }; + }; + }; + } + conf + ]; + }; +in { + options.${namespace}.services.communication.matrix = { + enable = mkEnableOption "Matrix server (Synapse)"; + }; + + config = mkIf cfg.enable { + ${namespace}.services = { + persistance.postgresql.enable = true; + + networking.caddy = { + hosts = let + server = { + "m.server" = "${fqn}:443"; + }; + client = { + "m.homeserver".base_url = "https://${fqn}"; + "m.identity_server".base_url = "https://auth.${domain}"; + "org.matrix.msc3575.proxy".url = "https://${domain}"; + "org.matrix.msc4143.rtc_foci" = [ + { + type = "livekit"; + livekit_service_url = "https://${domain}/livekit/jwt"; + } + ]; + }; + in { + "${domain}, darkch.at" = '' + # Route for lk-jwt-service + handle /livekit/jwt* { + uri strip_prefix /livekit/jwt + reverse_proxy http://[::1]:${toString config.services.lk-jwt-service.port} { + header_up Host {host} + header_up X-Forwarded-Server {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + } + } + + handle_path /livekit/sfu* { + reverse_proxy http://[::1]:${toString config.services.livekit.settings.port} { + header_up Host {host} + header_up X-Forwarded-Server {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + } + } + + header /.well-known/matrix/* Content-Type application/json + header /.well-known/matrix/* Access-Control-Allow-Origin * + respond /.well-known/matrix/server `${toJSON server}` + respond /.well-known/matrix/client `${toJSON client}` + ''; + + "${fqn}" = '' + reverse_proxy /_matrix/* http://[::1]:${toString port} + reverse_proxy /_synapse/client/* http://[::1]:${toString port} + ''; + }; + }; + }; + + services = mkMerge [ + (mkMautrix "mautrix-signal" 1 {}) + (mkMautrix "mautrix-telegram" 2 {}) + (mkMautrix "mautrix-whatsapp" 3 {}) + (mkMautrix "arrtrix" 4 { + environmentFile = config.sops.templates."arrtrix/secrets".path; + + settings = { + observability = { + otlp_grpc_endpoint = "http://[::1]:9071"; + service_name = "arrtrix"; + }; + + network.content = { + movies = { + url = "http://[::1]:${toString config.services.radarr.settings.server.port}"; + api_key = "$RADARR_APIKEY"; + root_folder_path = "/var/media/movies"; + quality_profile_id = 5; + }; + series = { + url = "http://[::1]:${toString config.services.sonarr.settings.server.port}"; + api_key = "$SONARR_APIKEY"; + root_folder_path = "/var/media/series"; + quality_profile_id = 5; + language_profile_id = 1; + }; + }; + }; + }) + { + matrix-synapse = { + enable = true; + + extras = ["oidc"]; + + extraConfigFiles = [ + config.sops.templates."synapse.yaml".path + config.sops.templates."synapse-oidc.yaml".path + ]; + + settings = { + server_name = domain; + public_baseurl = "https://${fqn}"; + + enable_metrics = true; + + url_preview_enabled = true; + precence.enabled = true; + + # Since we'll be using OIDC for auth disable all local options + enable_registration = false; + enable_registration_without_verification = false; + password_config.enabled = true; + backchannel_logout_enabled = true; + + # Element Call options + max_event_delay_duration = "24h"; + rc_message = { + per_second = 0.5; + burst_count = 30; + }; + rc_delayed_event_mgmt = { + per_second = 1; + burst_count = 20; + }; + turn_uris = ["turn:turn.${domain}:4004?transport=udp" "turn:turn.${domain}:4004?transport=tcp"]; + + experimental_features = { + # MSC2965: OAuth 2.0 Authorization Server Metadata discovery + msc2965_enabled = true; + + # MSC3266: Room summary API. Used for knocking over federation + msc3266_enabled = true; + # MSC4222 needed for syncv2 state_after. This allow clients to + # correctly track the state of the room. + msc4222_enabled = true; + }; + + sso = { + client_whitelist = ["http://[::1]:${toString config.services.zitadel.settings.Port}/" "https://auth.kruining.eu/"]; + update_profile_information = true; + }; + + database = { + # this is postgresql (also the default, but I prefer to be explicit) + name = "psycopg2"; + args = { + database = database; + user = database; + }; + }; + + listeners = [ + { + bind_addresses = ["::"]; + port = port; + type = "http"; + tls = false; + x_forwarded = true; + + resources = [ + { + names = ["client" "federation" "openid" "metrics" "media" "health"]; + compress = true; + } + ]; + } + ]; + }; + }; + + postgresql = { + ensureDatabases = [database]; + ensureUsers = [ + { + name = database; + ensureDBOwnership = true; + } + ]; + }; + + livekit = { + enable = true; + openFirewall = true; + inherit keyFile; + + settings = { + port = 4002; + room.auto_create = false; + }; + }; + + lk-jwt-service = { + enable = true; + port = 4003; + # can be on the same virtualHost as synapse + livekitUrl = "wss://${domain}/livekit/sfu"; + inherit keyFile; + }; + + coturn = rec { + enable = true; + listening-port = 4004; + tls-listening-port = 40004; + no-cli = true; + no-tcp-relay = true; + min-port = 50000; + max-port = 50100; + use-auth-secret = true; + static-auth-secret-file = config.sops.secrets."coturn/secret".path; + realm = "turn.${domain}"; + # cert = "${config.security.acme.certs.${realm}.directory}/full.pem"; + # pkey = "${config.security.acme.certs.${realm}.directory}/key.pem"; + extraConfig = '' + # for debugging + verbose + # ban private IP ranges + no-multicast-peers + denied-peer-ip=0.0.0.0-0.255.255.255 + denied-peer-ip=10.0.0.0-10.255.255.255 + denied-peer-ip=100.64.0.0-100.127.255.255 + denied-peer-ip=127.0.0.0-127.255.255.255 + denied-peer-ip=169.254.0.0-169.254.255.255 + denied-peer-ip=172.16.0.0-172.31.255.255 + denied-peer-ip=192.0.0.0-192.0.0.255 + denied-peer-ip=192.0.2.0-192.0.2.255 + denied-peer-ip=192.88.99.0-192.88.99.255 + denied-peer-ip=192.168.0.0-192.168.255.255 + denied-peer-ip=198.18.0.0-198.19.255.255 + denied-peer-ip=198.51.100.0-198.51.100.255 + denied-peer-ip=203.0.113.0-203.0.113.255 + denied-peer-ip=240.0.0.0-255.255.255.255 + denied-peer-ip=::1 + denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff + denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 + denied-peer-ip=100::-100::ffff:ffff:ffff:ffff + denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff + denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff + denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff + ''; + }; + } + ]; + + networking.firewall = { + allowedTCPPortRanges = []; + allowedTCPPorts = [ + # Synapse + port + + # coTURN ports + config.services.coturn.listening-port + config.services.coturn.alt-listening-port + config.services.coturn.tls-listening-port + config.services.coturn.alt-tls-listening-port + ]; + allowedUDPPortRanges = with config.services.coturn; + lib.singleton { + from = min-port; + to = max-port; + }; + allowedUDPPorts = [ + # coTURN ports + config.services.coturn.listening-port + config.services.coturn.alt-listening-port + ]; + }; + + systemd = { + services.livekit-key = { + before = ["lk-jwt-service.service" "livekit.service"]; + wantedBy = ["multi-user.target"]; + path = with pkgs; [livekit coreutils gawk]; + script = '' + echo "Key missing, generating key" + echo "lk-jwt-service: $(livekit-server generate-keys | tail -1 | awk '{print $3}')" > "${keyFile}" + ''; + serviceConfig.Type = "oneshot"; + unitConfig.ConditionPathExists = "!${keyFile}"; + }; + services.lk-jwt-service.environment.LIVEKIT_FULL_ACCESS_HOMESERVERS = "${domain}"; + }; + + sops = { + secrets = { + "synapse/oidc_id" = { + restartUnits = ["synapse-matrix.service"]; + }; + "synapse/oidc_secret" = { + restartUnits = ["synapse-matrix.service"]; + }; + "synapse/shared_secret" = { + restartUnits = ["synapse-matrix.service"]; + }; + "coturn/secret" = { + owner = config.systemd.services.coturn.serviceConfig.User; + group = config.systemd.services.coturn.serviceConfig.Group; + restartUnits = ["coturn.service"]; + }; + }; + + templates = { + "synapse.yaml" = { + owner = "matrix-synapse"; + content = '' + registration_shared_secret: ${config.sops.placeholder."synapse/shared_secret"} + ''; + restartUnits = ["matrix-synapse.service"]; + }; + "synapse-oidc.yaml" = { + owner = "matrix-synapse"; + content = '' + oidc_providers: + - discover: true + idp_id: zitadel + idp_name: Zitadel + issuer: "https://auth.kruining.eu" + scopes: + - openid + - profile + - email + - offline_access + client_id: '${config.sops.placeholder."synapse/oidc_id"}' + client_secret: '${config.sops.placeholder."synapse/oidc_secret"}' + backchannel_logout_enabled: true + user_profile_method: userinfo_endpoint + allow_existing_users: true + enable_registration: true + user_mapping_provider: + config: + localpart_template: "{{ user.preferred_username }}" + display_name_template: "{{ user.name }}" + email_template: "{{ user.email }}" + ''; + restartUnits = ["matrix-synapse.service"]; + }; + "arrtrix/secrets" = { + owner = "arrtrix"; + content = '' + RADARR_APIKEY=${config.sops.placeholder."radarr/apikey"} + SONARR_APIKEY=${config.sops.placeholder."sonarr/apikey"} + ''; + restartUnits = ["arrtrix.service"]; + }; + }; + }; + }; +} diff --git a/modules/nixos/services/development/forgejo.nix b/modules/nixos/services/development/forgejo/default.nix similarity index 96% rename from modules/nixos/services/development/forgejo.nix rename to modules/nixos/services/development/forgejo/default.nix index c468137..f190b0c 100644 --- a/modules/nixos/services/development/forgejo.nix +++ b/modules/nixos/services/development/forgejo/default.nix @@ -2,15 +2,16 @@ config, lib, pkgs, + namespace, ... }: let inherit (builtins) toString; inherit (lib) mkIf mkEnableOption mkOption; - cfg = config.sneeuwvlok.services.development.forgejo; + cfg = config.${namespace}.services.development.forgejo; domain = "git.amarth.cloud"; in { - options.sneeuwvlok.services.development.forgejo = { + options.${namespace}.services.development.forgejo = { enable = mkEnableOption "Forgejo"; port = mkOption { @@ -24,8 +25,8 @@ in { }; config = mkIf cfg.enable { - sneeuwvlok.services = { - persistence.postgresql.enable = true; + ${namespace}.services = { + persistance.postgresql.enable = true; virtualisation.podman.enable = true; networking.caddy = { diff --git a/modules/nixos/services/games/minecraft.nix b/modules/nixos/services/games/minecraft/default.nix similarity index 96% rename from modules/nixos/services/games/minecraft.nix rename to modules/nixos/services/games/minecraft/default.nix index 4d9b8b9..84567b3 100644 --- a/modules/nixos/services/games/minecraft.nix +++ b/modules/nixos/services/games/minecraft/default.nix @@ -1,15 +1,21 @@ { + inputs, config, lib, pkgs, + namespace, ... }: let inherit (lib) mkIf mkEnableOption mkOption; inherit (lib.types) str; - cfg = config.sneeuwvlok.services.games.minecraft; + cfg = config.${namespace}.services.games.minecraft; in { - options.sneeuwvlok.services.games.minecraft = { + imports = [ + inputs.nix-minecraft.nixosModules.minecraft-servers + ]; + + options.${namespace}.services.games.minecraft = { enable = mkEnableOption "Minecraft"; user = mkOption { diff --git a/modules/nixos/services/games/openrct.nix b/modules/nixos/services/games/openrct.nix index 196ae12..a36f0fb 100644 --- a/modules/nixos/services/games/openrct.nix +++ b/modules/nixos/services/games/openrct.nix @@ -1,14 +1,11 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.services.games.openrct; -in { - options.sneeuwvlok.services.games.openrct = { + cfg = config.${namespace}.services.games.openrct; +in +{ + options.${namespace}.services.games.openrct = { enable = mkEnableOption "OpenRCT2"; }; @@ -19,7 +16,7 @@ in { systemd.services.openrct = { enable = true; - after = ["network.target"]; + after = [ "network.target"]; description = "OpenRCT2 Server"; serviceConfig = { Type = ""; diff --git a/modules/nixos/services/games/palworld.nix b/modules/nixos/services/games/palworld/default.nix similarity index 83% rename from modules/nixos/services/games/palworld.nix rename to modules/nixos/services/games/palworld/default.nix index d6de43b..152891d 100644 --- a/modules/nixos/services/games/palworld.nix +++ b/modules/nixos/services/games/palworld/default.nix @@ -1,13 +1,14 @@ { config, lib, + namespace, ... }: let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.services.games.palworld; + cfg = config.${namespace}.services.games.palworld; in { - options.sneeuwvlok.services.games.palworld = { + options.${namespace}.services.games.palworld = { enable = mkEnableOption "Palworld"; }; diff --git a/modules/nixos/services/media/default.nix b/modules/nixos/services/media/default.nix index 0db854f..900eee4 100644 --- a/modules/nixos/services/media/default.nix +++ b/modules/nixos/services/media/default.nix @@ -1,15 +1,16 @@ { pkgs, lib, + namespace, config, ... }: let inherit (lib) mkIf mkEnableOption mkOption; inherit (lib.types) str; - cfg = config.sneeuwvlok.services.media; + cfg = config.${namespace}.services.media; in { - options.sneeuwvlok.services.media = { + options.${namespace}.services.media = { enable = mkEnableOption "Enable media services"; user = mkOption { @@ -63,7 +64,7 @@ in { openFirewall = true; user = cfg.user; group = cfg.group; - listenPort = 2005; + listenPort = 2050; }; postgresql = { diff --git a/modules/nixos/services/media/glance.nix b/modules/nixos/services/media/glance/default.nix similarity index 95% rename from modules/nixos/services/media/glance.nix rename to modules/nixos/services/media/glance/default.nix index 29e4cc6..bdd4c87 100644 --- a/modules/nixos/services/media/glance.nix +++ b/modules/nixos/services/media/glance/default.nix @@ -1,22 +1,23 @@ { config, lib, + namespace, ... }: let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.services.media.glance; + cfg = config.${namespace}.services.media.glance; in { - options.sneeuwvlok.services.media.glance = { + options.${namespace}.services.media.glance = { enable = mkEnableOption "Enable Glance"; }; config = mkIf cfg.enable { - sneeuwvlok.services.networking.caddy.hosts = { - "https://${config.networking.hostName}:443" = '' - reverse_proxy http://[::1]:2000 - ''; - }; + # ${namespace}.services.networking.caddy.hosts = { + # "https://${config.networking.hostName}.arda:443" = '' + # reverse_proxy http://[::1]:2000 + # ''; + # }; services.glance = { enable = true; diff --git a/modules/nixos/services/media/jellyfin.nix b/modules/nixos/services/media/jellyfin/default.nix similarity index 81% rename from modules/nixos/services/media/jellyfin.nix rename to modules/nixos/services/media/jellyfin/default.nix index 315838c..de19896 100644 --- a/modules/nixos/services/media/jellyfin.nix +++ b/modules/nixos/services/media/jellyfin/default.nix @@ -2,20 +2,22 @@ pkgs, config, lib, + namespace, inputs, + system, ... }: let inherit (builtins) toString; inherit (lib) mkIf mkEnableOption mkOption types; - cfg = config.sneeuwvlok.services.media.jellyfin; + cfg = config.${namespace}.services.media.jellyfin; in { - options.sneeuwvlok.services.media.jellyfin = { + options.${namespace}.services.media.jellyfin = { enable = mkEnableOption "Enable jellyfin server"; }; config = mkIf cfg.enable { - sneeuwvlok.services.networking.caddy = { + ${namespace}.services.networking.caddy = { hosts = { "jellyfin.kruining.eu" = '' reverse_proxy http://[::1]:8096 diff --git a/modules/nixos/services/media/mydia.nix b/modules/nixos/services/media/mydia/default.nix similarity index 90% rename from modules/nixos/services/media/mydia.nix rename to modules/nixos/services/media/mydia/default.nix index 9c305c9..9044c2e 100644 --- a/modules/nixos/services/media/mydia.nix +++ b/modules/nixos/services/media/mydia/default.nix @@ -1,13 +1,20 @@ { config, lib, + namespace, + inputs, + system, ... }: let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.services.media.mydia; + cfg = config.${namespace}.services.media.mydia; in { - options.sneeuwvlok.services.media.mydia = { + imports = [ + inputs.mydia.nixosModules.default + ]; + + options.${namespace}.services.media.mydia = { enable = mkEnableOption "Enable Mydia"; }; @@ -15,7 +22,7 @@ in { services.mydia = { enable = true; - port = 2010; + port = 2100; listenAddress = "0.0.0.0"; openFirewall = true; @@ -47,7 +54,7 @@ in { qbittorrent = { type = "qbittorrent"; host = "localhost"; - port = 2008; + port = 2080; username = "admin"; passwordFile = config.sops.secrets."mydia/qbittorrent_password".path; useSsl = false; diff --git a/modules/nixos/services/media/nextcloud.nix b/modules/nixos/services/media/nextcloud/default.nix similarity index 83% rename from modules/nixos/services/media/nextcloud.nix rename to modules/nixos/services/media/nextcloud/default.nix index eb8c9da..06904c6 100644 --- a/modules/nixos/services/media/nextcloud.nix +++ b/modules/nixos/services/media/nextcloud/default.nix @@ -2,14 +2,15 @@ config, lib, pkgs, + namespace, ... }: let inherit (lib) mkIf mkEnableOption mkOption; inherit (lib.types) str; - cfg = config.sneeuwvlok.services.media.nextcloud; + cfg = config.${namespace}.services.media.nextcloud; in { - options.sneeuwvlok.services.media.nextcloud = { + options.${namespace}.services.media.nextcloud = { enable = mkEnableOption "Nextcloud"; user = mkOption { @@ -24,7 +25,7 @@ in { }; config = mkIf cfg.enable { - sneeuwvlok.services.networking.caddy = { + ${namespace}.services.networking.caddy = { hosts."cloud.kruining.eu" = '' php_fastcgi unix//run/phpfpm/nextcloud.sock { env front_controller_active true @@ -40,14 +41,14 @@ in { groups.${cfg.group} = {}; }; - # home-manager.users.${cfg.user}.home = { - # stateVersion = config.system.stateVersion; + home-manager.users.${cfg.user}.home = { + stateVersion = config.system.stateVersion; - # file.".netrc".text = '' - # login root - # password KaasIsAwesome! - # ''; - # }; + file.".netrc".text = '' + login root + password KaasIsAwesome! + ''; + }; services.nextcloud = { enable = true; diff --git a/modules/nixos/services/media/nfs.nix b/modules/nixos/services/media/nfs/default.nix similarity index 59% rename from modules/nixos/services/media/nfs.nix rename to modules/nixos/services/media/nfs/default.nix index 1028c73..54b58e7 100644 --- a/modules/nixos/services/media/nfs.nix +++ b/modules/nixos/services/media/nfs/default.nix @@ -1,18 +1,16 @@ -{ - config, - lib, - ... -}: let +{ config, lib, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.services.media.nfs; -in { - options.sneeuwvlok.services.media.nfs = { + cfg = config.${namespace}.services.media.nfs; +in +{ + options.${namespace}.services.media.nfs = { enable = mkEnableOption "Enable NFS"; }; config = mkIf cfg.enable { - networking.firewall.allowedTCPPorts = [2049]; + networking.firewall.allowedTCPPorts = [ 2049 ]; services.nfs.server = { enable = true; diff --git a/modules/nixos/services/media/servarr.nix b/modules/nixos/services/media/servarr/default.nix similarity index 85% rename from modules/nixos/services/media/servarr.nix rename to modules/nixos/services/media/servarr/default.nix index 2c6125d..ed9b94a 100644 --- a/modules/nixos/services/media/servarr.nix +++ b/modules/nixos/services/media/servarr/default.nix @@ -2,17 +2,19 @@ pkgs, config, lib, - self, + namespace, + inputs, + system, ... }: let inherit (builtins) toString; inherit (lib) mkIf mkEnableOption mkOption types; - cfg = config.sneeuwvlok.services.media.servarr; + cfg = config.${namespace}.services.media.servarr; servarr = import ./lib.nix {inherit lib;}; anyEnabled = cfg |> lib.attrNames |> lib.length |> (l: l > 0); in { - options.sneeuwvlok.services.media = { + options.${namespace}.services.media = { servarr = mkOption { type = types.attrsOf (types.submodule ({name, ...}: { options = { @@ -54,7 +56,8 @@ in { auth.authenticationMethod = "External"; server = { - bindaddress = "0.0.0.0"; + # bindaddress = "0.0.0.0"; + bindaddress = "[::]"; port = port; }; @@ -77,7 +80,7 @@ in { qbittorrent = { enable = true; openFirewall = true; - webuiPort = 2008; + webuiPort = 2080; serverConfig = lib.mkForce {}; user = "qbittorrent"; @@ -98,9 +101,10 @@ in { settings = { misc = { host = "0.0.0.0"; - port = 2009; + port = 2090; host_whitelist = "${config.networking.hostName}"; + permissions = "770"; download_dir = "/var/media/downloads/incomplete"; complete_dir = "/var/media/downloads/done"; }; @@ -123,14 +127,15 @@ in { flaresolverr = { enable = true; openFirewall = true; - port = 2007; + port = 2070; }; - postgresql = { - ensureDatabases = cfg |> lib.attrNames; + postgresql = let + databases = [] ++ (cfg |> lib.attrNames); + in { + ensureDatabases = databases; ensureUsers = - cfg - |> lib.attrNames + databases |> lib.map (service: { name = service; ensureDBOwnership = true; @@ -153,8 +158,8 @@ in { config' = config; lib' = lib; - terraformConfiguration = self.inputs.terranix.lib.terranixConfiguration { - system = pkgs.stdenv.hostPlatform.system; + terraformConfiguration = inputs.terranix.lib.terranixConfiguration { + inherit system; modules = [ ({ @@ -190,7 +195,7 @@ in { source = "devopsarr/${service}"; version = { - radarr = "2.3.3"; + radarr = "2.3.5"; sonarr = "3.4.0"; prowlarr = "3.1.0"; lidarr = "1.13.0"; @@ -208,6 +213,23 @@ in { resource = { + "${service}_notification_webhook" = mkIf (lib.elem service ["radarr" "sonarr" "whisparr" "lidarr" "readarr"]) { + "arrtrix" = + { + method = 1; # HTTP METHOD 1=POST, 2=PUT + name = "Arrtrix"; + url = "http://localhost:${toString config'.services.arrtrix.settings.appservice.port}/_arrtrix/webhook"; + + on_grab = true; + on_download = true; + on_rename = true; + on_upgrade = true; + } + // (lib.optionalAttrs (lib.elem service ["radarr" "whisparr"]) { + on_movie_delete = true; + }); + }; + "${service}_root_folder" = mkIf (lib.elem service ["radarr" "sonarr" "whisparr"]) ( rootFolders |> lib.imap (i: f: lib.nameValuePair "local${toString i}" {path = f;}) @@ -223,20 +245,30 @@ in { username = "admin"; password = lib.tfRef "var.qbittorrent_api_key"; url_base = "/"; - port = 2008; + port = 2080; }; }; "${service}_download_client_sabnzbd" = mkIf (lib.elem service ["radarr" "sonarr" "lidarr" "whisparr"]) { - "main" = { - name = "SABnzbd"; - enable = true; - priority = 1; - host = "localhost"; - api_key = lib.tfRef "var.sabnzbd_api_key"; - url_base = "/"; - port = 2009; - }; + "main" = + { + name = "SABnzbd"; + enable = true; + priority = 1; + host = "localhost"; + api_key = lib.tfRef "var.sabnzbd_api_key"; + url_base = "/"; + port = 2090; + } + // ({ + radarr = {movie_category = "movies";}; + sonarr = {tv_category = "tv";}; + lidarr = {music_category = "audio";}; + whisparr = {movie_category = "movies";}; + readarr = {book_category = "Default";}; + }.${ + service + }); }; } // (lib.optionalAttrs (service == "prowlarr") ( @@ -339,11 +371,11 @@ in { } ''; - script = lib.replaceStrings ["\r"] [""] '' + script = '' # Sleep for a bit to give the service a chance to start up sleep 5s - if [ "$(systemctl is-active ${lib.escapeShellArg service})" != "active" ]; then + if [ "$(systemctl is-active "${service}")" != "active" ]; then echo "${service} is not running" exit 1 fi diff --git a/modules/nixos/services/media/servarr/lib.nix b/modules/nixos/services/media/servarr/lib.nix new file mode 100644 index 0000000..8ee412b --- /dev/null +++ b/modules/nixos/services/media/servarr/lib.nix @@ -0,0 +1,2 @@ +{lib, ...}: { +} diff --git a/modules/nixos/services/networking/caddy.nix b/modules/nixos/services/networking/caddy/default.nix similarity index 62% rename from modules/nixos/services/networking/caddy.nix rename to modules/nixos/services/networking/caddy/default.nix index 6194808..21ab908 100644 --- a/modules/nixos/services/networking/caddy.nix +++ b/modules/nixos/services/networking/caddy/default.nix @@ -2,37 +2,37 @@ config, pkgs, lib, + namespace, ... }: let inherit (builtins) length; inherit (lib) mkIf mkEnableOption mkOption types attrNames mapAttrs; - cfg = config.sneeuwvlok.services.networking.caddy; + cfg = config.${namespace}.services.networking.caddy; hasHosts = (cfg.hosts |> attrNames |> length) > 0; - caddyPackage = pkgs.caddy.withPlugins { - plugins = ["github.com/corazawaf/coraza-caddy/v2@v2.1.0"]; - hash = "sha256-pSXjLaZoRtKV3eFl2ySRSjl3yxi514G1Cb7pfrpxxtE="; - }; in { - options.sneeuwvlok.services.networking.caddy = { + options.${namespace}.services.networking.caddy = { enable = mkEnableOption "enable caddy" // {default = true;}; hosts = mkOption { type = types.attrsOf types.str; - default = {}; }; extraConfig = mkOption { type = types.str; - default = ""; }; }; config = mkIf hasHosts { + networking.firewall.allowedTCPPorts = [80 443]; + services.caddy = { enable = cfg.enable; - package = caddyPackage; + package = pkgs.caddy.withPlugins { + plugins = ["github.com/corazawaf/coraza-caddy/v2@v2.1.0"]; + hash = "sha256-pSXjLaZoRtKV3eFl2ySRSjl3yxi514G1Cb7pfrpxxtE="; + }; virtualHosts = cfg.hosts diff --git a/modules/nixos/services/networking/ssh.nix b/modules/nixos/services/networking/ssh/default.nix similarity index 67% rename from modules/nixos/services/networking/ssh.nix rename to modules/nixos/services/networking/ssh/default.nix index 60ca00a..5ebdfd2 100644 --- a/modules/nixos/services/networking/ssh.nix +++ b/modules/nixos/services/networking/ssh/default.nix @@ -1,14 +1,12 @@ -{ - config, - lib, - ... -}: let +{ config, lib, namespace, ... }: +let inherit (lib.modules) mkIf; inherit (lib.options) mkEnableOption; - cfg = config.sneeuwvlok.services.networking.ssh; -in { - options.sneeuwvlok.services.networking.ssh = { + cfg = config.${namespace}.services.networking.ssh; +in +{ + options.${namespace}.services.networking.ssh = { enable = mkEnableOption "enable ssh"; }; @@ -16,10 +14,10 @@ in { services.openssh = { enable = true; openFirewall = true; - ports = [22]; + ports = [ 22 ]; settings = { PasswordAuthentication = true; - AllowUsers = ["chris" "root"]; + AllowUsers = [ "chris" "root" ]; UseDns = true; UsePAM = true; PermitRootLogin = "prohibit-password"; diff --git a/modules/nixos/services/networking/wireguard.nix b/modules/nixos/services/networking/wireguard/default.nix similarity index 79% rename from modules/nixos/services/networking/wireguard.nix rename to modules/nixos/services/networking/wireguard/default.nix index 364395b..0cf5320 100644 --- a/modules/nixos/services/networking/wireguard.nix +++ b/modules/nixos/services/networking/wireguard/default.nix @@ -2,16 +2,17 @@ config, pkgs, lib, + namespace, ... }: let inherit (builtins) length; inherit (lib) mkIf mkEnableOption mkOption types attrNames attrsToList listToAttrs; - cfg = config.sneeuwvlok.services.networking.wireguard; + cfg = config.${namespace}.services.networking.wireguard; hasPeers = (cfg.peer |> attrNames |> length) > 0; in { - options.sneeuwvlok.services.networking.wireguard = { - enable = mkEnableOption "enable wireguard" // {default = true;}; + options.${namespace}.services.networking.wireguard = { + # enable = mkEnableOption "enable wireguard" // {default = true;}; peer = mkOption { type = types.attrsOf (types.submodule { @@ -28,11 +29,10 @@ in { }; }; }); - default = {}; }; }; - config = mkIf (cfg.enable && hasPeers) { + config = mkIf hasPeers { # networking.firewall.allowedUDPPorts = cfg.peer |> lib.attrValues |> lib.map (p: p.port); # networking.wq-quick = { # # enable = cfg.enable; diff --git a/modules/nixos/services/observability/alloy/default.nix b/modules/nixos/services/observability/alloy/default.nix new file mode 100644 index 0000000..3b64f2e --- /dev/null +++ b/modules/nixos/services/observability/alloy/default.nix @@ -0,0 +1,83 @@ +{ + config, + lib, + namespace, + ... +}: let + inherit (builtins) toString; + inherit (lib) mkEnableOption mkIf; + + cfg = config.${namespace}.services.observability.alloy; + + httpPort = 9070; + otlpGrpcPort = 9071; + otlpHttpPort = 9072; + tempoOtlpGrpcPort = 9062; +in { + options.${namespace}.services.observability.alloy = { + enable = mkEnableOption "enable Grafana Alloy"; + }; + + config = mkIf cfg.enable { + services.alloy = { + enable = true; + configPath = "/etc/alloy"; + extraFlags = [ + "--disable-reporting" + "--server.http.listen-addr=[::]:${toString httpPort}" + "--storage.path=/var/lib/alloy" + ]; + }; + + environment.etc."alloy/config.alloy".text = '' + otelcol.receiver.otlp "default" { + grpc { + endpoint = "[::1]:${toString otlpGrpcPort}" + } + + http { + endpoint = "[::1]:${toString otlpHttpPort}" + } + + output { + metrics = [otelcol.processor.batch.metrics.input] + traces = [otelcol.processor.batch.traces.input] + } + } + + otelcol.processor.batch "metrics" { + output { + metrics = [otelcol.exporter.prometheus.default.input] + } + } + + otelcol.processor.batch "traces" { + output { + traces = [otelcol.exporter.otlp.tempo.input] + } + } + + otelcol.exporter.prometheus "default" { + forward_to = [prometheus.remote_write.local.receiver] + } + + prometheus.remote_write "local" { + endpoint { + url = "http://[::1]:${toString config.services.prometheus.port}/api/v1/write" + } + } + + otelcol.exporter.otlp "tempo" { + client { + endpoint = "[::1]:${toString tempoOtlpGrpcPort}" + + tls { + insecure = true + } + } + } + ''; + + networking.firewall.allowedTCPPorts = [httpPort]; + }; +} diff --git a/modules/nixos/services/observability/grafana/default.nix b/modules/nixos/services/observability/grafana/default.nix index c3a5f9a..879ecdc 100644 --- a/modules/nixos/services/observability/grafana/default.nix +++ b/modules/nixos/services/observability/grafana/default.nix @@ -2,17 +2,18 @@ pkgs, config, lib, + namespace, ... }: let inherit (lib.modules) mkIf; inherit (lib.options) mkEnableOption; - cfg = config.sneeuwvlok.services.observability.grafana; + cfg = config.${namespace}.services.observability.grafana; db_user = "grafana"; db_name = "grafana"; in { - options.sneeuwvlok.services.observability.grafana = { + options.${namespace}.services.observability.grafana = { enable = mkEnableOption "enable Grafana"; }; @@ -24,8 +25,8 @@ in { settings = { server = { - http_port = 9001; - http_addr = "0.0.0.0"; + http_port = 9010; + http_addr = "::"; domain = "ulmo"; }; @@ -103,18 +104,38 @@ in { datasources.settings.datasources = [ { name = "Prometheus"; + uid = "prometheus"; type = "prometheus"; - url = "http://localhost:9005"; + url = "http://[::1]:9020"; isDefault = true; editable = false; } { name = "Loki"; + uid = "loki"; type = "loki"; - url = "http://localhost:9003"; + url = "http://[::1]:9030"; editable = false; } + + { + name = "Tempo"; + uid = "tempo"; + type = "tempo"; + url = "http://localhost:9060"; + editable = false; + jsonData = { + nodeGraph.enabled = true; + serviceMap.datasourceUid = "prometheus"; + tracesToLogsV2 = { + datasourceUid = "loki"; + filterByTraceID = true; + spanStartTimeShift = "-1h"; + spanEndTimeShift = "1h"; + }; + }; + } ]; }; }; diff --git a/modules/nixos/services/observability/loki.nix b/modules/nixos/services/observability/loki/default.nix similarity index 77% rename from modules/nixos/services/observability/loki.nix rename to modules/nixos/services/observability/loki/default.nix index e45d680..bab5b3f 100644 --- a/modules/nixos/services/observability/loki.nix +++ b/modules/nixos/services/observability/loki/default.nix @@ -1,15 +1,12 @@ -{ - pkgs, - config, - lib, - ... -}: let +{ pkgs, config, lib, namespace, ... }: +let inherit (lib.modules) mkIf; inherit (lib.options) mkEnableOption; - cfg = config.sneeuwvlok.services.observability.loki; -in { - options.sneeuwvlok.services.observability.loki = { + cfg = config.${namespace}.services.observability.loki; +in +{ + options.${namespace}.services.observability.loki = { enable = mkEnableOption "enable Grafana Loki"; }; @@ -20,7 +17,7 @@ in { auth_enabled = false; server = { - http_listen_port = 9003; + http_listen_port = 9030; }; common = { @@ -47,6 +44,6 @@ in { }; }; - networking.firewall.allowedTCPPorts = [9003]; + networking.firewall.allowedTCPPorts = [ 9030 ]; }; } diff --git a/modules/nixos/services/observability/prometheus.nix b/modules/nixos/services/observability/prometheus.nix deleted file mode 100644 index 06c496c..0000000 --- a/modules/nixos/services/observability/prometheus.nix +++ /dev/null @@ -1,51 +0,0 @@ -{ - pkgs, - config, - lib, - ... -}: let - inherit (builtins) toString; - inherit (lib) mkIf mkEnableOption; - - cfg = config.sneeuwvlok.services.observability.prometheus; -in { - options.sneeuwvlok.services.observability.prometheus = { - enable = mkEnableOption "enable Prometheus"; - }; - - config = mkIf cfg.enable { - services.prometheus = { - enable = true; - port = 9002; - - globalConfig.scrape_interval = "15s"; - - scrapeConfigs = [ - { - job_name = "prometheus"; - static_configs = [ - {targets = ["localhost:9002"];} - ]; - } - - { - job_name = "node"; - static_configs = [ - {targets = ["localhost:${toString config.services.prometheus.exporters.node.port}"];} - ]; - } - ]; - - exporters = { - node = { - enable = true; - port = 9005; - enabledCollectors = ["systemd"]; - openFirewall = true; - }; - }; - }; - - networking.firewall.allowedTCPPorts = [9002]; - }; -} diff --git a/modules/nixos/services/observability/prometheus/default.nix b/modules/nixos/services/observability/prometheus/default.nix new file mode 100644 index 0000000..c092286 --- /dev/null +++ b/modules/nixos/services/observability/prometheus/default.nix @@ -0,0 +1,67 @@ +{ pkgs, config, lib, namespace, ... }: +let + inherit (builtins) toString; + inherit (lib) mkEnableOption mkIf optionals; + + cfg = config.${namespace}.services.observability.prometheus; +in +{ + options.${namespace}.services.observability.prometheus = { + enable = mkEnableOption "enable Prometheus"; + }; + + config = mkIf cfg.enable { + services.prometheus = { + enable = true; + port = 9020; + extraFlags = optionals config.${namespace}.services.observability.alloy.enable [ + "--web.enable-remote-write-receiver" + ]; + + globalConfig.scrape_interval = "15s"; + + scrapeConfigs = [ + { + job_name = "prometheus"; + static_configs = [ + { targets = [ "localhost:9020" ]; } + ]; + } + + { + job_name = "node"; + static_configs = [ + { targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ]; } + ]; + } + ] + ++ optionals config.${namespace}.services.observability.alloy.enable [ + { + job_name = "alloy"; + static_configs = [ + { targets = [ "localhost:9070" ]; } + ]; + } + ] + ++ optionals config.${namespace}.services.observability.tempo.enable [ + { + job_name = "tempo"; + static_configs = [ + { targets = [ "localhost:9060" ]; } + ]; + } + ]; + + exporters = { + node = { + enable = true; + port = 9021; + enabledCollectors = [ "systemd" ]; + openFirewall = true; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ 9020 ]; + }; +} diff --git a/modules/nixos/services/observability/promtail.nix b/modules/nixos/services/observability/promtail/default.nix similarity index 79% rename from modules/nixos/services/observability/promtail.nix rename to modules/nixos/services/observability/promtail/default.nix index cf5e6c1..b852f1f 100644 --- a/modules/nixos/services/observability/promtail.nix +++ b/modules/nixos/services/observability/promtail/default.nix @@ -2,14 +2,15 @@ pkgs, config, lib, + namespace, ... }: let inherit (lib.modules) mkIf; inherit (lib.options) mkEnableOption; - cfg = config.sneeuwvlok.services.observability.promtail; + cfg = config.${namespace}.services.observability.promtail; in { - options.sneeuwvlok.services.observability.promtail = { + options.${namespace}.services.observability.promtail = { enable = mkEnableOption "enable Grafana Promtail"; }; @@ -24,7 +25,7 @@ in { configuration = { server = { - http_listen_port = 9004; + http_listen_port = 9040; grpc_listen_port = 0; }; @@ -34,7 +35,7 @@ in { clients = [ { - url = "http://[::1]:9003/loki/api/v1/push"; + url = "http://[::1]:9030/loki/api/v1/push"; } ]; @@ -59,6 +60,6 @@ in { }; }; - networking.firewall.allowedTCPPorts = [9004]; + networking.firewall.allowedTCPPorts = [9040]; }; } diff --git a/modules/nixos/services/observability/tempo/default.nix b/modules/nixos/services/observability/tempo/default.nix new file mode 100644 index 0000000..46339bc --- /dev/null +++ b/modules/nixos/services/observability/tempo/default.nix @@ -0,0 +1,51 @@ +{ + config, + lib, + namespace, + ... +}: let + inherit (lib) mkEnableOption mkIf; + + cfg = config.${namespace}.services.observability.tempo; + + httpPort = 9060; + grpcPort = 9061; + otlpGrpcPort = 9062; + otlpHttpPort = 9063; +in { + options.${namespace}.services.observability.tempo = { + enable = mkEnableOption "enable Grafana Tempo"; + }; + + config = mkIf cfg.enable { + services.tempo = { + enable = true; + settings = { + auth_enabled = false; + search_enabled = true; + + server = { + http_listen_address = "[::]"; + http_listen_port = httpPort; + grpc_listen_address = "[::1]"; + grpc_listen_port = grpcPort; + }; + + distributor.receivers.otlp.protocols = { + grpc.endpoint = "[::1]:${builtins.toString otlpGrpcPort}"; + http.endpoint = "[::1]:${builtins.toString otlpHttpPort}"; + }; + + storage.trace = { + backend = "local"; + wal.path = "/var/lib/tempo/wal"; + local.path = "/var/lib/tempo/traces"; + }; + + compactor.compaction.block_retention = "168h"; + }; + }; + + networking.firewall.allowedTCPPorts = [httpPort]; + }; +} diff --git a/modules/nixos/services/observability/uptime-kuma.nix b/modules/nixos/services/observability/uptime-kuma/default.nix similarity index 51% rename from modules/nixos/services/observability/uptime-kuma.nix rename to modules/nixos/services/observability/uptime-kuma/default.nix index fc089fd..af0cfa8 100644 --- a/modules/nixos/services/observability/uptime-kuma.nix +++ b/modules/nixos/services/observability/uptime-kuma/default.nix @@ -1,15 +1,12 @@ -{ - pkgs, - config, - lib, - ... -}: let +{ pkgs, config, lib, namespace, ... }: +let inherit (builtins) toString; inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.services.observability.uptime-kuma; -in { - options.sneeuwvlok.services.observability.uptime-kuma = { + cfg = config.${namespace}.services.observability.uptime-kuma; +in +{ + options.${namespace}.services.observability.uptime-kuma = { enable = mkEnableOption "enable uptime kuma"; }; @@ -18,11 +15,11 @@ in { enable = true; settings = { - PORT = toString 9006; + PORT = toString 9050; HOST = "0.0.0.0"; }; }; - - networking.firewall.allowedTCPPorts = [9006]; + + networking.firewall.allowedTCPPorts = [ 9050 ]; }; } diff --git a/modules/nixos/services/persistance/postgresql.nix b/modules/nixos/services/persistance/postgesql/default.nix similarity index 81% rename from modules/nixos/services/persistance/postgresql.nix rename to modules/nixos/services/persistance/postgesql/default.nix index f217dd5..403c07c 100644 --- a/modules/nixos/services/persistance/postgresql.nix +++ b/modules/nixos/services/persistance/postgesql/default.nix @@ -2,13 +2,14 @@ config, lib, pkgs, + namespace, ... }: let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.services.persistence.postgresql; + cfg = config.${namespace}.services.persistance.postgresql; in { - options.sneeuwvlok.services.persistence.postgresql = { + options.${namespace}.services.persistance.postgresql = { enable = mkEnableOption "Postgresql"; }; diff --git a/modules/nixos/services/security/vaultwarden.nix b/modules/nixos/services/security/vaultwarden/default.nix similarity index 62% rename from modules/nixos/services/security/vaultwarden.nix rename to modules/nixos/services/security/vaultwarden/default.nix index 47fe178..1660736 100644 --- a/modules/nixos/services/security/vaultwarden.nix +++ b/modules/nixos/services/security/vaultwarden/default.nix @@ -2,14 +2,13 @@ pkgs, config, lib, - self, + namespace, ... }: let inherit (builtins) toString; inherit (lib) mkIf mkEnableOption mkOption types getAttrs toUpper concatMapAttrsStringSep; - inherit (import ../../../../../lib/strings {inherit lib;}) strings; - cfg = config.sneeuwvlok.services.security.vaultwarden; + cfg = config.${namespace}.services.security.vaultwarden; databaseProviderSqlite = types.submodule ({...}: { options = { @@ -26,22 +25,15 @@ }; }); - databaseProviderPostgresql = types.submodule ({...}: { - options = { - type = mkOption { - type = types.enum ["postgresql"]; - }; - - host = mkOption { - type = types.str; - example = "host.tld"; + databaseProviderPostgresql = types.submodule ({...}: let + urlOptions = lib.${namespace}.options.mkUrlOptions { + host = { description = '' Hostname of the postgresql server ''; }; - port = mkOption { - type = types.port; + port = { default = 5432; example = "5432"; description = '' @@ -50,37 +42,41 @@ }; protocol = mkOption { - type = types.str; default = "postgres"; example = "postgres"; - description = '' - Which protocol to use when creating a url string - ''; - }; - - sslMode = mkOption { - type = types.enum ["verify-ca" "verify-full" "require" "prefer" "allow" "disabled"]; - default = "verify-full"; - example = "verify-ca"; - description = '' - How to verify the server's ssl - - | mode | eavesdropping protection | MITM protection | Statement | - |-------------|--------------------------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------| - | disable | No | No | I don't care about security, and I don't want to pay the overhead of encryption. | - | allow | Maybe | No | I don't care about security, but I will pay the overhead of encryption if the server insists on it. | - | prefer | Maybe | No | I don't care about encryption, but I wish to pay the overhead of encryption if the server supports it. | - | require | Yes | No | I want my data to be encrypted, and I accept the overhead. I trust that the network will make sure I always connect to the server I want. | - | verify-ca | Yes | Depends on CA policy | I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server that I trust. | - | verify-full | Yes | Yes | I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server I trust, and that it's the one I specify. | - - [Source](https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS) - ''; }; }; + in { + options = + { + type = mkOption { + type = types.enum ["postgresql"]; + }; + + sslMode = mkOption { + type = types.enum ["verify-ca" "verify-full" "require" "prefer" "allow" "disabled"]; + default = "verify-full"; + example = "verify-ca"; + description = '' + How to verify the server's ssl + + | mode | eavesdropping protection | MITM protection | Statement | + |-------------|--------------------------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------| + | disable | No | No | I don't care about security, and I don't want to pay the overhead of encryption. | + | allow | Maybe | No | I don't care about security, but I will pay the overhead of encryption if the server insists on it. | + | prefer | Maybe | No | I don't care about encryption, but I wish to pay the overhead of encryption if the server supports it. | + | require | Yes | No | I want my data to be encrypted, and I accept the overhead. I trust that the network will make sure I always connect to the server I want. | + | verify-ca | Yes | Depends on CA policy | I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server that I trust. | + | verify-full | Yes | Yes | I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server I trust, and that it's the one I specify. | + + [Source](https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS) + ''; + }; + } + // (urlOptions |> getAttrs ["protocol" "host" "port"]); }); in { - options.sneeuwvlok.services.security.vaultwarden = { + options.${namespace}.services.security.vaultwarden = { enable = mkEnableOption "enable vaultwarden"; database = mkOption { @@ -95,7 +91,7 @@ in { }; config = mkIf cfg.enable { - sneeuwvlok.services.networking.caddy.hosts = { + ${namespace}.services.networking.caddy.hosts = { "vault.kruining.eu" = '' encode zstd gzip @@ -139,7 +135,7 @@ in { SSO_ROLES_ENABLED = true; SSO_ORGANIZATIONS_ENABLED = true; SSO_ORGANIZATIONS_REVOCATION = true; - SSO_AUTHORITY = "https://auth.kruining.eu/"; + SSO_AUTHORITY = "https://auth.kruining.eu"; SSO_SCOPES = "email profile offline_access"; ROCKET_ADDRESS = "::1"; @@ -200,7 +196,7 @@ in { else if type == "postgresql" then { inherit (db) type; - url = strings.toUrl { + url = lib.${namespace}.strings.toUrl { inherit (db) protocol host port; path = "vaultwarden"; query = { diff --git a/modules/nixos/services/virtualisation/podman.nix b/modules/nixos/services/virtualisation/podman/default.nix similarity index 67% rename from modules/nixos/services/virtualisation/podman.nix rename to modules/nixos/services/virtualisation/podman/default.nix index 0d32495..0faf8ce 100644 --- a/modules/nixos/services/virtualisation/podman.nix +++ b/modules/nixos/services/virtualisation/podman/default.nix @@ -1,15 +1,11 @@ -{ - config, - options, - lib, - pkgs, - ... -}: let +{ config, options, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.services.virtualisation.podman; -in { - options.sneeuwvlok.services.virtualisation.podman = { + cfg = config.${namespace}.services.virtualisation.podman; +in +{ + options.${namespace}.services.virtualisation.podman = { enable = mkEnableOption "enable podman"; }; diff --git a/modules/nixos/shells/default.nix b/modules/nixos/shells/default.nix new file mode 100644 index 0000000..ea8f50d --- /dev/null +++ b/modules/nixos/shells/default.nix @@ -0,0 +1,2 @@ +{...}: { +} diff --git a/modules/nixos/shells/zsh.nix b/modules/nixos/shells/zsh/default.nix similarity index 62% rename from modules/nixos/shells/zsh.nix rename to modules/nixos/shells/zsh/default.nix index bb70922..399e7dd 100644 --- a/modules/nixos/shells/zsh.nix +++ b/modules/nixos/shells/zsh/default.nix @@ -1,15 +1,11 @@ -{ - inputs, - config, - lib, - pkgs, - ... -}: let +{ inputs, config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; - cfg = config.sneeuwvlok.shell.zsh; -in { - options.sneeuwvlok.shell.zsh = { + cfg = config.${namespace}.shell.zsh; +in +{ + options.${namespace}.shell.zsh = { enable = mkEnableOption "enable zsh shell"; }; diff --git a/modules/nixos/system/networking.nix b/modules/nixos/system/networking/default.nix similarity index 69% rename from modules/nixos/system/networking.nix rename to modules/nixos/system/networking/default.nix index 4bb580f..c61a81b 100644 --- a/modules/nixos/system/networking.nix +++ b/modules/nixos/system/networking/default.nix @@ -1,14 +1,11 @@ -{ - config, - lib, - pkgs, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkDefault; - cfg = config.sneeuwvlok.system.networking; -in { - options.sneeuwvlok.system.networking = {}; + cfg = config.${namespace}.system.networking; +in +{ + options.${namespace}.system.networking = {}; config = { systemd.services.NetworkManager-wait-online.enable = false; diff --git a/modules/nixos/system/security/boot.nix b/modules/nixos/system/security/boot/default.nix similarity index 90% rename from modules/nixos/system/security/boot.nix rename to modules/nixos/system/security/boot/default.nix index 920ef16..ccf1f83 100644 --- a/modules/nixos/system/security/boot.nix +++ b/modules/nixos/system/security/boot/default.nix @@ -1,15 +1,13 @@ +{ config, namespace, inputs, ... }: +let + cfg = config.${namespace}.system.security.boot; +in { - config, - inputs, - ... -}: let - cfg = config.sneeuwvlok.system.security.boot; -in { - options.sneeuwvlok.system.security.boot = {}; + options.${namespace}.system.security.boot = {}; config = { boot = { - kernelModules = ["tcp_bbr"]; + kernelModules = [ "tcp_bbr" ]; kernel.sysctl = { ## TCP hardening # Prevent bogus ICMP errors from filling up logs. @@ -45,4 +43,4 @@ in { }; }; }; -} +} \ No newline at end of file diff --git a/modules/nixos/system/security/default.nix b/modules/nixos/system/security/default.nix index fdd3416..e168543 100644 --- a/modules/nixos/system/security/default.nix +++ b/modules/nixos/system/security/default.nix @@ -1,11 +1,9 @@ +{ config, namespace, inputs, ... }: +let + cfg = config.${namespace}.system.security; +in { - config, - inputs, - ... -}: let - cfg = config.sneeuwvlok.system.security; -in { - options.sneeuwvlok.system.security = {}; + options.${namespace}.system.security = {}; config = { security = { @@ -22,4 +20,4 @@ in { programs.gnupg.agent.enable = true; }; -} +} \ No newline at end of file diff --git a/modules/nixos/system/security/sops.nix b/modules/nixos/system/security/sops.nix deleted file mode 100644 index e682f2c..0000000 --- a/modules/nixos/system/security/sops.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ - pkgs, - config, - self, - ... -}: let - cfg = config.sneeuwvlok.system.security.sops; -in { - options.sneeuwvlok.system.security.sops = {}; - - config = { - environment.systemPackages = with pkgs; [sops]; - - sops = { - defaultSopsFormat = "yaml"; - defaultSopsFile = self + "/systems/${pkgs.stdenv.hostPlatform.system}/${config.networking.hostName}/secrets.yml"; - - age = { - # keyFile = "~/.config/sops/age/keys.txt"; - # sshKeyPaths = [ "~/.ssh/id_ed25519" ]; - # generateKey = true; - }; - }; - }; -} diff --git a/modules/nixos/system/security/sops/default.nix b/modules/nixos/system/security/sops/default.nix new file mode 100644 index 0000000..bee7b3c --- /dev/null +++ b/modules/nixos/system/security/sops/default.nix @@ -0,0 +1,26 @@ +{ pkgs, config, namespace, inputs, system, ... }: +let + cfg = config.${namespace}.system.security.sops; +in +{ + imports = [ + inputs.sops-nix.nixosModules.sops + ]; + + options.${namespace}.system.security.sops = {}; + + config = { + environment.systemPackages = with pkgs; [ sops ]; + + sops = { + defaultSopsFormat = "yaml"; + defaultSopsFile = inputs.self + "/systems/${system}/${config.networking.hostName}/secrets.yml"; + + age = { + # keyFile = "~/.config/sops/age/keys.txt"; + # sshKeyPaths = [ "~/.ssh/id_ed25519" ]; + # generateKey = true; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/nixos/system/security/sudo.nix b/modules/nixos/system/security/sudo/default.nix similarity index 65% rename from modules/nixos/system/security/sudo.nix rename to modules/nixos/system/security/sudo/default.nix index ef41f6e..b79efbc 100644 --- a/modules/nixos/system/security/sudo.nix +++ b/modules/nixos/system/security/sudo/default.nix @@ -1,7 +1,9 @@ -{config, ...}: let - cfg = config.sneeuwvlok.system.security.sudo; -in { - options.sneeuwvlok.system.security.sudo = {}; +{ config, namespace, ... }: +let + cfg = config.${namespace}.system.security.sudo; +in +{ + options.${namespace}.system.security.sudo = {}; config = { security = { @@ -9,7 +11,7 @@ in { enable = false; execWheelOnly = true; }; - + sudo-rs = { enable = true; execWheelOnly = true; @@ -17,4 +19,4 @@ in { }; }; }; -} +} \ No newline at end of file diff --git a/modules/nixos/temp/services/arrtrix/default.nix b/modules/nixos/temp/services/arrtrix/default.nix new file mode 100644 index 0000000..6bb1d9f --- /dev/null +++ b/modules/nixos/temp/services/arrtrix/default.nix @@ -0,0 +1,214 @@ +{ + config, + lib, + pkgs, + namespace, + ... +}: let + inherit (lib) mkEnableOption mkPackageOption mkIf mkOption optionalAttrs recursiveUpdate types baseNameOf; + + cfg = config.services.arrtrix; + dataDir = "/var/lib/arrtrix"; + registrationFile = "${dataDir}/arrtrix-registration.yaml"; + settingsFile = "${dataDir}/config.yaml"; + settingsFileUnsubstituted = settingsFormat.generate "arrtrix-config-unsubstituted.json" cfg.settings; + settingsFormat = pkgs.formats.json {}; + + defaultConfig = { + bridge = { + command_prefix = "!arr"; + relay.enabled = true; + permissions."*" = "relay"; + }; + database = { + type = "sqlite3-fk-wal"; + uri = "file:${dataDir}/arrtrix.db?_txlock=immediate"; + }; + homeserver = { + address = "http://localhost:8448"; + domain = config.services.matrix-synapse.settings.server_name or "example.com"; + }; + appservice = { + hostname = "[::]"; + port = 29329; + id = "arrtrix"; + bot = { + username = "arrtrixbot"; + displayname = "arrtrix Bot"; + }; + as_token = ""; + hs_token = ""; + username_template = "arrtrix_{{.}}"; + }; + logging = { + min_level = "info"; + writers = lib.singleton { + type = "stdout"; + format = "pretty-colored"; + time_format = " "; + }; + }; + observability = { + otlp_grpc_endpoint = ""; + service_name = "arrtrix"; + resource_attributes = {}; + }; + network.content = { + movies = { + url = ""; + api_key = ""; + root_folder_path = ""; + quality_profile_id = 0; + minimum_availability = "released"; + search_on_add = true; + }; + series = { + url = ""; + api_key = ""; + root_folder_path = ""; + quality_profile_id = 0; + language_profile_id = 0; + season_folder = true; + series_type = "standard"; + search_on_add = true; + }; + }; + }; +in { + options.services.arrtrix = { + enable = mkEnableOption "Arr-focused Matrix appservice foundation"; + + package = mkPackageOption pkgs.${namespace} "arrtrix" {}; + + registerToSynapse = mkOption { + type = types.bool; + default = config.services.matrix-synapse.enable; + defaultText = lib.literalExpression '' + config.services.matrix-synapse.enable + ''; + description = '' + Whether to add the bridge's app service registration file to + `services.matrix-synapse.settings.app_service_config_files`. + ''; + }; + + settings = mkOption { + apply = lib.recursiveUpdate defaultConfig; + type = settingsFormat.type; + default = defaultConfig; + description = '' + {file}`config.yaml` configuration as a Nix attribute set. + Configuration options should match those described in the example configuration. + Get an example configuration by executing `arrtrix -c example.yaml --generate-example-config` + Secret tokens should be specified using {option}`environmentFile` + instead of this world-readable attribute set. + ''; + example = {}; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + File containing environment variables to be passed to the arrtrix service. + If an environment variable `ARRTRIX_BRIDGE_LOGIN_SHARED_SECRET` is set, + then its value will be used in the configuration file for the option + `double_puppet.secrets` without leaking it to the store, using the configured + `homeserver.domain` as key. + ''; + }; + + serviceDependencies = lib.mkOption { + type = with lib.types; listOf str; + default = + (lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit) + ++ (lib.optional config.services.matrix-conduit.enable "conduit.service"); + defaultText = lib.literalExpression '' + (optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit) + ++ (optional config.services.matrix-conduit.enable "conduit.service") + ''; + description = '' + List of systemd units to require and wait for when starting the application service. + ''; + }; + }; + + config = mkIf cfg.enable { + users = { + users."arrtrix" = { + isSystemUser = true; + group = "arrtrix"; + }; + groups."arrtrix" = {}; + }; + + services.matrix-synapse = lib.mkIf cfg.registerToSynapse { + settings.app_service_config_files = [registrationFile]; + }; + systemd.services.matrix-synapse = lib.mkIf cfg.registerToSynapse { + serviceConfig.SupplementaryGroups = ["arrtrix"]; + }; + + systemd.services.arrtrix = { + description = "arrtrix, A *arr stack to matrix bridge for *arr-notifications"; + + wantedBy = ["multi-user.target"]; + after = ["network-online.target"]; + wants = ["network-online.target"]; + restartTriggers = [settingsFileUnsubstituted]; + + preStart = '' + # substitute the settings file by environment variables + # in this case read from EnvironmentFile + test -f '${settingsFile}' && rm -f '${settingsFile}' + + old_umask=$(umask) + umask 0177 + ${lib.getExe pkgs.envsubst} -o '${settingsFile}' -i '${settingsFileUnsubstituted}' + umask $old_umask + + if [ ! -f '${registrationFile}' ]; then + ${lib.getExe cfg.package} --generate-registration --config='${settingsFile}' --registration='${registrationFile}' + fi + chmod 640 ${registrationFile} + ''; + + serviceConfig = { + Type = "simple"; + User = "arrtrix"; + Group = "arrtrix"; + + StateDirectory = baseNameOf dataDir; + WorkingDirectory = dataDir; + EnvironmentFile = cfg.environmentFile; + + ExecStart = '' + ${lib.getExe cfg.package} --config='${settingsFile}' --registration='${registrationFile}' + ''; + + Restart = "on-failure"; + RestartSec = "30s"; + + NoNewPrivileges = true; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = ["@system-service"]; + UMask = "0027"; + }; + }; + }; +} diff --git a/packages/arrtrix/cmd/arrtrix/main.go b/packages/arrtrix/cmd/arrtrix/main.go new file mode 100644 index 0000000..3fa476f --- /dev/null +++ b/packages/arrtrix/cmd/arrtrix/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "sneeuwvlok/packages/arrtrix/pkg/connector" + "sneeuwvlok/packages/arrtrix/pkg/runtime" +) + +var ( + Tag = "unknown" + Commit = "unknown" + BuildTime = "unknown" +) + +var m = runtime.Main{ + Name: "arrtrix", + URL: "https://github.com/chris-kruining/sneeuwvlok", + Description: "An Arr-focused Matrix appservice bridge.", + Version: "0.1.0", + Connector: &connector.ArrtrixConnector{}, +} + +func main() { + m.InitVersion(Tag, Commit, BuildTime) + m.Run() +} diff --git a/packages/arrtrix/default.nix b/packages/arrtrix/default.nix new file mode 100644 index 0000000..0113edb --- /dev/null +++ b/packages/arrtrix/default.nix @@ -0,0 +1,33 @@ +{ + buildGoModule, + lib, + olm, + versionCheckHook, +}: +buildGoModule rec { + pname = "arrtrix"; + version = "0.1.0"; + tag = "v0.1.0"; + + src = lib.cleanSource ./.; + + vendorHash = "sha256-UYRit+v41djnCx+GFdEl/8WQsp2DzF4ywT9iv3m1pSc="; + subPackages = ["cmd/arrtrix"]; + + buildInputs = [olm]; + + ldflags = [ + "-X main.Tag=${tag}" + ]; + + doInstallCheck = true; + nativeInstallCheckInputs = [versionCheckHook]; + + meta = { + description = "*arr-stack Matrix bridge"; + homepage = "https://github.com/chris-kruining/sneeuwvlok"; + license = lib.licenses.mit; + maintainers = []; + mainProgram = "arrtrix"; + }; +} diff --git a/packages/arrtrix/go.mod b/packages/arrtrix/go.mod new file mode 100644 index 0000000..81a6c93 --- /dev/null +++ b/packages/arrtrix/go.mod @@ -0,0 +1,60 @@ +module sneeuwvlok/packages/arrtrix + +go 1.25.0 + +require ( + github.com/rs/zerolog v1.34.0 + go.mau.fi/util v0.9.7 + go.mau.fi/zeroconfig v0.2.0 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 + go.opentelemetry.io/otel/log v0.19.0 + go.opentelemetry.io/otel/metric v1.43.0 + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/sdk/log v0.19.0 + go.opentelemetry.io/otel/sdk/metric v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 + gopkg.in/yaml.v3 v3.0.1 + maunium.net/go/mauflag v1.0.0 + maunium.net/go/mautrix v0.26.4 +) + +require ( + filippo.io/edwards25519 v1.2.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coder/websocket v1.8.14 // indirect + github.com/coreos/go-systemd/v22 v22.6.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/lib/pq v1.11.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.34 // indirect + github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/yuin/goldmark v1.7.16 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect +) diff --git a/packages/arrtrix/go.sum b/packages/arrtrix/go.sum new file mode 100644 index 0000000..8d8f5ab --- /dev/null +++ b/packages/arrtrix/go.sum @@ -0,0 +1,142 @@ +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= +github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE= +github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg= +go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE= +go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= +go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= +maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= +maunium.net/go/mautrix v0.26.4 h1:enHSnkf0L2V9+VnfJfNhKSReSW6pBKS/x3Su+v+Vovs= +maunium.net/go/mautrix v0.26.4/go.mod h1:YWw8NWTszsbyFAznboicBObwHPgTSLcuTbVX2kY7U2M= diff --git a/packages/arrtrix/pkg/arr/catalog.go b/packages/arrtrix/pkg/arr/catalog.go new file mode 100644 index 0000000..eb2f833 --- /dev/null +++ b/packages/arrtrix/pkg/arr/catalog.go @@ -0,0 +1,76 @@ +package arr + +import ( + "fmt" + "slices" + "strings" +) + +type ContentType string + +const ( + ContentTypeMovies ContentType = "movies" + ContentTypeSeries ContentType = "series" +) + +var supportedContentTypes = []ContentType{ + ContentTypeMovies, + ContentTypeSeries, +} + +var supportedEvents = map[ContentType][]string{ + ContentTypeMovies: {"Test", "Grab", "Download", "Rename", "MovieFileDelete", "MovieDelete"}, + ContentTypeSeries: {"Test", "Grab", "Download", "Rename", "EpisodeFileDelete", "SeriesDelete"}, +} + +func SupportedContentTypes() []ContentType { + return append([]ContentType(nil), supportedContentTypes...) +} + +func SupportedEventTypes(contentType ContentType) []string { + return append([]string(nil), supportedEvents[contentType]...) +} + +func ParseContentType(value string) (ContentType, error) { + contentType := ContentType(strings.ToLower(strings.TrimSpace(value))) + if slices.Contains(supportedContentTypes, contentType) { + return contentType, nil + } + return "", fmt.Errorf("unsupported content type %q (expected one of: %s)", value, Strings()) +} + +func ParseEventType(contentType ContentType, value string) (string, error) { + value = strings.TrimSpace(value) + if strings.EqualFold(value, "all") { + return "all", nil + } + for _, eventType := range supportedEvents[contentType] { + if strings.EqualFold(eventType, value) { + return eventType, nil + } + } + return "", fmt.Errorf("unsupported event type %q for %s", value, contentType) +} + +func SupportsEventType(contentType ContentType, eventType string) bool { + return slices.Contains(supportedEvents[contentType], strings.TrimSpace(eventType)) +} + +func (c ContentType) Label() string { + switch c { + case ContentTypeMovies: + return "movies" + case ContentTypeSeries: + return "series" + default: + return string(c) + } +} + +func Strings() string { + values := make([]string, 0, len(supportedContentTypes)) + for _, contentType := range supportedContentTypes { + values = append(values, string(contentType)) + } + return strings.Join(values, ", ") +} diff --git a/packages/arrtrix/pkg/arr/catalog_test.go b/packages/arrtrix/pkg/arr/catalog_test.go new file mode 100644 index 0000000..e3c2784 --- /dev/null +++ b/packages/arrtrix/pkg/arr/catalog_test.go @@ -0,0 +1,23 @@ +package arr + +import "testing" + +func TestParseContentType(t *testing.T) { + contentType, err := ParseContentType("Movies") + if err != nil { + t.Fatalf("ParseContentType returned error: %v", err) + } + if contentType != ContentTypeMovies { + t.Fatalf("expected movies content type, got %q", contentType) + } +} + +func TestParseEventType(t *testing.T) { + eventType, err := ParseEventType(ContentTypeSeries, "download") + if err != nil { + t.Fatalf("ParseEventType returned error: %v", err) + } + if eventType != "Download" { + t.Fatalf("expected Download event type, got %q", eventType) + } +} diff --git a/packages/arrtrix/pkg/arrclient/client.go b/packages/arrtrix/pkg/arrclient/client.go new file mode 100644 index 0000000..fc7fb53 --- /dev/null +++ b/packages/arrtrix/pkg/arrclient/client.go @@ -0,0 +1,363 @@ +package arrclient + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "html" + "io" + "mime" + "net/http" + "net/url" + "path" + "path/filepath" + "strings" + + "sneeuwvlok/packages/arrtrix/pkg/arr" +) + +type Client interface { + ContentType() arr.ContentType + Search(context.Context, string) ([]SearchResult, error) + List(context.Context, string) ([]ManagedItem, error) + Add(context.Context, SearchResult) (*ManagedItem, error) + SetMonitored(context.Context, int64, bool) (*ManagedItem, error) + Delete(context.Context, int64) error + FetchImage(context.Context, ManagedItem) (*MediaAsset, error) +} + +type SearchResult struct { + LookupID int64 + Title string + Year int + Overview string +} + +type ManagedItem struct { + ID int64 + LookupID int64 + Title string + Year int + Monitored bool + Path string + ImageURL string +} + +type MediaAsset struct { + Data []byte + FileName string + MimeType string +} + +type RadarrConfig struct { + URL string `yaml:"url"` + APIKey string `yaml:"api_key"` + RootFolderPath string `yaml:"root_folder_path"` + QualityProfileID int64 `yaml:"quality_profile_id"` + MinimumAvailability string `yaml:"minimum_availability"` + SearchOnAdd *bool `yaml:"search_on_add"` +} + +type SonarrConfig struct { + URL string `yaml:"url"` + APIKey string `yaml:"api_key"` + RootFolderPath string `yaml:"root_folder_path"` + QualityProfileID int64 `yaml:"quality_profile_id"` + LanguageProfileID int64 `yaml:"language_profile_id"` + SeasonFolder *bool `yaml:"season_folder"` + SeriesType string `yaml:"series_type"` + SearchOnAdd *bool `yaml:"search_on_add"` +} + +type httpClient struct { + baseURL *url.URL + apiKey string + httpClient *http.Client +} + +type mediaImage struct { + CoverType string `json:"coverType"` + URL string `json:"url"` + RemoteURL string `json:"remoteUrl"` +} + +func (c *RadarrConfig) ApplyDefaults() { + if c.MinimumAvailability == "" { + c.MinimumAvailability = "released" + } +} + +func (c RadarrConfig) Enabled() bool { + return strings.TrimSpace(c.URL) != "" || strings.TrimSpace(c.APIKey) != "" +} + +func (c RadarrConfig) Validate() error { + if !c.Enabled() { + return nil + } + switch { + case strings.TrimSpace(c.URL) == "": + return fmt.Errorf("network.content.movies.url must be set when movies content is configured") + case strings.TrimSpace(c.APIKey) == "": + return fmt.Errorf("network.content.movies.api_key must be set when movies content is configured") + case strings.TrimSpace(c.RootFolderPath) == "": + return fmt.Errorf("network.content.movies.root_folder_path must be set when movies content is configured") + case c.QualityProfileID <= 0: + return fmt.Errorf("network.content.movies.quality_profile_id must be set when movies content is configured") + case strings.TrimSpace(c.MinimumAvailability) == "": + return fmt.Errorf("network.content.movies.minimum_availability must not be empty") + default: + return nil + } +} + +func (c RadarrConfig) SearchOnAddValue() bool { + return boolValue(c.SearchOnAdd, true) +} + +func (c *SonarrConfig) ApplyDefaults() { + if c.SeriesType == "" { + c.SeriesType = "standard" + } +} + +func (c SonarrConfig) Enabled() bool { + return strings.TrimSpace(c.URL) != "" || strings.TrimSpace(c.APIKey) != "" +} + +func (c SonarrConfig) Validate() error { + if !c.Enabled() { + return nil + } + switch { + case strings.TrimSpace(c.URL) == "": + return fmt.Errorf("network.content.series.url must be set when series content is configured") + case strings.TrimSpace(c.APIKey) == "": + return fmt.Errorf("network.content.series.api_key must be set when series content is configured") + case strings.TrimSpace(c.RootFolderPath) == "": + return fmt.Errorf("network.content.series.root_folder_path must be set when series content is configured") + case c.QualityProfileID <= 0: + return fmt.Errorf("network.content.series.quality_profile_id must be set when series content is configured") + case c.LanguageProfileID <= 0: + return fmt.Errorf("network.content.series.language_profile_id must be set when series content is configured") + case strings.TrimSpace(c.SeriesType) == "": + return fmt.Errorf("network.content.series.series_type must not be empty") + default: + return nil + } +} + +func (c SonarrConfig) SeasonFolderValue() bool { + return boolValue(c.SeasonFolder, true) +} + +func (c SonarrConfig) SearchOnAddValue() bool { + return boolValue(c.SearchOnAdd, true) +} + +func newHTTPClient(rawURL, apiKey string) (*httpClient, error) { + parsedURL, err := url.Parse(strings.TrimRight(strings.TrimSpace(rawURL), "/")) + if err != nil { + return nil, err + } + return &httpClient{ + baseURL: parsedURL, + apiKey: apiKey, + httpClient: http.DefaultClient, + }, nil +} + +func (c *httpClient) do(ctx context.Context, method, requestPath string, query url.Values, body any, dest any) error { + endpoint := *c.baseURL + endpoint.Path = path.Join(endpoint.Path, requestPath) + if len(query) > 0 { + endpoint.RawQuery = query.Encode() + } + + var payload io.Reader + if body != nil { + data, err := json.Marshal(body) + if err != nil { + return err + } + payload = bytes.NewReader(data) + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), payload) + if err != nil { + return err + } + req.Header.Set("X-Api-Key", c.apiKey) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + return fmt.Errorf("%s %s returned %d: %s", method, endpoint.String(), resp.StatusCode, strings.TrimSpace(string(data))) + } + if dest == nil { + return nil + } + return json.NewDecoder(resp.Body).Decode(dest) +} + +func boolValue(value *bool, fallback bool) bool { + if value == nil { + return fallback + } + return *value +} + +func containsFold(haystack, needle string) bool { + return strings.Contains(strings.ToLower(haystack), strings.ToLower(strings.TrimSpace(needle))) +} + +func FormatSearchResult(result SearchResult) string { + if result.Year != 0 { + return fmt.Sprintf("%s (%d)", result.Title, result.Year) + } + return result.Title +} + +func FormatManagedItem(item ManagedItem) string { + if item.Year != 0 { + return fmt.Sprintf("%s (%d)", item.Title, item.Year) + } + return item.Title +} + +func EscapeText(text string) string { + return html.EscapeString(text) +} + +func (c *httpClient) FetchImage(ctx context.Context, item ManagedItem) (*MediaAsset, error) { + imageURL := strings.TrimSpace(item.ImageURL) + if imageURL == "" { + return nil, nil + } + + endpoint, err := url.Parse(imageURL) + if err != nil { + return nil, fmt.Errorf("parse image URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL.ResolveReference(endpoint).String(), nil) + if err != nil { + return nil, err + } + if sameHost(req.URL, c.baseURL) { + req.Header.Set("X-Api-Key", c.apiKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + return nil, fmt.Errorf("GET %s returned %d: %s", req.URL.String(), resp.StatusCode, strings.TrimSpace(string(data))) + } + + data, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) + if err != nil { + return nil, err + } + + mimeType := strings.TrimSpace(resp.Header.Get("Content-Type")) + if idx := strings.Index(mimeType, ";"); idx >= 0 { + mimeType = strings.TrimSpace(mimeType[:idx]) + } + if mimeType == "" { + mimeType = http.DetectContentType(data) + } + + return &MediaAsset{ + Data: data, + FileName: imageFileName(item, endpoint, mimeType), + MimeType: mimeType, + }, nil +} + +func (c *httpClient) imageURL(images []mediaImage) string { + for _, coverType := range []string{"poster", "cover", "fanart"} { + for _, image := range images { + if !strings.EqualFold(image.CoverType, coverType) { + continue + } + if resolved := c.resolveMediaURL(image); resolved != "" { + return resolved + } + } + } + return "" +} + +func (c *httpClient) resolveMediaURL(image mediaImage) string { + switch { + case strings.TrimSpace(image.URL) != "": + ref, err := url.Parse(strings.TrimSpace(image.URL)) + if err != nil { + return "" + } + return c.baseURL.ResolveReference(ref).String() + case strings.TrimSpace(image.RemoteURL) != "": + return strings.TrimSpace(image.RemoteURL) + default: + return "" + } +} + +func imageFileName(item ManagedItem, endpoint *url.URL, mimeType string) string { + baseName := sanitizeFileName(strings.TrimSpace(item.Title)) + if baseName == "" { + baseName = fmt.Sprintf("arrtrix-%d", item.ID) + } + + ext := strings.TrimSpace(filepath.Ext(endpoint.Path)) + if ext == "" && mimeType != "" { + if extensions, err := mime.ExtensionsByType(mimeType); err == nil && len(extensions) > 0 { + ext = extensions[0] + } + } + if ext == "" { + ext = ".jpg" + } + if item.ID != 0 { + return fmt.Sprintf("%s-%d%s", baseName, item.ID, ext) + } + return baseName + ext +} + +func sanitizeFileName(value string) string { + replacer := strings.NewReplacer( + "<", "", + ">", "", + ":", "", + "\"", "", + "/", "-", + "\\", "-", + "|", "-", + "?", "", + "*", "", + ) + value = replacer.Replace(value) + value = strings.Join(strings.Fields(value), "-") + return strings.Trim(value, ".- ") +} + +func sameHost(left, right *url.URL) bool { + if left == nil || right == nil { + return false + } + return strings.EqualFold(left.Scheme, right.Scheme) && strings.EqualFold(left.Host, right.Host) +} diff --git a/packages/arrtrix/pkg/arrclient/client_test.go b/packages/arrtrix/pkg/arrclient/client_test.go new file mode 100644 index 0000000..ecce6c3 --- /dev/null +++ b/packages/arrtrix/pkg/arrclient/client_test.go @@ -0,0 +1,80 @@ +package arrclient + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +func TestImageURLPrefersPosterAndResolvesRelativePath(t *testing.T) { + baseURL, err := url.Parse("https://radarr.example") + if err != nil { + t.Fatalf("failed to parse base URL: %v", err) + } + + client := &httpClient{baseURL: baseURL} + imageURL := client.imageURL([]mediaImage{ + {CoverType: "fanart", URL: "/MediaCover/1/fanart.jpg"}, + {CoverType: "poster", URL: "/MediaCover/1/poster.jpg"}, + }) + if imageURL != "https://radarr.example/MediaCover/1/poster.jpg" { + t.Fatalf("unexpected image URL %q", imageURL) + } +} + +func TestImageURLFallsBackToRemoteURL(t *testing.T) { + baseURL, err := url.Parse("https://sonarr.example") + if err != nil { + t.Fatalf("failed to parse base URL: %v", err) + } + + client := &httpClient{baseURL: baseURL} + imageURL := client.imageURL([]mediaImage{ + {CoverType: "poster", RemoteURL: "https://images.example/poster.jpg"}, + }) + if imageURL != "https://images.example/poster.jpg" { + t.Fatalf("unexpected remote image URL %q", imageURL) + } +} + +func TestFetchImageUsesAPIKeyForSameHost(t *testing.T) { + headers := make(chan string, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + headers <- r.Header.Get("X-Api-Key") + w.Header().Set("Content-Type", "image/jpeg") + _, _ = w.Write([]byte("jpeg-bytes")) + })) + defer server.Close() + + client, err := newHTTPClient(server.URL, "secret") + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + asset, err := client.FetchImage(context.Background(), ManagedItem{ + ID: 42, + Title: "Dune Part Two", + ImageURL: server.URL + "/MediaCover/42/poster.jpg", + }) + if err != nil { + t.Fatalf("failed to fetch image: %v", err) + } + if asset == nil { + t.Fatal("expected media asset") + } + if got := <-headers; got != "secret" { + t.Fatalf("expected API key header, got %q", got) + } + if got := string(asset.Data); got != "jpeg-bytes" { + t.Fatalf("unexpected media bytes %q", got) + } + if asset.MimeType != "image/jpeg" { + t.Fatalf("unexpected mime type %q", asset.MimeType) + } + if !strings.HasPrefix(asset.FileName, "Dune-Part-Two-42") || !strings.HasSuffix(asset.FileName, ".jpg") { + t.Fatalf("unexpected filename %q", asset.FileName) + } +} diff --git a/packages/arrtrix/pkg/arrclient/radarr.go b/packages/arrtrix/pkg/arrclient/radarr.go new file mode 100644 index 0000000..e214ce3 --- /dev/null +++ b/packages/arrtrix/pkg/arrclient/radarr.go @@ -0,0 +1,172 @@ +package arrclient + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "sneeuwvlok/packages/arrtrix/pkg/arr" +) + +type RadarrClient struct { + http *httpClient + config RadarrConfig +} + +type radarrMovie struct { + ID int64 `json:"id"` + Title string `json:"title"` + Year int `json:"year"` + TMDBID int64 `json:"tmdbId"` + Overview string `json:"overview"` + Monitored bool `json:"monitored"` + Path string `json:"path"` + Images []mediaImage `json:"images"` +} + +func NewRadarrClient(config RadarrConfig) (*RadarrClient, error) { + config.ApplyDefaults() + if err := config.Validate(); err != nil { + return nil, err + } + httpClient, err := newHTTPClient(config.URL, config.APIKey) + if err != nil { + return nil, err + } + return &RadarrClient{http: httpClient, config: config}, nil +} + +func (c *RadarrClient) ContentType() arr.ContentType { + return arr.ContentTypeMovies +} + +func (c *RadarrClient) Search(ctx context.Context, query string) ([]SearchResult, error) { + var response []radarrMovie + if err := c.http.do(ctx, http.MethodGet, "/api/v3/movie/lookup", url.Values{"term": {strings.TrimSpace(query)}}, nil, &response); err != nil { + return nil, err + } + + results := make([]SearchResult, 0, len(response)) + for _, movie := range response { + if movie.TMDBID == 0 { + continue + } + results = append(results, SearchResult{ + LookupID: movie.TMDBID, + Title: movie.Title, + Year: movie.Year, + Overview: movie.Overview, + }) + } + return results, nil +} + +func (c *RadarrClient) List(ctx context.Context, query string) ([]ManagedItem, error) { + var response []radarrMovie + if err := c.http.do(ctx, http.MethodGet, "/api/v3/movie", nil, nil, &response); err != nil { + return nil, err + } + + items := make([]ManagedItem, 0, len(response)) + for _, movie := range response { + if query != "" && !containsFold(movie.Title, query) && !containsFold(strconv.Itoa(movie.Year), query) { + continue + } + items = append(items, ManagedItem{ + ID: movie.ID, + LookupID: movie.TMDBID, + Title: movie.Title, + Year: movie.Year, + Monitored: movie.Monitored, + Path: movie.Path, + ImageURL: c.http.imageURL(movie.Images), + }) + } + return items, nil +} + +func (c *RadarrClient) Add(ctx context.Context, result SearchResult) (*ManagedItem, error) { + payload := map[string]any{ + "title": result.Title, + "tmdbId": result.LookupID, + "year": result.Year, + "qualityProfileId": c.config.QualityProfileID, + "rootFolderPath": c.config.RootFolderPath, + "minimumAvailability": c.config.MinimumAvailability, + "monitored": true, + "addOptions": map[string]any{ + "searchForMovie": c.config.SearchOnAddValue(), + }, + } + + var response radarrMovie + if err := c.http.do(ctx, http.MethodPost, "/api/v3/movie", nil, payload, &response); err != nil { + return nil, err + } + item := ManagedItem{ + ID: response.ID, + LookupID: response.TMDBID, + Title: response.Title, + Year: response.Year, + Monitored: response.Monitored, + Path: response.Path, + ImageURL: c.http.imageURL(response.Images), + } + return &item, nil +} + +func (c *RadarrClient) SetMonitored(ctx context.Context, id int64, monitored bool) (*ManagedItem, error) { + var movie map[string]any + endpoint := "/api/v3/movie/" + strconv.FormatInt(id, 10) + if err := c.http.do(ctx, http.MethodGet, endpoint, nil, nil, &movie); err != nil { + return nil, err + } + movie["monitored"] = monitored + + var response radarrMovie + if err := c.http.do(ctx, http.MethodPut, endpoint, nil, movie, &response); err != nil { + return nil, err + } + item := ManagedItem{ + ID: response.ID, + LookupID: response.TMDBID, + Title: response.Title, + Year: response.Year, + Monitored: response.Monitored, + Path: response.Path, + ImageURL: c.http.imageURL(response.Images), + } + return &item, nil +} + +func (c *RadarrClient) Delete(ctx context.Context, id int64) error { + return c.http.do(ctx, http.MethodDelete, "/api/v3/movie/"+strconv.FormatInt(id, 10), url.Values{ + "deleteFiles": {"false"}, + "addImportExclusion": {"false"}, + }, nil, nil) +} + +func (c *RadarrClient) FetchImage(ctx context.Context, item ManagedItem) (*MediaAsset, error) { + return c.http.FetchImage(ctx, item) +} + +func PickSingleResult(results []SearchResult, query string) (SearchResult, error) { + switch len(results) { + case 0: + return SearchResult{}, fmt.Errorf("no matching result found for %q", query) + case 1: + return results[0], nil + default: + normalized := strings.TrimSpace(strings.ToLower(query)) + for _, result := range results { + title := strings.ToLower(FormatSearchResult(result)) + if title == normalized { + return result, nil + } + } + return SearchResult{}, fmt.Errorf("multiple results matched %q", query) + } +} diff --git a/packages/arrtrix/pkg/arrclient/sonarr.go b/packages/arrtrix/pkg/arrclient/sonarr.go new file mode 100644 index 0000000..caa6cec --- /dev/null +++ b/packages/arrtrix/pkg/arrclient/sonarr.go @@ -0,0 +1,157 @@ +package arrclient + +import ( + "context" + "net/http" + "net/url" + "strconv" + "strings" + + "sneeuwvlok/packages/arrtrix/pkg/arr" +) + +type SonarrClient struct { + http *httpClient + config SonarrConfig +} + +type sonarrSeries struct { + ID int64 `json:"id"` + Title string `json:"title"` + Year int `json:"year"` + TVDBID int64 `json:"tvdbId"` + Overview string `json:"overview"` + Monitored bool `json:"monitored"` + Path string `json:"path"` + Images []mediaImage `json:"images"` +} + +func NewSonarrClient(config SonarrConfig) (*SonarrClient, error) { + config.ApplyDefaults() + if err := config.Validate(); err != nil { + return nil, err + } + httpClient, err := newHTTPClient(config.URL, config.APIKey) + if err != nil { + return nil, err + } + return &SonarrClient{http: httpClient, config: config}, nil +} + +func (c *SonarrClient) ContentType() arr.ContentType { + return arr.ContentTypeSeries +} + +func (c *SonarrClient) Search(ctx context.Context, query string) ([]SearchResult, error) { + var response []sonarrSeries + if err := c.http.do(ctx, http.MethodGet, "/api/v3/series/lookup", url.Values{"term": {strings.TrimSpace(query)}}, nil, &response); err != nil { + return nil, err + } + + results := make([]SearchResult, 0, len(response)) + for _, series := range response { + if series.TVDBID == 0 { + continue + } + results = append(results, SearchResult{ + LookupID: series.TVDBID, + Title: series.Title, + Year: series.Year, + Overview: series.Overview, + }) + } + return results, nil +} + +func (c *SonarrClient) List(ctx context.Context, query string) ([]ManagedItem, error) { + var response []sonarrSeries + if err := c.http.do(ctx, http.MethodGet, "/api/v3/series", nil, nil, &response); err != nil { + return nil, err + } + + items := make([]ManagedItem, 0, len(response)) + for _, series := range response { + if query != "" && !containsFold(series.Title, query) && !containsFold(strconv.Itoa(series.Year), query) { + continue + } + items = append(items, ManagedItem{ + ID: series.ID, + LookupID: series.TVDBID, + Title: series.Title, + Year: series.Year, + Monitored: series.Monitored, + Path: series.Path, + ImageURL: c.http.imageURL(series.Images), + }) + } + return items, nil +} + +func (c *SonarrClient) Add(ctx context.Context, result SearchResult) (*ManagedItem, error) { + payload := map[string]any{ + "title": result.Title, + "tvdbId": result.LookupID, + "qualityProfileId": c.config.QualityProfileID, + "languageProfileId": c.config.LanguageProfileID, + "rootFolderPath": c.config.RootFolderPath, + "seasonFolder": c.config.SeasonFolderValue(), + "monitored": true, + "seriesType": c.config.SeriesType, + "addOptions": map[string]any{ + "searchForMissingEpisodes": c.config.SearchOnAddValue(), + }, + } + if result.Year != 0 { + payload["year"] = result.Year + } + + var response sonarrSeries + if err := c.http.do(ctx, http.MethodPost, "/api/v3/series", nil, payload, &response); err != nil { + return nil, err + } + item := ManagedItem{ + ID: response.ID, + LookupID: response.TVDBID, + Title: response.Title, + Year: response.Year, + Monitored: response.Monitored, + Path: response.Path, + ImageURL: c.http.imageURL(response.Images), + } + return &item, nil +} + +func (c *SonarrClient) SetMonitored(ctx context.Context, id int64, monitored bool) (*ManagedItem, error) { + var series map[string]any + endpoint := "/api/v3/series/" + strconv.FormatInt(id, 10) + if err := c.http.do(ctx, http.MethodGet, endpoint, nil, nil, &series); err != nil { + return nil, err + } + series["monitored"] = monitored + + var response sonarrSeries + if err := c.http.do(ctx, http.MethodPut, endpoint, nil, series, &response); err != nil { + return nil, err + } + item := ManagedItem{ + ID: response.ID, + LookupID: response.TVDBID, + Title: response.Title, + Year: response.Year, + Monitored: response.Monitored, + Path: response.Path, + ImageURL: c.http.imageURL(response.Images), + } + return &item, nil +} + +func (c *SonarrClient) Delete(ctx context.Context, id int64) error { + return c.http.do(ctx, http.MethodDelete, "/api/v3/series/"+strconv.FormatInt(id, 10), url.Values{ + "deleteFiles": {"false"}, + "addImportListExclusion": {"false"}, + }, nil, nil) +} + +func (c *SonarrClient) FetchImage(ctx context.Context, item ManagedItem) (*MediaAsset, error) { + return c.http.FetchImage(ctx, item) +} diff --git a/packages/arrtrix/pkg/config/config.go b/packages/arrtrix/pkg/config/config.go new file mode 100644 index 0000000..ff97e98 --- /dev/null +++ b/packages/arrtrix/pkg/config/config.go @@ -0,0 +1,65 @@ +package config + +import ( + "go.mau.fi/util/dbutil" + "go.mau.fi/zeroconfig" + "gopkg.in/yaml.v3" + + "maunium.net/go/mautrix/bridgev2/bridgeconfig" + + "sneeuwvlok/packages/arrtrix/pkg/observability" +) + +type Config struct { + Network yaml.Node `yaml:"network"` + + Bridge bridgeconfig.BridgeConfig `yaml:"bridge"` + Database dbutil.Config `yaml:"database"` + Homeserver bridgeconfig.HomeserverConfig `yaml:"homeserver"` + AppService bridgeconfig.AppserviceConfig `yaml:"appservice"` + Logging zeroconfig.Config `yaml:"logging"` + + Observability observability.Config `yaml:"observability"` + EnvConfigPrefix string `yaml:"env_config_prefix"` + ManagementTexts bridgeconfig.ManagementRoomTexts `yaml:"management_room_texts"` +} + +func Load(data []byte) (*Config, error) { + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + cfg.applyDefaults() + return &cfg, nil +} + +func (c *Config) applyDefaults() { + if c.Homeserver.Software == "" { + c.Homeserver.Software = bridgeconfig.SoftwareStandard + } + c.Observability.ApplyDefaults() +} + +func (c *Config) Compile() bridgeconfig.Config { + return bridgeconfig.Config{ + Network: c.Network, + Bridge: c.Bridge, + Database: c.Database, + Homeserver: c.Homeserver, + AppService: c.AppService, + Logging: c.Logging, + EnvConfigPrefix: c.EnvConfigPrefix, + ManagementRoomTexts: c.ManagementTexts, + Matrix: bridgeconfig.MatrixConfig{ + MessageStatusEvents: false, + DeliveryReceipts: false, + MessageErrorNotices: true, + SyncDirectChatList: false, + FederateRooms: true, + }, + DoublePuppet: bridgeconfig.DoublePuppetConfig{ + Servers: map[string]string{}, + Secrets: map[string]string{}, + }, + } +} diff --git a/packages/arrtrix/pkg/config/config_test.go b/packages/arrtrix/pkg/config/config_test.go new file mode 100644 index 0000000..84b09df --- /dev/null +++ b/packages/arrtrix/pkg/config/config_test.go @@ -0,0 +1,159 @@ +package config + +import ( + "testing" + + "maunium.net/go/mautrix/bridgev2/bridgeconfig" +) + +func TestLoadDefaultsHomeserverSoftware(t *testing.T) { + cfg, err := Load([]byte(` +bridge: + command_prefix: "!arr" +homeserver: + address: http://127.0.0.1:8008 + domain: test.local +appservice: + id: arrtrix + bot: + username: arrtrixbot + displayname: Arrtrix Bot + username_template: arrtrix_{{.}} +database: + type: sqlite3-fk-wal + uri: file:arrtrix.db?_txlock=immediate +logging: + min_level: info + writers: + - type: stdout + format: pretty-colored +`)) + if err != nil { + t.Fatalf("Load returned error: %v", err) + } + + if cfg.Homeserver.Software != bridgeconfig.SoftwareStandard { + t.Fatalf("expected homeserver software default %q, got %q", bridgeconfig.SoftwareStandard, cfg.Homeserver.Software) + } +} + +func TestCompileSetsInternalDefaultsForHiddenSections(t *testing.T) { + cfg, err := Load([]byte(` +bridge: + command_prefix: "!arr" + permissions: + "*": relay +homeserver: + address: http://127.0.0.1:8008 + domain: test.local +appservice: + id: arrtrix + bot: + username: arrtrixbot + displayname: Arrtrix Bot + username_template: arrtrix_{{.}} +database: + type: sqlite3-fk-wal + uri: file:arrtrix.db?_txlock=immediate +logging: + min_level: info + writers: + - type: stdout + format: pretty-colored +`)) + if err != nil { + t.Fatalf("Load returned error: %v", err) + } + + runtimeCfg := cfg.Compile() + if !runtimeCfg.Matrix.MessageErrorNotices { + t.Fatalf("expected message error notices to stay enabled") + } + if !runtimeCfg.Matrix.FederateRooms { + t.Fatalf("expected federated rooms to stay enabled") + } + if runtimeCfg.DoublePuppet.Servers == nil || runtimeCfg.DoublePuppet.Secrets == nil { + t.Fatalf("expected hidden double puppet maps to be initialized") + } +} + +func TestLoadIgnoresLegacyHiddenSections(t *testing.T) { + cfg, err := Load([]byte(` +bridge: + command_prefix: "!arr" +homeserver: + address: http://127.0.0.1:8008 + domain: test.local +appservice: + id: arrtrix + bot: + username: arrtrixbot + displayname: Arrtrix Bot + username_template: arrtrix_{{.}} +database: + type: sqlite3-fk-wal + uri: file:arrtrix.db?_txlock=immediate +logging: + min_level: info + writers: + - type: stdout + format: pretty-colored +matrix: + federate_rooms: false +provisioning: + shared_secret: ignored +double_puppet: + secrets: + test.local: secret +encryption: + allow: true +`)) + if err != nil { + t.Fatalf("Load returned error: %v", err) + } + + runtimeCfg := cfg.Compile() + if !runtimeCfg.Matrix.FederateRooms { + t.Fatalf("expected runtime defaults to win for hidden legacy sections") + } + if len(runtimeCfg.DoublePuppet.Secrets) != 0 { + t.Fatalf("expected hidden double puppet secrets to stay internal-only") + } +} + +func TestLoadIgnoresLegacyWebhookSettings(t *testing.T) { + cfg, err := Load([]byte(` +network: + webhooks: + radarr: + enabled: true + path: /_arrtrix/webhooks/radarr + secret: legacy-secret +bridge: + command_prefix: "!arr" +homeserver: + address: http://127.0.0.1:8008 + domain: test.local +appservice: + id: arrtrix + bot: + username: arrtrixbot + displayname: Arrtrix Bot + username_template: arrtrix_{{.}} +database: + type: sqlite3-fk-wal + uri: file:arrtrix.db?_txlock=immediate +logging: + min_level: info + writers: + - type: stdout + format: pretty-colored +`)) + if err != nil { + t.Fatalf("Load returned error: %v", err) + } + + if cfg == nil { + t.Fatal("expected config to load") + } +} diff --git a/packages/arrtrix/pkg/connector/config.go b/packages/arrtrix/pkg/connector/config.go new file mode 100644 index 0000000..149fd32 --- /dev/null +++ b/packages/arrtrix/pkg/connector/config.go @@ -0,0 +1,74 @@ +package connector + +import ( + _ "embed" + "fmt" + "net/http" + + up "go.mau.fi/util/configupgrade" + "maunium.net/go/mautrix/bridgev2" + + "sneeuwvlok/packages/arrtrix/pkg/arr" + "sneeuwvlok/packages/arrtrix/pkg/arrclient" + "sneeuwvlok/packages/arrtrix/pkg/subscriptions" + "sneeuwvlok/packages/arrtrix/pkg/webhook" +) + +//go:embed example-config.yaml +var ExampleConfig string + +type Config struct { + Content ContentConfig `yaml:"content"` +} + +type ContentConfig struct { + Movies arrclient.RadarrConfig `yaml:"movies"` + Series arrclient.SonarrConfig `yaml:"series"` +} + +func upgradeConfig(helper up.Helper) {} + +func (s *ArrtrixConnector) GetConfig() (string, any, up.Upgrader) { + return ExampleConfig, &s.Config, up.SimpleUpgrader(upgradeConfig) +} + +func (s *ArrtrixConnector) ValidateConfig() error { + s.Config.Content.Movies.ApplyDefaults() + s.Config.Content.Series.ApplyDefaults() + if err := s.Config.Content.Movies.Validate(); err != nil { + return err + } + if err := s.Config.Content.Series.Validate(); err != nil { + return err + } + return nil +} + +func (s *ArrtrixConnector) MountRoutes(router *http.ServeMux) error { + if s.Bridge == nil { + return fmt.Errorf("bridge is not initialized") + } + return webhook.MountArr(router, s.Bridge, s.Subscriptions()) +} + +var _ bridgev2.ConfigValidatingNetwork = (*ArrtrixConnector)(nil) +var _ webhook.SubscriptionFilter = (*subscriptions.Repository)(nil) + +func (c ContentConfig) Client(contentType arr.ContentType) (arrclient.Client, bool, error) { + switch contentType { + case arr.ContentTypeMovies: + if !c.Movies.Enabled() { + return nil, false, nil + } + client, err := arrclient.NewRadarrClient(c.Movies) + return client, true, err + case arr.ContentTypeSeries: + if !c.Series.Enabled() { + return nil, false, nil + } + client, err := arrclient.NewSonarrClient(c.Series) + return client, true, err + default: + return nil, false, fmt.Errorf("unsupported content type %q", contentType) + } +} diff --git a/packages/arrtrix/pkg/connector/config_test.go b/packages/arrtrix/pkg/connector/config_test.go new file mode 100644 index 0000000..9516e37 --- /dev/null +++ b/packages/arrtrix/pkg/connector/config_test.go @@ -0,0 +1,23 @@ +package connector + +import "testing" + +func TestValidateConfigRejectsPartialMoviesConfig(t *testing.T) { + conn := &ArrtrixConnector{ + Config: Config{ + Content: ContentConfig{}, + }, + } + conn.Config.Content.Movies.URL = "http://radarr.test" + + if err := conn.ValidateConfig(); err == nil { + t.Fatal("expected partial movies config to fail validation") + } +} + +func TestValidateConfigAllowsEmptyContentConfig(t *testing.T) { + conn := &ArrtrixConnector{} + if err := conn.ValidateConfig(); err != nil { + t.Fatalf("ValidateConfig returned error: %v", err) + } +} diff --git a/packages/arrtrix/pkg/connector/connector.go b/packages/arrtrix/pkg/connector/connector.go new file mode 100644 index 0000000..4be007a --- /dev/null +++ b/packages/arrtrix/pkg/connector/connector.go @@ -0,0 +1,135 @@ +package connector + +import ( + "context" + "fmt" + "net/http" + + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/database" + "maunium.net/go/mautrix/bridgev2/networkid" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + + "sneeuwvlok/packages/arrtrix/pkg/arr" + "sneeuwvlok/packages/arrtrix/pkg/arrclient" + "sneeuwvlok/packages/arrtrix/pkg/subscriptions" +) + +type ArrtrixConnector struct { + Bridge *bridgev2.Bridge + Config Config + clients map[arr.ContentType]arrclient.Client + subscriptions *subscriptions.Repository +} + +var _ bridgev2.NetworkConnector = (*ArrtrixConnector)(nil) +var _ interface{ MountRoutes(*http.ServeMux) error } = (*ArrtrixConnector)(nil) + +func (s *ArrtrixConnector) GetName() bridgev2.BridgeName { + return bridgev2.BridgeName{ + DisplayName: "Arrtrix", + NetworkURL: "https://wiki.servarr.com/", + NetworkID: "arrtrix", + BeeperBridgeType: "arrtrix", + DefaultPort: 29329, + DefaultCommandPrefix: "!arr", + } +} + +func (s *ArrtrixConnector) Init(bridge *bridgev2.Bridge) { + s.Bridge = bridge + s.subscriptions = subscriptions.NewRepository(bridge.DB.Database, string(bridge.ID)) + s.clients = make(map[arr.ContentType]arrclient.Client) + for _, contentType := range arr.SupportedContentTypes() { + client, ok, err := s.Config.Content.Client(contentType) + if err != nil { + panic(err) + } + if ok { + s.clients[contentType] = client + } + } +} + +func (s *ArrtrixConnector) Start(context.Context) error { + return nil +} + +func (s *ArrtrixConnector) GetDBMetaTypes() database.MetaTypes { + return database.MetaTypes{} +} + +func (s *ArrtrixConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities { + return &bridgev2.NetworkGeneralCapabilities{} +} + +func (s *ArrtrixConnector) LoadUserLogin(_ context.Context, login *bridgev2.UserLogin) error { + login.Client = &ArrtrixClient{ + Main: s, + UserLogin: login, + } + return nil +} + +func (s *ArrtrixConnector) GetLoginFlows() []bridgev2.LoginFlow { + return nil +} + +func (s *ArrtrixConnector) CreateLogin(_ context.Context, _ *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) { + return nil, fmt.Errorf("login flow %q is not implemented", flowID) +} + +func (s *ArrtrixConnector) GetBridgeInfoVersion() (info, capabilities int) { + return 1, 1 +} + +type ArrtrixClient struct { + Main *ArrtrixConnector + UserLogin *bridgev2.UserLogin +} + +var _ bridgev2.NetworkAPI = (*ArrtrixClient)(nil) + +func (c *ArrtrixClient) Connect(context.Context) {} + +func (c *ArrtrixClient) Disconnect() {} + +func (c *ArrtrixClient) IsLoggedIn() bool { + return false +} + +func (c *ArrtrixClient) LogoutRemote(context.Context) {} + +func (c *ArrtrixClient) IsThisUser(context.Context, networkid.UserID) bool { + return false +} + +func (c *ArrtrixClient) GetChatInfo(context.Context, *bridgev2.Portal) (*bridgev2.ChatInfo, error) { + return &bridgev2.ChatInfo{}, nil +} + +func (c *ArrtrixClient) GetUserInfo(context.Context, *bridgev2.Ghost) (*bridgev2.UserInfo, error) { + return &bridgev2.UserInfo{}, nil +} + +func (c *ArrtrixClient) GetCapabilities(context.Context, *bridgev2.Portal) *event.RoomFeatures { + return &event.RoomFeatures{} +} + +func (c *ArrtrixClient) HandleMatrixMessage(context.Context, *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) { + return nil, fmt.Errorf("bridging Matrix messages is not implemented") +} + +func (c *ArrtrixClient) GenerateTransactionID(userID id.UserID, roomID id.RoomID, eventType event.Type) networkid.RawTransactionID { + return networkid.RawTransactionID("") +} + +func (s *ArrtrixConnector) ContentClient(contentType arr.ContentType) (arrclient.Client, bool) { + client, ok := s.clients[contentType] + return client, ok +} + +func (s *ArrtrixConnector) Subscriptions() *subscriptions.Repository { + return s.subscriptions +} diff --git a/packages/arrtrix/pkg/connector/example-config.yaml b/packages/arrtrix/pkg/connector/example-config.yaml new file mode 100644 index 0000000..a917e23 --- /dev/null +++ b/packages/arrtrix/pkg/connector/example-config.yaml @@ -0,0 +1,23 @@ +content: + movies: + # Radarr connection for movie management commands. + url: "" + api_key: "" + root_folder_path: "" + quality_profile_id: 0 + minimum_availability: released + search_on_add: true + + series: + # Sonarr connection for series management commands. + url: "" + api_key: "" + root_folder_path: "" + quality_profile_id: 0 + language_profile_id: 0 + season_folder: true + series_type: standard + search_on_add: true + +# Arr-stack webhooks are exposed automatically on the fixed built-in path: +# POST /_arrtrix/webhook diff --git a/packages/arrtrix/pkg/matrixcmd/download.go b/packages/arrtrix/pkg/matrixcmd/download.go new file mode 100644 index 0000000..23414b1 --- /dev/null +++ b/packages/arrtrix/pkg/matrixcmd/download.go @@ -0,0 +1,260 @@ +package matrixcmd + +import ( + "fmt" + "strconv" + "strings" + + "maunium.net/go/mautrix/id" + + "sneeuwvlok/packages/arrtrix/pkg/arr" + "sneeuwvlok/packages/arrtrix/pkg/arrclient" + "sneeuwvlok/packages/arrtrix/pkg/subscriptions" +) + +type commandServiceProvider interface { + ContentClient(arr.ContentType) (arrclient.Client, bool) + Subscriptions() *subscriptions.Repository +} + +func NewDownloadHandler() Handler { + return NewHandler(Meta{ + Name: "download", + Description: "Manage monitored movies and series in Arr.", + Usage: " [...]", + }, func(ctx *Context) { + if len(ctx.Args) < 2 { + ctx.Reply("Usage: `download [...]`") + return + } + + contentType, err := arr.ParseContentType(ctx.Args[1]) + if err != nil { + ctx.Reply(err.Error()) + return + } + + client, ok := contentClient(ctx, contentType) + if !ok { + ctx.Reply("No %s client is configured yet.", contentType.Label()) + return + } + + switch strings.ToLower(ctx.Args[0]) { + case "list": + handleDownloadList(ctx, client, contentType) + case "search": + handleDownloadSearch(ctx, client, contentType) + case "add": + handleDownloadAdd(ctx, client, contentType) + case "monitor": + handleDownloadMonitor(ctx, client, contentType) + case "remove": + handleDownloadRemove(ctx, client, contentType) + default: + ctx.Reply("Unknown download subcommand `%s`.", ctx.Args[0]) + } + }) +} + +func handleDownloadList(ctx *Context, client arrclient.Client, contentType arr.ContentType) { + query := strings.TrimSpace(strings.Join(ctx.Args[2:], " ")) + items, err := client.List(ctx.Ctx, query) + if err != nil { + ctx.Reply("Failed to list %s: %v", contentType.Label(), err) + return + } + if len(items) == 0 { + if query == "" { + ctx.Reply("No monitored %s are currently tracked.", contentType.Label()) + } else { + ctx.Reply("No %s matched `%s`.", contentType.Label(), query) + } + return + } + + count := len(items) + if count > 12 { + count = 12 + } + ctx.Reply("Tracked %s (showing %d of %d):", contentType.Label(), count, len(items)) + for i, item := range items { + if i == 12 { + break + } + if err := replyWithManagedItem(ctx, client, item); err != nil { + ctx.Log.Err(err).Int64("item_id", item.ID).Str("content_type", contentType.Label()).Msg("Failed to send Matrix-native image for download listing") + } + } + if len(items) > 12 { + ctx.Reply("…and %d more.", len(items)-12) + } +} + +func handleDownloadSearch(ctx *Context, client arrclient.Client, contentType arr.ContentType) { + query := strings.TrimSpace(strings.Join(ctx.Args[2:], " ")) + if query == "" { + ctx.Reply("Usage: `download search %s `", contentType.Label()) + return + } + results, err := client.Search(ctx.Ctx, query) + if err != nil { + ctx.Reply("Failed to search %s: %v", contentType.Label(), err) + return + } + replyWithSearchResults(ctx, contentType, query, results) +} + +func handleDownloadAdd(ctx *Context, client arrclient.Client, contentType arr.ContentType) { + query := strings.TrimSpace(strings.Join(ctx.Args[2:], " ")) + if query == "" { + ctx.Reply("Usage: `download add %s `", contentType.Label()) + return + } + results, err := client.Search(ctx.Ctx, query) + if err != nil { + ctx.Reply("Failed to search %s: %v", contentType.Label(), err) + return + } + result, err := arrclient.PickSingleResult(results, query) + if err != nil { + replyWithSearchResults(ctx, contentType, query, results) + return + } + item, err := client.Add(ctx.Ctx, result) + if err != nil { + ctx.Reply("Failed to add %s: %v", contentType.Label(), err) + return + } + ctx.Reply("Added %s to %s with id `%d`.", formatManagedItem(*item), contentType.Label(), item.ID) +} + +func handleDownloadMonitor(ctx *Context, client arrclient.Client, contentType arr.ContentType) { + if len(ctx.Args) < 4 { + ctx.Reply("Usage: `download monitor %s `", contentType.Label()) + return + } + itemID, err := strconv.ParseInt(ctx.Args[2], 10, 64) + if err != nil { + ctx.Reply("Invalid %s id `%s`.", contentType.Label(), ctx.Args[2]) + return + } + + state, err := parseEnabled(ctx.Args[3]) + if err != nil { + ctx.Reply(err.Error()) + return + } + item, err := client.SetMonitored(ctx.Ctx, itemID, state) + if err != nil { + ctx.Reply("Failed to update %s monitoring: %v", contentType.Label(), err) + return + } + ctx.Reply("%s is now monitored=%t.", formatManagedItem(*item), item.Monitored) +} + +func handleDownloadRemove(ctx *Context, client arrclient.Client, contentType arr.ContentType) { + if len(ctx.Args) < 3 { + ctx.Reply("Usage: `download remove %s `", contentType.Label()) + return + } + itemID, err := strconv.ParseInt(ctx.Args[2], 10, 64) + if err != nil { + ctx.Reply("Invalid %s id `%s`.", contentType.Label(), ctx.Args[2]) + return + } + if err = client.Delete(ctx.Ctx, itemID); err != nil { + ctx.Reply("Failed to remove %s: %v", contentType.Label(), err) + return + } + ctx.Reply("Removed `%d` from %s.", itemID, contentType.Label()) +} + +func contentClient(ctx *Context, contentType arr.ContentType) (arrclient.Client, bool) { + provider, ok := ctx.Bridge.Network.(commandServiceProvider) + if !ok { + return nil, false + } + return provider.ContentClient(contentType) +} + +func contentSubscriptions(ctx *Context) *subscriptions.Repository { + provider, ok := ctx.Bridge.Network.(commandServiceProvider) + if !ok { + return nil + } + return provider.Subscriptions() +} + +func replyWithSearchResults(ctx *Context, contentType arr.ContentType, query string, results []arrclient.SearchResult) { + if len(results) == 0 { + ctx.Reply("No %s matched `%s`.", contentType.Label(), query) + return + } + + var builder strings.Builder + builder.WriteString(fmt.Sprintf("Search results for `%s` in %s:\n", query, contentType.Label())) + for i, result := range results { + if i == 8 { + builder.WriteString("…\n") + break + } + builder.WriteString(fmt.Sprintf("- `%d` %s\n", result.LookupID, arrclient.FormatSearchResult(result))) + } + builder.WriteString(fmt.Sprintf("\nRefine the query and rerun `download add %s ` until only one match remains.", contentType.Label())) + ctx.Reply(builder.String()) +} + +func formatManagedItem(item arrclient.ManagedItem) string { + return arrclient.FormatManagedItem(item) +} + +func parseEnabled(value string) (bool, error) { + switch strings.ToLower(strings.TrimSpace(value)) { + case "on", "true", "yes", "enabled": + return true, nil + case "off", "false", "no", "disabled": + return false, nil + default: + return false, fmt.Errorf("expected `on` or `off`, got `%s`", value) + } +} + +func userIDString(userID id.UserID) string { + return userID.String() +} + +func replyWithManagedItem(ctx *Context, client arrclient.Client, item arrclient.ManagedItem) error { + details := formatDownloadListCaption(item) + if item.ImageURL != "" { + asset, err := client.FetchImage(ctx.Ctx, item) + if err != nil { + ctx.Log.Err(err).Int64("item_id", item.ID).Msg("Failed to fetch poster for Matrix listing") + } else if asset != nil { + if err := ctx.SendImage(asset, details); err != nil { + ctx.Log.Err(err).Int64("item_id", item.ID).Msg("Failed to upload poster for Matrix listing") + } else { + return nil + } + } else { + ctx.Log.Debug().Int64("item_id", item.ID).Msg("Poster was empty for Matrix listing") + } + } + ctx.Reply(details) + return nil +} + +func formatDownloadListCaption(item arrclient.ManagedItem) string { + return fmt.Sprintf("%s %s", monitoredIcon(item.Monitored), arrclient.FormatManagedItem(item)) +} + +func formatDownloadListFallbackCard(item arrclient.ManagedItem) string { + return formatDownloadListCaption(item) +} + +func monitoredIcon(monitored bool) string { + if monitored { + return "👁" + } + return "🚫" +} diff --git a/packages/arrtrix/pkg/matrixcmd/download_test.go b/packages/arrtrix/pkg/matrixcmd/download_test.go new file mode 100644 index 0000000..19b93b9 --- /dev/null +++ b/packages/arrtrix/pkg/matrixcmd/download_test.go @@ -0,0 +1,44 @@ +package matrixcmd + +import ( + "testing" + + "sneeuwvlok/packages/arrtrix/pkg/arrclient" +) + +func TestFormatDownloadListFallbackCardUsesMonitoredIcon(t *testing.T) { + item := arrclient.ManagedItem{ + ID: 1, + Title: "Severance", + Year: 2022, + Monitored: true, + } + + fallback := formatDownloadListFallbackCard(item) + if fallback != "👁 Severance (2022)" { + t.Fatalf("unexpected monitored fallback %q", fallback) + } +} + +func TestFormatDownloadListFallbackCardUsesUnmonitoredIcon(t *testing.T) { + item := arrclient.ManagedItem{ + ID: 7, + Title: "Andor", + Year: 2022, + Monitored: false, + } + + fallback := formatDownloadListFallbackCard(item) + if fallback != "🚫 Andor (2022)" { + t.Fatalf("unexpected unmonitored fallback %q", fallback) + } +} + +func TestMonitoredIcon(t *testing.T) { + if monitoredIcon(true) != "👁" { + t.Fatalf("expected monitored icon, got %q", monitoredIcon(true)) + } + if monitoredIcon(false) != "🚫" { + t.Fatalf("expected unmonitored icon, got %q", monitoredIcon(false)) + } +} diff --git a/packages/arrtrix/pkg/matrixcmd/help.go b/packages/arrtrix/pkg/matrixcmd/help.go new file mode 100644 index 0000000..7da0d84 --- /dev/null +++ b/packages/arrtrix/pkg/matrixcmd/help.go @@ -0,0 +1,60 @@ +package matrixcmd + +import ( + "fmt" + "sort" + "strings" +) + +func NewHelpHandler(proc *Processor) Handler { + return NewHandler(Meta{ + Name: "help", + Description: "Show this help message.", + }, func(ctx *Context) { + ctx.Reply(formatHelp(proc, ctx)) + }) +} + +func formatHelp(proc *Processor, ctx *Context) string { + var builder strings.Builder + + switch { + case ctx.RoomID == ctx.User.ManagementRoom: + builder.WriteString(fmt.Sprintf("This is your management room: prefixing commands with `%s` is not required.\n", ctx.Bridge.Config.CommandPrefix)) + case ctx.Portal != nil: + builder.WriteString(fmt.Sprintf("**This is a portal room**: you must always prefix commands with `%s`. Management commands will not be bridged.\n", ctx.Bridge.Config.CommandPrefix)) + default: + builder.WriteString(fmt.Sprintf("This is not your management room: prefixing commands with `%s` is required.\n", ctx.Bridge.Config.CommandPrefix)) + } + + builder.WriteString("Parameters in [square brackets] are optional, while parameters in are required.\n\n") + builder.WriteString("#### General\n") + + handlers := proc.Handlers() + sort.SliceStable(handlers, func(i, j int) bool { + return handlers[i].Meta().Name < handlers[j].Meta().Name + }) + for _, handler := range handlers { + meta := handler.Meta() + builder.WriteString("**") + builder.WriteString(meta.Name) + builder.WriteString("**") + if meta.Usage != "" { + builder.WriteByte(' ') + builder.WriteString(meta.Usage) + } + if meta.Description != "" { + builder.WriteString(" - ") + builder.WriteString(meta.Description) + } + builder.WriteByte('\n') + } + + if extra := strings.TrimSpace(ctx.Processor.texts.AdditionalHelp); extra != "" { + builder.WriteByte('\n') + builder.WriteString(extra) + builder.WriteByte('\n') + } + + return builder.String() +} diff --git a/packages/arrtrix/pkg/matrixcmd/help_test.go b/packages/arrtrix/pkg/matrixcmd/help_test.go new file mode 100644 index 0000000..817f7ed --- /dev/null +++ b/packages/arrtrix/pkg/matrixcmd/help_test.go @@ -0,0 +1,46 @@ +package matrixcmd + +import ( + "strings" + "testing" + + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/bridgeconfig" + "maunium.net/go/mautrix/bridgev2/database" + "maunium.net/go/mautrix/id" +) + +func TestFormatHelpManagementRoom(t *testing.T) { + roomID := id.RoomID("!arrtrix:test") + proc := &Processor{ + texts: bridgeconfig.ManagementRoomTexts{AdditionalHelp: "Extra help text."}, + command: make(map[string]Handler), + alias: make(map[string]string), + } + proc.Add(NewHelpHandler(proc)) + proc.Add(NewDownloadHandler()) + proc.Add(NewSubscriptionsHandler()) + + out := formatHelp(proc, &Context{ + Bridge: &bridgev2.Bridge{ + Config: &bridgeconfig.BridgeConfig{ + CommandPrefix: "!arr", + }, + }, + RoomID: roomID, + User: &bridgev2.User{User: &database.User{ManagementRoom: roomID}}, + Processor: proc, + }) + + for _, fragment := range []string{ + "prefixing commands with `!arr` is not required", + "**download** [...] - Manage monitored movies and series in Arr.", + "**help** - Show this help message.", + "**subscriptions** [movies|series] [event-type|all] - Manage notification subscriptions by content type and event type.", + "Extra help text.", + } { + if !strings.Contains(out, fragment) { + t.Fatalf("expected help output to contain %q, got:\n%s", fragment, out) + } + } +} diff --git a/packages/arrtrix/pkg/matrixcmd/processor.go b/packages/arrtrix/pkg/matrixcmd/processor.go new file mode 100644 index 0000000..e9d3980 --- /dev/null +++ b/packages/arrtrix/pkg/matrixcmd/processor.go @@ -0,0 +1,270 @@ +package matrixcmd + +import ( + "context" + "fmt" + "runtime/debug" + "sort" + "strings" + + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/bridgeconfig" + "maunium.net/go/mautrix/bridgev2/status" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" + + "sneeuwvlok/packages/arrtrix/pkg/arrclient" + "sneeuwvlok/packages/arrtrix/pkg/observability" +) + +type Handler interface { + Meta() Meta + Run(*Context) +} + +type Meta struct { + Name string + Description string + Usage string + Aliases []string +} + +type HandlerFunc struct { + meta Meta + run func(*Context) +} + +func NewHandler(meta Meta, run func(*Context)) Handler { + return HandlerFunc{meta: meta, run: run} +} + +func (h HandlerFunc) Meta() Meta { + return h.meta +} + +func (h HandlerFunc) Run(ctx *Context) { + h.run(ctx) +} + +type Processor struct { + bridge *bridgev2.Bridge + bot bridgev2.MatrixAPI + texts bridgeconfig.ManagementRoomTexts + command map[string]Handler + alias map[string]string + order []string +} + +type Context struct { + Bridge *bridgev2.Bridge + Bot bridgev2.MatrixAPI + RoomID id.RoomID + OrigRoomID id.RoomID + EventID id.EventID + ReplyTo id.EventID + User *bridgev2.User + Portal *bridgev2.Portal + Command string + Args []string + RawArgs string + Ctx context.Context + Log *zerolog.Logger + Processor *Processor +} + +var _ bridgev2.CommandProcessor = (*Processor)(nil) + +func NewProcessor(bridge *bridgev2.Bridge, texts bridgeconfig.ManagementRoomTexts) *Processor { + proc := &Processor{ + bridge: bridge, + bot: bridge.Bot, + texts: texts, + command: make(map[string]Handler), + alias: make(map[string]string), + } + proc.Add(NewHelpHandler(proc)) + proc.Add(NewDownloadHandler()) + proc.Add(NewSubscriptionsHandler()) + return proc +} + +func (p *Processor) Add(handler Handler) { + meta := handler.Meta() + p.command[meta.Name] = handler + p.order = append(p.order, meta.Name) + for _, alias := range meta.Aliases { + p.alias[alias] = meta.Name + } +} + +func (p *Processor) Handlers() []Handler { + names := append([]string(nil), p.order...) + sort.Strings(names) + + handlers := make([]Handler, 0, len(names)) + for _, name := range names { + handler, ok := p.command[name] + if ok { + handlers = append(handlers, handler) + } + } + return handlers +} + +func (p *Processor) Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user *bridgev2.User, message string, replyTo id.EventID) { + ctx, span := observability.StartSpan(ctx, "arrtrix.matrix.command") + defer span.End() + + ms := &bridgev2.MessageStatus{ + Step: status.MsgStepCommand, + Status: event.MessageStatusSuccess, + } + + logCopy := zerolog.Ctx(ctx).With().Logger() + log := &logCopy + outcome := "success" + commandName := "unknown-command" + + defer func() { + statusInfo := &bridgev2.MessageStatusEventInfo{ + RoomID: roomID, + SourceEventID: eventID, + EventType: event.EventMessage, + Sender: user.MXID, + } + + if recovered := recover(); recovered != nil { + logEvt := log.Error().Bytes(zerolog.ErrorStackFieldName, debug.Stack()) + if err, ok := recovered.(error); ok { + logEvt = logEvt.Err(err) + ms.InternalError = err + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } else { + logEvt = logEvt.Any(zerolog.ErrorFieldName, recovered) + ms.InternalError = fmt.Errorf("%v", recovered) + span.SetStatus(codes.Error, "panic") + } + logEvt.Msg("Panic in arrtrix Matrix command handler") + ms.Status = event.MessageStatusFail + ms.IsCertain = true + ms.ErrorAsMessage = true + outcome = "panic" + } + + observability.RecordCommand(ctx, commandName, outcome) + p.bridge.Matrix.SendMessageStatus(ctx, ms, statusInfo) + }() + + args := strings.Fields(message) + if len(args) == 0 { + args = []string{"unknown-command"} + } + + commandName = strings.ToLower(args[0]) + if actual, ok := p.alias[commandName]; ok { + commandName = actual + } + span.SetAttributes( + attribute.String("arrtrix.matrix.command.name", commandName), + attribute.String("matrix.room_id", roomID.String()), + ) + + portal, err := p.bridge.GetPortalByMXID(ctx, roomID) + if err != nil { + log.Err(err).Msg("Failed to get portal") + } + + commandCtx := &Context{ + Bridge: p.bridge, + Bot: p.bot, + RoomID: roomID, + OrigRoomID: roomID, + EventID: eventID, + ReplyTo: replyTo, + User: user, + Portal: portal, + Command: commandName, + Args: args[1:], + RawArgs: strings.TrimSpace(strings.TrimPrefix(message, args[0])), + Ctx: ctx, + Log: log, + Processor: p, + } + + handler, ok := p.command[commandName] + if !ok { + log.Debug().Str("mx_command", commandName).Msg("Received unknown Matrix room command") + span.SetStatus(codes.Error, "unknown command") + outcome = "unknown" + commandCtx.Reply("Unknown command, use the `help` command for help.") + return + } + + log.UpdateContext(func(c zerolog.Context) zerolog.Context { + return c.Str("mx_command", commandName) + }) + log.Debug().Msg("Received Matrix room command") + handler.Run(commandCtx) + span.SetStatus(codes.Ok, "") +} + +func (c *Context) Reply(message string, args ...any) { + message = strings.ReplaceAll(message, "$cmdprefix ", c.Bridge.Config.CommandPrefix+" ") + if len(args) > 0 { + message = fmt.Sprintf(message, args...) + } + + content := format.RenderMarkdown(message, true, false) + content.MsgType = event.MsgNotice + if err := c.sendNotice(&content); err != nil { + c.Log.Err(err).Msg("Failed to reply to Matrix room command") + } +} + +func (c *Context) ReplyFormatted(body, formattedBody string) { + content := &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: body, + Format: event.FormatHTML, + FormattedBody: formattedBody, + } + if err := c.sendNotice(content); err != nil { + c.Log.Err(err).Msg("Failed to reply to Matrix room command") + } +} + +func (c *Context) SendImage(asset *arrclient.MediaAsset, body string) error { + if asset == nil || len(asset.Data) == 0 { + return nil + } + + mxcURL, file, err := c.Bot.UploadMedia(c.Ctx, c.OrigRoomID, asset.Data, asset.FileName, asset.MimeType) + if err != nil { + return err + } + + content := &event.MessageEventContent{ + MsgType: event.MsgImage, + Body: body, + FileName: asset.FileName, + URL: mxcURL, + File: file, + Info: &event.FileInfo{ + MimeType: asset.MimeType, + Size: len(asset.Data), + }, + } + _, err = c.Bot.SendMessage(c.Ctx, c.OrigRoomID, event.EventMessage, &event.Content{Parsed: content}, nil) + return err +} + +func (c *Context) sendNotice(content *event.MessageEventContent) error { + _, err := c.Bot.SendMessage(c.Ctx, c.OrigRoomID, event.EventMessage, &event.Content{Parsed: content}, nil) + return err +} diff --git a/packages/arrtrix/pkg/matrixcmd/subscriptions.go b/packages/arrtrix/pkg/matrixcmd/subscriptions.go new file mode 100644 index 0000000..ed1a11f --- /dev/null +++ b/packages/arrtrix/pkg/matrixcmd/subscriptions.go @@ -0,0 +1,107 @@ +package matrixcmd + +import ( + "context" + "fmt" + "strings" + + "maunium.net/go/mautrix/id" + + "sneeuwvlok/packages/arrtrix/pkg/arr" + "sneeuwvlok/packages/arrtrix/pkg/subscriptions" +) + +func NewSubscriptionsHandler() Handler { + return NewHandler(Meta{ + Name: "subscriptions", + Aliases: []string{"subscription", "notify"}, + Description: "Manage notification subscriptions by content type and event type.", + Usage: " [movies|series] [event-type|all]", + }, func(ctx *Context) { + repo := contentSubscriptions(ctx) + if repo == nil { + ctx.Reply("Subscription storage is not available.") + return + } + if len(ctx.Args) == 0 || strings.EqualFold(ctx.Args[0], "list") { + handleSubscriptionList(ctx, repo) + return + } + if len(ctx.Args) < 3 { + ctx.Reply("Usage: `subscriptions `") + return + } + + contentType, err := arr.ParseContentType(ctx.Args[1]) + if err != nil { + ctx.Reply(err.Error()) + return + } + eventType, err := arr.ParseEventType(contentType, ctx.Args[2]) + if err != nil { + ctx.Reply(err.Error()) + return + } + + switch strings.ToLower(ctx.Args[0]) { + case "enable": + handleSubscriptionSet(ctx, repo, contentType, eventType, true) + case "disable": + handleSubscriptionSet(ctx, repo, contentType, eventType, false) + default: + ctx.Reply("Unknown subscriptions subcommand `%s`.", ctx.Args[0]) + } + }) +} + +func handleSubscriptionList(ctx *Context, repo subscriptionRepo) { + preferences, err := repo.List(ctx.Ctx, ctx.User.MXID) + if err != nil { + ctx.Reply("Failed to load subscriptions: %v", err) + return + } + + var builder strings.Builder + builder.WriteString("Current notification subscriptions:\n") + for _, contentType := range arr.SupportedContentTypes() { + builder.WriteString(fmt.Sprintf("\n**%s**\n", strings.Title(contentType.Label()))) + for _, eventType := range arr.SupportedEventTypes(contentType) { + enabled := findPreference(preferences, contentType, eventType) + builder.WriteString(fmt.Sprintf("- `%s`: %t\n", eventType, enabled)) + } + } + ctx.Reply(builder.String()) +} + +func handleSubscriptionSet(ctx *Context, repo subscriptionRepo, contentType arr.ContentType, eventType string, enabled bool) { + var err error + if eventType == "all" { + err = repo.SetAll(ctx.Ctx, ctx.User.MXID, contentType, enabled) + } else { + err = repo.Set(ctx.Ctx, ctx.User.MXID, contentType, eventType, enabled) + } + if err != nil { + ctx.Reply("Failed to update subscriptions: %v", err) + return + } + if eventType == "all" { + ctx.Reply("Set all `%s` notifications for %s to %t.", contentType.Label(), userIDString(ctx.User.MXID), enabled) + return + } + ctx.Reply("Set `%s/%s` notifications to %t.", contentType.Label(), eventType, enabled) +} + +type subscriptionRepo interface { + List(ctx context.Context, userID id.UserID) ([]subscriptions.Preference, error) + Set(ctx context.Context, userID id.UserID, contentType arr.ContentType, eventType string, enabled bool) error + SetAll(ctx context.Context, userID id.UserID, contentType arr.ContentType, enabled bool) error +} + +func findPreference(preferences []subscriptions.Preference, contentType arr.ContentType, eventType string) bool { + for _, preference := range preferences { + if preference.ContentType == contentType && preference.EventType == eventType { + return preference.Enabled + } + } + return true +} diff --git a/packages/arrtrix/pkg/observability/config.go b/packages/arrtrix/pkg/observability/config.go new file mode 100644 index 0000000..187c5b5 --- /dev/null +++ b/packages/arrtrix/pkg/observability/config.go @@ -0,0 +1,22 @@ +package observability + +import "strings" + +type Config struct { + OTLPGRPCEndpoint string `yaml:"otlp_grpc_endpoint"` + ServiceName string `yaml:"service_name"` + ResourceAttributes map[string]string `yaml:"resource_attributes"` +} + +func (c *Config) ApplyDefaults() { + if c.ServiceName == "" { + c.ServiceName = "arrtrix" + } + if c.ResourceAttributes == nil { + c.ResourceAttributes = map[string]string{} + } +} + +func (c Config) Enabled() bool { + return strings.TrimSpace(c.OTLPGRPCEndpoint) != "" +} diff --git a/packages/arrtrix/pkg/observability/otel.go b/packages/arrtrix/pkg/observability/otel.go new file mode 100644 index 0000000..2fe46ef --- /dev/null +++ b/packages/arrtrix/pkg/observability/otel.go @@ -0,0 +1,397 @@ +package observability + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + "sync" + "time" + + "github.com/rs/zerolog" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + otellog "go.opentelemetry.io/otel/log" + logglobal "go.opentelemetry.io/otel/log/global" + otelmetric "go.opentelemetry.io/otel/metric" + sdklog "go.opentelemetry.io/otel/sdk/log" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +const ( + instrumentationScope = "sneeuwvlok/packages/arrtrix" + logScope = instrumentationScope + "/logs" +) + +type Runtime struct { + traceProvider *sdktrace.TracerProvider + meterProvider *sdkmetric.MeterProvider + logProvider *sdklog.LoggerProvider + logHook zerolog.Hook +} + +type exporterEndpoint struct { + raw string + insecure bool +} + +type instruments struct { + webhookRequests otelCounter + webhookLatency otelHistogram + commandInvocations otelCounter + inviteEvents otelCounter + startupDuration otelHistogram +} + +type otelCounter interface { + Add(context.Context, int64, ...otelmetric.AddOption) +} + +type otelHistogram interface { + Record(context.Context, float64, ...otelmetric.RecordOption) +} + +var ( + mu sync.RWMutex + current instruments + tracer = otel.Tracer(instrumentationScope) + currentReady bool +) + +func Setup(ctx context.Context, cfg Config, version string) (*Runtime, error) { + cfg.ApplyDefaults() + if !cfg.Enabled() { + resetInstruments() + return &Runtime{}, nil + } + + res, err := buildResource(cfg, version) + if err != nil { + return nil, err + } + endpoint, err := parseEndpoint(cfg.OTLPGRPCEndpoint) + if err != nil { + return nil, err + } + + traceExporter, err := otlptracegrpc.New(ctx, traceOptions(endpoint)...) + if err != nil { + return nil, fmt.Errorf("create trace exporter: %w", err) + } + metricExporter, err := otlpmetricgrpc.New(ctx, metricOptions(endpoint)...) + if err != nil { + return nil, fmt.Errorf("create metric exporter: %w", err) + } + logExporter, err := otlploggrpc.New(ctx, logOptions(endpoint)...) + if err != nil { + return nil, fmt.Errorf("create log exporter: %w", err) + } + + traceProvider := sdktrace.NewTracerProvider( + sdktrace.WithResource(res), + sdktrace.WithBatcher(traceExporter), + ) + meterProvider := sdkmetric.NewMeterProvider( + sdkmetric.WithResource(res), + sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter, sdkmetric.WithInterval(30*time.Second))), + ) + logProvider := sdklog.NewLoggerProvider( + sdklog.WithResource(res), + sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)), + ) + + otel.SetTracerProvider(traceProvider) + otel.SetMeterProvider(meterProvider) + logglobal.SetLoggerProvider(logProvider) + + if err = setInstruments(meterProvider); err != nil { + _ = traceProvider.Shutdown(ctx) + _ = meterProvider.Shutdown(ctx) + _ = logProvider.Shutdown(ctx) + return nil, err + } + + tracer = otel.Tracer(instrumentationScope) + return &Runtime{ + traceProvider: traceProvider, + meterProvider: meterProvider, + logProvider: logProvider, + logHook: newLogHook(logglobal.Logger(logScope)), + }, nil +} + +func (r *Runtime) Enabled() bool { + return r != nil && r.traceProvider != nil +} + +func (r *Runtime) LoggerHook() zerolog.Hook { + if r == nil { + return nil + } + return r.logHook +} + +func (r *Runtime) Shutdown(ctx context.Context) error { + if r == nil || !r.Enabled() { + resetInstruments() + return nil + } + + var errs []error + if err := r.logProvider.Shutdown(ctx); err != nil { + errs = append(errs, fmt.Errorf("shutdown log provider: %w", err)) + } + if err := r.meterProvider.Shutdown(ctx); err != nil { + errs = append(errs, fmt.Errorf("shutdown meter provider: %w", err)) + } + if err := r.traceProvider.Shutdown(ctx); err != nil { + errs = append(errs, fmt.Errorf("shutdown trace provider: %w", err)) + } + resetInstruments() + return errors.Join(errs...) +} + +func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return tracer.Start(ctx, name, opts...) +} + +func RecordWebhook(ctx context.Context, eventType, outcome string, statusCode int, duration time.Duration) { + mu.RLock() + inst := current + ready := currentReady + mu.RUnlock() + if !ready { + return + } + attrs := otelmetric.WithAttributes( + attribute.String("event_type", eventType), + attribute.String("outcome", outcome), + attribute.Int("http.status_code", statusCode), + ) + inst.webhookRequests.Add(ctx, 1, attrs) + inst.webhookLatency.Record(ctx, duration.Seconds(), attrs) +} + +func RecordCommand(ctx context.Context, name, outcome string) { + mu.RLock() + inst := current + ready := currentReady + mu.RUnlock() + if !ready { + return + } + inst.commandInvocations.Add(ctx, 1, otelmetric.WithAttributes( + attribute.String("command", name), + attribute.String("outcome", outcome), + )) +} + +func RecordInvite(ctx context.Context, outcome string) { + mu.RLock() + inst := current + ready := currentReady + mu.RUnlock() + if !ready { + return + } + inst.inviteEvents.Add(ctx, 1, otelmetric.WithAttributes(attribute.String("outcome", outcome))) +} + +func RecordStartupPhase(ctx context.Context, phase, outcome string, duration time.Duration) { + mu.RLock() + inst := current + ready := currentReady + mu.RUnlock() + if !ready { + return + } + inst.startupDuration.Record(ctx, duration.Seconds(), otelmetric.WithAttributes( + attribute.String("phase", phase), + attribute.String("outcome", outcome), + )) +} + +func parseEndpoint(raw string) (exporterEndpoint, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return exporterEndpoint{}, errors.New("observability.otlp_grpc_endpoint must not be empty when observability is enabled") + } + if strings.Contains(raw, "://") { + u, err := url.Parse(raw) + if err != nil { + return exporterEndpoint{}, fmt.Errorf("parse observability.otlp_grpc_endpoint: %w", err) + } + if u.Scheme == "" || u.Host == "" { + return exporterEndpoint{}, fmt.Errorf("invalid observability.otlp_grpc_endpoint %q", raw) + } + return exporterEndpoint{raw: raw, insecure: u.Scheme == "http"}, nil + } + return exporterEndpoint{raw: "http://" + raw, insecure: true}, nil +} + +func buildResource(cfg Config, version string) (*resource.Resource, error) { + attrs := []attribute.KeyValue{ + attribute.String("service.name", cfg.ServiceName), + } + if version != "" { + attrs = append(attrs, attribute.String("service.version", version)) + } + for key, value := range cfg.ResourceAttributes { + attrs = append(attrs, attribute.String(key, value)) + } + return resource.Merge(resource.Default(), resource.NewWithAttributes("", attrs...)) +} + +func setInstruments(provider *sdkmetric.MeterProvider) error { + meter := provider.Meter(instrumentationScope) + + webhookRequests, err := meter.Int64Counter( + "arrtrix.webhook.requests", + otelmetric.WithDescription("Number of Arr webhook requests handled by arrtrix."), + ) + if err != nil { + return fmt.Errorf("create webhook request counter: %w", err) + } + webhookLatency, err := meter.Float64Histogram( + "arrtrix.webhook.duration.seconds", + otelmetric.WithDescription("Duration of Arr webhook request handling."), + otelmetric.WithUnit("s"), + ) + if err != nil { + return fmt.Errorf("create webhook duration histogram: %w", err) + } + commandInvocations, err := meter.Int64Counter( + "arrtrix.matrix.commands", + otelmetric.WithDescription("Number of Matrix management-room commands handled by arrtrix."), + ) + if err != nil { + return fmt.Errorf("create command counter: %w", err) + } + inviteEvents, err := meter.Int64Counter( + "arrtrix.matrix.invites", + otelmetric.WithDescription("Number of management-room invite flows observed by arrtrix."), + ) + if err != nil { + return fmt.Errorf("create invite counter: %w", err) + } + startupDuration, err := meter.Float64Histogram( + "arrtrix.runtime.phase.duration.seconds", + otelmetric.WithDescription("Duration of arrtrix runtime startup and shutdown phases."), + otelmetric.WithUnit("s"), + ) + if err != nil { + return fmt.Errorf("create runtime duration histogram: %w", err) + } + + mu.Lock() + current = instruments{ + webhookRequests: webhookRequests, + webhookLatency: webhookLatency, + commandInvocations: commandInvocations, + inviteEvents: inviteEvents, + startupDuration: startupDuration, + } + currentReady = true + mu.Unlock() + return nil +} + +func resetInstruments() { + mu.Lock() + current = instruments{} + currentReady = false + mu.Unlock() +} + +func traceOptions(endpoint exporterEndpoint) []otlptracegrpc.Option { + opts := []otlptracegrpc.Option{otlptracegrpc.WithEndpointURL(endpoint.raw)} + if endpoint.insecure { + opts = append(opts, otlptracegrpc.WithInsecure()) + } + return opts +} + +func metricOptions(endpoint exporterEndpoint) []otlpmetricgrpc.Option { + opts := []otlpmetricgrpc.Option{otlpmetricgrpc.WithEndpointURL(endpoint.raw)} + if endpoint.insecure { + opts = append(opts, otlpmetricgrpc.WithInsecure()) + } + return opts +} + +func logOptions(endpoint exporterEndpoint) []otlploggrpc.Option { + opts := []otlploggrpc.Option{otlploggrpc.WithEndpointURL(endpoint.raw)} + if endpoint.insecure { + opts = append(opts, otlploggrpc.WithInsecure()) + } + return opts +} + +type otelLogHook struct { + logger otellog.Logger +} + +func newLogHook(logger otellog.Logger) zerolog.Hook { + return otelLogHook{logger: logger} +} + +func (h otelLogHook) Run(e *zerolog.Event, level zerolog.Level, message string) { + if h.logger == nil { + return + } + ctx := e.GetCtx() + if ctx == nil { + ctx = context.Background() + } + + severity := mapSeverity(level) + if !h.logger.Enabled(ctx, otellog.EnabledParameters{Severity: severity}) { + return + } + + now := time.Now() + record := otellog.Record{} + record.SetTimestamp(now) + record.SetObservedTimestamp(now) + record.SetSeverity(severity) + record.SetSeverityText(strings.ToUpper(level.String())) + record.SetBody(otellog.StringValue(message)) + record.AddAttributes(otellog.String("log.scope", logScope)) + + if spanCtx := trace.SpanContextFromContext(ctx); spanCtx.IsValid() { + record.AddAttributes( + otellog.String("trace_id", spanCtx.TraceID().String()), + otellog.String("span_id", spanCtx.SpanID().String()), + ) + } + + h.logger.Emit(ctx, record) +} + +func mapSeverity(level zerolog.Level) otellog.Severity { + switch level { + case zerolog.TraceLevel: + return otellog.SeverityTrace + case zerolog.DebugLevel: + return otellog.SeverityDebug + case zerolog.InfoLevel: + return otellog.SeverityInfo + case zerolog.WarnLevel: + return otellog.SeverityWarn + case zerolog.ErrorLevel: + return otellog.SeverityError + case zerolog.FatalLevel: + return otellog.SeverityFatal + case zerolog.PanicLevel: + return otellog.SeverityFatal4 + default: + return otellog.SeverityUndefined + } +} diff --git a/packages/arrtrix/pkg/observability/otel_test.go b/packages/arrtrix/pkg/observability/otel_test.go new file mode 100644 index 0000000..4dd8e3e --- /dev/null +++ b/packages/arrtrix/pkg/observability/otel_test.go @@ -0,0 +1,54 @@ +package observability + +import "testing" + +func TestConfigDefaults(t *testing.T) { + var cfg Config + cfg.ApplyDefaults() + + if cfg.ServiceName != "arrtrix" { + t.Fatalf("expected default service name arrtrix, got %q", cfg.ServiceName) + } + if cfg.ResourceAttributes == nil { + t.Fatal("expected resource attributes map to be initialized") + } + if cfg.Enabled() { + t.Fatal("expected observability to be disabled by default") + } +} + +func TestParseEndpointSupportsURLAndBareHost(t *testing.T) { + tests := []struct { + name string + input string + wantRaw string + insecure bool + wantError bool + }{ + {name: "https url", input: "https://otel.example:4317", wantRaw: "https://otel.example:4317"}, + {name: "http url", input: "http://127.0.0.1:4317", wantRaw: "http://127.0.0.1:4317", insecure: true}, + {name: "bare host", input: "collector:4317", wantRaw: "http://collector:4317", insecure: true}, + {name: "invalid", input: "://bad", wantError: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseEndpoint(tt.input) + if tt.wantError { + if err == nil { + t.Fatal("expected error") + } + return + } + if err != nil { + t.Fatalf("parseEndpoint returned error: %v", err) + } + if got.raw != tt.wantRaw { + t.Fatalf("expected raw endpoint %q, got %q", tt.wantRaw, got.raw) + } + if got.insecure != tt.insecure { + t.Fatalf("expected insecure=%t, got %t", tt.insecure, got.insecure) + } + }) + } +} diff --git a/packages/arrtrix/pkg/onboarding/welcome.go b/packages/arrtrix/pkg/onboarding/welcome.go new file mode 100644 index 0000000..e96ea7a --- /dev/null +++ b/packages/arrtrix/pkg/onboarding/welcome.go @@ -0,0 +1,171 @@ +package onboarding + +import ( + "context" + "fmt" + "strings" + + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/bridgeconfig" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" + + "sneeuwvlok/packages/arrtrix/pkg/observability" +) + +const handledInviteEventType = "com.arrtrix.handled_invite" + +func HandleBotInvite(ctx context.Context, bridge *bridgev2.Bridge, texts bridgeconfig.ManagementRoomTexts, evt *event.Event) { + if evt.Type != event.StateMember || + evt.GetStateKey() != bridge.Bot.GetMXID().String() || + evt.Content.AsMember().Membership != event.MembershipInvite { + return + } + + ctx, span := observability.StartSpan(ctx, "arrtrix.matrix.invite") + defer span.End() + span.SetAttributes( + attribute.String("matrix.room_id", evt.RoomID.String()), + attribute.String("matrix.sender", evt.Sender.String()), + ) + outcome := "ignored" + defer observability.RecordInvite(ctx, outcome) + + log := zerolog.Ctx(ctx) + sender, err := bridge.GetUserByMXID(ctx, evt.Sender) + if err != nil { + outcome = "user_lookup_failed" + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + log.Err(err).Msg("Failed to load sender for bot invite") + return + } + if !sender.Permissions.Commands { + outcome = "permission_denied" + span.SetStatus(codes.Error, "sender lacks command permission") + return + } + + if err = bridge.Bot.EnsureJoined(ctx, evt.RoomID); err != nil { + outcome = "join_failed" + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + log.Err(err).Msg("Failed to accept invite to room") + return + } + + members, err := bridge.Matrix.GetMembers(ctx, evt.RoomID) + if err != nil { + outcome = "member_lookup_failed" + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + log.Err(err).Msg("Failed to get members of room after accepting invite") + return + } + if len(members) != 2 { + outcome = "non_management_room" + span.SetStatus(codes.Error, "invite room is not a direct management room") + return + } + + assignedManagementRoom := sender.ManagementRoom == "" + if assignedManagementRoom { + sender.ManagementRoom = evt.RoomID + if err = sender.Save(ctx); err != nil { + outcome = "management_room_save_failed" + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + log.Err(err).Msg("Failed to update user's management room in database") + return + } + } + + message := buildWelcomeMessage(bridge, texts, sender, assignedManagementRoom) + content := format.RenderMarkdown(message, true, false) + if _, err = bridge.Bot.SendMessage(ctx, evt.RoomID, event.EventMessage, &event.Content{Parsed: &content}, nil); err != nil { + outcome = "welcome_send_failed" + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + log.Err(err).Msg("Failed to send welcome message to room") + return + } + + outcome = "welcomed" + span.SetStatus(codes.Ok, "") + evt.Type = event.Type{Type: handledInviteEventType} +} + +func buildWelcomeMessage(bridge *bridgev2.Bridge, texts bridgeconfig.ManagementRoomTexts, sender *bridgev2.User, assignedManagementRoom bool) string { + return composeWelcomeMessage( + bridge.Network.GetName().DisplayName, + bridge.Config.CommandPrefix, + bridge.Bot.GetMXID(), + texts, + sender.GetDefaultLogin() != nil, + assignedManagementRoom, + ) +} + +func composeWelcomeMessage( + bridgeName string, + commandPrefix string, + botMXID id.UserID, + texts bridgeconfig.ManagementRoomTexts, + connected bool, + assignedManagementRoom bool, +) string { + replacer := strings.NewReplacer( + "$cmdprefix", commandPrefix, + "$bridge", bridgeName, + "$bot", string(botMXID), + ) + + var parts []string + + base := strings.TrimSpace(texts.Welcome) + if base == "" { + base = fmt.Sprintf("Hello, I'm the %s bot.", bridgeName) + } + parts = append(parts, replacer.Replace(base)) + + if assignedManagementRoom { + parts = append(parts, "This room has been marked as your management room.") + } else { + parts = append(parts, fmt.Sprintf("Use `%s help` to see available commands in this room.", commandPrefix)) + } + + if connected { + connected := strings.TrimSpace(texts.WelcomeConnected) + if connected == "" { + connected = "You're connected. Use `help` to see the commands available right now." + } + parts = append(parts, replacer.Replace(connected)) + } else { + unconnected := strings.TrimSpace(texts.WelcomeUnconnected) + if unconnected == "" { + unconnected = "Use `help` to see the commands available right now." + } + parts = append(parts, replacer.Replace(unconnected)) + } + + if extra := strings.TrimSpace(texts.AdditionalHelp); extra != "" { + parts = append(parts, replacer.Replace(extra)) + } + + return strings.Join(parts, "\n\n") +} + +func IsHandledInviteEvent(evt *event.Event) bool { + return evt.Type.Type == handledInviteEventType +} + +func IsBotInviteFor(roomBot id.UserID, evt *event.Event) bool { + return evt.Type == event.StateMember && + evt.GetStateKey() == roomBot.String() && + evt.Content.AsMember().Membership == event.MembershipInvite +} diff --git a/packages/arrtrix/pkg/onboarding/welcome_test.go b/packages/arrtrix/pkg/onboarding/welcome_test.go new file mode 100644 index 0000000..de6f42a --- /dev/null +++ b/packages/arrtrix/pkg/onboarding/welcome_test.go @@ -0,0 +1,56 @@ +package onboarding + +import ( + "strings" + "testing" + + "maunium.net/go/mautrix/bridgev2/bridgeconfig" + "maunium.net/go/mautrix/id" +) + +func TestComposeWelcomeMessageDefaults(t *testing.T) { + out := composeWelcomeMessage( + "Arrtrix", + "!arr", + id.UserID("@arrtrixbot:test"), + bridgeconfig.ManagementRoomTexts{}, + false, + true, + ) + + for _, fragment := range []string{ + "Hello, I'm the Arrtrix bot.", + "This room has been marked as your management room.", + "Use `help` to see the commands available right now.", + } { + if !strings.Contains(out, fragment) { + t.Fatalf("expected welcome output to contain %q, got:\n%s", fragment, out) + } + } +} + +func TestComposeWelcomeMessageTemplateValues(t *testing.T) { + out := composeWelcomeMessage( + "Arrtrix", + "!arr", + id.UserID("@arrtrixbot:test"), + bridgeconfig.ManagementRoomTexts{ + Welcome: "Welcome to $bridge.", + WelcomeConnected: "Talk to $bot with $cmdprefix help.", + AdditionalHelp: "Custom footer for $bridge.", + }, + true, + false, + ) + + for _, fragment := range []string{ + "Welcome to Arrtrix.", + "Use `!arr help` to see available commands in this room.", + "Talk to @arrtrixbot:test with !arr help.", + "Custom footer for Arrtrix.", + } { + if !strings.Contains(out, fragment) { + t.Fatalf("expected templated welcome output to contain %q, got:\n%s", fragment, out) + } + } +} diff --git a/packages/arrtrix/pkg/runtime/envconfig.go b/packages/arrtrix/pkg/runtime/envconfig.go new file mode 100644 index 0000000..173de78 --- /dev/null +++ b/packages/arrtrix/pkg/runtime/envconfig.go @@ -0,0 +1,256 @@ +package runtime + +import ( + "fmt" + "os" + "reflect" + "strconv" + "strings" +) + +const fileEnvPrefix = "READFILE:" + +func updateConfigFromEnv(cfg, networkData any, prefix string) error { + if prefix == "" { + return nil + } + + cfgVal := reflect.ValueOf(cfg) + networkVal := reflect.ValueOf(networkData) + + for _, env := range os.Environ() { + if !strings.HasPrefix(env, prefix) { + continue + } + + keyValue := strings.SplitN(env, "=", 2) + if len(keyValue) != 2 { + continue + } + + key := strings.TrimPrefix(keyValue[0], prefix) + value := keyValue[1] + if strings.HasSuffix(key, "_FILE") { + key = strings.TrimSuffix(key, "_FILE") + value = fileEnvPrefix + value + } + + key = strings.ToLower(key) + lookupKey := key + if !strings.ContainsRune(key, '.') { + key = strings.ReplaceAll(key, "__", ".") + } + + path := strings.Split(key, ".") + field, ok := reflectGetFromMainOrNetwork(cfgVal, networkVal, path) + if !ok && !strings.ContainsRune(lookupKey, '.') { + field, ok = reflectGetFromMainOrNetworkTokens(cfgVal, networkVal, strings.Split(lookupKey, "_")) + } + if !ok { + return fmt.Errorf("%s not found", formatKey(path)) + } + + if strings.HasPrefix(value, fileEnvPrefix) { + filePath := strings.TrimPrefix(value, fileEnvPrefix) + fileData, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read file %s for %s: %w", filePath, formatKey(path), err) + } + value = strings.TrimSpace(string(fileData)) + } + + if err := setReflectedValue(field, path, value); err != nil { + return err + } + } + + return nil +} + +type reflectedField struct { + value reflect.Value + valueKind reflect.Kind + remainingPath []string +} + +func formatKey(path []string) string { + return strings.Join(path, "->") +} + +func reflectGetFromMainOrNetwork(main, network reflect.Value, path []string) (*reflectedField, bool) { + if len(path) > 0 && path[0] == "network" { + return reflectGetYAML(network, path[1:]) + } + return reflectGetYAML(main, path) +} + +func reflectGetFromMainOrNetworkTokens(main, network reflect.Value, tokens []string) (*reflectedField, bool) { + if len(tokens) > 0 && normalizeKey(tokens[0]) == "network" { + return reflectGetYAMLTokens(network, tokens[1:]) + } + return reflectGetYAMLTokens(main, tokens) +} + +func reflectGetYAML(value reflect.Value, path []string) (*reflectedField, bool) { + if len(path) == 0 { + return &reflectedField{value: value, valueKind: value.Kind()}, true + } + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + + switch value.Kind() { + case reflect.Map: + return &reflectedField{ + value: value, + valueKind: value.Type().Elem().Kind(), + remainingPath: path, + }, true + case reflect.Struct: + fields := reflect.VisibleFields(value.Type()) + for _, field := range fields { + if yamlFieldName(field) != path[0] { + continue + } + return reflectGetYAML(value.FieldByIndex(field.Index), path[1:]) + } + } + + return nil, false +} + +func reflectGetYAMLTokens(value reflect.Value, tokens []string) (*reflectedField, bool) { + if len(tokens) == 0 { + return &reflectedField{value: value, valueKind: value.Kind()}, true + } + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + + switch value.Kind() { + case reflect.Map: + return &reflectedField{ + value: value, + valueKind: value.Type().Elem().Kind(), + remainingPath: []string{strings.Join(tokens, "_")}, + }, true + case reflect.Struct: + fields := reflect.VisibleFields(value.Type()) + for _, field := range fields { + name := yamlFieldName(field) + if name == "" { + continue + } + normalizedFieldName := normalizeKey(name) + for i := len(tokens); i >= 1; i-- { + if normalizeKey(strings.Join(tokens[:i], "_")) != normalizedFieldName { + continue + } + return reflectGetYAMLTokens(value.FieldByIndex(field.Index), tokens[i:]) + } + } + } + + return nil, false +} + +func yamlFieldName(field reflect.StructField) string { + parts := strings.SplitN(field.Tag.Get("yaml"), ",", 2) + switch name := parts[0]; { + case name == "-" && len(parts) == 1: + return "" + case name == "": + return strings.ToLower(field.Name) + default: + return name + } +} + +func normalizeKey(value string) string { + return strings.ReplaceAll(strings.ToLower(value), "_", "") +} + +func setReflectedValue(field *reflectedField, path []string, raw string) error { + parsed, err := parseValue(field.valueKind, raw, path) + if err != nil { + return err + } + + value := field.value + if value.Kind() == reflect.Ptr { + if value.IsNil() { + value.Set(reflect.New(value.Type().Elem())) + } + value = value.Elem() + } + + if value.Kind() == reflect.Map { + if value.Type().Key().Kind() != reflect.String { + return fmt.Errorf("unsupported map key type %s in %s", value.Type().Key().Kind(), formatKey(path)) + } + key := strings.Join(field.remainingPath, ".") + value.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(parsed)) + return nil + } + + value.Set(reflect.ValueOf(parsed)) + return nil +} + +func parseValue(kind reflect.Kind, raw string, path []string) (any, error) { + switch kind { + case reflect.String: + return raw, nil + case reflect.Bool: + parsed, err := strconv.ParseBool(raw) + if err != nil { + return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err) + } + return parsed, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + parsed, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err) + } + switch kind { + case reflect.Int8: + return int8(parsed), nil + case reflect.Int16: + return int16(parsed), nil + case reflect.Int32: + return int32(parsed), nil + case reflect.Int64: + return parsed, nil + default: + return int(parsed), nil + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + parsed, err := strconv.ParseUint(raw, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err) + } + switch kind { + case reflect.Uint8: + return uint8(parsed), nil + case reflect.Uint16: + return uint16(parsed), nil + case reflect.Uint32: + return uint32(parsed), nil + case reflect.Uint64: + return parsed, nil + default: + return uint(parsed), nil + } + case reflect.Float32, reflect.Float64: + parsed, err := strconv.ParseFloat(raw, 64) + if err != nil { + return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err) + } + if kind == reflect.Float32 { + return float32(parsed), nil + } + return parsed, nil + default: + return nil, fmt.Errorf("unsupported type %s in %s", kind, formatKey(path)) + } +} diff --git a/packages/arrtrix/pkg/runtime/envconfig_test.go b/packages/arrtrix/pkg/runtime/envconfig_test.go new file mode 100644 index 0000000..6381a47 --- /dev/null +++ b/packages/arrtrix/pkg/runtime/envconfig_test.go @@ -0,0 +1,57 @@ +package runtime + +import ( + "os" + "testing" + + "maunium.net/go/mautrix/bridgev2/bridgeconfig" + + "sneeuwvlok/packages/arrtrix/pkg/connector" +) + +func TestUpdateConfigFromEnvSupportsFlatUnderscorePaths(t *testing.T) { + t.Setenv("ARRTRIX_NETWORK_CONTENT_MOVIES_APIKEY", "radarr-secret") + + cfg := &bridgeconfig.Config{} + network := &connector.Config{} + if err := updateConfigFromEnv(cfg, network, "ARRTRIX_"); err != nil { + t.Fatalf("updateConfigFromEnv returned error: %v", err) + } + + if network.Content.Movies.APIKey != "radarr-secret" { + t.Fatalf("expected movies api key to be overridden, got %q", network.Content.Movies.APIKey) + } +} + +func TestUpdateConfigFromEnvSupportsExplicitUnderscoredFieldNames(t *testing.T) { + t.Setenv("ARRTRIX_NETWORK_CONTENT_MOVIES_ROOT_FOLDER_PATH", "/data/movies") + + cfg := &bridgeconfig.Config{} + network := &connector.Config{} + if err := updateConfigFromEnv(cfg, network, "ARRTRIX_"); err != nil { + t.Fatalf("updateConfigFromEnv returned error: %v", err) + } + + if network.Content.Movies.RootFolderPath != "/data/movies" { + t.Fatalf("expected root folder path to be overridden, got %q", network.Content.Movies.RootFolderPath) + } +} + +func TestUpdateConfigFromEnvSupportsDoubleUnderscorePaths(t *testing.T) { + t.Setenv("ARRTRIX_NETWORK__CONTENT__SERIES__API_KEY", "sonarr-secret") + + cfg := &bridgeconfig.Config{} + network := &connector.Config{} + if err := updateConfigFromEnv(cfg, network, "ARRTRIX_"); err != nil { + t.Fatalf("updateConfigFromEnv returned error: %v", err) + } + + if network.Content.Series.APIKey != "sonarr-secret" { + t.Fatalf("expected series api key to be overridden, got %q", network.Content.Series.APIKey) + } +} + +func TestMain(m *testing.M) { + code := m.Run() + os.Exit(code) +} diff --git a/packages/arrtrix/pkg/runtime/example.go b/packages/arrtrix/pkg/runtime/example.go new file mode 100644 index 0000000..c8d7ca4 --- /dev/null +++ b/packages/arrtrix/pkg/runtime/example.go @@ -0,0 +1,76 @@ +package runtime + +import ( + "fmt" + "strings" + + "maunium.net/go/mautrix/bridgev2" +) + +func makeExampleConfig(networkName bridgev2.BridgeName, networkExample string) string { + var builder strings.Builder + + builder.WriteString("# Network-specific config options\n") + builder.WriteString("network:\n") + for _, line := range strings.Split(strings.TrimRight(networkExample, "\n"), "\n") { + if line == "" { + builder.WriteString(" \n") + continue + } + builder.WriteString(" ") + builder.WriteString(line) + builder.WriteByte('\n') + } + builder.WriteByte('\n') + + builder.WriteString(fmt.Sprintf(`bridge: + command_prefix: "%s" + permissions: + "*": relay + "@admin:example.com": admin + +database: + type: sqlite3-fk-wal + uri: file:arrtrix.db?_txlock=immediate + +homeserver: + address: http://example.localhost:8008 + domain: example.com + software: standard + +appservice: + address: http://localhost:%d + hostname: 127.0.0.1 + port: %d + id: %s + bot: + username: %s + displayname: %s + as_token: This value is generated when generating the registration + hs_token: This value is generated when generating the registration + username_template: %s_{{.}} + +logging: + min_level: info + writers: + - type: stdout + format: pretty-colored + +observability: + # OTLP/gRPC endpoint for logs, traces, and metrics. + # Set to e.g. http://127.0.0.1:4317 to enable export. + otlp_grpc_endpoint: "" + service_name: arrtrix + resource_attributes: {} + +management_room_texts: + welcome: "" + welcome_connected: "" + welcome_unconnected: "" + additional_help: "" + +env_config_prefix: "" +`, networkName.DefaultCommandPrefix, networkName.DefaultPort, networkName.DefaultPort, networkName.NetworkID, "arrtrixbot", "Arrtrix Bot", networkName.NetworkID)) + + return builder.String() +} diff --git a/packages/arrtrix/pkg/runtime/main.go b/packages/arrtrix/pkg/runtime/main.go new file mode 100644 index 0000000..c685706 --- /dev/null +++ b/packages/arrtrix/pkg/runtime/main.go @@ -0,0 +1,486 @@ +package runtime + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "os/signal" + "runtime" + "strings" + "syscall" + "time" + + "github.com/rs/zerolog" + "go.mau.fi/util/dbutil" + "go.mau.fi/util/exerrors" + "go.mau.fi/util/exzerolog" + "go.mau.fi/util/progver" + "go.opentelemetry.io/otel/codes" + "gopkg.in/yaml.v3" + flag "maunium.net/go/mauflag" + "maunium.net/go/mautrix/appservice" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/bridgeconfig" + "maunium.net/go/mautrix/bridgev2/commands" + "maunium.net/go/mautrix/bridgev2/matrix" + "maunium.net/go/mautrix/event" + + arrconfig "sneeuwvlok/packages/arrtrix/pkg/config" + "sneeuwvlok/packages/arrtrix/pkg/matrixcmd" + "sneeuwvlok/packages/arrtrix/pkg/observability" + "sneeuwvlok/packages/arrtrix/pkg/onboarding" + "sneeuwvlok/packages/arrtrix/pkg/subscriptions" +) + +var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String() +var writeExampleConfig = flag.MakeFull("e", "generate-example-config", "Save the example config to the config path and quit.", "false").Bool() +var dontSaveConfig = flag.MakeFull("n", "no-update", "Don't save updated config to disk.", "false").Bool() +var registrationPath = flag.MakeFull("r", "registration", "The path where to save the appservice registration.", "registration.yaml").String() +var generateRegistration = flag.MakeFull("g", "generate-registration", "Generate registration and quit.", "false").Bool() +var version = flag.MakeFull("v", "version", "View bridge version and quit.", "false").Bool() +var versionJSON = flag.Make().LongKey("version-json").Usage("Print a JSON object representing the bridge version and quit.").Default("false").Bool() +var ignoreUnsupportedDatabase = flag.Make().LongKey("ignore-unsupported-database").Usage("Run even if the database schema is too new").Default("false").Bool() +var ignoreForeignTables = flag.Make().LongKey("ignore-foreign-tables").Usage("Run even if the database contains tables from other programs (like Synapse)").Default("false").Bool() +var ignoreUnsupportedServer = flag.Make().LongKey("ignore-unsupported-server").Usage("Run even if the Matrix homeserver is outdated").Default("false").Bool() +var wantHelp, _ = flag.MakeHelpFlag() + +type Main struct { + Name string + Description string + URL string + Version string + + Connector bridgev2.NetworkConnector + PostInit func() + PostStart func() + + Log *zerolog.Logger + DB *dbutil.Database + PublicConfig *arrconfig.Config + Config *bridgeconfig.Config + Matrix *matrix.Connector + Bridge *bridgev2.Bridge + OTEL *observability.Runtime + + ConfigPath string + RegistrationPath string + SaveConfig bool + + ver progver.ProgramVersion + manualStop chan int +} + +type versionJSONOutput struct { + progver.ProgramVersion + + OS string + Arch string + + Mautrix struct { + Version string + Commit string + } +} + +type routeMounter interface { + MountRoutes(*http.ServeMux) error +} + +func (m *Main) Run() { + m.PreInit() + m.Init() + m.Start() + exitCode := m.WaitForInterrupt() + m.Stop() + os.Exit(exitCode) +} + +func (m *Main) PreInit() { + m.manualStop = make(chan int, 1) + flag.SetHelpTitles( + fmt.Sprintf("%s - %s", m.Name, m.Description), + fmt.Sprintf("%s [-hgvn] [-c ] [-r ]", m.Name), + ) + + err := flag.Parse() + m.ConfigPath = *configPath + m.RegistrationPath = *registrationPath + m.SaveConfig = !*dontSaveConfig + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + flag.PrintHelp() + os.Exit(1) + } + + switch { + case *wantHelp: + flag.PrintHelp() + os.Exit(0) + case *version: + fmt.Println(m.ver.VersionDescription) + os.Exit(0) + case *versionJSON: + output := versionJSONOutput{ + ProgramVersion: m.ver, + OS: runtime.GOOS, + Arch: runtime.GOARCH, + } + output.Mautrix.Version = mautrix.Version + output.Mautrix.Commit = mautrix.Commit + _ = json.NewEncoder(os.Stdout).Encode(output) + os.Exit(0) + case *writeExampleConfig: + m.writeExampleConfig() + os.Exit(0) + } + + m.LoadConfig() + if *generateRegistration { + m.GenerateRegistration() + os.Exit(0) + } +} + +func (m *Main) writeExampleConfig() { + if *configPath != "-" { + if _, err := os.Stat(*configPath); !errors.Is(err, os.ErrNotExist) { + _, _ = fmt.Fprintln(os.Stderr, *configPath, "already exists, please remove it if you want to generate a new example") + os.Exit(1) + } + } + + networkExample, _, _ := m.Connector.GetConfig() + example := makeExampleConfig(m.Connector.GetName(), networkExample) + if *configPath == "-" { + fmt.Print(example) + return + } + + exerrors.PanicIfNotNil(os.WriteFile(*configPath, []byte(example), 0o600)) + fmt.Println("Wrote example config to", *configPath) +} + +func (m *Main) GenerateRegistration() { + if !m.SaveConfig { + _, _ = fmt.Fprintln(os.Stderr, "--no-update is not compatible with --generate-registration") + os.Exit(5) + } + if m.Config.Homeserver.Domain == "example.com" { + _, _ = fmt.Fprintln(os.Stderr, "Homeserver domain is not set") + os.Exit(20) + } + + registration := m.Config.GenerateRegistration() + if err := registration.Save(m.RegistrationPath); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to save registration:", err) + os.Exit(21) + } + + if err := m.saveConfig(); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to save config:", err) + os.Exit(22) + } + + fmt.Println("Registration generated. See https://docs.mau.fi/bridges/general/registering-appservices.html for instructions on installing the registration.") +} + +func (m *Main) LoadConfig() { + configData, err := os.ReadFile(m.ConfigPath) + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to read config:", err) + os.Exit(10) + } + + publicConfig, err := arrconfig.Load(configData) + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to parse config:", err) + os.Exit(10) + } + cfg := publicConfig.Compile() + if err = m.loadRegistrationTokens(&cfg); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to parse registration:", err) + os.Exit(10) + } + + _, networkData, _ := m.Connector.GetConfig() + if networkData != nil { + if err = cfg.Network.Decode(networkData); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to parse network config:", err) + os.Exit(10) + } + } + + cfg.Bridge.Backfill = cfg.Backfill + if err = updateConfigFromEnv(&cfg, networkData, cfg.EnvConfigPrefix); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to parse environment variables:", err) + os.Exit(10) + } + + m.PublicConfig = publicConfig + m.Config = &cfg +} + +func (m *Main) loadRegistrationTokens(cfg *bridgeconfig.Config) error { + if m.RegistrationPath == "" { + return nil + } + + data, err := os.ReadFile(m.RegistrationPath) + if errors.Is(err, os.ErrNotExist) { + return nil + } else if err != nil { + return err + } + + var tokens struct { + AppToken string `yaml:"as_token"` + ServerToken string `yaml:"hs_token"` + } + if err = yaml.Unmarshal(data, &tokens); err != nil { + return err + } + + if tokens.AppToken != "" { + cfg.AppService.ASToken = tokens.AppToken + } + if tokens.ServerToken != "" { + cfg.AppService.HSToken = tokens.ServerToken + } + return nil +} + +func (m *Main) Init() { + start := time.Now() + ctx := context.Background() + var err error + m.Log, err = m.Config.Logging.Compile() + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to initialize logger:", err) + os.Exit(12) + } + exzerolog.SetupDefaults(m.Log) + + if err = m.validateConfig(); err != nil { + m.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Configuration error") + m.Log.Info().Msg("See https://docs.mau.fi/faq/field-unconfigured for more info") + os.Exit(11) + } + + otelCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + m.OTEL, err = observability.Setup(otelCtx, m.PublicConfig.Observability, m.Version) + cancel() + if err != nil { + m.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to initialize observability") + os.Exit(15) + } + if hook := m.OTEL.LoggerHook(); hook != nil { + logger := m.Log.Hook(hook) + m.Log = &logger + exzerolog.SetupDefaults(m.Log) + } + + ctx = m.Log.WithContext(context.Background()) + ctx, span := observability.StartSpan(ctx, "arrtrix.runtime.init") + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + observability.RecordStartupPhase(ctx, "init", "error", time.Since(start)) + return + } + span.SetStatus(codes.Ok, "") + observability.RecordStartupPhase(ctx, "init", "ok", time.Since(start)) + }() + defer span.End() + + m.Log.Info(). + Str("name", m.Name). + Str("version", m.ver.FormattedVersion). + Time("built_at", m.ver.BuildTime). + Str("go_version", runtime.Version()). + Msg("Initializing bridge") + + m.initDB() + if err = subscriptions.EnsureSchema(ctx, m.DB); err != nil { + m.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to initialize subscription schema") + os.Exit(14) + } + m.Matrix = matrix.NewConnector(m.Config) + m.Matrix.OnWebsocketReplaced = func() { + m.TriggerStop(0) + } + m.Matrix.IgnoreUnsupportedServer = *ignoreUnsupportedServer + m.Bridge = bridgev2.NewBridge("", m.DB, *m.Log, &m.Config.Bridge, m.Matrix, m.Connector, commands.NewProcessor) + m.Bridge.Commands = matrixcmd.NewProcessor(m.Bridge, m.Config.ManagementRoomTexts) + + if m.Matrix.EventProcessor != nil { + if m.Config.AppService.AsyncTransactions { + m.Matrix.EventProcessor.ExecMode = appservice.AsyncLoop + } else { + m.Matrix.EventProcessor.ExecMode = appservice.Sync + } + m.Matrix.EventProcessor.PrependHandler(event.StateMember, func(ctx context.Context, evt *event.Event) { + onboarding.HandleBotInvite(ctx, m.Bridge, m.Config.ManagementRoomTexts, evt) + }) + } + + m.Matrix.AS.DoublePuppetValue = m.Name + if mounter, ok := m.Connector.(routeMounter); ok { + if err = mounter.MountRoutes(m.Matrix.AS.Router); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to mount HTTP routes:", err) + os.Exit(13) + } + } + + if m.PostInit != nil { + m.PostInit() + } +} + +func (m *Main) Start() { + start := time.Now() + ctx := m.Log.WithContext(context.Background()) + ctx, span := observability.StartSpan(ctx, "arrtrix.runtime.start") + defer func() { + if r := recover(); r != nil { + span.SetStatus(codes.Error, "panic") + observability.RecordStartupPhase(ctx, "start", "panic", time.Since(start)) + span.End() + panic(r) + } + span.End() + }() + if err := m.Bridge.Start(ctx); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + observability.RecordStartupPhase(ctx, "start", "error", time.Since(start)) + m.Log.Fatal().Err(err).Msg("Failed to start bridge") + } + span.SetStatus(codes.Ok, "") + observability.RecordStartupPhase(ctx, "start", "ok", time.Since(start)) + if m.PostStart != nil { + m.PostStart() + } +} + +func (m *Main) Stop() { + start := time.Now() + ctx := m.Log.WithContext(context.Background()) + ctx, span := observability.StartSpan(ctx, "arrtrix.runtime.stop") + defer span.End() + + m.Bridge.StopWithTimeout(5 * time.Second) + span.SetStatus(codes.Ok, "") + observability.RecordStartupPhase(ctx, "stop", "ok", time.Since(start)) + + if m.OTEL != nil { + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := m.OTEL.Shutdown(shutdownCtx); err != nil && m.Log != nil { + m.Log.Error().Err(err).Msg("Failed to shut down observability") + } + } +} + +func (m *Main) WaitForInterrupt() int { + interrupts := make(chan os.Signal, 1) + signal.Notify(interrupts, os.Interrupt, syscall.SIGTERM) + select { + case <-interrupts: + m.Log.Info().Msg("Interrupt signal received from OS") + return 0 + case exitCode := <-m.manualStop: + m.Log.Info().Msg("Internal stop signal received") + return exitCode + } +} + +func (m *Main) TriggerStop(exitCode int) { + select { + case m.manualStop <- exitCode: + default: + } +} + +func (m *Main) InitVersion(tag, commit, rawBuildTime string) { + m.ver = progver.ProgramVersion{ + Name: m.Name, + URL: m.URL, + BaseVersion: m.Version, + }.Init(tag, commit, rawBuildTime) + mautrix.DefaultUserAgent = fmt.Sprintf("%s/%s %s", m.Name, m.ver.FormattedVersion, mautrix.DefaultUserAgent) + m.Version = m.ver.FormattedVersion +} + +func (m *Main) validateConfig() error { + switch { + case m.Config.Homeserver.Address == "http://example.localhost:8008": + return errors.New("homeserver.address not configured") + case m.Config.Homeserver.Domain == "example.com": + return errors.New("homeserver.domain not configured") + case !bridgeconfig.AllowedHomeserverSoftware[m.Config.Homeserver.Software]: + return errors.New("invalid value for homeserver.software (use `standard` if you don't know what the field is for)") + case m.Config.AppService.ASToken == "This value is generated when generating the registration": + return errors.New("appservice.as_token not configured. Did you forget to generate the registration?") + case m.Config.AppService.HSToken == "This value is generated when generating the registration": + return errors.New("appservice.hs_token not configured. Did you forget to generate the registration?") + case m.Config.Database.URI == "postgres://user:password@host/database?sslmode=disable": + return errors.New("database.uri not configured") + case !m.Config.Bridge.Permissions.IsConfigured(): + return errors.New("bridge.permissions not configured") + case !strings.Contains(m.Config.AppService.FormatUsername("1234567890"), "1234567890"): + return errors.New("username template is missing user ID placeholder") + default: + if validator, ok := m.Connector.(bridgev2.ConfigValidatingNetwork); ok { + return validator.ValidateConfig() + } + return nil + } +} + +func (m *Main) initDB() { + if m.Config.Database.Type == "sqlite3" { + m.Log.WithLevel(zerolog.FatalLevel).Msg("Invalid database type sqlite3. Use sqlite3-fk-wal instead.") + os.Exit(14) + } + if (m.Config.Database.Type == "sqlite3-fk-wal" || m.Config.Database.Type == "litestream") && + m.Config.Database.MaxOpenConns != 1 && + !strings.Contains(m.Config.Database.URI, "_txlock=immediate") { + var fixedURI string + switch { + case !strings.HasPrefix(m.Config.Database.URI, "file:"): + fixedURI = fmt.Sprintf("file:%s?_txlock=immediate", m.Config.Database.URI) + case !strings.ContainsRune(m.Config.Database.URI, '?'): + fixedURI = fmt.Sprintf("%s?_txlock=immediate", m.Config.Database.URI) + default: + fixedURI = fmt.Sprintf("%s&_txlock=immediate", m.Config.Database.URI) + } + m.Log.Warn().Str("fixed_uri_example", fixedURI).Msg("Using SQLite without _txlock=immediate is not recommended") + } + + var err error + m.DB, err = dbutil.NewFromConfig("megabridge/"+m.Name, m.Config.Database, dbutil.ZeroLogger(m.Log.With().Str("db_section", "main").Logger())) + if err != nil { + m.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to initialize database connection") + os.Exit(14) + } + m.DB.IgnoreUnsupportedDatabase = *ignoreUnsupportedDatabase + m.DB.IgnoreForeignTables = *ignoreForeignTables +} + +func (m *Main) saveConfig() error { + publicConfig := *m.PublicConfig + publicConfig.AppService.ASToken = m.Config.AppService.ASToken + publicConfig.AppService.HSToken = m.Config.AppService.HSToken + + configData, err := yaml.Marshal(&publicConfig) + if err != nil { + return err + } + return os.WriteFile(m.ConfigPath, configData, 0o600) +} diff --git a/packages/arrtrix/pkg/runtime/main_test.go b/packages/arrtrix/pkg/runtime/main_test.go new file mode 100644 index 0000000..f54201b --- /dev/null +++ b/packages/arrtrix/pkg/runtime/main_test.go @@ -0,0 +1,30 @@ +package runtime + +import ( + "os" + "path/filepath" + "testing" + + "maunium.net/go/mautrix/bridgev2/bridgeconfig" +) + +func TestLoadRegistrationTokens(t *testing.T) { + tempDir := t.TempDir() + registrationPath := filepath.Join(tempDir, "registration.yaml") + if err := os.WriteFile(registrationPath, []byte("as_token: app-token\nhs_token: hs-token\n"), 0o600); err != nil { + t.Fatalf("failed to write registration file: %v", err) + } + + cfg := &bridgeconfig.Config{} + main := &Main{RegistrationPath: registrationPath} + if err := main.loadRegistrationTokens(cfg); err != nil { + t.Fatalf("loadRegistrationTokens returned error: %v", err) + } + + if cfg.AppService.ASToken != "app-token" { + t.Fatalf("expected as token to be loaded, got %q", cfg.AppService.ASToken) + } + if cfg.AppService.HSToken != "hs-token" { + t.Fatalf("expected hs token to be loaded, got %q", cfg.AppService.HSToken) + } +} diff --git a/packages/arrtrix/pkg/subscriptions/repo.go b/packages/arrtrix/pkg/subscriptions/repo.go new file mode 100644 index 0000000..85c6b57 --- /dev/null +++ b/packages/arrtrix/pkg/subscriptions/repo.go @@ -0,0 +1,141 @@ +package subscriptions + +import ( + "context" + "fmt" + + "go.mau.fi/util/dbutil" + "maunium.net/go/mautrix/id" + + "sneeuwvlok/packages/arrtrix/pkg/arr" +) + +type Preference struct { + ContentType arr.ContentType + EventType string + Enabled bool +} + +type Repository struct { + db *dbutil.Database + bridgeID string +} + +func EnsureSchema(ctx context.Context, db *dbutil.Database) error { + _, err := db.Exec(ctx, ` + CREATE TABLE IF NOT EXISTS arrtrix_subscription ( + bridge_id TEXT NOT NULL, + user_mxid TEXT NOT NULL, + content_type TEXT NOT NULL, + event_type TEXT NOT NULL, + enabled BOOLEAN NOT NULL, + PRIMARY KEY (bridge_id, user_mxid, content_type, event_type) + ) + `) + return err +} + +func NewRepository(db *dbutil.Database, bridgeID string) *Repository { + return &Repository{db: db, bridgeID: bridgeID} +} + +func (r *Repository) EnsureDefaults(ctx context.Context, userID id.UserID) error { + var existing int + if err := r.db.QueryRow(ctx, `SELECT COUNT(*) FROM arrtrix_subscription WHERE bridge_id=$1 AND user_mxid=$2`, r.bridgeID, userID.String()).Scan(&existing); err != nil { + return err + } + if existing > 0 { + return nil + } + + for _, contentType := range arr.SupportedContentTypes() { + for _, eventType := range arr.SupportedEventTypes(contentType) { + if _, err := r.db.Exec(ctx, ` + INSERT INTO arrtrix_subscription (bridge_id, user_mxid, content_type, event_type, enabled) + VALUES ($1, $2, $3, $4, TRUE) + `, r.bridgeID, userID.String(), string(contentType), eventType); err != nil { + return err + } + } + } + return nil +} + +func (r *Repository) List(ctx context.Context, userID id.UserID) ([]Preference, error) { + if err := r.EnsureDefaults(ctx, userID); err != nil { + return nil, err + } + + rows, err := r.db.Query(ctx, ` + SELECT content_type, event_type, enabled + FROM arrtrix_subscription + WHERE bridge_id=$1 AND user_mxid=$2 + ORDER BY content_type, event_type + `, r.bridgeID, userID.String()) + if err != nil { + return nil, err + } + defer rows.Close() + + var preferences []Preference + for rows.Next() { + var contentType string + var preference Preference + if err = rows.Scan(&contentType, &preference.EventType, &preference.Enabled); err != nil { + return nil, err + } + preference.ContentType = arr.ContentType(contentType) + preferences = append(preferences, preference) + } + if err = rows.Err(); err != nil { + return nil, err + } + return preferences, nil +} + +func (r *Repository) Set(ctx context.Context, userID id.UserID, contentType arr.ContentType, eventType string, enabled bool) error { + if err := r.EnsureDefaults(ctx, userID); err != nil { + return err + } + if _, err := r.db.Exec(ctx, ` + INSERT INTO arrtrix_subscription (bridge_id, user_mxid, content_type, event_type, enabled) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (bridge_id, user_mxid, content_type, event_type) + DO UPDATE SET enabled=excluded.enabled + `, r.bridgeID, userID.String(), string(contentType), eventType, enabled); err != nil { + return err + } + return nil +} + +func (r *Repository) SetAll(ctx context.Context, userID id.UserID, contentType arr.ContentType, enabled bool) error { + if err := r.EnsureDefaults(ctx, userID); err != nil { + return err + } + for _, eventType := range arr.SupportedEventTypes(contentType) { + if err := r.Set(ctx, userID, contentType, eventType, enabled); err != nil { + return err + } + } + return nil +} + +func (r *Repository) Allows(ctx context.Context, userID id.UserID, contentType arr.ContentType, eventType string) (bool, error) { + if !arr.SupportsEventType(contentType, eventType) { + return true, nil + } + if err := r.EnsureDefaults(ctx, userID); err != nil { + return false, err + } + + var enabled bool + err := r.db.QueryRow(ctx, ` + SELECT enabled + FROM arrtrix_subscription + WHERE bridge_id=$1 AND user_mxid=$2 AND content_type=$3 AND event_type=$4 + `, r.bridgeID, userID.String(), string(contentType), eventType).Scan(&enabled) + if err != nil { + return false, fmt.Errorf("query subscription: %w", err) + } + return enabled, nil +} diff --git a/packages/arrtrix/pkg/webhook/arr.go b/packages/arrtrix/pkg/webhook/arr.go new file mode 100644 index 0000000..5446825 --- /dev/null +++ b/packages/arrtrix/pkg/webhook/arr.go @@ -0,0 +1,349 @@ +package webhook + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" + + "sneeuwvlok/packages/arrtrix/pkg/arr" + "sneeuwvlok/packages/arrtrix/pkg/observability" +) + +const ArrWebhookPath = "/_arrtrix/webhook" + +var ( + ErrNoManagementRoom = errors.New("no management room configured") + ErrAmbiguousManagementRoom = errors.New("multiple management rooms configured") +) + +type payload struct { + EventType string `json:"eventType"` + Movie *movie `json:"movie"` + MovieFile *movieFile `json:"movieFile"` + Series *series `json:"series"` + Episodes []episode `json:"episodes"` + EpisodeFile *episodeFile `json:"episodeFile"` + IsUpgrade bool `json:"isUpgrade"` +} + +type movie struct { + Title string `json:"title"` + Year int `json:"year"` + ImdbID string `json:"imdbId"` + TmdbID int `json:"tmdbId"` + Path string `json:"path"` +} + +type movieFile struct { + Quality string `json:"quality"` + RelativePath string `json:"relativePath"` + SceneName string `json:"sceneName"` + ReleaseGroup string `json:"releaseGroup"` +} + +type series struct { + Title string `json:"title"` + Year int `json:"year"` + Path string `json:"path"` +} + +type episode struct { + SeasonNumber int `json:"seasonNumber"` + EpisodeNumber int `json:"episodeNumber"` + Title string `json:"title"` +} + +type episodeFile struct { + Quality string `json:"quality"` + RelativePath string `json:"relativePath"` + SceneName string `json:"sceneName"` +} + +type managementTarget struct { + UserID id.UserID + RoomID id.RoomID +} + +type roomResolver interface { + ResolveManagementRoom(context.Context) (managementTarget, error) +} + +type noticeSender interface { + SendNotice(context.Context, id.RoomID, string) error +} + +type SubscriptionFilter interface { + Allows(context.Context, id.UserID, arr.ContentType, string) (bool, error) +} + +type ArrHandler struct { + resolver roomResolver + sender noticeSender + subscriptions SubscriptionFilter +} + +func MountArr(router *http.ServeMux, bridge *bridgev2.Bridge, subscriptions SubscriptionFilter) error { + if bridge == nil { + return fmt.Errorf("bridge is not initialized") + } + handler := &ArrHandler{ + resolver: bridgeRoomResolver{bridge: bridge}, + sender: bridgeNoticeSender{bridge: bridge}, + subscriptions: subscriptions, + } + router.Handle(fmt.Sprintf("POST %s", ArrWebhookPath), handler) + return nil +} + +func (h *ArrHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + start := time.Now() + ctx, span := observability.StartSpan(r.Context(), "arrtrix.webhook.handle", trace.WithSpanKind(trace.SpanKindServer)) + defer span.End() + + statusCode := http.StatusAccepted + outcome := "ok" + eventType := "" + defer func() { + observability.RecordWebhook(ctx, eventType, outcome, statusCode, time.Since(start)) + }() + + var body payload + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + statusCode = http.StatusBadRequest + outcome = "invalid_payload" + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + http.Error(w, "invalid webhook payload", http.StatusBadRequest) + return + } + if strings.TrimSpace(body.EventType) == "" { + statusCode = http.StatusBadRequest + outcome = "missing_event_type" + span.SetStatus(codes.Error, "missing eventType") + http.Error(w, "missing eventType", http.StatusBadRequest) + return + } + eventType = body.EventType + span.SetAttributes( + attribute.String("arrtrix.webhook.event_type", body.EventType), + attribute.String("http.method", r.Method), + attribute.String("http.route", ArrWebhookPath), + ) + + target, err := h.resolver.ResolveManagementRoom(ctx) + if err != nil { + statusCode = http.StatusInternalServerError + outcome = "resolve_failed" + if errors.Is(err, ErrNoManagementRoom) || errors.Is(err, ErrAmbiguousManagementRoom) { + statusCode = http.StatusConflict + outcome = "routing_conflict" + } + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + http.Error(w, err.Error(), statusCode) + return + } + + contentType, ok := body.ContentType() + if ok && h.subscriptions != nil { + allowed, filterErr := h.subscriptions.Allows(ctx, target.UserID, contentType, body.EventType) + if filterErr != nil { + statusCode = http.StatusInternalServerError + outcome = "subscription_check_failed" + span.RecordError(filterErr) + span.SetStatus(codes.Error, filterErr.Error()) + http.Error(w, "failed to evaluate subscriptions", statusCode) + return + } + if !allowed { + outcome = "filtered" + span.SetStatus(codes.Ok, "filtered") + w.WriteHeader(statusCode) + return + } + } + + if err = h.sender.SendNotice(ctx, target.RoomID, renderNotice(body)); err != nil { + statusCode = http.StatusBadGateway + outcome = "delivery_failed" + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + http.Error(w, "failed to deliver webhook", http.StatusBadGateway) + return + } + + span.SetStatus(codes.Ok, "") + w.WriteHeader(statusCode) +} + +type bridgeRoomResolver struct { + bridge *bridgev2.Bridge +} + +func (r bridgeRoomResolver) ResolveManagementRoom(ctx context.Context) (managementTarget, error) { + ctx, span := observability.StartSpan(ctx, "arrtrix.webhook.resolve_management_room") + defer span.End() + + rows, err := r.bridge.DB.Query(ctx, `SELECT mxid, management_room FROM "user" WHERE bridge_id=$1 AND management_room IS NOT NULL AND management_room <> ''`, r.bridge.ID) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return managementTarget{}, fmt.Errorf("failed to query management rooms: %w", err) + } + defer rows.Close() + + var target managementTarget + var owners []id.UserID + for rows.Next() { + var mxid, managementRoom string + if err = rows.Scan(&mxid, &managementRoom); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return managementTarget{}, fmt.Errorf("failed to scan management room: %w", err) + } + owners = append(owners, id.UserID(mxid)) + if target.RoomID == "" { + target = managementTarget{ + UserID: id.UserID(mxid), + RoomID: id.RoomID(managementRoom), + } + } + } + if err = rows.Err(); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return managementTarget{}, fmt.Errorf("failed to iterate management rooms: %w", err) + } + + switch len(owners) { + case 0: + span.SetStatus(codes.Error, ErrNoManagementRoom.Error()) + return managementTarget{}, ErrNoManagementRoom + case 1: + span.SetAttributes(attribute.Int("arrtrix.management_room.count", 1)) + span.SetStatus(codes.Ok, "") + return target, nil + default: + span.SetAttributes(attribute.Int("arrtrix.management_room.count", len(owners))) + span.SetStatus(codes.Error, ErrAmbiguousManagementRoom.Error()) + return managementTarget{}, fmt.Errorf("%w: %s", ErrAmbiguousManagementRoom, strings.Join(convertUserIDs(owners), ", ")) + } +} + +type bridgeNoticeSender struct { + bridge *bridgev2.Bridge +} + +func (s bridgeNoticeSender) SendNotice(ctx context.Context, roomID id.RoomID, markdown string) error { + ctx, span := observability.StartSpan(ctx, "arrtrix.webhook.send_notice") + defer span.End() + span.SetAttributes(attribute.String("matrix.room_id", roomID.String())) + + if err := s.bridge.Bot.EnsureJoined(ctx, roomID); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return err + } + content := format.RenderMarkdown(markdown, true, false) + _, err := s.bridge.Bot.SendMessage(ctx, roomID, event.EventMessage, &event.Content{Parsed: &content}, nil) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return err + } + span.SetStatus(codes.Ok, "") + return err +} + +func renderNotice(body payload) string { + lines := []string{fmt.Sprintf("**Arr %s**", body.EventType)} + + switch contentType, ok := body.ContentType(); { + case ok && contentType == arr.ContentTypeMovies: + title := body.Movie.Title + if body.Movie.Year != 0 { + title = fmt.Sprintf("%s (%d)", title, body.Movie.Year) + } + lines = append(lines, fmt.Sprintf("Movie: %s", title)) + if body.MovieFile != nil && body.MovieFile.Quality != "" { + lines = append(lines, fmt.Sprintf("Quality: %s", body.MovieFile.Quality)) + } + if body.MovieFile != nil && body.MovieFile.RelativePath != "" { + lines = append(lines, fmt.Sprintf("File: `%s`", body.MovieFile.RelativePath)) + } + if body.EventType == "Download" { + lines = append(lines, fmt.Sprintf("Upgrade: %t", body.IsUpgrade)) + } + if body.Movie.ImdbID != "" { + lines = append(lines, fmt.Sprintf("IMDb: `%s`", body.Movie.ImdbID)) + } + case ok && contentType == arr.ContentTypeSeries: + title := body.Series.Title + if body.Series.Year != 0 { + title = fmt.Sprintf("%s (%d)", title, body.Series.Year) + } + lines = append(lines, fmt.Sprintf("Series: %s", title)) + if len(body.Episodes) > 0 { + lines = append(lines, fmt.Sprintf("Episodes: %s", renderEpisodes(body.Episodes))) + } + if body.EpisodeFile != nil && body.EpisodeFile.Quality != "" { + lines = append(lines, fmt.Sprintf("Quality: %s", body.EpisodeFile.Quality)) + } + if body.EpisodeFile != nil && body.EpisodeFile.RelativePath != "" { + lines = append(lines, fmt.Sprintf("File: `%s`", body.EpisodeFile.RelativePath)) + } + default: + if body.EventType != "Test" { + lines = append(lines, "Payload received.") + } + } + + return strings.Join(lines, "\n") +} + +func convertUserIDs(users []id.UserID) []string { + out := make([]string, len(users)) + for i, user := range users { + out[i] = string(user) + } + return out +} + +var _ roomResolver = bridgeRoomResolver{} +var _ noticeSender = bridgeNoticeSender{} +var _ http.Handler = (*ArrHandler)(nil) + +func (p payload) ContentType() (arr.ContentType, bool) { + switch { + case p.Movie != nil: + return arr.ContentTypeMovies, true + case p.Series != nil: + return arr.ContentTypeSeries, true + default: + return "", false + } +} + +func renderEpisodes(episodes []episode) string { + parts := make([]string, 0, len(episodes)) + for _, item := range episodes { + if item.Title != "" { + parts = append(parts, fmt.Sprintf("S%02dE%02d %s", item.SeasonNumber, item.EpisodeNumber, item.Title)) + continue + } + parts = append(parts, fmt.Sprintf("S%02dE%02d", item.SeasonNumber, item.EpisodeNumber)) + } + return strings.Join(parts, ", ") +} diff --git a/packages/arrtrix/pkg/webhook/arr_test.go b/packages/arrtrix/pkg/webhook/arr_test.go new file mode 100644 index 0000000..e7e89f6 --- /dev/null +++ b/packages/arrtrix/pkg/webhook/arr_test.go @@ -0,0 +1,145 @@ +package webhook + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "maunium.net/go/mautrix/id" + + "sneeuwvlok/packages/arrtrix/pkg/arr" +) + +type stubRoomResolver struct { + target managementTarget + err error +} + +func (s stubRoomResolver) ResolveManagementRoom(context.Context) (managementTarget, error) { + return s.target, s.err +} + +type stubNoticeSender struct { + roomID id.RoomID + message string + err error +} + +type stubSubscriptionFilter struct { + allowed bool + err error +} + +func (s *stubNoticeSender) SendNotice(_ context.Context, roomID id.RoomID, message string) error { + s.roomID = roomID + s.message = message + return s.err +} + +func (s stubSubscriptionFilter) Allows(context.Context, id.UserID, arr.ContentType, string) (bool, error) { + return s.allowed, s.err +} + +func TestMountArrRequiresBridge(t *testing.T) { + router := http.NewServeMux() + if err := MountArr(router, nil, nil); err == nil { + t.Fatal("expected nil bridge to fail") + } +} + +func TestArrHandlerDeliversNotice(t *testing.T) { + sender := &stubNoticeSender{} + handler := &ArrHandler{ + resolver: stubRoomResolver{target: managementTarget{UserID: "@user:test", RoomID: "!room:test"}}, + sender: sender, + } + + req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Download","movie":{"title":"Dune","year":2021,"imdbId":"tt1160419"},"movieFile":{"quality":"1080p","relativePath":"Dune (2021)/Dune.mkv"},"isUpgrade":false}`)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusAccepted { + t.Fatalf("expected accepted status, got %d", rec.Code) + } + if sender.roomID != "!room:test" { + t.Fatalf("expected notice sent to management room, got %q", sender.roomID) + } + if !strings.Contains(sender.message, "**Arr Download**") || !strings.Contains(sender.message, "Dune (2021)") { + t.Fatalf("unexpected message: %s", sender.message) + } +} + +func TestArrHandlerReportsAmbiguousManagementRoom(t *testing.T) { + handler := &ArrHandler{ + resolver: stubRoomResolver{err: ErrAmbiguousManagementRoom}, + sender: &stubNoticeSender{}, + } + + req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Test"}`)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusConflict { + t.Fatalf("expected conflict status, got %d", rec.Code) + } +} + +func TestRenderNoticeForTestEvent(t *testing.T) { + msg := renderNotice(payload{EventType: "Test"}) + if strings.TrimSpace(msg) != "**Arr Test**" { + t.Fatalf("unexpected test-event message: %q", msg) + } +} + +func TestArrHandlerReturnsBadGatewayOnSendFailure(t *testing.T) { + handler := &ArrHandler{ + resolver: stubRoomResolver{target: managementTarget{UserID: "@user:test", RoomID: "!room:test"}}, + sender: &stubNoticeSender{err: errors.New("send failed")}, + } + + req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Test"}`)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadGateway { + t.Fatalf("expected bad gateway status, got %d", rec.Code) + } +} + +func TestArrHandlerRejectsMissingEventType(t *testing.T) { + handler := &ArrHandler{ + resolver: stubRoomResolver{target: managementTarget{UserID: "@user:test", RoomID: "!room:test"}}, + sender: &stubNoticeSender{}, + } + + req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"movie":{"title":"Dune"}}`)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("expected bad request status, got %d", rec.Code) + } +} + +func TestArrHandlerFiltersDisabledSubscriptions(t *testing.T) { + sender := &stubNoticeSender{} + handler := &ArrHandler{ + resolver: stubRoomResolver{target: managementTarget{UserID: "@user:test", RoomID: "!room:test"}}, + sender: sender, + subscriptions: stubSubscriptionFilter{allowed: false}, + } + + req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Download","movie":{"title":"Dune","year":2021}}`)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusAccepted { + t.Fatalf("expected accepted status, got %d", rec.Code) + } + if sender.roomID != "" { + t.Fatalf("expected no notice to be sent, got room %q", sender.roomID) + } +} diff --git a/packages/flake-module.nix b/packages/flake-module.nix deleted file mode 100644 index dfe7214..0000000 --- a/packages/flake-module.nix +++ /dev/null @@ -1,13 +0,0 @@ -{inputs, ...}: { - imports = []; - - perSystem = { - system, - pkgs, - ... - }: { - packages = { - studio = pkgs.callPackage ./studio {erosanix = inputs.erosanix.lib.${system};}; - }; - }; -} diff --git a/packages/studio/default.nix b/packages/studio/default.nix index 10ae62c..cb628c9 100644 --- a/packages/studio/default.nix +++ b/packages/studio/default.nix @@ -1,18 +1,18 @@ { pkgs, - erosanix, + inputs, }: let - inherit (builtins) fetchurl replaceStrings; + inherit (builtins) fetchurl; inherit (pkgs) makeDesktopItem copyDesktopItems wineWow64Packages; - inherit (erosanix) mkWindowsAppNoCC makeDesktopIcon copyDesktopIcons; + inherit (inputs.erosanix.lib.x86_64-linux) mkWindowsAppNoCC makeDesktopIcon copyDesktopIcons; wine = wineWow64Packages.base; in mkWindowsAppNoCC rec { inherit wine; - pname = "studio"; - version = "2.25.12"; + pname = "studio"; + version = "2.25.12"; src = fetchurl { url = "https://studio.download.bricklink.info/Studio2.0+EarlyAccess/Archive/2.25.12_1/Studio+2.0+EarlyAccess.exe"; @@ -36,6 +36,15 @@ in persistRuntimeLayer = true; inputHashMethod = "version"; + # Can be used to precisely select the Direct3D implementation. + # + # | enableVulkan | rendererOverride | Direct3D implementation | + # |--------------|------------------|-------------------------| + # | false | null | OpenGL | + # | true | null | Vulkan (DXVK) | + # | * | dxvk-vulkan | Vulkan (DXVK) | + # | * | wine-opengl | OpenGL | + # | * | wine-vulkan | Vulkan (VKD3D) | enableVulkan = false; rendererOverride = null; @@ -47,24 +56,24 @@ in nativeBuildInputs = [copyDesktopIcons copyDesktopItems]; - winAppInstall = replaceStrings ["\r"] [""] '' + winAppInstall = '' wine64 ${src} wineserver -W wine64 reg add 'HKEY_CURRENT_USER\Software\Wine\X11 Driver' /t REG_SZ /v UseTakeFocus /d N /f ''; - winAppPreRun = replaceStrings ["\r"] [""] '' + winAppPreRun = '' wineserver -W wine64 reg add 'HKEY_CURRENT_USER\Software\Wine\X11 Driver' /t REG_SZ /v UseTakeFocus /d N /f ''; - winAppRun = replaceStrings ["\r"] [""] '' + winAppRun = '' wine64 "$WINEPREFIX/drive_c/Program Files/Studio 2.0/Studio.exe" "$ARGS" ''; winAppPostRun = ""; - installPhase = replaceStrings ["\r"] [""] '' + installPhase = '' runHook preInstall ln -s $out/bin/.launcher $out/bin/${pname} diff --git a/scratchpad b/scratchpad deleted file mode 100644 index afff9b8..0000000 --- a/scratchpad +++ /dev/null @@ -1 +0,0 @@ ---resume=18a19308-41c9-4898-ab01-594195fd75a1 diff --git a/script/.shared/pwgen b/script/.shared/pwgen new file mode 100644 index 0000000..85fc69f --- /dev/null +++ b/script/.shared/pwgen @@ -0,0 +1,3 @@ +#!/bin/bash + +pwgen -s 128 1 diff --git a/script/qbittorrent/hash.py b/script/qbittorrent/hash.py new file mode 100644 index 0000000..a92343f --- /dev/null +++ b/script/qbittorrent/hash.py @@ -0,0 +1,19 @@ +#!/usr/bin/bash + +import base64 +import hashlib +import sys +import uuid + +password = sys.argv[1] +salt = uuid.uuid4() +salt_bytes = salt.bytes + +password = str.encode(password) +hashed_password = hashlib.pbkdf2_hmac("sha512", password, salt_bytes, 100000, dklen=64) +b64_salt = base64.b64encode(salt_bytes).decode("utf-8") +b64_password = base64.b64encode(hashed_password).decode("utf-8") +password_string = "@ByteArray({salt}:{password})".format( + salt=b64_salt, password=b64_password +) +print(password_string) diff --git a/script/qbittorrent/password b/script/qbittorrent/password new file mode 100644 index 0000000..85fc69f --- /dev/null +++ b/script/qbittorrent/password @@ -0,0 +1,3 @@ +#!/bin/bash + +pwgen -s 128 1 diff --git a/script/qbittorrent/password_hash b/script/qbittorrent/password_hash new file mode 100644 index 0000000..86ba315 --- /dev/null +++ b/script/qbittorrent/password_hash @@ -0,0 +1,3 @@ +#!/bin/bash + +python ./hash.py "$(just vars get ulmo qbittorrent/password | jq -r)" diff --git a/script/synapse/shared_secret b/script/synapse/shared_secret new file mode 100644 index 0000000..85fc69f --- /dev/null +++ b/script/synapse/shared_secret @@ -0,0 +1,3 @@ +#!/bin/bash + +pwgen -s 128 1 diff --git a/shells/default/default.nix b/shells/default/default.nix new file mode 100644 index 0000000..76d15b7 --- /dev/null +++ b/shells/default/default.nix @@ -0,0 +1,23 @@ +{ + mkShell, + inputs, + pkgs, + stdenv, + ... +}: +mkShell { + packages = with pkgs; [ + bash + sops + just + yq + pwgen + alejandra + nil + nixd + openssl + inputs.clan-core.packages.${stdenv.hostPlatform.system}.clan-cli + nix-output-monitor + dos2unix + ]; +} diff --git a/sops/machines/ulmo/key.json b/sops/machines/ulmo/key.json deleted file mode 100644 index 90b904f..0000000 --- a/sops/machines/ulmo/key.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "publickey": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "type": "age" - } -] \ No newline at end of file diff --git a/sops/users/chris/key.json b/sops/users/chris/key.json deleted file mode 100644 index 90b904f..0000000 --- a/sops/users/chris/key.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "publickey": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "type": "age" - } -] \ No newline at end of file diff --git a/systems/x86_64-install-iso/minimal/default.nix b/systems/x86_64-install-iso/minimal/default.nix new file mode 100644 index 0000000..7d80104 --- /dev/null +++ b/systems/x86_64-install-iso/minimal/default.nix @@ -0,0 +1,121 @@ +{ pkgs, lib, ... }: +let + inherit (lib) mkForce; +in +{ + boot = { + supportedFilesystems = mkForce ["btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs"]; + + loader.efi.canTouchEfiVariables = true; + }; + + networking = { + wireless.enable = mkForce false; + networkmanager.enable = true; + }; + + nix = { + enable = true; + extraOptions = "experimental-features = nix-command flakes"; + channel.enable = false; + + settings = { + experimental-features = [ "nix-command" "flakes" ]; + allowed-users = [ "@wheel" ]; + trusted-users = [ "@wheel" ]; + + auto-optimise-store = true; + connect-timeout = 5; + http-connections = 50; + log-lines = 50; # more log lines in case of error + min-free = 1 * (1024 * 1024 * 1024); # GiB # start garbage collector + max-free = 50 * (1024 * 1024 * 1024); # GiB # until + warn-dirty = false; + }; + }; + + services = { + qemuGuest.enable = true; + openssh = { + enable = true; + settings.PermitRootLogin = mkForce "yes"; + }; + }; + + users.users.nixos = { + initialPassword = "kaas"; + initialHashedPassword = mkForce null; + extraGroups = [ "networkmanager" ]; + }; + + environment.systemPackages = with pkgs; [ + # sbctl + git + # gum + # ( + # writeShellScriptBin "rescue" '' + # #!/usr/bin/env bash + # set -euo pipefail + + # gum "device name" + + # sudo mkdir -p /mnt/{dev,proc,sys,boot} + # sudo mount -o bind /dev /mnt/dev + # sudo mount -o bind /proc /mnt/proc + # sudo mount -o bind /sys /mnt/sys + # sudo chroot /mnt /nix/var/nix/profiles/system/activate + # sudo chroot /mnt /run/current-system/sw/bin/bash + + # sudo mount /dev/vda1 /mnt/boot + # sudo cryptsetup open /dev/vda3 cryptroot + # sudo mount /dev/mapper/cryptroot /mnt/ + + # sudo nixos-enter + # '' + # ) + # ( + # writeShellScriptBin "nix_installer" + # '' + # #!/usr/bin/env bash + # set -euo pipefail + + # if [ "$(id -u)" -eq 0 ]; then + # echo "ERROR! $(basename "$0") should be run as a regular user" + # exit 1 + # fi + + # if [ ! -d "$HOME/github/sneeuwvlok/.git" ]; then + # git clone https://github.com/chris-kruining/sneeuwvlok.git "$HOME/github/sneeuwvlok" + # fi + + # TARGET_HOST=$(ls -1 ~/github/sneeuwvlok/systems/*/default.nix | cut -d'/' -f6 | grep -v iso | gum choose) + + # if [ ! -e "$HOME/github/sneeuwvlok/hosts/$TARGET_HOST/disks.nix" ]; then + # echo "ERROR! $(basename "$0") could not find the required $HOME/github/sneeuwvlok/hosts/$TARGET_HOST/disks.nix" + # exit 1 + # fi + + # gum confirm --default=false \ + # "🔥 🔥 🔥 WARNING!!!! This will ERASE ALL DATA on the disk $TARGET_HOST. Are you sure you want to continue?" + + # echo "Partitioning Disks" + # sudo nix run github:nix-community/disko \ + # --extra-experimental-features "nix-command flakes" \ + # --no-write-lock-file \ + # -- \ + # --mode zap_create_mount \ + # "$HOME/dotfiles/hosts/$TARGET_HOST/disks.nix" + + # #echo "Creating blank volume" + # #sudo btrfs subvolume snapshot -r /mnt/ /mnt/root-blank + + # #echo "Set up attic binary cache" + # #attic use prod || true + + # sudo nixos-install --flake "$HOME/dotfiles#$TARGET_HOST" + # '' + # ) + ]; + + system.stateVersion = "23.11"; +} diff --git a/machines/aule/README.md b/systems/x86_64-linux/aule/README.md similarity index 100% rename from machines/aule/README.md rename to systems/x86_64-linux/aule/README.md diff --git a/machines/mandos/README.md b/systems/x86_64-linux/mandos/README.md similarity index 100% rename from machines/mandos/README.md rename to systems/x86_64-linux/mandos/README.md diff --git a/systems/x86_64-linux/mandos/default.nix b/systems/x86_64-linux/mandos/default.nix new file mode 100644 index 0000000..b1605f8 --- /dev/null +++ b/systems/x86_64-linux/mandos/default.nix @@ -0,0 +1,31 @@ +{ ... }: +{ + imports = [ + ./disks.nix + ./hardware.nix + ]; + + sneeuwvlok = { + hardware.has = { + gpu.nvidia = true; + audio = true; + }; + + boot = { + quiet = true; + animated = true; + }; + + desktop.use = "gamescope"; + + application = { + steam.enable = true; + }; + + editor = { + nano.enable = true; + }; + }; + + system.stateVersion = "23.11"; +} diff --git a/machines/mandos/disks.nix b/systems/x86_64-linux/mandos/disks.nix similarity index 100% rename from machines/mandos/disks.nix rename to systems/x86_64-linux/mandos/disks.nix diff --git a/machines/mandos/hardware.nix b/systems/x86_64-linux/mandos/hardware.nix similarity index 80% rename from machines/mandos/hardware.nix rename to systems/x86_64-linux/mandos/hardware.nix index ebed139..60759bd 100644 --- a/machines/mandos/hardware.nix +++ b/systems/x86_64-linux/mandos/hardware.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, modulesPath, ... }: +{ config, lib, pkgs, modulesPath, system, ... }: let inherit (lib.modules) mkDefault; in @@ -13,6 +13,6 @@ in extraModulePackages = [ ]; }; - nixpkgs.hostPlatform = mkDefault pkgs.stdenv.hostPlatform.system; + nixpkgs.hostPlatform = mkDefault system; hardware.cpu.intel.updateMicrocode = mkDefault config.hardware.enableRedistributableFirmware; } diff --git a/machines/manwe/README.md b/systems/x86_64-linux/manwe/README.md similarity index 100% rename from machines/manwe/README.md rename to systems/x86_64-linux/manwe/README.md diff --git a/systems/x86_64-linux/manwe/default.nix b/systems/x86_64-linux/manwe/default.nix new file mode 100644 index 0000000..a1b421b --- /dev/null +++ b/systems/x86_64-linux/manwe/default.nix @@ -0,0 +1,42 @@ +{ pkgs, ...}: { + imports = [ + ./disks.nix + ./hardware.nix + ]; + + system.activationScripts.remove-gtkrc.text = "rm -f /home/chris/.gtkrc-2.0"; + + services.logrotate.checkConfig = false; + + environment.systemPackages = with pkgs; [ beyond-all-reason openrct2 ]; + + sneeuwvlok = { + hardware.has = { + gpu.amd = true; + bluetooth = true; + audio = true; + }; + + boot = { + quiet = true; + animated = true; + }; + + desktop.use = "plasma"; + + application = { + steam.enable = true; + }; + + editor = { + nano.enable = true; + }; + }; + + services.displayManager.autoLogin = { + enable = true; + user = "chris"; + }; + + system.stateVersion = "23.11"; +} diff --git a/machines/manwe/disks.nix b/systems/x86_64-linux/manwe/disks.nix similarity index 100% rename from machines/manwe/disks.nix rename to systems/x86_64-linux/manwe/disks.nix diff --git a/systems/x86_64-linux/manwe/hardware.nix b/systems/x86_64-linux/manwe/hardware.nix new file mode 100644 index 0000000..33ae3b5 --- /dev/null +++ b/systems/x86_64-linux/manwe/hardware.nix @@ -0,0 +1,18 @@ +{ config, lib, pkgs, modulesPath, system, ... }: +let + inherit (lib.modules) mkDefault; +in +{ + imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; + + boot = { + initrd.availableKernelModules = [ "xhci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ]; + initrd.kernelModules = [ ]; + kernelModules = [ "kvm-amd" ]; + kernelParams = []; + extraModulePackages = [ ]; + }; + + nixpkgs.hostPlatform = mkDefault system; + hardware.cpu.amd.updateMicrocode = mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/machines/melkor/README.md b/systems/x86_64-linux/melkor/README.md similarity index 100% rename from machines/melkor/README.md rename to systems/x86_64-linux/melkor/README.md diff --git a/machines/orome/README.md b/systems/x86_64-linux/orome/README.md similarity index 100% rename from machines/orome/README.md rename to systems/x86_64-linux/orome/README.md diff --git a/systems/x86_64-linux/orome/default.nix b/systems/x86_64-linux/orome/default.nix new file mode 100644 index 0000000..e155461 --- /dev/null +++ b/systems/x86_64-linux/orome/default.nix @@ -0,0 +1,30 @@ +{pkgs, ...}: { + imports = [ + ./disks.nix + ./hardware.nix + ]; + + environment.systemPackages = with pkgs; [ + azure-cli + github-copilot-cli + ]; + + sneeuwvlok = { + hardware.has = { + bluetooth = true; + audio = true; + }; + + authentication.himmelblau.enable = true; + + application = { + steam.enable = true; + }; + + editor = { + nano.enable = true; + }; + }; + + system.stateVersion = "23.11"; +} diff --git a/machines/orome/disks.nix b/systems/x86_64-linux/orome/disks.nix similarity index 100% rename from machines/orome/disks.nix rename to systems/x86_64-linux/orome/disks.nix diff --git a/machines/orome/hardware.nix b/systems/x86_64-linux/orome/hardware.nix similarity index 80% rename from machines/orome/hardware.nix rename to systems/x86_64-linux/orome/hardware.nix index 2390ffd..ee52810 100644 --- a/machines/orome/hardware.nix +++ b/systems/x86_64-linux/orome/hardware.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, modulesPath, ... }: +{ config, lib, pkgs, modulesPath, system, ... }: let inherit (lib.modules) mkDefault; in @@ -13,6 +13,6 @@ in extraModulePackages = [ ]; }; - nixpkgs.hostPlatform = mkDefault pkgs.stdenv.hostPlatform.system; + nixpkgs.hostPlatform = mkDefault system; hardware.cpu.intel.updateMicrocode = mkDefault config.hardware.enableRedistributableFirmware; } diff --git a/machines/tulkas/README.md b/systems/x86_64-linux/tulkas/README.md similarity index 100% rename from machines/tulkas/README.md rename to systems/x86_64-linux/tulkas/README.md diff --git a/systems/x86_64-linux/tulkas/default.nix b/systems/x86_64-linux/tulkas/default.nix new file mode 100644 index 0000000..40d1673 --- /dev/null +++ b/systems/x86_64-linux/tulkas/default.nix @@ -0,0 +1,32 @@ +{ ... }: +{ + imports = [ + ./disks.nix + ./hardware.nix + ]; + + sneeuwvlok = { + hardware.has = { + gpu.amd = true; + bluetooth = true; + audio = true; + }; + + boot = { + quiet = true; + animated = true; + }; + + desktop.use = "gamescope"; + + application = { + steam.enable = true; + }; + + editor = { + nano.enable = true; + }; + }; + + system.stateVersion = "23.11"; +} diff --git a/machines/tulkas/disks.nix b/systems/x86_64-linux/tulkas/disks.nix similarity index 100% rename from machines/tulkas/disks.nix rename to systems/x86_64-linux/tulkas/disks.nix diff --git a/machines/tulkas/hardware.nix b/systems/x86_64-linux/tulkas/hardware.nix similarity index 80% rename from machines/tulkas/hardware.nix rename to systems/x86_64-linux/tulkas/hardware.nix index cf287de..950d7cc 100644 --- a/machines/tulkas/hardware.nix +++ b/systems/x86_64-linux/tulkas/hardware.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, modulesPath, ... }: +{ config, lib, pkgs, modulesPath, system, ... }: let inherit (lib.modules) mkDefault; in @@ -13,6 +13,6 @@ in extraModulePackages = [ ]; }; - nixpkgs.hostPlatform = mkDefault pkgs.stdenv.hostPlatform.system; + nixpkgs.hostPlatform = mkDefault system; hardware.cpu.intel.updateMicrocode = mkDefault config.hardware.enableRedistributableFirmware; } diff --git a/systems/x86_64-linux/ulmo/default.nix b/systems/x86_64-linux/ulmo/default.nix new file mode 100644 index 0000000..fd25824 --- /dev/null +++ b/systems/x86_64-linux/ulmo/default.nix @@ -0,0 +1,263 @@ +{ + pkgs, + config, + ... +}: { + imports = [ + ./disks.nix + ./hardware.nix + ]; + + environment.systemPackages = with pkgs; [bup]; + services.postgresqlBackup = { + enable = true; + backupAll = true; + startAt = "*-*-* 01:00:00"; + location = "/var/backup/postgresql"; + }; + + networking = { + interfaces.enp2s0 = { + ipv6.addresses = [ + { + address = "2a0d:6e00:1dc9:0::dead:beef"; + prefixLength = 64; + } + ]; + + useDHCP = true; + }; + + defaultGateway = { + address = "192.168.1.1"; + interface = "enp2s0"; + }; + + defaultGateway6 = { + address = "fe80::1"; + interface = "enp2s0"; + }; + }; + + sneeuwvlok = { + services = { + backup.borg.enable = true; + + authentication.zitadel = { + enable = true; + + organization = { + nix = { + user = { + chris = { + email = "chris@kruining.eu"; + firstName = "Chris"; + lastName = "Kruining"; + + roles = ["ORG_OWNER"]; + instanceRoles = ["IAM_OWNER"]; + }; + + kaas = { + email = "chris+kaas@kruining.eu"; + firstName = "Kaas"; + lastName = "Kruining"; + }; + }; + + project = { + ulmo = { + projectRoleCheck = true; + projectRoleAssertion = true; + hasProjectCheck = true; + + role = { + jellyfin = { + group = "jellyfin"; + }; + jellyfin_admin = { + group = "jellyfin"; + }; + }; + + assign = { + chris = ["jellyfin" "jellyfin_admin"]; + kaas = ["jellyfin"]; + }; + + application = { + jellyfin = { + redirectUris = ["https://jellyfin.kruining.eu/sso/OID/redirect/zitadel"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + + forgejo = { + redirectUris = ["https://git.amarth.cloud/user/oauth2/zitadel/callback"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + + vaultwarden = { + redirectUris = ["https://vault.kruining.eu/identity/connect/oidc-signin"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + exportMap = { + client_id = "SSO_CLIENT_ID"; + client_secret = "SSO_CLIENT_SECRET"; + }; + }; + + matrix = { + redirectUris = ["https://matrix.kruining.eu/_synapse/client/oidc/callback"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + + mydia = { + redirectUris = ["http://localhost:2100/auth/oidc/callback"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + + grafana = { + redirectUris = ["http://localhost:9010/login/generic_oauth"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + }; + }; + + convex = { + projectRoleCheck = true; + projectRoleAssertion = true; + hasProjectCheck = true; + + application = { + scry = { + redirectUris = ["https://nautical-salamander-320.eu-west-1.convex.cloud/api/auth/callback/zitadel"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + }; + }; + }; + + action = { + flattenRoles = { + script = '' + (ctx, api) => { + if (ctx.v1.user.grants == undefined || ctx.v1.user.grants.count == 0) { + return; + } + + const roles = ctx.v1.user.grants.grants.flatMap(({ roles, projectId }) => roles.map(role => projectId + ':' + role)); + + api.v1.claims.setClaim('nix:zitadel:custom', JSON.stringify({ roles })); + }; + ''; + }; + }; + + triggers = [ + { + flowType = "customiseToken"; + triggerType = "preUserinfoCreation"; + actions = ["flattenRoles"]; + } + { + flowType = "customiseToken"; + triggerType = "preAccessTokenCreation"; + actions = ["flattenRoles"]; + } + ]; + }; + }; + }; + + communication.matrix.enable = true; + + development.forgejo.enable = true; + + networking.ssh.enable = true; + networking.caddy.hosts = { + # Expose amarht cloud stuff like this until I have a proper solution + "auth.amarth.cloud" = '' + reverse_proxy http://192.168.1.223:9092 + ''; + + "amarth.cloud" = '' + reverse_proxy http://192.168.1.223:8080 + ''; + }; + + media.enable = true; + media.glance.enable = true; + media.mydia.enable = true; + media.nfs.enable = true; + media.jellyfin.enable = true; + media.servarr = { + radarr = { + enable = true; + port = 2010; + rootFolders = [ + "/var/media/movies" + ]; + }; + + sonarr = { + enable = true; + # debug = true; + port = 2020; + rootFolders = [ + "/var/media/series" + ]; + }; + + lidarr = { + enable = true; + debug = true; + port = 2030; + rootFolders = [ + "/var/media/music" + ]; + }; + + prowlarr = { + enable = true; + # debug = true; + port = 2040; + }; + }; + + observability = { + alloy.enable = true; + grafana.enable = true; + loki.enable = true; + prometheus.enable = true; + promtail.enable = true; + tempo.enable = true; + # uptime-kuma.enable = true; + }; + + security.vaultwarden = { + enable = true; + database = { + # type = "sqlite"; + # file = "/var/lib/vaultwarden/state.db"; + + type = "postgresql"; + host = "localhost"; + port = 5432; + sslMode = "disabled"; + }; + }; + }; + + editor = { + nano.enable = true; + }; + }; + + system.stateVersion = "23.11"; +} diff --git a/machines/ulmo/disks.nix b/systems/x86_64-linux/ulmo/disks.nix similarity index 100% rename from machines/ulmo/disks.nix rename to systems/x86_64-linux/ulmo/disks.nix diff --git a/systems/x86_64-linux/ulmo/hardware.nix b/systems/x86_64-linux/ulmo/hardware.nix new file mode 100644 index 0000000..4479a12 --- /dev/null +++ b/systems/x86_64-linux/ulmo/hardware.nix @@ -0,0 +1,18 @@ +{ config, lib, pkgs, modulesPath, system, ... }: +let + inherit (lib.modules) mkDefault; +in +{ + imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; + + boot = { + initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" ]; + initrd.kernelModules = [ ]; + kernelModules = [ "kvm-intel" ]; + kernelParams = []; + extraModulePackages = [ ]; + }; + + nixpkgs.hostPlatform = mkDefault system; + hardware.cpu.intel.updateMicrocode = mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/systems/x86_64-linux/ulmo/secrets.yml b/systems/x86_64-linux/ulmo/secrets.yml index 005042c..869e63e 100644 --- a/systems/x86_64-linux/ulmo/secrets.yml +++ b/systems/x86_64-linux/ulmo/secrets.yml @@ -4,12 +4,13 @@ email: zitadel: masterKey: ENC[AES256_GCM,data:4MPvBo407qrS7NF4oUTf84tZoPkSRmiHdD7qpkYeHME=,iv:H2NIAN0xBUDqnyco9gA3zYAsKtSeA/JpqYrPhc1eqc0=,tag:6OFGDfsucG5gDerImgpuXA==,type:str] nix: {} - users: ENC[AES256_GCM,data:w/2Vdq0EHXaJ5u/aA/reSCtwRHreWm1U1WoJT927xV81zoN0ytoYOwush610caZu8vVXkL4b0hysK77dyWJkdkYpwLY8xG9pLkYlU3lN5E/2tgEjB7Dd7oY7TFTCNuypmIzYh6V74KiHMeA0vlyWUp9lLNt40Ro3MZLT42DyTYjF6YBoUHUp0fS0rKypILJGobJBrwz2YWagXj80IqaaUmmsIcYAaM2u3dQviLlRkIyUxPd1wjFoMc/OMp5Y8A4ZHroCN0wJitGeEEP33GD+MUy58u05pA430AD5Mo4H2V7b3t0qIkOQ8a0BgSVA8UqmrcY/TfikuIZ1kTyCxvD7kmjPq5tG+bhtHt85wgk1XffVO3NDTK7UrltO8R6KolQ5bBgcKgl7YnFTN5qSAT+xrYg8oZaPrGQBTx6eEVETKHKe4oSDkGlAle86lenhF+jm3k2ALmH9X3P/TpAtfRhuU+sUKqhrqQ2Nf4M7LfBtd7lyt2ESqilKokcl51gWCY+1B75dCEIdb/BPmpwzJBGFOI2nZqhxFnVa8TyMpT7C2TxK7rCBPDt5NnNvWYc4+8sRXHBz7s2R5NTk4gaJODlo3HvyL0MV,iv:XlO48HKJWRgwsozmgXstfirwb5CUY+ywelbgLlcx/n4=,tag:GuQMkL2mpNkTJIep79x0zw==,type:str] + users: ENC[AES256_GCM,data:ikpAuiQT32i4+aaVPz/nRqlf5ESID3khat2MrOySOfF9duJaQLWBonaKau6JVRljnGb+RGTiEH/EpxzXHnNydfHrir/jS4cDFMUMNV9aee0CyEbfqHAFqbC3B4ReZZE+XCkiq1j5jLnRg7EiGRK5+g+ul2iGIAwJ5SoHiOSSBcJ2E4B+AdkhGVO6Qsf+DW3hUZ/MsoaDsOB3IX15iC6/9z+NT+/Jefz5In6jn/vdYpD2i/zWvNHHPVXIkK1Co8FUidRdOjyWiiCb4+A0DI5v9E69xKe4zl26GHv3+1aK7cTxq2meDI4AXKhaTpak0A/neO/E6Xrc78752rTNRUDre9jJNrip/UPu8KvaCzpUi8Y4aN2Qg6ICF6JudzgouFyOGJ/JyxjVcJhUBOof/vCOcihdmHlo8sgyAi5mn/70VqnEF6Ei4KkRMAMlz9mfEVHDmjWMP1wHLw8eJD+Vhn/AJ76VecSCr51OHYtwgEcQXC6ikyPwBn8XQ5CNae/XGhcs0c8UbAcUXCH40zxvn4DFYHzJCkwurqv2iiV5zRN+rre6SoEWIToByq5KAwzkgLrLIVIbYWcLXlBYLvuMjnHbRknqWndQS72fRds0EWg+/OfjO+0SrPkJIoHkMNiUUmoq17ouwz0mcKVEh3o1Wptrp54ArDLkUjdtbOhaGTEzpGH+y0b+LITiN0erGPFITjf8sgGtvg+fRnoqCxPpex99,iv:+MjTW26sd8csWm4RXscFMgUm3wNY5Yj+qP8Xfg/WvsQ=,tag:mXjrEJqpbuqaVLa8EJpjoQ==,type:str] forgejo: action_runner_token: ENC[AES256_GCM,data:yJ6OnRq5kinbuhvH06K5o3l86EafuBoojMwg/qhP+cgeH+BwPeE+Ng==,iv:IeXJahPxgLNIUFmkgp495tLVh8UyQBmJ2SnVEUhlhHs=,tag:XYQi613CxSp8AQeilJMrsg==,type:str] synapse: oidc_id: ENC[AES256_GCM,data:XbCpyGq0LeRJWq8dv/5Dipvp,iv:YDhgl26z1NBbIQLoLdGVz0+ze6o1ZcmgVHPfwoRj57I=,tag:y2vUuqnDmtTvVQmZCAlnLg==,type:str] oidc_secret: ENC[AES256_GCM,data:nVFi5EFbNMZ0mvrDHVYC0NiwJlo2eEw44D+Fcv9SKSb2oO00lGEDkP/oXDj5YgDq6RLQSe3f/SUOn77ntwnZYg==,iv:awe7VNUYOn9ofl1QlQTrEN5d0i5WkVM35qndruL4VXo=,tag:8Yoc9lFF9aWbtAa5fzQGEA==,type:str] + shared_secret: ENC[AES256_GCM,data:IkzZ6QV1gLzChAFSsYsK3HM5dKFD4AoDJ53xgoxNpgt5tb45mMw/LRxu4NArGVLUtVGBy6jk6arU+Nxvi8bxPOC8c2UFCRUF+FM1phICEbb4Chgy5g803VKNFOu6BLaEmwDmuZSQP7CwX1hy8TX8yChboHGp7hH+n5SAZpejrLg=,iv:d+Ab91yCltYwudDWhrWPw0Xod/TKriCsoGD8i6PD4H4=,tag:xOXnzNuajcOz+imjMJr3Dg==,type:str] radarr: apikey: ENC[AES256_GCM,data:G141GW4PyS5pbAV39HcVscMw3s30txOgTZzWaL7o+ccZfnfDLv796O6xKXdqGZ8saLsveghLw9Z6a5luusHyQ3Q5ESL6W7SVeZVTuSqSC3i/4jl75FJxhnsgVsfrnYxzLGpKiw==,iv:sZl/XLh6y3WgSAn6nH3sFB6atBifZdghm+QsCNDbcjY=,tag:Tw+R80nrF0T0yDti0Uf+ig==,type:str] sonarr: @@ -40,6 +41,8 @@ coturn: qbittorrent: password: ENC[AES256_GCM,data:LIDxh0Ni0JgQGWFix/Ihw7IlUPgzMhrMlWNP5LKkAnEM6EoqA9kFwiPeizB0CZ20+vSqRiL9fikBf8qGLA17L7AKh8I4OTFDlpKpMRtRlMq9S5UBEyOqtOMcvkCSf6/qGoORd1KJSlaitZk47SYRuccOpy/2vAvbMRdLm0SYEqc=,iv:tQdN1N9kXoq7OZbR2eYyy50FltsMAAUI4Lr7U4/SpJE=,tag:3ZOLvjHXD7i7WFy1/Ggqtg==,type:str] password_hash: ENC[AES256_GCM,data:urufJbSErLqPdU6jLLZk+27fe4k+cKLXcGRGSqroUDdGMzDnhSF+ZWuPxwDlJQR3ws2GnuiEASncwNO/SALKXFDk2V2gsKJ4hsjyiIbsqCwSEFB/XMY0nY/x0xrcIfMVE0HdrNYeQ3zT01Z5jQpSd7wo2M63LaULL/Av498=,iv:tnUVhOgrImKa6iii2hJZn5LKrySM5v47B2zDZMgmUow=,tag:g3xa/4Z+t1Q9Wnd4XzefLg==,type:str] +backup: + ssh-key: ENC[AES256_GCM,data:aRY+9mYssEXPmfJQ2KOYU4wxkgzgYbv3GJ4KUkECSZ6IdQVv4CpKMg75dEhO5/t7MYjiNXze5WibZ0UHSTnUv4OB6NP6Mp1HZjIZb6paCJxjkoul0BVwtF5AKViJe0LIKoh+,iv:kZgZTqgYdqJSD6rO3lj/IFqhO9mYgZ7YYOCS2b+xpXQ=,tag:xPh0yL2uMyqgrioC36PPpA==,type:str] sops: age: - recipient: age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq @@ -60,7 +63,7 @@ sops: TTRWaHhpNWlkVDFmMFN4ZTNHMUxyNVkKV693pzTKRkZboQCMPr9IyMGSgxfuHXcb Y6BNcp6Qg6PWtX5QI7wRkPNINAK1TEbRBba+b8h6gMmVU4DliQyFiQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-03-05T10:38:19Z" - mac: ENC[AES256_GCM,data:gS6YTRTl6UdOC7Afrj1LrkgA7MWRLF0HNWytfzhkvThLW+JJrHPEhvWiYrsPW1Bm6o2JkKqVP5HfzcuGNIHJySkEQ4HV02BbibtMNiUKqk+voATsWOpo6957bwRJaTbvDvxmzIQ38TSUoj/pt8Z8WTl0hSPAlqNlWYffXX0y8K4=,iv:53R2bKYKiHJi9DTecg7hiuGNb3Kj9rA2U/oPJ+AFO5I=,tag:5uqvmEJCaCS/yNqyt/FPZg==,type:str] + lastmodified: "2026-04-16T05:20:18Z" + mac: ENC[AES256_GCM,data:YqkxwV30uqSHhsn4niFEODxxl9R2ZuiyyX4g8zONVjMvdA52C08zPpxdxjtXnUT9m3sT7iSmWcJJZwhMhRIb8LJ2sdIJ4v+wpG9I4pPokhEXI2ozqbzw3k68GnZOzYu3kePQBJjQx1fmlM63dgILIwx7ytPnpm9arQ1rszZynNs=,iv:hxdhU5oH9h9mRH3m76oFkYVNA68PnivVJpJRjxSRtTw=,tag:Fyyg6cWPb96c/Vap+PifUQ==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0 diff --git a/machines/varda/README.md b/systems/x86_64-linux/varda/README.md similarity index 100% rename from machines/varda/README.md rename to systems/x86_64-linux/varda/README.md diff --git a/machines/yavanna/README.md b/systems/x86_64-linux/yavanna/README.md similarity index 100% rename from machines/yavanna/README.md rename to systems/x86_64-linux/yavanna/README.md diff --git a/users/chris/mandos.nix b/users/chris/mandos.nix deleted file mode 100644 index a883431..0000000 --- a/users/chris/mandos.nix +++ /dev/null @@ -1,38 +0,0 @@ -{...}: { - home-manager.users.chris = {osConfig, ...}: { - home.stateVersion = osConfig.system.stateVersion; - - programs.git = { - settings.user = { - name = "Chris Kruining"; - email = "chris@kruining.eu"; - }; - }; - - sneeuwvlok = { - defaults = { - shell = "zsh"; - terminal = "ghostty"; - browser = "zen"; - editor = "zed"; - }; - - shell = { - corePkgs.enable = true; - }; - - themes = { - enable = true; - theme = "everforest"; - polarity = "dark"; - }; - - application = { - bitwarden.enable = true; - teamspeak.enable = true; - steam.enable = true; - zen.enable = true; - }; - }; - }; -} diff --git a/users/chris/manwe.nix b/users/chris/manwe.nix deleted file mode 100644 index a4f077b..0000000 --- a/users/chris/manwe.nix +++ /dev/null @@ -1,61 +0,0 @@ -{...}: { - home-manager.users.chris = {osConfig, ...}: { - home.stateVersion = osConfig.system.stateVersion; - - programs.git = { - settings.user = { - name = "Chris Kruining"; - email = "chris@kruining.eu"; - }; - }; - - sneeuwvlok = { - defaults = { - shell = "zsh"; - terminal = "ghostty"; - browser = "zen"; - editor = "zed"; - }; - - shell = { - corePkgs.enable = true; - }; - - themes = { - enable = true; - theme = "everforest"; - polarity = "dark"; - }; - - development = { - rust.enable = true; - javascript.enable = true; - dotnet.enable = true; - }; - - application = { - bitwarden.enable = true; - discord.enable = true; - ladybird.enable = true; - matrix.enable = true; - obs.enable = true; - onlyoffice.enable = true; - signal.enable = true; - steam.enable = true; - studio.enable = true; - teamspeak.enable = true; - thunderbird.enable = true; - zen.enable = true; - }; - - shell.zsh.enable = true; - terminal.ghostty.enable = true; - - editor = { - zed.enable = true; - nvim.enable = true; - nano.enable = true; - }; - }; - }; -} diff --git a/users/chris/orome.nix b/users/chris/orome.nix deleted file mode 100644 index ad588d0..0000000 --- a/users/chris/orome.nix +++ /dev/null @@ -1,51 +0,0 @@ -{...}: { - home-manager.users.chris = {osConfig, ...}: { - home.stateVersion = osConfig.system.stateVersion; - - programs.git = { - settings.user = { - name = "Chris Kruining"; - email = "chris@kruining.eu"; - }; - }; - - sneeuwvlok = { - defaults = { - shell = "zsh"; - terminal = "ghostty"; - browser = "zen"; - editor = "zed"; - }; - - shell = { - corePkgs.enable = true; - }; - - themes = { - enable = true; - theme = "everforest"; - polarity = "dark"; - }; - - development = { - javascript.enable = true; - dotnet.enable = true; - }; - - application = { - bitwarden.enable = true; - onlyoffice.enable = true; - signal.enable = true; - zen.enable = true; - }; - - shell.zsh.enable = true; - terminal.ghostty.enable = true; - - editor = { - zed.enable = true; - nano.enable = true; - }; - }; - }; -} diff --git a/users/chris/tulkas.nix b/users/chris/tulkas.nix deleted file mode 100644 index a883431..0000000 --- a/users/chris/tulkas.nix +++ /dev/null @@ -1,38 +0,0 @@ -{...}: { - home-manager.users.chris = {osConfig, ...}: { - home.stateVersion = osConfig.system.stateVersion; - - programs.git = { - settings.user = { - name = "Chris Kruining"; - email = "chris@kruining.eu"; - }; - }; - - sneeuwvlok = { - defaults = { - shell = "zsh"; - terminal = "ghostty"; - browser = "zen"; - editor = "zed"; - }; - - shell = { - corePkgs.enable = true; - }; - - themes = { - enable = true; - theme = "everforest"; - polarity = "dark"; - }; - - application = { - bitwarden.enable = true; - teamspeak.enable = true; - steam.enable = true; - zen.enable = true; - }; - }; - }; -} diff --git a/users/default.nix b/users/default.nix deleted file mode 100644 index c6867e1..0000000 --- a/users/default.nix +++ /dev/null @@ -1,78 +0,0 @@ -{ - config, - inputs, - lib, - mkPkgs, - sharedContext, - ... -}: let - inherit (lib) mkOption types; - - mkHomeUserModule = spec: - (import spec.path {}).home-manager.users.${spec.user}; -in { - options.localUsers = { - homeEntries = mkOption { - type = types.attrsOf types.raw; - default = {}; - }; - - homeSharedModules = mkOption { - type = types.listOf types.raw; - default = []; - }; - }; - - config = { - localUsers.homeEntries = { - "chris@mandos" = { - machine = "mandos"; - user = "chris"; - path = ../users/chris/mandos.nix; - }; - "chris@manwe" = { - machine = "manwe"; - user = "chris"; - path = ../users/chris/manwe.nix; - }; - "chris@orome" = { - machine = "orome"; - user = "chris"; - path = ../users/chris/orome.nix; - }; - "chris@tulkas" = { - machine = "tulkas"; - user = "chris"; - path = ../users/chris/tulkas.nix; - }; - }; - - localUsers.homeSharedModules = - [ - inputs.stylix.homeModules.stylix - inputs.plasma-manager.homeModules.plasma-manager - inputs.zen-browser.homeModules.default - ] - ++ [ ../modules/home ]; - - flake.homeConfigurations = lib.mapAttrs (_: spec: - inputs.home-manager.lib.homeManagerConfiguration { - pkgs = mkPkgs "x86_64-linux"; - extraSpecialArgs = - sharedContext - // { - osConfig = config.flake.nixosConfigurations.${spec.machine}.config; - }; - modules = - config.localUsers.homeSharedModules - ++ [ - { - home.username = spec.user; - home.homeDirectory = "/home/${spec.user}"; - } - (mkHomeUserModule spec) - ]; - }) - config.localUsers.homeEntries; - }; -} diff --git a/vars/per-machine/ulmo/lidarr/api_key/secret b/vars/per-machine/ulmo/lidarr/api_key/secret deleted file mode 100644 index a1bfef9..0000000 --- a/vars/per-machine/ulmo/lidarr/api_key/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:o7q3dG3EYv6Cet3YSRUBtWD++epjsDvKDxlnvlDTV2BNNDmWEmWCmxIwpw37idRCSHSowuIdG9npYMu2cXXNAcDkRbRA5MveQZOLu8qODiSvJJstOrAGUh6oYMvogAEkoaeogRhPx4SgX0j4KPmxdCsgWIrCWEkaWF1OmhFY9ZUv,iv:rv4Q2OShrAmEcgUq9ch3KpQMQpqx7BoNj0zEqDkyaaY=,tag:EkuoxsqdbNSzhu8fwTaKJQ==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5Uzg0OVlTZjBGamwrcmNC\nODBhUVM1dFFhNWY3Y3VYRDRoMEZvOHFEQ1dzCjh1bm9MdG45LzEvcnZvb0FzRCtL\nWGhvampIQ3JYYUdKckhncGFVakk3aTAKLS0tIDRxZ2ZQNkhnZy90SUhhVDkwb0hD\neWhKQkF6ankrQ25uSW9QeEhiN1czTlEKnDr97KT9ULu+IJ93I+4noedX+O0MWsa/\nmTGLZA/F3k3OinMmKeFcYdv/grKda8L4QmbwQzUs8s2MjYDDtLNiBQ==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:49Z", - "mac": "ENC[AES256_GCM,data:nqC6JEmJavjeyz3OWSgsEdlT5oxzn5/N283VPwUaGz4ugUgTmKBn9eVEGKjeMPfmdd+wnExUzokT19z9vmVo/tWAopnTs9t0krGPjWHbSTLGXANNiR2DXGCHMLT0ebPiTcZNpIl3OUTQAF2bs4kBK/ImmD8Pbz228PmbLOE6H+A=,iv:2XT+wRTqw4VNJfJkWt7n0sNQINrt2AGMHIWN3nj3Kxc=,tag:ds/UsEG7fUnGIHxslotbOQ==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/lidarr/config.env/secret b/vars/per-machine/ulmo/lidarr/config.env/secret deleted file mode 100644 index 2b4b8b4..0000000 --- a/vars/per-machine/ulmo/lidarr/config.env/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:CGCmRQVpfm0GZVKXxABAAH5kgXuvKwG2ei2eIYm2zLqyzP5Vt0Z815uH5qdqwfhxb7/LaXd2i24yD9AgeX6kLjsg9VfLmoAvpeywrU1wFKdgosri5PES+0MEk7lsgXz8uI5JRvRohuFUXLB5cMkvAaj+zy9Rwc9B2nJdgz2tFimceP1ALsxu+pHGnG/CIiXjo0GVwX+2505OaRiGywB6LIDNxXYUzJ+6nZpqARI9Xy3eoQTAj+SJ4yVBFGclL1JJWddROqdLuEORXzcIeQc1aa0CkPI7Q6sH2xv3XJ33JxsT6CNCBQL/p6ngR855OY+Hpx1bvISsWV3SzyyW0IdI6hbPBEc6s0opM/w9llf1pvCfK13AWWf3F9A5gEnI4c4VZM61AzRg4OaHVId1GawzLhvwWq7E,iv:hfAMtVVY/Bnw79mFnU9d+fLEJqwuWHcb8dL5PLDp6oE=,tag:XuHrYsZjomVP9tRmP41Iqg==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaWnhkckxYaWpvakxJNHZB\ndVNGRHN4UHgwTkowTEEwMFI2OEhwajNBMGt3CitLWEd6UHl2b2kxMEgvRHRKaGxr\nWGluV29TTkNveXUyTEIvb05BSlE5QUUKLS0tIE1mTEZTYlBPLytEYXJWM2dwV2xz\nZFU0eWZxSlZFejlIMnBGNkFGdXBkNHMKvimcV4BI36cFwHvU+235npFgqzHrC+FI\nxy+oohK3XOaM3iZhpo4T1s++mCQx9hvsyHKG4z5Lt09BvkXw795tIw==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:49Z", - "mac": "ENC[AES256_GCM,data:wPehGqpajsKHixAH2HS2oYwc4oW+8vOI7OURyFJItBP3IhOz+HZzBC7I4XD0IvH3Nk7K1dFQuzCgqo/X3sFqWY6Qd6xzAf45ZBQyj+JeGQgbvPcrOzrGjEsnAsRv9EBrNtIWNFV+mt8G+thmmSvE23EqMWwQVBGzd5brq1ZXBoc=,iv:023iBpil4E1XA03HBM4DCaVCr8DFk+zRxf8+2UOGXzE=,tag:BfBDsHCfRTfDJX/3TGNGPA==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/.pgpass/secret b/vars/per-machine/ulmo/postgresql/.pgpass/secret deleted file mode 100644 index 160c934..0000000 --- a/vars/per-machine/ulmo/postgresql/.pgpass/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:2U554h8cF6ua7k9ZSKBdUfv4RHy9B+qtwNoymyKcBPx5l+cjO+7V1d3fvT3zmflGhmCvhBUdu38QwnaOkL3+4cNpZjFlaqSeuiddXe6DGNMwFB+MKIIFfdNSb5hSi00vQS9Aucl+x8sPZ1K8bKETQSKnTJXw0cW/O8z0MPT+ixpw3PWZd/vGHyqOA31obokhfa8BloQ/NJcDm7X0/LM97vf0BqsdMSQE3jUMZ/MNZZ3SAgNj5jKuRJipWrDeOgVexie0CySWkmFZDFGK7j3jglfY9WQGJa1t8mDrkABTvEm3imXwxS4ahq6SgRf3UvW+6rqERb/u0h0/2WbjTGRbBEBtuh9rKRIHHVE7XIiiUsrGHiDI0yAjlggXDM45tKfYp5emPutjK/XOYJ1XF12J+q2dnbOz1hxFZV5o2W+b6mKSMWBqE6jttRTPPZrb54QyC63n+w0n7kf+uVvJHh56XqhHSvrXBU2l5TTTz+uGTGHsRiohTE1oMbTyUDr5U66Tc5LHRDdGgabLDbpjXvQTm7unhNM5uUI8fprzo0SYepmwq6fVEpRidoJ69qqH+tT0vDI/+58LuTQupBXqPQCon2B8qXLR6IFX/17imxv9F419CRqq0TOgeRu/mVLFz9cYn2N1sC+qWvOlM6nd6H0Xgb8lGzVFgt+ufy7SxGl784/TTZdxlAkSCn22f5yaejRGjkU11kzeUEo2UedTeNZc5PzoJc7wtp9tEkf/KsVIU68TKjGVEomfPGTBMBvMfKZj72GyXJ+daDTVwPvcKD47VI+CJpHIDznHHlzOqKqK2TJZ6htvtUxhUfO8KreSDZqQ1npwidrZ6W/4JmoV0JU=,iv:i5BmzaIRsxZDL+1s2Wv4BYYJdXCnShwPgq4aSzSaf+g=,tag:lvkS1GksA6RArl60Hsplsg==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhTXJLOXpaVDF4YzRUNmhB\nMHk3U0hxdkpBK3h3Rjdxd1U5ZUoySldnQWprCjYrTzdZQ3RzT0NtVituSnV2SWZ3\nc0xrTFdRVWlLdVpVNlQ4WllTclo3ejAKLS0tIGx5dnFHOUlwOFRKUS9vRDY0TS95\na3g3MEsxY0psd3dPaklRamNTUzcvNDAK+6Q/FZsRy6jKs4THvoctrF48hnK97TR6\neC6qSL2ix4pPt0BhPb+Ixcf7KMDR99wwved78qJioe7bt8859RXltg==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:42Z", - "mac": "ENC[AES256_GCM,data:8363f6CzLK1ZipGSVxaQcMMy+mTh1AL0U4W8FiqXuLZt6tzcLbVUqfSVzuzAmjFaiOldfriPiTghnQGqEZhe2wdbeOiSAv0AIw19GXTYsslbdeNSDXbxUZ+p6mMESkn2xi365mfF7UgNZulJLa+SvSHiuPkxb/ESauVrXw11ixk=,iv:8j2U9pgjsURZ4bAvBKzIJdix/FLml4METeoikTl7jE0=,tag:K1ycWq23HYT1PIbZM93zkg==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/lidarr_hash/secret b/vars/per-machine/ulmo/postgresql/lidarr_hash/secret deleted file mode 100644 index e18d52c..0000000 --- a/vars/per-machine/ulmo/postgresql/lidarr_hash/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:Xeu4y5K4WbCig3U0e9wGZ8izK4nz6Su406XDU54RqcTg5pMvIZYUwBnubzwcezO2tQlRNkD6fUrF19Lg8C7VKKZcspLBZFgT+v9Q0wlLMFxcPSaccL/x2jEPS8aCRYIq0IxJ6YEM/52DXYq8lh94WwImXz4IyHny4cIBHsROnqnISeSGeGM=,iv:jdWF0xOnTXh31qOq6FqYsWt5RthCu8wQj10ByyqNTdc=,tag:l936Z/8YFGyHO6e2QqKbfw==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLNk1sN2tya05USHo0bko2\nUVZ5elJaeVVDeWk1WWo5MHIrbkJBeHcvb0dzCnJxUE9jaGlJdVFqTlBoUjRZVUxZ\nT1R5Ym44bC9CY3FaV0UvV1ZieXRPRm8KLS0tIGFabzFKQXYxOVJTSW1rNWlLZTUv\ncWpLaUN5RUJnc2pTem9pb0FqS29SU1EKUNIoNqv7djVOIahGFpTPx2r723cr2Nam\nfkSw75HkZQfap8Y2J+VMEVKSeBHFGudweZ3ifnl9p5W+WN12xb0btQ==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T08:04:41Z", - "mac": "ENC[AES256_GCM,data:cLBNG0UUn1D9CdPlhQrkDxety8/cKyuxZctPqzKiHUqOLb9Wm1RwlwfKz9zCQ+nl++jUp5ZSszbcBkwXl7zA9x0K28GVJiKOTL6od5+HkiaEAL/b+qKviYLrLgGbD3mUOyuG0V6gKua/22yvnowtJ02TSK+kxR/xzaL2whIJdAc=,iv:/rE7yeXQA30K/NwPoFlEQTi7THv7din0nzTM4rSfS00=,tag:1lZrzkXpcvNmmxkqbxl3fg==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/lidarr_password/secret b/vars/per-machine/ulmo/postgresql/lidarr_password/secret deleted file mode 100644 index 35fb209..0000000 --- a/vars/per-machine/ulmo/postgresql/lidarr_password/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:gg2RWY7LWZatJHN5mWf0YQUIMHnM1NR9qao4WjNGZV3K1spzamyf8kZNyFC6vKxsIa73Ticf8WVHCheEPlAVWojcHqBiABIvxtD7tMdJO2w3T10RslKMW+NXGNh27mcSBBGepCb7a9/O5D0M+3/m0Pa8BX9gbvidRlJ82rW9baJf,iv:tx3HXIYzNOB7zeNs+tVt6Ti5Aa7/vvuhEAblv6E/Roo=,tag:ic/74zvvT3OJyC/sRbdKQg==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6S0dRVWpsM0ljbzFvNHk0\neE1DTWVWSk83YmFhZ3BOc0tjTWtJOUZVNzFBClM2eENWTlVEcVhNbThyZ3JwMnFK\ndjFLT2dKZXN1eGJvcFllVzVsNVNTTVkKLS0tIHkzZThXUGN2MWRmVE9rNTZEQnlj\nWERqd093dmFCeTBneTFZcm5oZU1HU0UKRs9RoYMB0FpqWIQwve4WYuKcH4KVkf+2\nl3npJK0gX0N6EUGUFtcOuXy+A5WSx31/SnwdaNo5wnH/9MCJT1wisg==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:42Z", - "mac": "ENC[AES256_GCM,data:IyQs9bLqyo+L7A0/9EU3LxBj8XplTbQn3Fo+/goZzpLW7ec3LQ0BpVo2I4OmXGBNg7FZxPWSRZGFBYY8wGIsnR/ds08klRiZAubeO2gG0mZGCf4HAywEJ+CL8aTv/fxm3tRAEidMd4eIXyul6AoYIwY54pOf0DBepgwieJQyBmM=,iv:7QNLVgVWlLmsa7Ol0sK7ugy2yLy9TNZapLpi1wMKTtM=,tag:VY9x0qKXyWp9JnSQu+k78Q==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/prowlarr_hash/secret b/vars/per-machine/ulmo/postgresql/prowlarr_hash/secret deleted file mode 100644 index 796bbee..0000000 --- a/vars/per-machine/ulmo/postgresql/prowlarr_hash/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:/J87IC/tXapUNAkQAc4h4efJtWd0CX0jQyqA0XeudiPiOACLs4DYxVMC87j9mkKk0ezPmaE0JENESs20XIzE8AoWjORslvoBKUCEyA0yd9Uzh52nEINZXtU3x6ABAlCgSa3TzCFU4VkuLj9AEJ2p7Y809nIFerz+QtVL+MqKxBP9q5nHiYc=,iv:ba1vRxlNJQ6KxGP6nHG6tkpNOAKkFN8bfzoeLcJqxnY=,tag:YrmOAl5HIs2glM2cQTgUOQ==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVYjN0REV3VzJBdlpGa2d2\nK3V2akpsUVlIbnNMVmt1STJLTHVoNXQrb3hNCjRMWE4wdjcwS0orYXhaeEx5QlhJ\nT3FBZStLVWtwcG5IRlpqbmQ3UWxka3MKLS0tIFNJeG0zZlkvOHlrSFgrQnBWV3hT\nQTBoZWU5U2pnZk5WM0owSEY5Z0dqZnMKbd38p09lTl+ZW+RFTTt+le/wU+KCq6Jy\n9q8KmRzWtpoQCrecqFAgNazFxpp236jo8Q8JeSqSiPn/zOuNXg5gog==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T08:04:41Z", - "mac": "ENC[AES256_GCM,data:AufJSGx+8KvZ/I8IgQgch8XvKchYOwrSCb+DMJNiGmx7H20DoZlvWW/dVafSFtSgHNyWek8le4wqB4yoTNs9UXLoGoShVGCTQOq+ZOTJkx3DTk76OOyLjcb2mnNo8TUYz1HjYZ0ox1//povwhO40c0i7DAT22XnTUONBQ2gko1s=,iv:cW19v2fR56IQbauUCf1tjOdI5tL/XfiGEqIumISbA8Q=,tag:690f4UNtT9lxGq0dWku7fQ==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/prowlarr_password/secret b/vars/per-machine/ulmo/postgresql/prowlarr_password/secret deleted file mode 100644 index 6e5726a..0000000 --- a/vars/per-machine/ulmo/postgresql/prowlarr_password/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:oKz9oMJcfmZ+3UdbzlErTRQ5Tfxcc1jO4XXZrPI14l+DZl9TuzgQ+bTpKu5pQ3eD6TDPb928pHvxhWunuX4/1mp8cZAQqQ4JsCbqIbqeqRnXFkLAodIBiLHrHYv6QWMHPadLt4zlxgy1Mxxhi5lUchBkWyPcMFddHnWWnw2wQyq7,iv:Sw97Mn3CDnyBs8z3zVKR4Hlc50KRz4Mx1KZt13JnhpM=,tag:HAwyjFkG1Rbk8lVs6QU/mg==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhWDlpa3NWcFNpMU9Rc3Yv\nbFZLUVIzRWJtZTl6VGtPZ3h3ZGQ4ME9Sc3hVCitjNXh2MEYxTGUycUpxRjFNUFgx\nZ2tHUFJybW1iK0xPcXdvcGgvd1lVdGMKLS0tIGtpVWwxdmpMR1ArcENpZnZSRm1S\nTndHM0RzeWpheStqSlZzaVp1RXVLNFEKPpp7JRibn9gc1QafRXqLEAxX73kx9Aki\nwnNXbN1fE+sAanBFKRATMDEZAYNHuAoCEQCJ85DtW3tCNrDyjZ0UhQ==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:43Z", - "mac": "ENC[AES256_GCM,data:TdbZGcbTkHwOmzu+y0MbERHp70FsHambcJU2lrGMZix+weC13c/zKbs91DbUlNSW1+ah6iSqJwubmSx5JAP8N3B6cKZRybF9XdOKaTycYgBJVdjHJOKyzlF7Puzxl17SEDWeT4kMlSXuARsJA8OPXHOPbE0PNowzhA8dCRpQeKU=,iv:TfL+8cnBXnscoYe+5B3+JBfjNC6J5QX2yXRh1tSo3D8=,tag:hRfs6efTh09N97P2Dmk0xA==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/radarr_hash/secret b/vars/per-machine/ulmo/postgresql/radarr_hash/secret deleted file mode 100644 index 66926e3..0000000 --- a/vars/per-machine/ulmo/postgresql/radarr_hash/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:yUkkTf7xUyLoQd+aAykxhQwYnP/1Un9wFGss1MunpUQFjTQwYY8DIb27rknc/2V5haSDTZOqq2o7r/CzyTJ4gXROIu2zI2Nh7i8v3HP16nAoBUxcweTDMdGoLeBMtYSpbbA056eOsq4P1qj6UHl//x43gKIjDdl/nc78VQbEw157/7ln2AA=,iv:UxE+6/uUdGAr3AlH1TSvImi7JwpufNQ0sbVRsjVzXvA=,tag:snEnHNYafi3xi/qczfbl8g==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5MWdHUWxwem5UdHFKa2xK\nS3ZnQjNOc2RFQmtBY0FUVG9xYTVxMWg4R3hJCkxiS1dEZEVIV05vUXVTQWVaN3FV\nSDJkTlFJU1B4OVYzTWM0czA5YzBNQXcKLS0tIHlmQUIyemxVYVI2TmxkU1FuYml6\nNXY5d0JMQTJlWDJjTGgvSVpCUjkyNnMK3HhQo5yJOs/uA2Qbd2pazvQZzSiM/TVE\nAfwlO+h+co9snnlcwO1Uy9NxPGpv+6KqlEgtCnYdXR/V+NlbtPbzBg==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T08:04:41Z", - "mac": "ENC[AES256_GCM,data:D9s6HC1/5J/uHf4mI4b1wJySXGbQIrUazc3BmIVLdLfRAau+RiBRQ45BkNMIyNIVHk3QCFbKNN3SpL3VIgkJB+RsuIc1B3em62PV7GcpI07tT8rapCeKv6PFZTapBllltbFtVq0H3PGiAeymwB4C17YN4e7LcaqSfdSdDAKgZTU=,iv:s0AjDgdkR8ebA0mn694CzUywIv2mzgiHehJfs2Xbhpo=,tag:W5xvv/Pdv9oTtKseFzNzbA==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/radarr_password/secret b/vars/per-machine/ulmo/postgresql/radarr_password/secret deleted file mode 100644 index d9148b5..0000000 --- a/vars/per-machine/ulmo/postgresql/radarr_password/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:qWVsbRFZ5Tn8/6kOF/ejIvVTlmFjn7Ow83n141Ev8uR7KjyHtdrfliAm13RS/Sy6NvroADt48bYlzgchLMq3r5vKLwfmKqeoE7u3MSMO8OPdu8AGOaP/swYulrMgqPDFi6PeOUHZXTJuPL7+jf/zEF5DqVC6QV/EJoXMyZiUPAYs,iv:77opMiQpFJ3McSG2CQTU3ShIZvgm2NqNWjdqarwvM8E=,tag:dx6x90jNc5+eoLUDqtKZgA==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzbmlRSkVxSXBxNmI0WWxh\nS09sbE9DODJmYmJmMHJuV2pFQjU2TGZJQW04Cm1SN0hvSjExMjhSWURSRGdjZVFv\nUWZuVitaZlZUZER5ekJobUs5L2tBaVUKLS0tIDl0a0FLTm1RSlJLT0w1V1VHWHdn\nMmlpSWtubkNXckZCZjJ1cTJvL1hBRG8KDwZyVu4S8oIBzikYfpLI4vgngSAmHvjx\nuJvy2sHoGqcnczi530XHsmviBSMZNSqFhvbtcHzmxdVRbc2cxgewxA==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:43Z", - "mac": "ENC[AES256_GCM,data:OOFL6q5kMKMQV5NCvN2TFiDdumjgSGeyLmqDmdC2Q5/Q9tsM5PsCifySxZTZ1krhxzV6ZHpXNg+Po38MXc2VyIniDX7MMEIB+d3J0I5KWgHwsWIOaqkntyyy4dEXlbqwSeLP0bOKWBY2h3dtve4LY0lGB79V1iuOXxV2w7JPG5Y=,iv:Q2vFr1ETtrUoZ+hc6rAczBFoT2sbydPJ9y2uqgoqtfk=,tag:9O2gfeFs3/KjMMGPW1mHEA==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/server.crt/secret b/vars/per-machine/ulmo/postgresql/server.crt/secret deleted file mode 100644 index bac7194..0000000 --- a/vars/per-machine/ulmo/postgresql/server.crt/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:llIuY73eyaF3kPOR8gegnn3J9xyiqwjsrc+zqyYx07mTpVxMRGT+KKF3iqsP8NLAxMOa3afulk7qzdBlzkKhlJGpdy2lR1tPCOD2eCj35NlF7jtAqGrdb3jPe10FkSEuz4YBI8atmRZviAMyFV1h080P8kQ64p9lANijw/5sd/uU/sKFzlZPtpb5MgqliBPdQ140AZ0w/EJrI2vuyBMloFfm11MSMqt0Zrg9dTLWTrnKYbjVNSap8xys/gTkAkeuYsnUCTm4VSfD43obrn624jox4lpNLk+kTOgQfLO+JNfA5jeGXTbMF1Q0pahY0n1/LwENwGX712v32flpgJdEZii+Lb44AeYA+xMREQ9JWSbGfwigvznZDbTT/8NelZPJv7Os/M3O8PC1z9ImRA0q0hSWj2skDRqB+KbZ/+w9VPHll+D0VzoiprXRdhMX958P6Kfgr16lE0xA+f0dOmfVbzuWd4mOJKBI7EvfbeWjF3JRBJj0eVxDc8UQJ2V3FQJay62AeCIVsrix5jm6vz0omHsVIyBGzTLTxeLPs9tGe77AfIfg7Am6MJ77TS8LTHFmT5tFsllxXvGjolsJxynsGc7xGMEUdGq11vumq411ffLt5C9sYykoSyRnXJah2vIFkTtRO88VCvcEuFWB2GWcDQhAxuqnAK4EDbc7cuErCTJf4cgBIv7ODkUYrQBid6R/8l8es2RMnh2nybclqfn94uW4wQSKH3d3DVAMW15Es+H2cpYgKbdcFf0BTEhEvWP9ovpo5V+9utgz1vFnUrvsYQmGQeWmU1TwANcnZeiZO/2IJJCrXkiwUNkaSeWs2V9T5d6bEJFyMxFbXGfrO3uymO0nJzrv7Lq3sNy8hXcs6lxOO8g36jKuQ41kl9N1ZyhMo2lhWE6RdjW4/zbCsXxwEhMSnf86o2jB01y2/zpjCUfXBZVLb+7SbAtYYNCYx0IdL+KqcRi+kyNpsmjV156JEEFgxFkyTqGl0Vj1Y2BnVCDBfgBrovlq35xUIDmLDDBKI4zSe7mw0Va7jBiqP6W/ckCuK5BBVBI8WOc+qHvnh+EE8dCFGPeMlFktjxzJJPkp7vR2ZaPhyozl6uF5ZityZzF4aT61SiKcQxiS1k6Pd2rEih5Bp7E36PFn0EqBGAxtCNVWEcRG5QXFOCRUqOvj5r5/F+fYSvqftbqnPmRdi3VdeCAlVdeJ9LiaR2P8HHfYOLjbbbYFSybsu41oeSE0P0lfeiKbwLz8/wtReFNyPda+qDZr/ce6YbwrlqxieepTKOqLBlzGsOximsl10HuS1+3rf5HWgLBPQBmIKvYyAdg0msyQKFhY28XJksHfgtEIMoCz5sFVUX8m/CO0l4BGQ0pI8EUzGEonuviLlPkBL0VQzqiKI0iZ3LGH9PGIQ96+HpNGnPhiEnoqM3IH2gB3vWqcprgCjUKzzSIJl09DPtY4Ue+Ik3hJSPEa3853Xjh3FyBw52I2Fv2qR7wbv7//u48dObIvelyLxKrpt9Brsh6Z7a+KWK8xuWgqJXAMGk6zpJak/DpBqpPn1Rsclk4yWm5+w4pT7iORSyRIwt936mQESCRKq66aXOb3j/BE+2i2n2ZokA+DmzXHD6QDeJLWad3iw5/3GS81t6PR1tSGqRJ/6sEP3Gb9Fgxew/KoH+MCDKqwSA29Mk3rHfPddTp62+OSBFnIcyG5aAVCbJlzPDLl1SSaJLLibSpvr/z1i071gCPdjdpCatorIN/oNkBH4IWnaK/6s9eM/LE7vjUQJSz/E4WSFFKK1yCOEIBnG1LrP0xyDcTgkp1dM7GEx127gzY00KtUJyHhQsgnf/zO0DTH3T7RaDYIBMPChU5VvB9t3+NMThpZ59O4JXbVKDd1KtDgp0c/9GLyMTLUUxRKBzvZNpNK6ySFllNyZ0/JTADJ9tvmPA8inKJplOV+EbU4Eo7VH8B/VEBzO/Tj/uC3JqQLyVG+ZewFY2fnYp3JgEp300b9NZBcx+1Tq4DjMiEgYTiOFEsl2N+4m5VO5r0rsXELfVSgPlbLg1BIWL3JPKY5R/k1ql4GaNGSXoJ2DwfdvvVR5xHgGYZEbyBiXOmIS6yOS4UK6RVXmb47oFUWdqZKAOSioIHnAxc0/KQoqmBAihcL2sgUWnj725i0lH/ky5wLVFTNpldF0lohK8hw+5zZlO6qplr1nFPvO8N69KXdhWCjykOO4f+aLHyU3LqHpnBmozVfFR4/CisdehsXEaa4nh+6Yr2HxJkd7ar3V2n/L9dCvnMpovvau+gVvqUC/5yOVfF9ZFLz1M4Sp0zAikW9bfWNLM6MNZv20J/JHQseZRZY5ONRj109DItlgzS2axP8Le/2M06BNePa0MDSr/ogQv33eQ+urnY1ES9aYlqDzoJNQ5FHkFcnAfGBd4woWS5ocNrlDFBEjaZ5+6dWp/T54O//coHZE5JdnHO8oe+pwKboUFuyuAbbrVHXL/pe91cUpwHEJmRrfiDcOQgribRkO5a4wzvv8Vz8esUYpsUiO+0nkzXccJhzkICFDu+se8EioUjno0cfqAFvKIBP3diE43lshAAOE9mRxanvKzOz7fwe2hvKT9LZCcf8W81fw3+c7qnoTyNiEjm3Ift0xM+nE60UbtlyqY66P1eSK1dwcORfwrHo1rUwT97pN/FvSqdKPipVjEXz19DtIymfQghZU8pFwMdhRhVocDjXgz6ha+Uo+7zFTL7w4TC5B1MqfRuk9B+CbQ2dgqC1A/OBga+B2dYMb0J2etyOpUyg9rc53P12VIDQy4JI5KIE4I+bB9kxiLJrd+FQ4P5tWZbAnTWF4WPjrWdTLEn9Y/Au8tDqaTYMa42T0e4Yj21DCNU3KElm1Jv/xo7N1XAKYnCAcdQnhhVc67Xr1foKlsBMAClTk4FMKS5Cz3NLjT7nf96o2qvFbfADI1k8koJ2J599MdudxTqTfG27r1hWiuwSC8nGRlwNgt0wd+qDRj6A7OeJJyorZxwWlMo5P3jlUuiNvxWoCmGJ0XZlmhIjJqRqv6dh+hkGXfVrJ8F1nIad3MAs6rObXxr73+RzOufUdbXPj6wd9dbVsaIaoLPz4E25i2n3p0zFtfTyOHzb87Qu0tzzW6Ca9S3TqDoJq0Z+5/bJmz1GqwU7mXEYzmkTu0rF80GRH34plf8H0pd9aDV1E+4jgsS+AUpKP9WuiMmI73TML9Vbu1XCIne1nlfLLwAFxPn3oXnOjTnnxp4bZxvM0BYh0pXBevuAIYwTbiX7S5j7FTaK/0J15lqyHbFbfEL9MV8KYlV+yndu0EIYZlYM3qNqvIplXgdHh5XF2mxiSlkEz02bjyKakoK3oRwwXwK3bIsHD8DL58eSq9aaqYfXwFTiv8hqMHroDA24lt4FQ3EFXIy8NN6NKvtbUaNwJfmaP8pXuPugPTAhSR+kb6sEVXWQsaBcVZKp8ss+6Uy0BY5SLggu55zuJKvyS4KRyv2bAnvuebEyJIaer78nb6PArF2AQoJi+tNlcMfbW33z+RWmh5pr990xcRswtx+FMK6ze5UBwEiepAEOqJXiMnSsvo7Ary/JT1JcFz51zQpUlyuvqeVJ4m+PfcatAVp4z3BgF/NQGMMEvxpAWrbVYJ5EqxPQ5EZyBTzyO1vWZt622A1btEYNWvZFxupKPK9MvoV7SeYhf+0l22zXo62ZMKcG8vTPthoVsndeqEBa5HlwGxBXbltKyHEUm/zI+3J1pI9uL+HGH1UVQ+HYytDucWl45y/tq/AXBq6azMS43huv1+UwBS14vYYkRsGR4tmi3vHqKxyZnOugfRtn4YgtCe4TNjSaQ4E57D+wN0S0QCv0LH0Ny97CM+wp5TskJRvLwPOuxt5QRwms773uOF7JCDRyFaZskHVbcL3/P73mfVvO/eCQbgAGowNdNRO8dkveZldmHki8Aa8TBy4d2dtxfzLbol881LXjUNo87fABnB4UxuzSlyKOwnxcAYI3SA/Qe9WI7xe9Hj2HMtUhW/R7LZT1V5gLWKE6jrx+OigfB+FoxaUcya/hvtcACXSBusacE/SDQogrM35Wa1ZoiixyAR1iB3f+w9TDi1XKkHVE5ihwLQl+f3BcOYWgEji92xJmlEqQ7Pq5XvzetUsp1IWW1zsKME/rmkCnIVVqPjU/eBoDjWaS93qTFEnajwNVtFBQjo8paZgWPQRLKLASwBl4FCPNtZTUKO965oMekZIybB93ZwzP96qZR+sV5o5upljN+3FfDQYZa11z4jttmGm9/BKni26tQRfaMxqNhMkmiDUZjI8HU4L/DUP+abNfzARig7QW6gOSrXRKm2qlaPbrfuPapXT/yBLpzXxwVQj/oza8X0I39YF1QVbPG9MGz4RV7eGeblyPGQAyco4UYPUO2VSrvtj41x/PtFkOS0Uo3udymXHKSkl55+Pj3OkDnWpNOZaHkCiBPfO3cTTXVObzr77bm0aoeM/dkY9nhur8mbilZOGqyrVvqvo5k7Tz8zT7g6REmoQJmay6ouZeEwsYSpAcJchNYuXnVMu5ZAttg/fkO/pBub+XvGDNHkEcZvq2UtajGfIyQWJ782eoE136UpwJvbFPzR3LhVb2wagTui0sUmEuvYM5E8WhP/jXfcWpqSvR+Bn53fy317dKqOksrha7ceDma4tVQcYPSDbiWRh2gLpw+qoOxBYWkq/k8uvXfmNgoUohH3qUuFY74p8k/Tx/uRFGaAUXhhPk4vHIey6Jkd/5B5Fc96L5HWQsqOn6OU6qcw5rRcZQgIp/qvYiD9vlEQkzlUWGmLPsWgAKwcgIIUHmZ/b1pfEmWThJ4mSIQo+DZnfGCZpSXmKyxLyc9vWQqX17xY6aA3MYMNlqbu+UH+zw2+GCNfj2YhEkbZAZA49c5r3PYfnmxs5yyJorRjQbw8e1+h/8pEw/4pTmyanmy3tAFheJQPtwNkS8UMM8YREx1GsIWpZSzh+K4pouOcJ6acn5ZbBmFicw7Ru9W9Xhp2WKyNK2R5C4vzsihFXn7HwXOI458/11pfxliigSV/IYgScKY/hlnFGpjm32ytz1/vvciJgzoyO+ONW63TgGu/H/E2fjOsEQoxClQiwglmH40PMmoTjwROh+7pTKPhNL5ANZa53Um5DuJoKxhrnD9fkP6Ta0Uozuogg+/IXzuf3TGIIMRLnh797L6f2kn0L+hW/CakJKkKU2qi6qCU9/EkYvgUAVNMH/2Ob63uKG4KbMNyqjdCfKLao02uArMBtoaEvN+bHGETMm/jKZXqYlwI4NdIZqFXNcuIxEKnmMo5eR0g5IqJCmMw6bTJtakwygQW7ehDCBalxjIyuhbqDu3si1iMvLzh/I+JcpVfeBP+THRE/ns/28DwUveEwIQFXsHb2SdCyLv97gZc6R1wEeULai3RSGkcjZ9GzOfZtq1uiMURdgsbmNDgukhzLU49uDwOtT8IiD8hlMeCzrWmoffN6J5GVNFckpnhHthoJRHnkX5O7JDv/q6fNVP8i/H6QsGJ0V310VjbwnXAdawi01wgtkC4dJpai7,iv:bDfn9fe9reKcaDG0GkLAlmGHA8USqOTcGgAmUH1oz34=,tag:dNiFZkU5PyS//1ATdKK7Aw==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4czF3VjB1MWs3bjloRXI0\nWVdUYnR4dHdsczZxdXNaRFhVVTE3OW9KekhZCnJRQy9hbTc5U1hwNElqMFlNSmFj\nTXVjWlowQUZGaG5ZelN2cW54Q1NVeE0KLS0tIGUyOHFadUszWDhFQi9xczFMWG40\nWXhJRzZCbkhlQzZ3NDUxOXE1aURCVGcKA05+YfD2QCCeI/oKDvhU8hHC4ljgUJoZ\nEWS1F5D95GHEYeusViqQsQHFTuO8xtEkjBHKLt+TEJcsHGeTlTqTyA==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:43Z", - "mac": "ENC[AES256_GCM,data:CywKU/pXCcEwOKshnWl9/4C+lIvhLUJLPu4ly2bTUq1zpY5309c9+FdimXKHQwnTlmhGM2TqP4Kb544wpAmv+7+uH9cR4BuK3oGFUSuIQFpQ/xabMFSom1+eMpf1r5cdtUD6mvOvFSD/V3WX7H/asgRgJfnjppz25zDw3+/SLus=,iv:gPLGZUSLWSI+mUAzLnabHlejGmdb+Utt1y0J2wXqQvk=,tag:2Kcihhy4Tz7oPfSiAS+ONQ==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/server.key/secret b/vars/per-machine/ulmo/postgresql/server.key/secret deleted file mode 100644 index 5d8f4fb..0000000 --- a/vars/per-machine/ulmo/postgresql/server.key/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:i3O9P8heWlhBqlA+Xq8xxESlph80TdLmbX9pIKIQ86ugS2KRNXv+BMeP1nKamm1H/pUtjHCidMvALB2+PjlQBwm3gqeD+3UEJ2Wx4r0kvERsV2AGC9+esIIPOInJmhZDeeY8bGtsoUtRL+tprWJiBA0m4x4lINlpz8z+JO0lsYaw37wnhQLlnUA1rAW+E1CU4WHcVzl1PBtv2uvtQvQYaDF8ijWOP1SPCGWvFBMXqaERfBafTwlwvaUIm2A0kweot89fENooC+XAVmuVZkgW1D3lI7PpYIMg6dOtInXd/APgPYLRpeC3I7CqHJYw5v7lRdJRVkb6jJ9dTLQKodEW9SJsCdpNISqb7B7X/n3woBA2A1W5vScNUpUaFbGD5jXgALQfrJg92GzN28e/g4zI+EgmtB1wVxpD5RctBUWxnQUlYPNxFkZMDAiI4/jCC44xFijmePiTfFqzCTdIHCk0d3PCej3N6MLWviQ9uQsxVW6ePoQVsDV3vy+wrKJYzyy7subwxquO6EI5S0/tQTn++3HzArTvSjsPlROj4Yc/xaE0hPCnBYGcTnPel9hikkcWfZvis4HBepM4EWyivrgibH2OEtGNmEnLoypAxkmnhydelfiI0VYI1pyK2LyryLTj8agHEgCasnqLwYA5OPDRGOTrkuyFnGOi2Jtt1Rft3+4A45PH6KfRM5webdDsVcsKNVQpxJYU6Xbx2KHY5zmaWK/zl7eN6ELvSr6POUcMJLtcR0XqOogu2629A0VF7g1S5YXfHztrR9kq1gBMJyIlaHIpBnBG35UX49wFFDdZqkgncOsJ61UVwAQ4iS2QRhrGJmiKLITAX2ScYmVK09bOBbW8iNNvLlHbKnANwmA2H3wAw9PklUnjcNWsCLRr1Xf+5o2iD8J3Gm76tGhcy7zskleBP26o1pDDXVp6aSzlEnentKdJ/16nYf1CogHBcxC33U5p28KT8sn/7CtvoHbJ6mPf/pdPbBZ0eUrkg26SGax2FnFWUeyVnLeytOs7mtJKQDNV9OatjpLCO3g4IezGPc44AODSFlEll6nxC92zEotEucCMCtymgGFGXamzsTF2esoVDxqpcFKgffJhf3JURzZlGBt3IW7fEb/bcDEyzgbjgBpR4i+sTREIxrRcdgGsm7omizW+BAg/wr/TiyBYfdwiTtjI4yS3jj5kl/drqsqurjAxZUD9qqTtrJpB+nieHyPGfv2KZOGozMynq7XlpO1yHEwqebCB4o/GatuZiMUbiZpntt+SfwTA3XRxNKK9hzPxIGgcBxc41OdfwvLTJx3cAyefYxXOlboGEbRaZ7Yqo5QSy6BWV3DkcogqEksSriz9Fa1hP8LU8VBimniYgPY+ErPmLtbcp5n6hPWyCN2xiGvKb/EpyOpJNo7Kai2re/bKGJzCV/3BiDOiGdnnyPYhEjR77hR5C5ZIaF/UqMaX6tJQPSYm4xtQ8LH/byBhkKVOOzx8qJyioi8R+L7qyUbF8ySDMv5buKA1cWuDAtiY3awFe7MNsebiAXbNuE8KEUVnNF2sbiqQbxlot802TPBqRLbR/vyWiMcwsZtF1OUsIWSlS0vI5Ngvz0s4ruBimqZ65INf8LZYb/X6MpieVY6bj0G8MeK1/uY0ODo2WX3hUOq8SFFHLFd5bKiFs7dvuUs1XavTWoMub7HJkwimRkWGf3z3rsu6/O/vocfUyFg637TFOg+pTa015JId7LDBhKe0c0+Py6pmHc2ilwO3A9DO6EbqY+WQhJNz/sRut2cI/vR03+Eh4UdYjBYHEpKo7rPvAqyfmSwj6wuWmW1RmysgQL0uPwc/oyB7HOPDybMr02HT5QhOeVCI9lWOuHSE6dxYmpQhnurKZou1hsrI9JtKVFAFvs7KVGJeGbxoc9U50SVkpUXyWWiw/RTKff6nnlBSkt7RX0w/1Lz31AhK1WVrS+cjLK+MTywxjOUE3CIZFDYB0lYIT/Ogacz3e21+zk8mr634JKQ79rsSf5oSaqh1MBWvGhgP9j2KixbwavmlEteIEJlNJAqor9jzYkrugVBeCij3dnzfDEXPid6CiYptVk75XhIim8EmoDshIZdb1YablKQfYB2Ot3JgVA/Wicyl0RyaPTEnsNHCINnHQpM+oceBRGz/LD0W+hy7j0VGNfS5aKstCXsDNvTmwaREz72m0chduyz+GBq4sWiKMX7colf9P8wYoi2VCGa+2xt2rA5boEnxb3SwyWKyHYWrOVbR0JQHomuZJPHRfNvq1et22EPToFT8,iv:T197Ofh7nK/vjXVz35FLj5/NaLIaTVTsKVNDBtD/XwU=,tag:jscU6JYKIWH+dVIUrHGWTQ==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByS3EybHUzN3V3VnZGWmVN\nS002RG1yUktBa0NJVm01YkJNeWd2eXd0WFRBCjQrbDhOMFVuKzRYdGZwZTFqN1NP\nSmZpVWVzbkpRTnplNXZHWlBPUVVIa28KLS0tIDhYTy9zTjR6U3FyTEluS1RtKzd3\nY2pOWmtQWUJJWDVjOXRwQndWN2UzMjQKA6rqtGDHB9PpCTqva4h98MDibWdjqIxU\nqQVNisc0Hv4LhQMShR8//VRmupaIuDgI/6rcLi4L2xjIJPKtsXOlzg==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:43Z", - "mac": "ENC[AES256_GCM,data:Ur0QS5NfS9ffUfFCf5DOnkgrAedMX/15mm6CguMyJcnJpZ1vDGJ3Wzh+z0gsA3KwNPuQzujEuiiWxYVC/Oeikvj9m7loRz7udHDHKRJ50C/4lnLdVPYuUIf9tU7U1vyhfEO7qsKmDhlymds71OABZte2slKFS1Y3yHChEZozDSE=,iv:2swsqpEYFq8MSbKZlK/Y3zF872S60qck5IRmfX6JkDM=,tag:hSoRWI6PHlYO5ap6CSsm3g==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/sonarr_hash/secret b/vars/per-machine/ulmo/postgresql/sonarr_hash/secret deleted file mode 100644 index b8e12b6..0000000 --- a/vars/per-machine/ulmo/postgresql/sonarr_hash/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:vzXckwwFhF4wB84YLxDcK1XjyVt6tUgRFfBq8IyXBzTLsDmRlHT6SZwwdcCTQvOSwcHLaz9rEL5on410haOEGjrw5lbW830XUi0wKZs/We6av7JLNtye1amU4htCEb1mDDoyLzJi0z2nRC2vAGpwGwMjNykFZ11oRmxjALlivtWJtNvR78M=,iv:monIY0F7qlntov0L78G3pS2WWLRzKm2VAGC+QXJmVOI=,tag:L7KmaLETovL5eULz91s20Q==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOMk1JYkxTdzZyN3g0Y0lx\nTlhLUk5zd0JiM0Jtc2dGMndiMnpBaVZ0cGxnCm1INk1RL2ZsUSswVFRRdmtqNFN2\nT01tRVp4UThLeVlveXVGaVlnVnJWSDgKLS0tIEJhRnc3RFlES21SWkloTG84eWRt\nakpWckJzU1BPUTJMK1JBemUyb01tZmcK9L8xAgwfEla1sUpOIm4wY3dyyEjDf5wZ\n5dinJUepK1rj5M/O8J9EpxCG4hNcmwNNaH7jGLMRdpT0fQmLLBGH3w==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T08:04:42Z", - "mac": "ENC[AES256_GCM,data:mzPlHpBE6NE0LXPEvlz3MSH0AzjNUQOpGMawWdvtm9BiJ/SGpdn7B1gpCcCnBfOUPnYvTrYZHCiOQ0XAv8WLOj6EXGtqQMwvGKQzLc9ShSIfD0zqf4kNPQQP4c2uSkZpFIf4poLhWM6/k7oLeDFP77lzQNuKW1IugigK9xU153A=,iv:8jN/g8O9UIZF4EuOuEW4XOsnro31SHR0XgNm5IPhXbI=,tag:kY194TSK5OVVf7a3yt5+GA==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/postgresql/sonarr_password/secret b/vars/per-machine/ulmo/postgresql/sonarr_password/secret deleted file mode 100644 index 59e2cf8..0000000 --- a/vars/per-machine/ulmo/postgresql/sonarr_password/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:uGqwITMDYsOFdkGNqcmuS7cDxOyfsFd6FAThy1FrcCrei9G++XaRAQymaektzUebvOn/r1Je5hGSnCAwiLPgcL+3k253tOrLZbwqDFx7Hfns/ceKnD5CWs9Vp6JAO+6Md7VBRkcqBaZLBJ2oqdrGxpTqgkB3AyxWhNc8y+6L7m7X,iv:YFoLawKVZ90ZTvloCjQZfds7Z2pk1QkET3bBs/vWwo4=,tag:mU8rGiNASRBVAxcQj4ADvw==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2OHZiVEQrTFhUVVFNSmFR\naEdQMmxEaG0wMklURTZ4VWQwU3dOT0NHSWdnCi9LQWJoZkNMTzNWSTc1UTVpVTFU\nWUYxcUxSbGc5MkdYblVqZHYzTGhMK3MKLS0tIGhsMmZqeCtRSUZnTGxiNmttK2VV\nY3ljMnZXQTFBSldFQjhQYkxhbW1qV1EKm/XLLAu1QBxELCg56iSToLa2qTDSan91\ndvrlHkGiplG3w2lnufi/s6PvmmjHHvEbHuqiISavt9qUylsqAtfQrw==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:43Z", - "mac": "ENC[AES256_GCM,data:Nom2uZ0g5tORzhjApl5VlKEgIhjd5ehGTygBG0ycA29D2OtkRKxjcjbim8fiCn1Mo9O/lXwhXbcM9H5UpChMf8bIWHnVI8Z74/BTsCwfFuwNVF/hH75CKI0GR6XxRyCshhUDefHrHy1xZObJSHoAV1LJlenk59D+2zaExE6ATOM=,iv:TpqdOO+z4WPzyRW/Ze+39HNjlhDYCFltkycN0ly/Qqk=,tag:8PgBTtvb5NiffKXdQkDhkQ==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/prowlarr/api_key/secret b/vars/per-machine/ulmo/prowlarr/api_key/secret deleted file mode 100644 index 20fd3fa..0000000 --- a/vars/per-machine/ulmo/prowlarr/api_key/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:DO1QVBMqPr4/CFEHpE7sWgGopxqJO3voovhKFr+NR1W0KHMDmaNH5hBwMWmUj9ocOEn3elvUC68LGibIGkqYGQORSnjZ4Z4ETViissg75V1CvTdNqIZ6wgSSoaZ7GV2RWPR7hKZ7Hji5KwLj8WYVOH/OfivVcFAFtLGSm/BtPDTf,iv:kiKwYLz9OeLrl62+V/Ur443etsJVlv8OuVMj+tClHSI=,tag:LZZvcxhoDKn9oJcFAV1iOg==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArQ2tNRDVaY25SbUxJOGda\nallxSklnYTFVazBkbFZ1ZEpQZDA0Rzl6aG1zCkI1cDhrNEpEU3AxSnB3QlZ3STVN\nWUMrMEdJbk9Lcm5WUG9kSjRLS0NjWTgKLS0tIFg2eUViUi9KU0FFcnRQTGNsaHRW\nRlp1a24yRWZMSVJvbVFYMjVFdi9Qa2cK+Ll5fdIbJeh/8IlMJIMnqKU6t3AnL5Uq\nBS+72/nSdGk7XVhZJSSa6L7ra4iejNJev+X+zkFXVhPeHiEO6sWQOA==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:52Z", - "mac": "ENC[AES256_GCM,data:2rPNs+BHnqXZNy82RBU74p3tA5tVpNLKxuU0r0xXmUEin5sIEXslVY9M5c7SHNNG2HKWtvH9dzB5tUXLM5PAf39gEjITUN1n6MQIrvirXVxNKfM2d6oSXX+v53tTBIC7zjVkr/DLJR0kk850oxElg/RjXzPaczisIEv622VXRbQ=,iv:fkJ/sv+ljdmHCxX+wiF/S38MIj6neC11q2mMFhyyPok=,tag:lIeelWnmYcyK44L+VcHIQg==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/prowlarr/config.env/secret b/vars/per-machine/ulmo/prowlarr/config.env/secret deleted file mode 100644 index b5f8486..0000000 --- a/vars/per-machine/ulmo/prowlarr/config.env/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:pfVu0v6C7fq25CiToA+FI/JqC3sHjuG8jj9ZaBqwFS9mfsLnqL9cS+fk+UfMVvQpqrK/86pzvzdabIVhQBB5kHXuo613jXm/0DXSbKAOV84zxr8j6jEYtUseItcuuoKk4vJper5HFhtfb2umjieSWNUw7RTn30H3ifxbTN8n0+Ut8zV33ipeFLVdJzd4bBzU98axrYmy2lYR/p8EuEt0MohPkLIX/izjczApX7M3kIWn5u4ggYlnSvwaif2f63UGKxzk31aigB5uwHfhs0FjfA5kdIIdIm44SVidfp5rXTTJrrHMfukfHo+UgEgTl4qMpt80M3STXPNQmeK6GYwRJ0aTNT9uU9Lq+JlibWWof6IR8TEyUec/UAGoMfi6EwEFFOJgs7WhFsPGisvLg9YNYSTD4JRE8+EAQg==,iv:yeISxnSx9BsQCPlni6L0Cp9Ect0Lha7aKt1Nes12Vew=,tag:kela+RtMHvVqd/HEEb6NIg==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwd3FicHRoZ0poQ0pOVGRE\nK3B3VCt1VkJsNUFvVzdlR2kzQ2VObmc5QzJRClovN2ZNR2NLcGpDNmRWNjRFZDNz\nZW1YVC9JSFFCaGUyc3BYYTNMaitITWMKLS0tIGF1c3kvcE9ZSlVhTTRjZ1g4QXFD\ndXozRTBBcFc2VE5GTVN5U3JEcTZWY28KmQNuVk1bWKC6ZRDy1Eu6MXy8ENpaVvRv\nkIEl4iQfB7wt2eTF+/e/oHw/SzSJZ/mwBaxTDAWyBs0qPNelKxgC6w==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:52Z", - "mac": "ENC[AES256_GCM,data:5UHxsMrqyjqicGg3gajGj6qQEh3WSRCLOSLrbdGuFbWg0qS75QxyM8rz1/Z8JirMNRp+ACYHWXeP6RIt1ifZgmcEmZTv2xYFnuhelFkT5EOFmdBBETPB3EKbTP9DxKXhUPBQ0ObIyfJW3SAhPorBCRpW7+E9bxyaD3+sg4qElaE=,iv:SUNrGE6iEwV99jxBhViFvqoZx6XAdXdsCmMNSRNER4k=,tag:s4ebVBrkLXIddJG0DNSCdg==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/qbittorrent/password/secret b/vars/per-machine/ulmo/qbittorrent/password/secret deleted file mode 100644 index f609b86..0000000 --- a/vars/per-machine/ulmo/qbittorrent/password/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:mM/ifrTA1aSOtCtVJvjF55WOu9SEgN96QqKK7N8Z+qWHHFpoEc1J9KpjKLH6X3pOQawxpSXHcfOr5iEE0tyeXK0+/G0o1w23YlARroqKIqjUK5NkVHH6BlOQEjuqlBX9D37O5N9rbs5BQ0urXUJ4TP1dmWUOABeEVFkWLeSBv1wa,iv:JUfoo7wmBA82SjpRBVGGR4N6SbURmA/raDFMcR+vyzE=,tag:cOu0oz3i1A33/Cc0BlSBMQ==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpSFUvRjNZOWJ1N250ODNH\nbXk5ZklvSFhGR3JFc2tKenZZYm5YRlVJSEVNCjdxK0R3dzFHcUdrb25MajRMVHpP\ncjZtT2FzZG5WS3l1TTRvczR3cHpIdmsKLS0tIHVKK1Nhb3kzRW4vNVV3MVVyNVIw\neVJsU2pRZTM0N0xPUTlKQWxQZ3Jab00K+07ETKcNVJ67in/yTRPadLOBMOt/assB\nRdiraOoaw1W7nKMZ81pw2teV8BUWrl2ZvWHGAk9B5M+LNm2U/nZ/zA==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-03-31T06:02:33Z", - "mac": "ENC[AES256_GCM,data:DSIFCYcpJFrIgLUmQ8M0gTFL3aKkwUJ5S46RYUgwn6Lu5BKJMVdpOGpYloinvRYlMHyK+Diva/tL/xnBtjG7Ug5gKqY4NAf/2yZLjO+y8+FMApn4KiTp7Nqe8Q93psJlIoe5ZUGxl2wERuXnmSnmFmfOjAYb6c/XtR+4oTVqvcg=,iv:wRrA2+eHduRpArHQ8ZghXf1LlezWZKuZkH4KGxJ/KcQ=,tag:RrVAzeL6aZEURY9DDLQu/Q==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/qbittorrent/password_hash/secret b/vars/per-machine/ulmo/qbittorrent/password_hash/secret deleted file mode 100644 index d6ae7d3..0000000 --- a/vars/per-machine/ulmo/qbittorrent/password_hash/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:JBLJryQ1s+yY7ukjCdToghQrTEcE3DgEhMSl9eyKQPMARm7EX8/82H07CPHCjYGOb+NtZle3a+AAwv39XKUM6OybOiN484rDa2zV6lvFA5dcd/sbuJH6S6Y43iUbUHnScV7iYX2OGW85dSo3Wwcb5SKbXSb3+nhcXouRi0gn,iv:UQjJhIT4AbYACcmjkmeTnpXgXLAbhtDZoF8JUSfOdH8=,tag:928Qkgls5vGx5pBk0BJ1wQ==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnREhIZmJVTnUvd0hPbm5R\ndlp5aWE4V0doWDVzQzl6aW5rWU96QjZnNXkwCmV0SlM5NXFFeisyVTB3NDAvMXNz\nZzRMcmdyM0tyNkttVU1xN3E2cEtMYmMKLS0tIHJXWW9BRjIydlVmYStnZXIyZnBh\naCtYMWpwVWlJK0IxUWRPdEhVMnVYencKom7jxQwua8Poe4d9wR8sMXZCQQYM12YW\nR8hxaVFwXMIrg6qMRwTK2O1m/fkcVCV6qidjsLERb+laH+W8Nn/urQ==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-03-31T06:02:33Z", - "mac": "ENC[AES256_GCM,data:ikSkLjZtN+P5mOtljcOVo9oEIGlVi+mNO2nURtAgosN27TmQUA5lysUNAZLROD0UsnJAsghXCK4tt9vF68jAOPkBtAnWgL59hgoZcBSiOBr7oRxd4WIGXLhyqPlcDJV7VevNJWl0JLk1NA3+3HQZho4YUzQUNs0GtiBoJj67Uqc=,iv:nPLGU4UHCmY4MUP0j6ZXWyMmyAvs3xNMzKDcgoWo5qw=,tag:JMqKvVHVVfNOVt/TQu9kYQ==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/qbittorrent/qBittorrent.conf/secret b/vars/per-machine/ulmo/qbittorrent/qBittorrent.conf/secret deleted file mode 100644 index 054243b..0000000 --- a/vars/per-machine/ulmo/qbittorrent/qBittorrent.conf/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:QmSqL0OQvu2l4NsJHfI/SaEk2Sdx1jMf0AKbbxmPaGnSnN3O3c2ATrIwgTHTumLankI/uXxbrV5vIXTpJU5oSXjbv+Rx0hU+c2Tic/E8ORX1/1SW50Y0ax4IxFat6mwhndte+4dX00KyCCnaRDvnFlR9Eag1C9wQY/GwAeMJY6Dqv9oWSBVRsIONatSogYZ9mTk5ueDrPbfBbVzLKs/5ZbA3RScxKOe5fiCiZ3RQCdPbrd356rGwCzD/k008zGxJkW7aeFxBhpTy7XRKhUUjw4gH7FOwZ71qFlYxvNkOu6QnpFXez3ks9fAIBL2uMHlQfARZGwm84MdEOHCPKq1TI6NmKsqEfXS91hW5iKvH9KUa0FOEhFHY5W6tR1gx0zfGvjv1ofqSMdUSBhHPOuc3HouBK96MzStfyNzePGuAMbsAC+ogn2Dsy55KQiHKvnAW4G1gvQ==,iv:4QN2aeK0jbbzR+qIHHSCLV7/OnZ3vQAHpUovlmnftno=,tag:vFMX8Ep5xJz8eBlQmKXCxw==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArRjlwVmpOU3l3aXIybU5N\nckJrb29rZE9OS1dQTzRSVitCZ3pIOExUM1JnCk1vMWZ3THFXR253L3Nmc1g5NnMz\nV2Jwc0VqYWx4eFJLcTRNMDh0V2M1bkUKLS0tIFFLdFJIclVZMmsvWTFKSm9BMDVj\nb3hrd1ZNL00rNHFTc1d6azYxT016NnMK06sT9B1bV3Cst+SQsZWhFAxxN9wEzQ62\nEMfKu8flDkpXOfYhT7O5gjMMYmC0m6UIt8Gth0E2PQsqx0WfcoshIw==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-03-31T06:02:33Z", - "mac": "ENC[AES256_GCM,data:JYeelZkxYyfciMcksE3b8YKfefzlTJc4Piua8A1Lg4O2Xqz5TpZ7sLyAiqdkKAQsorFYnIjGwhIHx9PrDme1+oioCHJqCsO9Kqs9RLcwVZbBIw+UKlFdc5wNLc55qvP3GSVMLO0/IuXETaOd6IVzFxX76EJT4LTOF4Vv1pz/ymU=,iv:Sll8IvjiGO5kPbdEDXry8j+0pSMIioeIx4LrcfpyUxo=,tag:tGnRLCXcCKlifLp8JNcE8g==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/radarr/api_key/secret b/vars/per-machine/ulmo/radarr/api_key/secret deleted file mode 100644 index 33bcc77..0000000 --- a/vars/per-machine/ulmo/radarr/api_key/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:aXUOYSlckxmajWwXgypF/ZR2zWIuenIdTLbntSuitbMj3JqiXa5+vb152w8cSz/7u4PCbri5sdoenGOV6LZar8ZpI937py7e93F0QIvuk629MTJMqFAkOtWxo/iW0u6SEH5TZXb+BaULJ42el/uQRKooumvC+R8oF6SAhG4Iw/xf,iv:wfSWMQcEjur4WgMyooAa2Mz30EW43/oCJKg7jVOUU/M=,tag:1wFqb/rkJpa2+QJuJcMerg==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvWm4rOC9KdHN6UkRBTkt2\nY1VTRkFORnUwVDk1Rll4SklmOWJnVXZpdGhFCkxZRzlZK2dVZGRSZHBqd0hoekdK\nUTNnNjFkNWdLR1dyc204dU9hZk43ZUUKLS0tIEpDYWFpeUUrV0Z0a0t3K2VyRUh6\ncG9YOVlqZ3hMVXhTUnM4L2xZcndnR2sKA8Yb2lU7MgrrtYqTbxLFFo8F+OX9Gz8P\nBm23AO8MrM13NDP+TydE+1cgg2DtFDxJVE9dVNCrZyVDOFvz0ymT9g==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:54Z", - "mac": "ENC[AES256_GCM,data:zg6RfqrPbVV61NkY+2diHRb4nY/Z4lmrIhdtc4l/Tp9im6tqDy3/mkybElX6qzEeT27x/kukqBL8IGDFKBcilfgXKzyV0uACvMRO4vntIpHr4sFjdsieECcLFEI4bGD0hSPEu9Pvq7QAKRYL+F8/C4MX8zopEA+fnXQ4HfWkBEQ=,iv:LIasq3mOq3ELb2xrWAMQ+V7TgD8XhZl3XTWZRDAIW+Y=,tag:YspXHQWp4g6ToALONfY/cw==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/radarr/config.env/secret b/vars/per-machine/ulmo/radarr/config.env/secret deleted file mode 100644 index 48d16b7..0000000 --- a/vars/per-machine/ulmo/radarr/config.env/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:f6GLpyp4xAFuocaf6tPbysWIbPQfzn9SCXZ1AyPOJ9T8B1+6ssM5zFGKGfcWvzm7A0S6fthV+uQrFvlJkpcx+/W9p8yt2ii1YpcmPGzL/2kOW5Ye3Vuo46kHfOzs2UqXoW/u/L1ymBnLaD8R0crdrLYrbKzDbSycGzuLS1Prn6XiSoGFRUp0WGhhqJL3fVuSSF6bXuJgt0cmwVcO7BvigCcKWuKYt1DfJevHbae666k5MNxWKphPNTeBkQM/eXSZtRrT3pBEt+RGssAqfU+vvxafdxGtzGEYINQZSRcPHGC4cBLWKBuJ67+T9tTNRE40bPu3RVF6luBjMiJETqPwQPnJU8PXcvy1k+vxo4/Mx4gExX8fB39yxL2cxYSDB3ttiS3+HbLr6i9MPIpJ2TPsMdY+eOhZ,iv:CGOI7PlHRQ/z+ocR4EL1iJ93BcQJxHNu0FF6VMx/bvM=,tag:8RwjHKLOZRgevWpAlbvkug==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxWTFPaGdaU1c1Q0RucjVZ\naU9GdlAvUHpIOXFtZ2czdWh0WUJzbVJTOHhrCnl1ZHpXR1hxWFljUW9JRTlicWZK\nLzdVckcwUzIzSzRZZy94ckNtaWxCbTgKLS0tIDZaQzdzZEFTUzN5cE9vVUphWnVL\nZEN2N0p4RTZwM0lRL093OTZlcUllOU0K6nrSoA89k+y7XZ1/Y4QWAoDt6hUfAC+c\n33IIKdjGd8hEHj7ThvqQQfOmfWDpPCzA0prHLzSW6FDTeUm5CQGxFQ==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:55Z", - "mac": "ENC[AES256_GCM,data:pLIVtYn6GOb0cWJRdQRo2YDnO8MteNeNx/hAzfxHgOAmBbz6NlMRAiGrqGro4H8KI+osaZ8NHcyCh4mnj9i/i4dV+pa3X8PSSxAnahK6vjA+B8sf85WPhsFcoBFCtjT6CR1CkR8ktTE+s3SbjdRVk0Hh25T7UzhmFZIgKFvexZ8=,iv:UKXwBVX0R3zmvz6QJuUPtX+WRuE/0L+JlItc7vqpY60=,tag:6SwncaLMWt70WcpDp5u7Qw==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/sabnzbd/api_key/secret b/vars/per-machine/ulmo/sabnzbd/api_key/secret deleted file mode 100644 index 74393fe..0000000 --- a/vars/per-machine/ulmo/sabnzbd/api_key/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:Hjg1GjSENrhiqIa3E0bMsmVwKFBI5PtRu+FeCdhICpKUY5UIgdnf5DiM+hv2ZYIP6iP/t4KN2aOQsYBD7UgJeS1fNuDn9UD7ZhipJVqG0vR0CwnVl5/ynJaYAWpVQyA5v+jWpSXp5kNa+sSYfwPKJgoWwWnsji157iQ1CVQkQ43f,iv:y/loJ87JCFQv4hAfRmhFOG//qkYCB0iE6WLH+MOoeM8=,tag:X+N7M1sAv0fRKXE9pOyQkg==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsYjhvWTI1Y2FkOFQwMjUr\ncmd3eXN3SSt2NGN1eWp2RTZYd3AyaTQwSUQ4CnR2VURjLzcvdFRKbWlwc1dJYytZ\ndjUyT0o0eC8yQmpCU0orU3UwbS9BNmcKLS0tIHpaTDcrckJyZlF5cW5NeW1wOTdx\nMXQrZ3FzeDZFaUg5MmxoMGQzRTVpT2sKXUKhnjPqpIVDcJzV9BlCrc5hJhx6bsTs\nOpak6eXFkery72dZdOXvijQ8DGUl5tfLXmlHd77kszuSOOTHutqnmA==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-03-30T13:05:06Z", - "mac": "ENC[AES256_GCM,data:9ERg0A43TKDudtI5Vq4sNmBV1z/6WG8VcTHykVoRsSvEIESPxKveSFOvoa/2pifM7xsGqHabPcLBzJU+/sOKTLVK6IdL80EbCilZFCQsUYxc0zu41+NdTzLBmD50fW8G9H93B5osOcmcTtKPWgwvyEoJ+O/aVzQWlukEwDo+vcI=,iv:kJmOlShNhtF03CxjoQfUv65ricphC3KrbTfMvlUBa/s=,tag:huaI5sxj9JIKYEqrstw7KQ==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/sabnzbd/config.ini/secret b/vars/per-machine/ulmo/sabnzbd/config.ini/secret deleted file mode 100644 index ddeebc8..0000000 --- a/vars/per-machine/ulmo/sabnzbd/config.ini/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:WVP52sevgJzYb4oB9N24uhKqo+YV/WFjHmmFDO80SNyARTueG0s2eXQ+UeXbodo0nGk/f7tTxYJRcmZyBLMGuks9YKKvn6zOZ7LZY72csO14XAI19P7sTupGOs43PpvQE8212/M18RMPNRIoj0od0HeDxhhFa61yhtZdclppJw3YIxyAfLGaCqV3LUrFIv3+hlM2zymJTi3QDucxL9h63tlv8ijwFg1ZOV/80tSQ4aEeMmic8LCbAbETtdYViQXHJLTNEEDFK/vL8eMHEwULPmw5yOjbWEyqVGO8FHDVbo6Aaj5EZGkiiEQccbJUbuM46J1LzLEjvobg8LKFcA/sVoiiqGp8b47qHbb+dswM5e5vQx11vqDI0UyQcxHuSxtSXJWqi9xKRf32sNhs2P/u1VDLBNCZDtc6z4cTzDHWKn5Dpm+G3oe9OtcA4Nzk/T49pGsb0AZ9WKSo2aAXlyoHfA3bfm+pWyAhEn5wvlhWojhJX/wRTg==,iv:Jdx8BJPx/Emszd/Y3y8y+j3Ogfp0I9fXdhAU5SToTj4=,tag:npMFKmFv+4dcp5Gi5Vyvkg==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXMmM0UmtEZ2JDMnFKaitk\nZjh4NXU5Sk15aGNBbVdJa2xSRTVmbUxFc2pBCkhPSjN1b2Z1Y01XYTlNaXpObFp4\ndSs3QlFXbTB5YnRNZzZaejFjVms4dncKLS0tIGwrckxId1F6OGVlZGg4UWI2cWdI\nKzBZU1gxNlpNN2FaTVRHd3FiV3ZzMkkKsi+bSSHNZ5UR//r7Md5Cds7ZjaAgHLqn\nh9gsf9q8Q1dM8LXyu7s0UEApcW9PVWoFAJKttn5mK0VMl0CkUlhTYw==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-03-30T13:05:07Z", - "mac": "ENC[AES256_GCM,data:U1oKfi5RKpLIJOS3mM5y/pQ+8cxGk3dmza2YXk9kfBuzV1LfjCpSoFFGkuLWESA2rbZZEjjqRYfcgi0n8bTzWN8JZFZ9ZTI1ffSjKhVWdC5vqW2XmBlUC3GvAg/huGLd4FGO02qJMcqmnH94pzAglH1uhB7sP6K54w6GEUNEBnQ=,iv:mt+DmM/V1u5mCXjRx7y5RDW0gkmKHC8jYFCrqmbeiWk=,tag:+0f7k/bLwHznNtDt7boRmQ==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/sabnzbd/nzb_key/secret b/vars/per-machine/ulmo/sabnzbd/nzb_key/secret deleted file mode 100644 index 9ef806a..0000000 --- a/vars/per-machine/ulmo/sabnzbd/nzb_key/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:PconALoYtJ9/rw1rlo1jPAZTbcEYbBCHOC2Yt4V/gunFGOGoRZqENAJgOA8DICNvbv/04Mv2R9kJtHIwNXOfij0J0ySo20Tj6hTgTFIPOmNts/7lA3GFtCzsDWPokGmLwYzq/cowNTs2Vh2tZBJvL8qZtBBY607fmxlL9b44wjKm,iv:6BdN5nXBYtKHPdYn74AxetgllLFXIza/jktIGt6uMU0=,tag:riFepVvAaFcDSZHz48q5KA==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqd1B1eVBVUkVYZDV0NVlB\nUWNDYkZrbndOZkI3THJWa3YvV0dqc055c2tnClV6MGdsKzFRaTEwRlU4dHVXdGJR\nUzc1M3dKWkFPZ243UzA5ZnYvVzVjYjQKLS0tIHhhMnJ6MmcrTFZvc0ZQUDNLb1Bp\nbWdsOGhSYjJkZTRNTXpsV0hxV0I3cTQKjKfRJ7B4wDi0Z7ELkyosdxWPT4qdHtNu\njHDRfgmhD0EGgntwDohSCuxTyp04jQX0gO2/wF0oIFddP5cJQBZm+w==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-03-30T13:05:07Z", - "mac": "ENC[AES256_GCM,data:p6VDaZr2vq1As/qe8n+fjV31+IurhYcgctUTJ1Zh7QkQRKBRXqfHQAZIjJttKUbhEp7hJBVGT316lDNJ9uKLm2vkO3CKrb15Jd0nKBWIsAknOF+ldhADQ9+Vt2W+tW+RMvVjq09fyKihU+j/5NtoaFFnZoK6gPjCWOcKuDVYor4=,iv:3Mn5MDdZmYhS9d0C6IcV6OWxrIej+N9RmVwHRa3N5Ys=,tag:pWa4Cj/X2wZiuGsFrVbwYg==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/sabnzbd/password/secret b/vars/per-machine/ulmo/sabnzbd/password/secret deleted file mode 100644 index 757c532..0000000 --- a/vars/per-machine/ulmo/sabnzbd/password/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:SkOlXNgGnZ1w,iv:SL+wXz7Ifpinqbrlv6Z6Lw2OYugZviGK1yLyHkwY1xM=,tag:iebP8vOoFCTWesHbq/QKvw==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzV0VuUDh0K2l5bXYwMWJu\nVnJ0dnhncEtpU1JQTXNwSXZXczBvb25qRnk4CmZYUjJDYXAvTHZPSmtjQkdHYjlh\nbWxmaXFFZGY3b0ZDR3NQOTlBYnBVZmMKLS0tIExFTXVBUU5GOEJWdEZ3VnpWTU9Q\nWEg3TjBxM21EbWszSkh3c25oeEVNTEkK3FemB2jA6fVil0z2FA6u/4nH0CScrE2B\nKS37nP35ufv+m/5x00+WNOgppfj3X61mKLmeJZ+Vj01hr4tTPegddg==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-03-30T13:05:07Z", - "mac": "ENC[AES256_GCM,data:YVBRGtUoLSLl/B6JOwckAh2olXCrbbu7S/jE4DyHqO41KdSufQ1EUm+gzaXYnW64oiyKtMAHZPYJS8z9i91zGDs6oUOtW/2vr0LWyIkhEfXAB5Lbm4Im9SqJ8uOthjVQNVErMJOu87P16Ud37v71wkIXa6UImwPLtn5nf2lD5ho=,iv:L8BpU1VEWCtwTeXDwAMqWzKdvzxZbkYPC854g5Lfrs4=,tag:ILEjAo1f8i791ORUQgzGoA==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/sabnzbd/username/secret b/vars/per-machine/ulmo/sabnzbd/username/secret deleted file mode 100644 index 317403a..0000000 --- a/vars/per-machine/ulmo/sabnzbd/username/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:GOtu+wDVCaCaOIcAjMZFpQOvwg==,iv:05f4WFx/u/zkBOfD3woUnlgoab8qqgCxc7tJvLd4FBI=,tag:0GcqnjlKC34dWWQdNDaVwA==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsVGJWMzVvdjZhM1Biam1P\nN2ZNY2dBSTBLejh0TTBkV2pVcVEyUEhxQ3dzCjRtTzY4NmI0UERTeThGMmZ4aUdI\nbVZ6eFNQZHNzNy9pTS9DYjROZGt6Y28KLS0tIExuVnp2V05iMHlBR1BqWE1scDFk\ndS96eWFSeEozTXJ2VWtJNUxFVXA5K2cKo/7Xybb8tzJQa2zUHmZMJir27ytBoCx2\nI2XBaM8L4eT+OJzO6PodSaQCfQlfEEVlqZupRA2YzeYXUGZqzPBgpg==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-03-30T13:05:08Z", - "mac": "ENC[AES256_GCM,data:X5V3TeZbT8YkYwL7blD8p70A8cER28DBNJT2EC3EdXfd3ATcGrwfpQYt6v18p0msSMRWHP0BIxKutDHUzWjERk7U2Lj221zm5GwjMevZDAXFNKKk4GXmCdpSeVpX43+37UzSKA78cEWca77Yor3fEBfusGux8fMG6XD1wFoRXqI=,iv:36IjknMbyu372BegqV1OBPS+8e3J5WaQ2WYAHQ93Jps=,tag:FWN4tkv4LXvelotNjD2ieA==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/servarr/config.tfvars/secret b/vars/per-machine/ulmo/servarr/config.tfvars/secret deleted file mode 100644 index da73b4f..0000000 --- a/vars/per-machine/ulmo/servarr/config.tfvars/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:51cLOUJ3H/3423I/GbU6hpcCTABxz73xLX6eJgf93+se/COTByNELcej9AI0pcVpBlkhe4Cun8rf8BRSMunZbt4RQl5LdXZdD+6MNAP/aAZUVv4I3ZcdEE0vS7IPcYx5IB6E1EdvQ4QjuCsit65wt9OeP0NqPULoNbvjitMaDrr1tWcJyLougLkKeTi525LbxFx+gNRaqCUdZT5yKlql+Xc+wUbiTCRKUOUFxOGh1uwx1/T24vnQWpqMFzEVe1NT6yKBIe4Vj4fAe+D5Xeg6YO5Adspq72jhMjMV0WJTAXoTPqmjnpRDqzk/HJglGWmZXgsMrsptn6SA20sn7Fersp0wi0n+1iINalQaS2okzGQItX+ouBj0VEBsauYhrrh/DLuEiWDH/EfMgkxCEBKmUrRPKm2sa5e4A2Hz86PFW1nzQJI9eIi6ibmMYwWILbqQgHwQeDw3sYFgtwIHpM9fei51KAdNT+fA6cLZPQKCzUxU8OIV0w5ZiNRu5FMmKXFoYIbOqUolGH9U5CqvD5Ep80d9140SkxPdX5gTdpYAhkIZvhGkIADIimk3YMuUSP5+IgATw1rX1bbwgEsCjdPZFzzS/i5jshxw3WcGNKFvmxeoeZbCu5jt2tISR1XXNa301r1lofGLKu2PneXotnuzjYZc4W/g6gDzrzh+kFV3X66p/4705m7td0KVw5ESgbCal1SZv5DQmvNaDXJutDGPz/zsfC98qSyWBK3ObRgO29TkcDzrrnkh8KyiUtbKelE1I/dRzoe2SGFKZNYJporomE949xsKsS6IcXjxCWTHw66AW5ETVqy2vGoW4zoWQimQj9FNYBMAGGXEHJUyORqHqqGAY80qxkC0r4DGfMI0P0F5IoFvELnuMKkSFJyzucJTxjv+I7Se4XdmGBzy3z7fASiFpiWxwOQi2CShRUypTHE+Lpa8hLRqnvEq5U7NvXSwNZEs9E0HLjoJrsQL8wfOq7eCtJtKVSCpTuz3HKldt7lMzF1Bt0J9qCNdvTgjm50UIDQeAAw0E54PdKDLBoaNV6G1brpYmlW0NY/0hloWmGlfTArhdh0REH0Pzb8P0b5F66yQsVNyMStGPMkRo5btgHMHaQPoL7cSXZxWPgvBXbw5nzusi2xy64mjrz7YRY/jRgNsxrjWICOYIFnAyX/3eqlSzga2H3MAeMGw6SCu5Zc=,iv:u/2+hqF3Z1rkfTtziaCYYphBNnQjRpm2/pOtatq2ku4=,tag:8Z2Ahw1rKfgElGKBfjZL8A==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCWTUwNzVwd0ZQZXJ1SjRq\nYUg1MHNBc3RiQm1iclg2cGhJd1YxNHE2bFRZCmlxTjRPeUI5ZkwyTTlLWitNM1Nh\nYS9CMVEvTWdqU2hQa1MvbUVrM0xqREUKLS0tIDVrcmloaFJaOWNHSFUyUFZ5eGtl\nNUFaRTBZbUZYU2d4bmh0R0E1eUxFeGsKh2kSO3CRjcHcP4QzaJOY01mHx+M5TGZJ\n58Fd4lAk9CAjdoXBvlNsn5WshYpPpCq6jX9gLffn2rDUMKMDl7wXNQ==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:26:02Z", - "mac": "ENC[AES256_GCM,data:O4HiHY1pm5Re/kIcI2vTPL8vFjTFcr1rFBW8ddBu4BGoFTaJLqFsaeTV2NzxWMr3Pag0JfSK4D9SsWn/n4bEO2R37aXTox2fH8UPJ90jqAkevAb85eJlC3Jk45W+fzQ2exCFBjqD7xIRtJ+r1Jyu4gGUZMHuRgAlR2rZllVHAbI=,iv:ePxwH2SfFe9U8zJqQE8P7lsBreSBcQc9qhPjFJw3e7I=,tag:Mcelz9XNBokPftpMqrsySA==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/sonarr/api_key/secret b/vars/per-machine/ulmo/sonarr/api_key/secret deleted file mode 100644 index b8328cc..0000000 --- a/vars/per-machine/ulmo/sonarr/api_key/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:C1ajlE0mhyVKJ4uKYLuwJyZCUUiox6gLoVz+4n2y44HE1pFMMgjhNa/i/Z95mjS5otAW5IbruDavhxpepzpKgddec1rmf5og2E8FQauZpED2PiyX6+/sbRa9rzoKP73aYwtlkL8i0KbyeSshKqXIVlJwv4RYqdrusVOnEWS4RgdM,iv:z5p8Bepg+wg0vgllQmqLa29Q3YJqg/V2FlIV04nc8Rc=,tag:PcHKoSHzI0AYsyKj9Mw4pg==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXZHljenNaRXZXd3ptWTdB\nYVlNeTV0RXU2RmM0T3ZlamsyR2d5WVlQZ0R3CnMvekMyb3pScUI0OVpqaDFGRmdY\nK3NBOE5XQTM1b3gzclFoYjNKNjVWZWMKLS0tIHZjaHVwOEwrTktPSllvb1o2VlBH\nT1BldzBDY2orenJKUnRZdk5MNWVqNWMKnw9mXQhaIiNC60YdDC6U6Vkn41fIlX0o\nGXKPpUwwkDrHD5ZmDrYm23C0Id5dlAAD8Typt6vowTqVR9E6plTDEg==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:58Z", - "mac": "ENC[AES256_GCM,data:/ebZBVAEEc5WkNbEoDwkak+MMALV0RpGb/gpOrp75QGliZvtaADgEhD0GekBv4c4v7f4jE06Ed/waYozF96lqRVmzUZVt9pBT5ugsF8UC9WaJAOCZyIgfgWiAGo2k/MdAhjw6efWWqpCb+TwyziqLHQfGUkqsn1LjYcTqO3iDUM=,iv:4K8rgHnrE6E5fQk8VqHT2bu6wD2NOrKtIFCIp7oLyxw=,tag:MhB4gbjdVPa8JEFHuIQmNQ==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/sonarr/config.env/secret b/vars/per-machine/ulmo/sonarr/config.env/secret deleted file mode 100644 index 9e5ffaa..0000000 --- a/vars/per-machine/ulmo/sonarr/config.env/secret +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:wHSARDRyTbBqReZ6xmcw0Mb6NfgP5dVztrYnRbQTjV/hpn+i5f/7MaM83MrkNAyrt0bd2p5sSWHhUqWczhv7EFVeioccIWHD7SSVHzw/2y4q2ptq5IZJ1KTsXQvTITdfwsGacN4K0OdG5BbMJhbE5QItI6OoV3B2qOP2llw0bVHivlwFsqx/AtIDSbAFGb+j2eMV8bIXpx3fcKeo2S+8V0931rD7kftreKEhtS5DRk+OUrT/YbJ1M+6kp4eGYUl0c4cC2BhIjnJcEufnxrqiCH+Q/1L3k0HgSZuaAMM15yVo+xv/wQZSpHBexHhIojdIKLdHsexsMf2alEi0JyAEMaMRJBriErRIlq1fj85MXNjI/dYUCdeBPk5CN/Kqw7UQIOOPxbjiepga5J+UYNa4y9myvHOT,iv:LpmsdRVgQNeligzICGdxliFr1ukqOHTaY1rcJJ11nvs=,tag:ZuRd3P3KeDBOoc1VKR4xPQ==,type:str]", - "sops": { - "age": [ - { - "recipient": "age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq", - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEeTdCZEg2cUxtNXFoTndJ\nQnZFWEYzbHp2WU9mT1BWUG5rMUVQU3F3TzNVClpTOTFCOWMrK0ovQjhybjI4TlEv\ndGZkekxZc1ZvbURrU3NTZFhwMkZCSkkKLS0tIHZYOU5DbHhiQ3lhOHZBaHgrTGdi\naW5ZTUFtZlkxQmVsZFBRd0pBcUpZc0kKB+dJmF88tlmFDOdG9BJCSwGFBG69Ja4b\nGV0bqJpiojcxYiscuvY1o98YlbhXqdIC5mDzLJqWARhsRORFCc17mg==\n-----END AGE ENCRYPTED FILE-----\n" - } - ], - "lastmodified": "2026-04-02T09:25:58Z", - "mac": "ENC[AES256_GCM,data:zgmV6N1vUf0P6nRB3sGobaM6u81yg5HrQHLsIYZj/+4gTZWznuodX34r4uZ4csdOpmJ1MvvUie399CJrnESw5b2eaNvXBeSinBRlgKxamvPkj881VeW+vuFXb8w5E5gmaOs/Vi81BhaWC7Ifw+YbpfzvZIMGrs2UqtQGhRckZPE=,iv:y1JlvYj+eZo5vhgpADJk48aUAik5CRKlfmZKaDZts8w=,tag:8C29f8g5n4xUNKM6MpcUsw==,type:str]", - "version": "3.12.1" - } -} diff --git a/vars/per-machine/ulmo/state-version/version/value b/vars/per-machine/ulmo/state-version/version/value deleted file mode 100644 index abbaa1c..0000000 --- a/vars/per-machine/ulmo/state-version/version/value +++ /dev/null @@ -1 +0,0 @@ -23.11 \ No newline at end of file