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}