Compare commits

..

No commits in common. "64bc77a73e95b74e5915021e3c2ee581652be3b8" and "59e8ca812c06bb0dae11636bb12602e69660b46e" have entirely different histories.

13 changed files with 188 additions and 612 deletions

View file

@ -14,47 +14,7 @@ in {
type = types.submoduleWith { type = types.submoduleWith {
modules = [../types/endpoint.nix]; modules = [../types/endpoint.nix];
}; };
default = {}; default = name;
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 { # protocol = mkOption {

View file

@ -2,18 +2,17 @@
inherit (lib) mkOption types; inherit (lib) mkOption types;
in { in {
options = { options = {
protocol = mkOption {
type = types.str;
default = "http";
};
host = mkOption { host = mkOption {
type = types.str; type = types.str;
default = "localhost"; default = "localhost";
}; };
port = mkOption { port = mkOption {
type = types.nullOr types.port; type = types.port;
};
protocol = mkOption {
type = types.nullOr types.str;
default = null; default = null;
}; };

View file

@ -49,12 +49,14 @@ in {
|> lib.concatLists |> lib.concatLists
|> lib.map ({ |> lib.map ({
name, name,
endpoint, protocol,
host,
port,
}: { }: {
name = "${name}.${machine.name}.arda"; name = "${name}.${machine.name}.arda";
value = { value = {
extraConfig = '' extraConfig = ''
reverse_proxy ${toString endpoint} reverse_proxy ${protocol}://${host}:${toString port}
''; '';
}; };
}) })

View file

@ -4,15 +4,14 @@
exports, exports,
... ...
}: let }: let
inherit (builtins) toString readFile; inherit (builtins) toString;
inherit (lib) mkMerge mkIf;
in { in {
_class = "clan.service"; _class = "clan.service";
manifest = { manifest = {
name = "arda/identity"; name = "arda/identity";
description = '' description = ''
''; '';
readme = readFile ./README.md; readme = builtins.readFile ./README.md;
exports = { exports = {
inputs = ["persistence"]; inputs = ["persistence"];
out = ["gateway" "persistence"]; out = ["gateway" "persistence"];
@ -32,7 +31,7 @@ in {
}; };
database = mkOption { database = mkOption {
type = types.anything; type = types.anything; #ardaLib.types.endpoint;
}; };
port = mkOption { port = mkOption {
@ -333,15 +332,22 @@ in {
mkExports, mkExports,
settings, settings,
machine, machine,
instanceName,
... ...
}: { }: let
exports = mkExports (mkMerge [ database =
{ exports
gateway.services.identity = {endpoint.port = settings.port;}; |> clanLib.getExport {
serviceName = "arda/persistence";
roleName = "default";
machineName = machine.name;
instanceName = settings.persistence_instance;
} }
(mkIf (settings.driver == "zitadel") { |> (v: v.persistence.driver.postgresql);
gateway.functions.auth = { in {
exports = mkExports {
gateway = {
services.identity = {endpoint.port = settings.port;};
functions.auth = {
body = '' body = ''
forward_auth h2c://[::1]:${toString settings.port} { forward_auth h2c://[::1]:${toString settings.port} {
uri /api/authz/forward-auth uri /api/authz/forward-auth
@ -349,26 +355,21 @@ in {
} }
''; '';
}; };
};
persistence.databases = ["zitadel"]; persistence.databases = ["zitadel"];
}) };
]);
nixosModule = args@{ nixosModule = {
lib, lib,
pkgs, pkgs,
config, config,
... ...
}: let }: let
vars = config.clan.core.vars.generators.zitadel.files; inherit (lib) mkMerge mkIf;
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 { in {
config = mkMerge [ config = mkMerge [
(mkIf (settings.driver == "zitadel") ({ (lib.mkIf (settings.driver == "zitadel") {
clan.core.vars.generators.zitadel = { clan.core.vars.generators.zitadel = {
dependencies = ["persistence"]; dependencies = ["persistence"];
@ -386,29 +387,12 @@ in {
group = "zitadel"; group = "zitadel";
restartUnits = ["zitadel.service"]; 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 = '' script = ''
pwgen -s 32 1 > $out/masterKey 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 cat << EOL > $out/settings
Database: Database:
postgres: 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; [ environment.systemPackages = with pkgs; [
zitadel zitadel
]; ];
services.zitadel = { services.zitadel = {
enable = true; enable = true;
masterKeyFile = vars.masterKey.path; masterKeyFile = config.clan.core.vars.generators.zitadel.files.masterKey.path;
tlsMode = "external"; tlsMode = "external";
extraSettingsPaths = [ extraSettingsPaths = [
vars.settings.path config.clan.core.vars.generators.zitadel.files.settings.path
]; ];
settings = { settings = {
@ -491,7 +437,7 @@ in {
Database.postgres = { Database.postgres = {
Host = settings.database.host; Host = settings.database.host;
Port = settings.database.port; Port = settings.database.port;
Database = "zitadel"; Databae = "zitadel";
User = { User = {
Username = "zitadel"; Username = "zitadel";
}; };
@ -499,18 +445,15 @@ in {
Username = "zitadel"; Username = "zitadel";
}; };
}; };
};
SystemAPIUsers = { steps = {
infra = { InstanceName = "eu";
Path = vars.infraPublicKey.path;
Memberships = [ MachineKeyPath = "/var/lib/zitadel/machine-key.json";
{ MemberType = "System"; Roles = [ "SYSTEM_OWNER" "IAM_OWNER" "ORG_OWNER" ]; }
];
};
};
}; };
}; };
} // (zLib.createInfra { inherit users email_password; key_file = vars.infraPrivateKey.path; }))) })
]; ];
}; };
}; };

View file

@ -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";
};
};
};
}

View file

@ -92,7 +92,17 @@ in {
services = settings.services |> lib.attrNames; services = settings.services |> lib.attrNames;
service_count = services |> lib.length; 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 { in {
imports = [ imports = [
(import ./sabnzbd.nix (args (import ./sabnzbd.nix (args

View file

@ -4,6 +4,7 @@
lib, lib,
pkgs, pkgs,
settings, settings,
database,
... ...
}: let }: let
inherit (lib) mkIf; inherit (lib) mkIf;
@ -67,8 +68,8 @@
# Password provided via environment file # Password provided via environment file
postgres = { postgres = {
host = settings.database.host; host = database.host;
port = toString settings.database.port; port = toString database.port;
user = service; user = service;
maindb = service; maindb = service;
logdb = service; logdb = service;
@ -99,7 +100,7 @@
wants = ["${service}.service"]; wants = ["${service}.service"];
preStart = '' 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 options.rootFolders
|> lib.map (folder: "install -d -m 0770 -o media -g media ${folder}") |> 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;}); clan.core.vars.generators.${service} = createGenerator (args // {inherit service options;});
services.${service} = createService (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; |> lib.mkMerge;
}; };

27
lib/default.nix Normal file
View file

@ -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;
};
}

View file

@ -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 {});
};
}

35
lib/options/default.nix Normal file
View file

@ -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 {});
};
};
}

View file

@ -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}";
}

55
lib/strings/default.nix Normal file
View file

@ -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}";
};
}

View file

@ -355,7 +355,8 @@ in
for item in ${src} : for item in ${src} :
"''${item.org}_''${item.name}" => item "''${item.org}_''${item.name}" => item
}''; }'';
} // set; }
// set;
in in
{ {
terraform.required_providers.zitadel = { terraform.required_providers.zitadel = {
@ -565,16 +566,17 @@ in
"d /var/lib/zitadel/clients 0755 zitadel zitadel -" "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"; description = "Zitadel terraform apply";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
wants = [ "zitadel.service" ]; wants = [ "zitadel.service" ];
script = '' script =
let
tofu = lib.getExe pkgs.opentofu;
in
lib.replaceStrings ["\r"] [""] ''
if [ "$(systemctl is-active zitadel)" != "active" ]; then if [ "$(systemctl is-active zitadel)" != "active" ]; then
echo "Zitadel is not running" echo "Zitadel is not running"
exit 1 exit 1