1use std::{
11 env,
12 fs::OpenOptions,
13 io::Write,
14 path::{Path, PathBuf},
15 process::Command,
16};
17
18use mz_cloud_api::client::region::RegionInfo;
19use mz_frontegg_auth::AppPassword;
20use url::Url;
21
22use crate::error::Error;
23
24const PG_APPLICATION_NAME: &str = "mz_psql";
27
28const PG_PSQLRC_MZ_FILENAME: &str = ".psqlrc-mz";
30
31const PG_PSQLRC_MZ_DEFAULT_CONTENT: &str = "\\timing\n\\include ~/.psqlrc";
35
36pub struct ClientConfig {
38 pub app_password: AppPassword,
41}
42
43pub struct Client {
44 pub(crate) app_password: AppPassword,
45}
46
47impl Client {
48 pub fn new(config: ClientConfig) -> Client {
50 Client {
51 app_password: config.app_password,
52 }
53 }
54
55 fn build_psql_url(&self, region_info: &RegionInfo, user: &str, cluster: Option<String>) -> Url {
57 let mut url = Url::parse(&format!("postgres://{}", region_info.sql_address))
58 .expect("url known to be valid");
59 url.set_username(user).unwrap();
60 url.set_path("materialize");
61
62 if let Some(cert_file) = openssl_probe::probe().cert_file {
63 url.query_pairs_mut()
64 .append_pair("sslmode", "verify-full")
65 .append_pair("sslrootcert", &cert_file.to_string_lossy());
66 } else {
67 url.query_pairs_mut().append_pair("sslmode", "require");
68 }
69
70 if let Some(cluster) = cluster {
71 url.query_pairs_mut()
72 .append_pair("options", &format!("--cluster={}", cluster));
73 }
74
75 url
76 }
77
78 fn create_file_with_content_if_not_exists(
81 &self,
82 path: &PathBuf,
83 content: Option<&[u8]>,
84 ) -> Result<(), Error> {
85 match OpenOptions::new().write(true).create_new(true).open(path) {
88 Ok(mut file) => {
89 if let Some(content) = content {
90 let _ = file.write_all(content);
91 }
92 }
93 Err(e) => {
94 if e.kind() == std::io::ErrorKind::AlreadyExists {
95 } else {
97 return Err(Error::IOError(e));
98 }
99 }
100 }
101
102 Ok(())
103 }
104
105 pub fn configure_psqlrc(&self) -> Result<(), Error> {
109 let Some(mut path) = dirs::home_dir() else {
111 return Err(Error::HomeDirNotFoundError);
112 };
113 path.push(PG_PSQLRC_MZ_FILENAME);
114
115 let _ = self.create_file_with_content_if_not_exists(
116 &path,
117 Some(PG_PSQLRC_MZ_DEFAULT_CONTENT.as_bytes()),
118 );
119
120 path.pop();
124 path.push(".psqlrc");
125 let _ = self.create_file_with_content_if_not_exists(&path, None);
126
127 Ok(())
128 }
129
130 pub fn shell(&self, region_info: &RegionInfo, user: &str, cluster: Option<String>) -> Command {
132 let _ = self.configure_psqlrc();
136
137 let mut command = Command::new("psql");
138 command
139 .arg(self.build_psql_url(region_info, user, cluster).as_str())
140 .env("PGPASSWORD", &self.app_password.to_string())
141 .env("PGAPPNAME", PG_APPLICATION_NAME)
142 .env("PSQLRC", "~/.psqlrc-mz");
143
144 command
145 }
146
147 fn find<P>(&self, exe_name: P) -> Option<PathBuf>
148 where
149 P: AsRef<Path>,
150 {
151 env::var_os("PATH").and_then(|paths| {
152 env::split_paths(&paths)
153 .filter_map(|dir| {
154 let full_path = dir.join(&exe_name);
155 if full_path.is_file() {
156 Some(full_path)
157 } else {
158 None
159 }
160 })
161 .next()
162 })
163 }
164
165 pub fn is_ready(&self, region_info: &RegionInfo, user: &str) -> Result<bool, Error> {
167 if self.find("pg_isready").is_some() {
168 let mut command = Command::new("pg_isready");
169 Ok(command
170 .args(vec![
171 "-q",
172 "-d",
173 self.build_psql_url(region_info, user, None).as_str(),
174 ])
175 .env("PGPASSWORD", &self.app_password.to_string())
176 .env("PGAPPNAME", PG_APPLICATION_NAME)
177 .output()?
178 .status
179 .success())
180 } else {
181 panic!("the pg_isready program is not present. Make sure it is available in the $PATH.")
182 }
183 }
184}