1use crate::config::Config;
2use crate::{InstanceMap, InstanceName};
3
4#[derive(Debug, thiserror::Error)]
5pub enum Error {
6 #[error(transparent)]
7 Command(#[from] cmd_proc::CommandError),
8 #[error(transparent)]
9 Config(#[from] crate::config::Error),
10 #[error(transparent)]
11 Container(#[from] crate::container::Error),
12 #[error("Unknown instance: {0}")]
13 UnknownInstance(InstanceName),
14}
15
16#[derive(Clone, Debug, Default)]
17pub enum ConfigFileSource {
18 #[default]
19 Implicit,
20 Explicit(std::path::PathBuf),
21 None,
22}
23
24impl ConfigFileSource {
25 fn from_arguments(config_file: Option<std::path::PathBuf>, no_config_file: bool) -> Self {
26 match (config_file, no_config_file) {
27 (Some(path), false) => Self::Explicit(path),
28 (None, true) => Self::None,
29 (None, false) => Self::Implicit,
30 (Some(_), true) => unreachable!("clap conflicts_with prevents this"),
31 }
32 }
33}
34
35#[derive(Clone, Debug, clap::Parser)]
36#[command(after_help = "INSTANCE SELECTION:
37 All commands target the \"main\" instance by default.
38 Use --instance <NAME> to target a different instance.")]
39#[command(version = crate::VERSION_STR)]
40pub struct App {
41 #[arg(long, conflicts_with = "no_config_file")]
46 config_file: Option<std::path::PathBuf>,
47 #[arg(long, conflicts_with = "config_file")]
49 no_config_file: bool,
50 #[arg(long)]
56 backend: Option<ociman::backend::Selection>,
57 #[arg(long)]
59 image: Option<crate::image::Image>,
60 #[arg(long)]
62 ssl_hostname: Option<pg_client::HostName>,
63 #[clap(subcommand)]
64 command: Option<Command>,
65}
66
67impl App {
68 pub async fn run(&self) -> Result<(), Error> {
69 let overwrites = crate::config::InstanceDefinition {
70 backend: self.backend,
71 image: self.image.clone(),
72 seeds: indexmap::IndexMap::new(),
73 ssl_config: self
74 .ssl_hostname
75 .clone()
76 .map(|hostname| crate::config::SslConfigDefinition { hostname }),
77 wait_available_timeout: None,
78 };
79
80 let config_file_source =
81 ConfigFileSource::from_arguments(self.config_file.clone(), self.no_config_file);
82
83 let instance_map = match config_file_source {
84 ConfigFileSource::Explicit(config_file) => {
85 Config::load_toml_file(&config_file, &overwrites)?
86 }
87 ConfigFileSource::None => {
88 log::debug!("--no-config-file specified, using default instance map");
89 crate::Config::default().instance_map(&overwrites)?
90 }
91 ConfigFileSource::Implicit => {
92 log::debug!("No config file specified, trying to load from default location");
93
94 match Config::load_toml_file("database.toml", &overwrites) {
95 Ok(value) => value,
96 Err(crate::config::Error::IO(crate::config::IoError(
97 std::io::ErrorKind::NotFound,
98 ))) => {
99 log::debug!(
100 "Config file does not exist in default location, using default instance map"
101 );
102 crate::Config::default().instance_map(&overwrites)?
103 }
104 Err(error) => return Err(error.into()),
105 }
106 }
107 };
108
109 self.command
110 .clone()
111 .unwrap_or_default()
112 .run(&instance_map)
113 .await?;
114
115 Ok(())
116 }
117}
118
119#[derive(Clone, Debug, clap::Parser)]
120pub enum CacheCommand {
121 Status {
123 #[arg(long)]
125 json: bool,
126 },
127 Reset {
129 #[arg(long)]
131 force: bool,
132 },
133 Populate,
135}
136
137#[derive(Clone, Debug, clap::Parser)]
138pub enum Command {
139 Cache {
141 #[arg(long = "instance", default_value_t)]
143 instance_name: InstanceName,
144 #[clap(subcommand)]
145 command: CacheCommand,
146 },
147 #[command(name = "container-psql")]
149 ContainerPsql {
150 #[arg(long = "instance", default_value_t)]
152 instance_name: InstanceName,
153 },
154 List,
156 #[command(name = "container-schema-dump")]
158 ContainerSchemaDump {
159 #[arg(long = "instance", default_value_t)]
161 instance_name: InstanceName,
162 },
163 #[command(name = "container-shell")]
165 ContainerShell {
166 #[arg(long = "instance", default_value_t)]
168 instance_name: InstanceName,
169 },
170 #[command(name = "integration-server")]
178 IntegrationServer {
179 #[arg(long = "instance", default_value_t)]
181 instance_name: InstanceName,
182 #[arg(long)]
184 result_fd: std::os::fd::RawFd,
185 #[arg(long)]
187 control_fd: std::os::fd::RawFd,
188 },
189 Psql {
191 #[arg(long = "instance", default_value_t)]
193 instance_name: InstanceName,
194 },
195 RunEnv {
201 #[arg(long = "instance", default_value_t)]
203 instance_name: InstanceName,
204 command: String,
206 arguments: Vec<String>,
208 },
209 #[command(name = "platform")]
211 Platform {
212 #[clap(subcommand)]
213 command: PlatformCommand,
214 },
215}
216
217#[derive(Clone, Debug, clap::Parser)]
218pub enum PlatformCommand {
219 Support,
224 TestBacktrace,
229}
230
231impl PlatformCommand {
232 fn run(&self) {
233 match self {
234 Self::Support => match ociman::platform::support() {
235 Ok(()) => {
236 std::process::exit(0);
237 }
238 Err(error) => {
239 log::info!("pg-ephemeral is not supported on this platform: {error}");
240 std::process::exit(1);
241 }
242 },
243 Self::TestBacktrace => {
244 trigger_test_panic();
245 }
246 }
247 }
248}
249
250#[inline(never)]
251fn trigger_test_panic() {
252 inner_function_for_backtrace_test();
253}
254
255#[inline(never)]
256fn inner_function_for_backtrace_test() {
257 panic!("intentional panic for backtrace testing");
258}
259
260impl Default for Command {
261 fn default() -> Self {
262 Self::Psql {
263 instance_name: InstanceName::default(),
264 }
265 }
266}
267
268impl Command {
269 pub async fn run(&self, instance_map: &InstanceMap) -> Result<(), Error> {
270 match self {
271 Self::Cache {
272 instance_name,
273 command,
274 } => match command {
275 CacheCommand::Status { json } => {
276 let definition = Self::get_instance(instance_map, instance_name)?
277 .definition(instance_name)
278 .await
279 .unwrap();
280 definition.print_cache_status(instance_name, *json).await?
281 }
282 CacheCommand::Reset { force } => {
283 let definition = Self::get_instance(instance_map, instance_name)?
284 .definition(instance_name)
285 .await
286 .unwrap();
287 let name: ociman::reference::Name =
288 format!("pg-ephemeral/{instance_name}").parse().unwrap();
289 let references = definition.backend.image_references_by_name(&name).await;
290 for reference in &references {
291 if *force {
292 definition.backend.remove_image_force(reference).await;
293 } else {
294 definition.backend.remove_image(reference).await;
295 }
296 println!("Removed: {reference}");
297 }
298 }
299 CacheCommand::Populate => {
300 let definition = Self::get_instance(instance_map, instance_name)?
301 .definition(instance_name)
302 .await
303 .unwrap();
304 definition.populate_cache(instance_name).await?;
305 definition.print_cache_status(instance_name, false).await?;
306 }
307 },
308 Self::ContainerPsql { instance_name } => {
309 let definition = Self::get_instance(instance_map, instance_name)?
310 .definition(instance_name)
311 .await
312 .unwrap();
313 definition.with_container(container_psql).await?
314 }
315 Self::ContainerSchemaDump { instance_name } => {
316 let definition = Self::get_instance(instance_map, instance_name)?
317 .definition(instance_name)
318 .await
319 .unwrap();
320 definition.with_container(container_schema_dump).await?
321 }
322 Self::ContainerShell { instance_name } => {
323 let definition = Self::get_instance(instance_map, instance_name)?
324 .definition(instance_name)
325 .await
326 .unwrap();
327 definition.with_container(container_shell).await?
328 }
329 Self::IntegrationServer {
330 instance_name,
331 result_fd,
332 control_fd,
333 } => {
334 let definition = Self::get_instance(instance_map, instance_name)?
335 .definition(instance_name)
336 .await
337 .unwrap();
338 definition
339 .run_integration_server(*result_fd, *control_fd)
340 .await?
341 }
342 Self::List => {
343 for instance_name in instance_map.keys() {
344 println!("{instance_name}")
345 }
346 }
347 Self::Psql { instance_name } => {
348 let definition = Self::get_instance(instance_map, instance_name)?
349 .definition(instance_name)
350 .await
351 .unwrap();
352 definition.with_container(host_psql).await??
353 }
354 Self::RunEnv {
355 instance_name,
356 command,
357 arguments,
358 } => {
359 let definition = Self::get_instance(instance_map, instance_name)?
360 .definition(instance_name)
361 .await
362 .unwrap();
363 definition
364 .with_container(async |container| {
365 host_command(container, command, arguments).await
366 })
367 .await??
368 }
369 Self::Platform { command } => command.run(),
370 }
371
372 Ok(())
373 }
374
375 fn get_instance<'a>(
376 instance_map: &'a InstanceMap,
377 instance_name: &InstanceName,
378 ) -> Result<&'a crate::config::Instance, Error> {
379 instance_map
380 .get(instance_name)
381 .ok_or_else(|| Error::UnknownInstance(instance_name.clone()))
382 }
383}
384
385async fn host_psql(container: &crate::container::Container) -> Result<(), cmd_proc::CommandError> {
386 cmd_proc::Command::new("psql")
387 .envs(container.pg_env())
388 .status()
389 .await
390}
391
392async fn host_command(
393 container: &crate::container::Container,
394 command: &str,
395 arguments: &Vec<String>,
396) -> Result<(), cmd_proc::CommandError> {
397 cmd_proc::Command::new(command)
398 .arguments(arguments)
399 .envs(container.pg_env())
400 .env(&crate::ENV_DATABASE_URL, container.database_url())
401 .status()
402 .await
403}
404
405async fn container_psql(container: &crate::container::Container) {
406 container.exec_psql().await
407}
408
409async fn container_schema_dump(container: &crate::container::Container) {
410 let pg_schema_dump = pg_client::PgSchemaDump::new();
411 println!("{}", container.exec_schema_dump(&pg_schema_dump).await);
412}
413
414async fn container_shell(container: &crate::container::Container) {
415 container.exec_container_shell().await
416}