diff --git a/clan/interfaces/gateway.nix b/clan/interfaces/gateway.nix index c8faf04..8353ae6 100644 --- a/clan/interfaces/gateway.nix +++ b/clan/interfaces/gateway.nix @@ -14,47 +14,7 @@ in { 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}"; - }; + default = name; }; # protocol = mkOption { diff --git a/clan/types/endpoint.nix b/clan/types/endpoint.nix index fab5a86..a3f82ae 100644 --- a/clan/types/endpoint.nix +++ b/clan/types/endpoint.nix @@ -2,18 +2,17 @@ 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; + type = types.port; + }; + + protocol = mkOption { + type = types.nullOr types.str; default = null; }; diff --git a/clanServices/gateway/default.nix b/clanServices/gateway/default.nix index 2c6a311..ce837fd 100644 --- a/clanServices/gateway/default.nix +++ b/clanServices/gateway/default.nix @@ -49,12 +49,14 @@ in { |> lib.concatLists |> lib.map ({ name, - endpoint, + protocol, + host, + port, }: { name = "${name}.${machine.name}.arda"; value = { extraConfig = '' - reverse_proxy ${toString endpoint} + reverse_proxy ${protocol}://${host}:${toString port} ''; }; }) diff --git a/clanServices/identity/default.nix b/clanServices/identity/default.nix index 1030ebb..caaf194 100644 --- a/clanServices/identity/default.nix +++ b/clanServices/identity/default.nix @@ -4,15 +4,14 @@ exports, ... }: let - inherit (builtins) toString readFile; - inherit (lib) mkMerge mkIf; + inherit (builtins) toString; in { _class = "clan.service"; manifest = { name = "arda/identity"; description = '' ''; - readme = readFile ./README.md; + readme = builtins.readFile ./README.md; exports = { inputs = ["persistence"]; out = ["gateway" "persistence"]; @@ -32,7 +31,7 @@ in { }; database = mkOption { - type = types.anything; + type = types.anything; #ardaLib.types.endpoint; }; port = mkOption { @@ -333,15 +332,22 @@ in { mkExports, settings, machine, - instanceName, ... - }: { - exports = mkExports (mkMerge [ - { - gateway.services.identity = {endpoint.port = settings.port;}; + }: let + database = + exports + |> clanLib.getExport { + serviceName = "arda/persistence"; + roleName = "default"; + machineName = machine.name; + instanceName = settings.persistence_instance; } - (mkIf (settings.driver == "zitadel") { - gateway.functions.auth = { + |> (v: v.persistence.driver.postgresql); + in { + exports = mkExports { + gateway = { + services.identity = {endpoint.port = settings.port;}; + functions.auth = { body = '' forward_auth h2c://[::1]:${toString settings.port} { uri /api/authz/forward-auth @@ -349,26 +355,21 @@ in { } ''; }; + }; - persistence.databases = ["zitadel"]; - }) - ]); + persistence.databases = ["zitadel"]; + }; - nixosModule = args@{ + nixosModule = { 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;}); + inherit (lib) mkMerge mkIf; in { config = mkMerge [ - (mkIf (settings.driver == "zitadel") ({ + (lib.mkIf (settings.driver == "zitadel") { clan.core.vars.generators.zitadel = { dependencies = ["persistence"]; @@ -386,29 +387,12 @@ in { 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]; + runtimeInputs = with pkgs; [pwgen]; 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: @@ -420,56 +404,18 @@ in { ''; }; - 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; + masterKeyFile = config.clan.core.vars.generators.zitadel.files.masterKey.path; tlsMode = "external"; extraSettingsPaths = [ - vars.settings.path + config.clan.core.vars.generators.zitadel.files.settings.path ]; settings = { @@ -491,7 +437,7 @@ in { Database.postgres = { Host = settings.database.host; Port = settings.database.port; - Database = "zitadel"; + Databae = "zitadel"; User = { Username = "zitadel"; }; @@ -499,18 +445,15 @@ in { Username = "zitadel"; }; }; + }; - SystemAPIUsers = { - infra = { - Path = vars.infraPublicKey.path; - Memberships = [ - { MemberType = "System"; Roles = [ "SYSTEM_OWNER" "IAM_OWNER" "ORG_OWNER" ]; } - ]; - }; - }; + steps = { + InstanceName = "eu"; + + MachineKeyPath = "/var/lib/zitadel/machine-key.json"; }; }; - } // (zLib.createInfra { inherit users email_password; key_file = vars.infraPrivateKey.path; }))) + }) ]; }; }; 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/servarr/default.nix b/clanServices/servarr/default.nix index c06a89b..e86bf2e 100644 --- a/clanServices/servarr/default.nix +++ b/clanServices/servarr/default.nix @@ -92,7 +92,17 @@ in { services = settings.services |> lib.attrNames; service_count = services |> lib.length; - servarr = import ./lib.nix (args // {inherit settings;}); + database = + exports + |> clanLib.getExport { + serviceName = "arda/persistence"; + roleName = "default"; + machineName = machine.name; + instanceName = settings.persistence_instance; + } + |> (v: v.persistence.driver.postgresql); + + servarr = import ./lib.nix (args // {inherit settings database;}); in { imports = [ (import ./sabnzbd.nix (args diff --git a/clanServices/servarr/lib.nix b/clanServices/servarr/lib.nix index 4a15ca7..5bcd6a5 100644 --- a/clanServices/servarr/lib.nix +++ b/clanServices/servarr/lib.nix @@ -4,6 +4,7 @@ lib, pkgs, settings, + database, ... }: let inherit (lib) mkIf; @@ -67,8 +68,8 @@ # Password provided via environment file postgres = { - host = settings.database.host; - port = toString settings.database.port; + host = database.host; + port = toString database.port; user = service; maindb = service; logdb = service; @@ -99,7 +100,7 @@ wants = ["${service}.service"]; preStart = '' - install -d -m 0770 -o ${service} -g media /var/lib/infra-${service} + install -d -m 0770 -o ${service} -g media /var/lib/${service}-apply-infra ${ options.rootFolders |> lib.map (folder: "install -d -m 0770 -o media -g media ${folder}") @@ -322,7 +323,11 @@ 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;})); + # services.caddy.virtualHosts."${service}.ulmo.arda".extraConfig = '' + # reverse_proxy http://[::1]:${toString options.port} + # ''; + + systemd.services."${service}-apply-infra" = lib.mkIf settings.enable (createSystemdService (args // {inherit service options;})); }) |> lib.mkMerge; }; diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..e8edaf1 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,27 @@ +{ + config, + inputs, + lib, + ... +}: let + inherit (lib) mkOption types; +in { + imports = [ + ./options + ./strings + ]; + + config = { + _module.args = { + inherit + baseNixosModules + channelConfig + mkPkgs + sharedContext + systemOverlays + ; + }; + + flake.lib = config.localLib; + }; +} 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..579b3de --- /dev/null +++ b/lib/options/default.nix @@ -0,0 +1,35 @@ +{lib, ...}: let + inherit (lib) mkOption types; +in { + localLib.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 {}); + }; + }; +} 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..7ae1d78 --- /dev/null +++ b/lib/strings/default.nix @@ -0,0 +1,55 @@ +{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}"; + }; +} diff --git a/modules/nixos/services/authentication/zitadel.nix b/modules/nixos/services/authentication/zitadel.nix index e120d32..bc83385 100644 --- a/modules/nixos/services/authentication/zitadel.nix +++ b/modules/nixos/services/authentication/zitadel.nix @@ -355,7 +355,8 @@ in for item in ${src} : "''${item.org}_''${item.name}" => item }''; - } // set; + } + // set; in { terraform.required_providers.zitadel = { @@ -565,16 +566,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 + lib.replaceStrings ["\r"] [""] '' if [ "$(systemctl is-active zitadel)" != "active" ]; then echo "Zitadel is not running" exit 1