omry_integration/
flora_image.rs1use std::borrow::Cow;
2use std::path::Path;
3
4use secrecy::{ExposeSecret, SecretBox};
5use testcontainers::core::{ContainerPort, ContainerRequest, Mount, WaitFor};
6use testcontainers::runners::AsyncRunner;
7use testcontainers::{ContainerAsync, Image, ImageExt};
8use tracing::Level;
9use url::Url;
10
11use crate::containers::{ContainerConfig, ContainerLogConsumer, ImageConfig};
12use crate::helpers::{self, TempPathWithString};
13
14const DATABASE_URL_KEY: &str = "FLORA_DATABASE_URL";
16const READY_MESSAGE_KEY: &str = "FLORA_READY_MESSAGE";
17const TRACING_LEVEL_KEY: &str = "FLORA_TRACING_LEVEL";
18const TYPESENSE_API_KEY_KEY: &str = "FLORA_TYPESENSE_API_KEY";
19const TYPESENSE_URL_KEY: &str = "FLORA_TYPESENSE_URL";
20
21const DATA_PATH_CONTAINER: &str = "/usr/local/etc/flora";
22
23const IMAGE_NAME: &str = "localhost:8777/omry-server";
25const IMAGE_TAG: &str = "latest";
26const CONTAINER_PORT: u16 = 37373;
27const READY_MESSAGE: &str = "flora is ready!";
28
29#[derive(Debug)]
32pub enum EnvVar {
33 Plain {
35 key: String,
37
38 value: String,
40 },
41
42 Secret {
44 key: String,
46
47 value: SecretBox<str>,
49 },
50}
51
52impl EnvVar {
53 fn as_key_value_pair(&self) -> (&str, &str) {
54 match self {
55 Self::Plain { key, value } => (key, value),
56 Self::Secret { key, value } => (key, value.expose_secret()),
57 }
58 }
59}
60
61#[derive(Debug)]
63pub struct FloraConfig {
64 database_url: EnvVar,
65 ready_message: EnvVar,
66 tracing_level: Option<EnvVar>,
67 typesense_api_key: EnvVar,
68 typesense_url: EnvVar,
69}
70
71impl FloraConfig {
72 #[must_use]
74 pub fn new(typesense_url: &Url, typesense_api_key: SecretBox<str>) -> Self {
75 let typesense_api_key = EnvVar::Secret {
76 key: TYPESENSE_API_KEY_KEY.to_string(),
77 value: typesense_api_key,
78 };
79 let typesense_url = EnvVar::Plain {
80 key: TYPESENSE_URL_KEY.to_string(),
81 value: typesense_url.to_string(),
82 };
83
84 let ready_message = EnvVar::Plain {
86 key: READY_MESSAGE_KEY.to_string(),
87 value: READY_MESSAGE.to_string(),
88 };
89
90 let db_path = Path::new(DATA_PATH_CONTAINER)
91 .join("flora.db")
92 .display()
93 .to_string();
94
95 let database_url = EnvVar::Plain {
97 key: DATABASE_URL_KEY.into(),
98 value: db_path,
99 };
100
101 Self {
102 typesense_api_key,
103 typesense_url,
104 ready_message,
105 database_url,
106 tracing_level: None,
107 }
108 }
109
110 #[must_use]
112 pub fn with_tracing_level(mut self, tracing_level: Level) -> Self {
113 let tracing_level = EnvVar::Plain {
114 key: TRACING_LEVEL_KEY.into(),
115 value: tracing_level.to_string(),
116 };
117 self.tracing_level = Some(tracing_level);
118 self
119 }
120
121 fn into_vec(self) -> Vec<EnvVar> {
122 let mut vars = vec![
123 self.database_url,
124 self.ready_message,
125 self.typesense_url,
126 self.typesense_api_key,
127 ];
128
129 if let Some(tracing_level) = self.tracing_level {
130 vars.push(tracing_level);
131 }
132
133 vars
134 }
135}
136
137#[derive(Debug)]
139pub struct FloraImage {
140 name: String,
141 tag: String,
142 env: Vec<EnvVar>,
143 mounts: Vec<Mount>,
144 container_ports: Vec<ContainerPort>,
145 ready_message: String,
146 host_port: Option<u16>,
147 #[allow(dead_code)]
150 data_temp_path: TempPathWithString,
151}
152
153impl FloraImage {
154 pub fn new(flora_config: FloraConfig, image_config: ImageConfig) -> anyhow::Result<Self> {
160 let data_temp_path = TempPathWithString::new()?;
161 let mount = Mount::bind_mount(data_temp_path.as_str(), DATA_PATH_CONTAINER);
162 let container_ports = if image_config.should_expose_ports {
163 vec![ContainerPort::Tcp(CONTAINER_PORT)]
164 } else {
165 vec![]
166 };
167
168 let (_, ready_message) = flora_config.ready_message.as_key_value_pair();
169 let ready_message = ready_message.into();
170
171 Ok(Self {
172 name: IMAGE_NAME.to_string(),
173 tag: IMAGE_TAG.to_string(),
174 env: flora_config.into_vec(),
175 data_temp_path,
176 mounts: vec![mount],
177 container_ports,
178 ready_message,
179 host_port: None,
180 })
181 }
182
183 fn with_auto_host_port(mut self) -> ContainerRequest<Self> {
184 let (_, host_port) = helpers::free_port_ipv6();
185 self.host_port = Some(host_port);
186 self.with_mapped_port(host_port, CONTAINER_PORT.into())
187 }
188
189 #[must_use]
191 pub fn host_port(&self) -> Option<u16> {
192 self.host_port
193 }
194}
195
196impl Image for FloraImage {
197 fn name(&self) -> &str {
198 &self.name
199 }
200
201 fn tag(&self) -> &str {
202 &self.tag
203 }
204
205 fn ready_conditions(&self) -> Vec<WaitFor> {
206 vec![WaitFor::message_on_stdout(&self.ready_message)]
207 }
208
209 fn env_vars(
210 &self,
211 ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
212 self.env.iter().map(EnvVar::as_key_value_pair)
213 }
214
215 fn mounts(&self) -> impl IntoIterator<Item = &Mount> {
216 &self.mounts
217 }
218
219 fn expose_ports(&self) -> &[ContainerPort] {
220 &self.container_ports
221 }
222}
223
224pub type FloraContainer = ContainerAsync<FloraImage>;
226
227pub async fn start_flora_container<L>(
232 flora_config: FloraConfig,
233 container_config: ContainerConfig<L>,
234) -> anyhow::Result<FloraContainer>
235where
236 L: ContainerLogConsumer,
237{
238 let flora_container =
239 FloraImage::new(flora_config, container_config.image_config)?.with_auto_host_port();
240 let flora_container = container_config.configure_container(flora_container);
241 Ok(flora_container.start().await?)
242}