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