postgresfixture/cluster.rs
1//! Create, start, introspect, stop, and destroy PostgreSQL clusters.
2
3mod error;
4
5#[cfg(test)]
6mod tests;
7
8use std::ffi::{OsStr, OsString};
9use std::os::unix::prelude::OsStringExt;
10use std::path::{Path, PathBuf};
11use std::process::{Command, ExitStatus};
12use std::{env, fs, io};
13
14use nix::errno::Errno;
15use shell_quote::sh::escape_into;
16
17use crate::runtime;
18use crate::version;
19pub use error::ClusterError;
20
21/// Representation of a PostgreSQL cluster.
22///
23/// The cluster may not yet exist on disk. It may exist but be stopped, or it
24/// may be running. The methods here can be used to create, start, introspect,
25/// stop, and destroy the cluster. There's no protection against concurrent
26/// changes to the cluster made by other processes, but the functions in the
27/// [`coordinate`][`crate::coordinate`] module may help.
28pub struct Cluster {
29 /// The data directory of the cluster.
30 ///
31 /// Corresponds to the `PGDATA` environment variable.
32 datadir: PathBuf,
33 /// How to select the PostgreSQL installation to use with this cluster.
34 strategy: Box<dyn runtime::strategy::RuntimeStrategy>,
35}
36
37impl Cluster {
38 /// Represent a cluster at the given path.
39 pub fn new<P: AsRef<Path>, S: runtime::strategy::RuntimeStrategy>(
40 datadir: P,
41 strategy: S,
42 ) -> Result<Self, ClusterError> {
43 Ok(Self {
44 datadir: datadir.as_ref().to_owned(),
45 strategy: Box::new(strategy),
46 })
47 }
48
49 /// Determine the runtime to use with this cluster.
50 fn runtime(&self) -> Result<runtime::Runtime, ClusterError> {
51 match version(self)? {
52 None => self
53 .strategy
54 .fallback()
55 .ok_or_else(|| ClusterError::RuntimeDefaultNotFound),
56 Some(version) => self
57 .strategy
58 .select(&version)
59 .ok_or_else(|| ClusterError::RuntimeNotFound(version)),
60 }
61 }
62
63 fn ctl(&self) -> Result<Command, ClusterError> {
64 let mut command = self.runtime()?.execute("pg_ctl");
65 command.env("PGDATA", &self.datadir);
66 command.env("PGHOST", &self.datadir);
67 Ok(command)
68 }
69
70 /// Check if this cluster is running.
71 ///
72 /// Tries to distinguish carefully between "definitely running", "definitely
73 /// not running", and "don't know". The latter results in `ClusterError`.
74 pub fn running(&self) -> Result<bool, ClusterError> {
75 let output = self.ctl()?.arg("status").output()?;
76 let code = match output.status.code() {
77 // Killed by signal; return early.
78 None => return Err(ClusterError::Other(output)),
79 // Success; return early (the server is running).
80 Some(0) => return Ok(true),
81 // More work required to decode what this means.
82 Some(code) => code,
83 };
84 let runtime = self.runtime()?;
85 // PostgreSQL has evolved to return different error codes in
86 // later versions, so here we check for specific codes to avoid
87 // masking errors from insufficient permissions or missing
88 // executables, for example.
89 let running = match runtime.version {
90 // PostgreSQL 10.x and later.
91 version::Version::Post10(_major, _minor) => {
92 // PostgreSQL 10
93 // https://www.postgresql.org/docs/10/static/app-pg-ctl.html
94 match code {
95 // 3 means that the data directory is present and
96 // accessible but that the server is not running.
97 3 => Some(false),
98 // 4 means that the data directory is not present or is
99 // not accessible. If it's missing, then the server is
100 // not running. If it is present but not accessible
101 // then crash because we can't know if the server is
102 // running or not.
103 4 if !exists(self) => Some(false),
104 // For anything else we don't know.
105 _ => None,
106 }
107 }
108 // PostgreSQL 9.x only.
109 version::Version::Pre10(9, point, _minor) => {
110 // PostgreSQL 9.4+
111 // https://www.postgresql.org/docs/9.4/static/app-pg-ctl.html
112 // https://www.postgresql.org/docs/9.5/static/app-pg-ctl.html
113 // https://www.postgresql.org/docs/9.6/static/app-pg-ctl.html
114 if point >= 4 {
115 match code {
116 // 3 means that the data directory is present and
117 // accessible but that the server is not running.
118 3 => Some(false),
119 // 4 means that the data directory is not present or is
120 // not accessible. If it's missing, then the server is
121 // not running. If it is present but not accessible
122 // then crash because we can't know if the server is
123 // running or not.
124 4 if !exists(self) => Some(false),
125 // For anything else we don't know.
126 _ => None,
127 }
128 }
129 // PostgreSQL 9.2+
130 // https://www.postgresql.org/docs/9.2/static/app-pg-ctl.html
131 // https://www.postgresql.org/docs/9.3/static/app-pg-ctl.html
132 else if point >= 2 {
133 match code {
134 // 3 means that the data directory is present and
135 // accessible but that the server is not running OR
136 // that the data directory is not present.
137 3 => Some(false),
138 // For anything else we don't know.
139 _ => None,
140 }
141 }
142 // PostgreSQL 9.0+
143 // https://www.postgresql.org/docs/9.0/static/app-pg-ctl.html
144 // https://www.postgresql.org/docs/9.1/static/app-pg-ctl.html
145 else {
146 match code {
147 // 1 means that the server is not running OR the data
148 // directory is not present OR that the data directory
149 // is not accessible.
150 1 => Some(false),
151 // For anything else we don't know.
152 _ => None,
153 }
154 }
155 }
156 // All other versions.
157 version::Version::Pre10(_major, _point, _minor) => None,
158 };
159
160 match running {
161 Some(running) => Ok(running),
162 // TODO: Perhaps include the exit code from `pg_ctl status` in the
163 // error message, and whatever it printed out.
164 None => Err(ClusterError::UnsupportedVersion(runtime.version)),
165 }
166 }
167
168 /// Return the path to the PID file used in this cluster.
169 ///
170 /// The PID file does not necessarily exist.
171 pub fn pidfile(&self) -> PathBuf {
172 self.datadir.join("postmaster.pid")
173 }
174
175 /// Return the path to the log file used in this cluster.
176 ///
177 /// The log file does not necessarily exist.
178 pub fn logfile(&self) -> PathBuf {
179 self.datadir.join("postmaster.log")
180 }
181
182 /// Create the cluster if it does not already exist.
183 pub fn create(&self) -> Result<bool, ClusterError> {
184 match self._create() {
185 Err(ClusterError::UnixError(Errno::EAGAIN)) if exists(self) => Ok(false),
186 Err(ClusterError::UnixError(Errno::EAGAIN)) => Err(ClusterError::InUse),
187 other => other,
188 }
189 }
190
191 fn _create(&self) -> Result<bool, ClusterError> {
192 if exists(self) {
193 // Nothing more to do; the cluster is already in place.
194 Ok(false)
195 } else {
196 // Create the cluster and report back that we did so.
197 fs::create_dir_all(&self.datadir)?;
198 #[allow(clippy::suspicious_command_arg_space)]
199 self.ctl()?
200 .arg("init")
201 .arg("-s")
202 .arg("-o")
203 // Passing multiple flags in a single `arg(...)` is
204 // intentional. These constitute the single value for the
205 // `-o` flag above.
206 .arg("-E utf8 --locale C -A trust")
207 .env("TZ", "UTC")
208 .output()?;
209 Ok(true)
210 }
211 }
212
213 // Start the cluster if it's not already running.
214 pub fn start(&self) -> Result<bool, ClusterError> {
215 match self._start() {
216 Err(ClusterError::UnixError(Errno::EAGAIN)) if self.running()? => Ok(false),
217 Err(ClusterError::UnixError(Errno::EAGAIN)) => Err(ClusterError::InUse),
218 other => other,
219 }
220 }
221
222 fn _start(&self) -> Result<bool, ClusterError> {
223 // Ensure that the cluster has been created.
224 self._create()?;
225 // Check if we're running already.
226 if self.running()? {
227 // We didn't start this cluster; say so.
228 return Ok(false);
229 }
230 // Next, invoke `pg_ctl` to start the cluster.
231 // pg_ctl options:
232 // -l <file> -- log file.
233 // -s -- no informational messages.
234 // -w -- wait until startup is complete.
235 // postgres options:
236 // -h <arg> -- host name; empty arg means Unix socket only.
237 // -k -- socket directory.
238 self.ctl()?
239 .arg("start")
240 .arg("-l")
241 .arg(self.logfile())
242 .arg("-s")
243 .arg("-w")
244 .arg("-o")
245 .arg({
246 let mut arg = b"-h '' -k "[..].into();
247 escape_into(&self.datadir, &mut arg);
248 OsString::from_vec(arg)
249 })
250 .output()?;
251 // We did actually start the cluster; say so.
252 Ok(true)
253 }
254
255 // Connect to this cluster.
256 pub fn connect(&self, database: &str) -> Result<postgres::Client, ClusterError> {
257 let user = &env::var("USER").unwrap_or_else(|_| "USER-not-set".to_string());
258 let host = self.datadir.to_string_lossy(); // postgres crate API limitation.
259 let client = postgres::Client::configure()
260 .user(user)
261 .dbname(database)
262 .host(&host)
263 .connect(postgres::NoTls)?;
264 Ok(client)
265 }
266
267 pub fn shell(&self, database: &str) -> Result<ExitStatus, ClusterError> {
268 let mut command = self.runtime()?.execute("psql");
269 command.arg("--quiet");
270 command.env("PGDATA", &self.datadir);
271 command.env("PGHOST", &self.datadir);
272 command.env("PGDATABASE", database);
273 Ok(command.spawn()?.wait()?)
274 }
275
276 pub fn exec<T: AsRef<OsStr>>(
277 &self,
278 database: &str,
279 command: T,
280 args: &[T],
281 ) -> Result<ExitStatus, ClusterError> {
282 let mut command = self.runtime()?.command(command);
283 command.args(args);
284 command.env("PGDATA", &self.datadir);
285 command.env("PGHOST", &self.datadir);
286 command.env("PGDATABASE", database);
287 Ok(command.spawn()?.wait()?)
288 }
289
290 // The names of databases in this cluster.
291 pub fn databases(&self) -> Result<Vec<String>, ClusterError> {
292 let mut conn = self.connect("template1")?;
293 let rows = conn.query(
294 "SELECT datname FROM pg_catalog.pg_database ORDER BY datname",
295 &[],
296 )?;
297 let datnames: Vec<String> = rows.iter().map(|row| row.get(0)).collect();
298 Ok(datnames)
299 }
300
301 /// Create the named database.
302 pub fn createdb(&self, database: &str) -> Result<bool, ClusterError> {
303 let statement = format!(
304 "CREATE DATABASE {}",
305 postgres_protocol::escape::escape_identifier(database)
306 );
307 self.connect("template1")?
308 .execute(statement.as_str(), &[])?;
309 Ok(true)
310 }
311
312 /// Drop the named database.
313 pub fn dropdb(&self, database: &str) -> Result<bool, ClusterError> {
314 let statement = format!(
315 "DROP DATABASE {}",
316 postgres_protocol::escape::escape_identifier(database)
317 );
318 self.connect("template1")?
319 .execute(statement.as_str(), &[])?;
320 Ok(true)
321 }
322
323 // Stop the cluster if it's running.
324 pub fn stop(&self) -> Result<bool, ClusterError> {
325 match self._stop() {
326 Err(ClusterError::UnixError(Errno::EAGAIN)) if !self.running()? => Ok(false),
327 Err(ClusterError::UnixError(Errno::EAGAIN)) => Err(ClusterError::InUse),
328 other => other,
329 }
330 }
331
332 fn _stop(&self) -> Result<bool, ClusterError> {
333 // If the cluster's not already running, don't do anything.
334 if !self.running()? {
335 return Ok(false);
336 }
337 // pg_ctl options:
338 // -w -- wait for shutdown to complete.
339 // -m <mode> -- shutdown mode.
340 self.ctl()?
341 .arg("stop")
342 .arg("-s")
343 .arg("-w")
344 .arg("-m")
345 .arg("fast")
346 .output()?;
347 Ok(true)
348 }
349
350 // Destroy the cluster if it exists, after stopping it.
351 pub fn destroy(&self) -> Result<bool, ClusterError> {
352 match self._destroy() {
353 Err(ClusterError::UnixError(Errno::EAGAIN)) => Err(ClusterError::InUse),
354 other => other,
355 }
356 }
357
358 fn _destroy(&self) -> Result<bool, ClusterError> {
359 if self._stop()? || self.datadir.is_dir() {
360 fs::remove_dir_all(&self.datadir)?;
361 Ok(true)
362 } else {
363 Ok(false)
364 }
365 }
366}
367
368impl AsRef<Path> for Cluster {
369 fn as_ref(&self) -> &Path {
370 &self.datadir
371 }
372}
373
374/// A fairly simplistic but quick check: does the directory exist and does it
375/// look like a PostgreSQL cluster data directory, i.e. does it contain a file
376/// named `PG_VERSION`?
377///
378/// [`version()`] provides a more reliable measure, plus yields the version of
379/// PostgreSQL required to use the cluster.
380pub fn exists<P: AsRef<Path>>(datadir: P) -> bool {
381 let datadir = datadir.as_ref();
382 datadir.is_dir() && datadir.join("PG_VERSION").is_file()
383}
384
385/// Yields the version of PostgreSQL required to use a cluster.
386///
387/// This returns the version from the file named `PG_VERSION` in the data
388/// directory if it exists, otherwise this returns `None`. For PostgreSQL
389/// versions before 10 this is typically (maybe always) the major and point
390/// version, e.g. 9.4 rather than 9.4.26. For version 10 and above it appears to
391/// be just the major number, e.g. 14 rather than 14.2.
392pub fn version<P: AsRef<Path>>(
393 datadir: P,
394) -> Result<Option<version::PartialVersion>, ClusterError> {
395 let version_file = datadir.as_ref().join("PG_VERSION");
396 match std::fs::read_to_string(version_file) {
397 Ok(version) => Ok(Some(version.parse()?)),
398 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
399 Err(err) => Err(err)?,
400 }
401}