testcontainers_modules/zitadel/
mod.rs

1use std::{borrow::Cow, collections::HashMap};
2
3use testcontainers::{
4    core::{wait::HttpWaitStrategy, ContainerPort, WaitFor},
5    Image,
6};
7
8const NAME: &str = "ghcr.io/zitadel/zitadel";
9const TAG: &str = "v3.0.0-rc.2";
10const DEFAULT_MASTER_KEY: &str = "MasterkeyNeedsToHave32Characters";
11
12/// Port that the [`Zitadel`] container has internally
13/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
14///
15/// [`Zitadel`]: https://zitadel.com/
16pub const ZITADEL_PORT: ContainerPort = ContainerPort::Tcp(8080);
17
18// Zitadel testcontainer module
19//
20// This module provides a [Zitadel] container that can be used for testing purposes.
21// It uses the official Zitadel Docker image and configures it with a default master key
22// and in-memory database for quick testing.
23//
24// # Example
25// ```rust,no_run
26// use testcontainers_modules::zitadel::Zitadel;
27//
28// #[tokio::main]
29// async fn main() {
30//     let zitadel = Zitadel::default().start().await?;
31//     let host_port = zitadel_node.get_host_port_ipv4(zitadel::ZITADEL_PORT).await?;
32//     println!("Zitadel is running on port: {}", host_port);
33// }
34// ```
35
36/// Configuration for the Zitadel container.
37#[derive(Debug, Clone, Default)]
38pub struct Zitadel {
39    env_vars: HashMap<String, String>,
40}
41
42impl Zitadel {
43    // Helper function to convert bool to "true" or "false" string
44    fn bool_to_string(value: bool) -> String {
45        (if value { "true" } else { "false" }).to_owned()
46    }
47
48    // https://zitadel.com/docs/self-hosting/manage/configure
49
50    /// Configures external secure for the [`Zitadel`] instance.
51    /// ExternalSecure specifies if ZITADEL is exposed externally using HTTPS or HTTP.
52    /// Read more about external access: https://zitadel.com/docs/self-hosting/manage/custom-domain
53    /// Default is `true` if not overridden by this function
54    ///
55    /// See the [official docs for this option](https://zitadel.com/docs/self-hosting/manage/configure)
56    pub fn with_external_secure(mut self, external_secure: bool) -> Self {
57        self.env_vars.insert(
58            "ZITADEL_EXTERNALSECURE".to_owned(),
59            Self::bool_to_string(external_secure),
60        );
61        self
62    }
63
64    /// Sets the Postgres database for the Zitadel instance.
65    pub fn with_postgres_database(
66        mut self,
67        host: Option<String>,
68        port: Option<u16>,
69        database: Option<String>,
70    ) -> Self {
71        match host {
72            Some(host) => self
73                .env_vars
74                .insert("ZITADEL_DATABASE_POSTGRES_HOST".to_owned(), host.to_owned()),
75            None => self.env_vars.remove("ZITADEL_DATABASE_POSTGRES_HOST"),
76        };
77        match port {
78            Some(port) => self.env_vars.insert(
79                "ZITADEL_DATABASE_POSTGRES_PORT".to_owned(),
80                port.to_string(),
81            ),
82            None => self.env_vars.remove("ZITADEL_DATABASE_POSTGRES_PORT"),
83        };
84        match database {
85            Some(database) => self.env_vars.insert(
86                "ZITADEL_DATABASE_POSTGRES_DATABASE".to_owned(),
87                database.to_owned(),
88            ),
89            None => self.env_vars.remove("ZITADEL_DATABASE_POSTGRES_DATABASE"),
90        };
91        self
92    }
93
94    /// Sets the Postgres database user for the Zitadel instance.
95    pub fn with_postgres_database_user(
96        mut self,
97        username: Option<String>,
98        password: Option<String>,
99        ssl_mode: Option<String>,
100    ) -> Self {
101        match username {
102            Some(username) => self.env_vars.insert(
103                "ZITADEL_DATABASE_POSTGRES_USER_USERNAME".to_owned(),
104                username.to_owned(),
105            ),
106            None => self
107                .env_vars
108                .remove("ZITADEL_DATABASE_POSTGRES_USER_USERNAME"),
109        };
110        match password {
111            Some(password) => self.env_vars.insert(
112                "ZITADEL_DATABASE_POSTGRES_USER_PASSWORD".to_owned(),
113                password.to_owned(),
114            ),
115            None => self
116                .env_vars
117                .remove("ZITADEL_DATABASE_POSTGRES_USER_PASSWORD"),
118        };
119        match ssl_mode {
120            Some(ssl_mode) => self.env_vars.insert(
121                "ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE".to_owned(),
122                ssl_mode.to_owned(),
123            ),
124            None => self
125                .env_vars
126                .remove("ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE"),
127        };
128        self
129    }
130
131    /// Sets the Postgres database admin for the Zitadel instance.
132    pub fn with_postgres_database_admin(
133        mut self,
134        username: Option<String>,
135        password: Option<String>,
136        ssl_mode: Option<String>,
137    ) -> Self {
138        match username {
139            Some(username) => self.env_vars.insert(
140                "ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME".to_owned(),
141                username.to_owned(),
142            ),
143            None => self
144                .env_vars
145                .remove("ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME"),
146        };
147        match password {
148            Some(password) => self.env_vars.insert(
149                "ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD".to_owned(),
150                password.to_owned(),
151            ),
152            None => self
153                .env_vars
154                .remove("ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD"),
155        };
156        match ssl_mode {
157            Some(ssl_mode) => self.env_vars.insert(
158                "ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE".to_owned(),
159                ssl_mode.to_owned(),
160            ),
161            None => self
162                .env_vars
163                .remove("ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE"),
164        };
165        self
166    }
167}
168
169impl Image for Zitadel {
170    fn name(&self) -> &str {
171        NAME
172    }
173
174    fn tag(&self) -> &str {
175        TAG
176    }
177
178    fn ready_conditions(&self) -> Vec<WaitFor> {
179        vec![
180            WaitFor::message_on_stderr("server is listening on"),
181            WaitFor::http(
182                HttpWaitStrategy::new("/debug/healthz")
183                    .with_port(ZITADEL_PORT)
184                    .with_expected_status_code(200_u16)
185                    .with_body("ok".as_bytes()),
186            ),
187        ]
188    }
189
190    fn env_vars(
191        &self,
192    ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
193        &self.env_vars
194    }
195
196    fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> {
197        [
198            "start-from-init",
199            "--masterkey",
200            DEFAULT_MASTER_KEY,
201            "--tlsMode",
202            "disabled",
203        ]
204    }
205
206    fn expose_ports(&self) -> &[ContainerPort] {
207        &[ZITADEL_PORT]
208    }
209}