Skip to main content

pg_embedded_setup_unpriv/extensions/
mod.rs

1//! Prebuilt extension installation for embedded `PostgreSQL` trees.
2//!
3//! The Theseus binaries carry only the in-tree contrib extensions. This module
4//! installs additional, prebuilt extensions (for example `pgvector`) from a
5//! digest-pinned manifest before the server starts. It never compiles
6//! anything: when no matching, verified archive exists it fails closed.
7//!
8//! Consumers declare extensions through the environment:
9//!
10//! | Variable | Meaning |
11//! | --- | --- |
12//! | `PG_EXTENSIONS` | Comma-separated `CREATE EXTENSION` names |
13//! | `PG_EXTENSIONS_MANIFEST` | `https://` URL or filesystem path of `manifest.json` |
14//! | `PG_EXTENSIONS_MANIFEST_SHA256` | Hex SHA-256 of the manifest (required for HTTPS) |
15//! | `PG_EXTENSIONS_CACHE_DIR` | Where verified archives are kept between runs |
16//!
17//! [`TestCluster`](crate::TestCluster), [`bootstrap_for_tests`](crate::bootstrap_for_tests)
18//! and the `pg_embedded_setup_unpriv` CLI honour them automatically; consumers
19//! that manage their own `Settings` can call [`install_extensions`] directly.
20//!
21//! # Examples
22//!
23//! ```no_run
24//! use camino::Utf8Path;
25//! use pg_embedded_setup_unpriv::{
26//!     PgEnvCfg,
27//!     extensions::{ExtensionRequest, install_extensions},
28//! };
29//!
30//! # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
31//! let cfg = PgEnvCfg::load()?;
32//! if let Some(request) = ExtensionRequest::from_config(&cfg)? {
33//!     let installed = install_extensions(&request, Utf8Path::new("/var/tmp/pg/install/17.11.0"))?;
34//!     for extension in installed {
35//!         println!(
36//!             "{} {} ({} files)",
37//!             extension.name,
38//!             extension.version,
39//!             extension.files.len()
40//!         );
41//!     }
42//! }
43//! # Ok(())
44//! # }
45//! ```
46
47mod archive;
48mod config;
49mod digest;
50mod http;
51mod install;
52mod layout;
53mod manifest;
54mod name;
55mod tree;
56mod version;
57mod write;
58
59#[cfg(test)]
60mod tests;
61
62use camino::{Utf8Path, Utf8PathBuf};
63use color_eyre::eyre::Report;
64use tracing::info;
65
66pub use self::{
67    config::resolve_extension_cache_dir,
68    digest::{InvalidDigest, Sha256Hex},
69    http::{is_permitted_url, redact_url},
70    layout::{ALLOWED_PREFIXES, classify_entry_path},
71    manifest::{
72        ArtifactQuery,
73        Manifest,
74        ManifestArtifact,
75        ManifestExtension,
76        ManifestSource,
77        ManifestSourceInfo,
78        SUPPORTED_SCHEMA_VERSION,
79        Selection,
80    },
81    name::ExtensionName,
82    version::{parse_pg_config_version, running_version},
83};
84use crate::error::{BootstrapError, BootstrapErrorKind, BootstrapResult};
85
86/// Observability target for extension installation events.
87pub(crate) const LOG_TARGET: &str = "pg_embed::extensions";
88
89/// A validated declaration of which extensions to install and from where.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct ExtensionRequest {
92    /// Extension names in declaration order, deduplicated.
93    pub names: Vec<ExtensionName>,
94    /// Manifest location and pin.
95    pub manifest: ManifestSource,
96    /// Directory holding verified archives between runs.
97    pub cache_dir: Utf8PathBuf,
98}
99
100/// Whether an archive came from the local cache or was downloaded.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum ArchiveOrigin {
103    /// A verified copy already existed under the cache directory.
104    Cached,
105    /// The archive was downloaded and verified during this install.
106    Downloaded,
107}
108
109/// What the hook did for one extension.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct InstalledExtension {
112    /// The `CREATE EXTENSION` name.
113    pub name: ExtensionName,
114    /// Extension version from the manifest, for example `0.8.6`.
115    pub version: String,
116    /// Theseus release the archive was built against, for example `17.11.0`.
117    pub postgresql: String,
118    /// Target triple of the archive, for example `x86_64-unknown-linux-gnu`.
119    pub target: String,
120    /// Digest of the archive that was installed.
121    pub archive_sha256: Sha256Hex,
122    /// Whether the archive came from the cache or was downloaded.
123    pub origin: ArchiveOrigin,
124    /// Installed files relative to the install root, sorted.
125    pub files: Vec<Utf8PathBuf>,
126}
127
128/// The compile target of this crate, matching Theseus asset names.
129///
130/// Baked in by `build.rs` as `PG_EMBED_TARGET`, so it names the target this
131/// binary was built for rather than the host it runs on. It is one of the
132/// three artefact match keys, beside the `PostgreSQL` major and minor.
133///
134/// # Examples
135///
136/// ```
137/// use pg_embedded_setup_unpriv::extensions::compile_target;
138///
139/// let target = compile_target();
140/// assert!(!target.is_empty(), "the target triple is always known");
141/// assert_eq!(target.split('-').count() >= 3, true, "{target} is a triple");
142/// ```
143#[must_use]
144pub const fn compile_target() -> &'static str { env!("PG_EMBED_TARGET") }
145
146/// Builds a categorized extension error.
147pub(crate) const fn extension_error(kind: BootstrapErrorKind, report: Report) -> BootstrapError {
148    BootstrapError::new(kind, report)
149}
150
151/// Installs the requested extensions into the embedded tree at `install_dir`.
152///
153/// `install_dir` is the versioned `PostgreSQL` root (the directory holding
154/// `bin/`, `lib/` and `share/`). The manifest is fetched and verified, one
155/// artefact is selected per name for the running `PostgreSQL` major *and*
156/// minor and the compile target, each archive is verified against the manifest
157/// digest, and its files are validated in full before any file is written.
158/// Re-installing an archive that is already in place is a reporting no-op.
159///
160/// Both version components are match keys. There is no cross-minor fallback:
161/// the manifest pins one digest per name, version and target, so a
162/// neighbouring minor would install bytes the pinned digest does not describe
163/// for the running server.
164///
165/// Every requested name is resolved against the manifest before any archive
166/// is acquired or written, so an unknown name or an unmatched version fails
167/// with nothing on disk; a failure while acquiring or writing a later archive
168/// leaves the earlier archives installed. The error identifies the failing
169/// archive and, for a partial write, the files written for it; it does not
170/// list the earlier archives.
171///
172/// The function is synchronous and blocks the calling thread for the whole
173/// install. When called from inside a Tokio runtime it moves the work to a
174/// scoped helper thread so the blocking HTTP client never runs on an
175/// executor thread, but the calling task still waits; on a current-thread
176/// runtime that parks the only worker, so async code should prefer
177/// [`install_extensions_async`], which uses `spawn_blocking`.
178///
179/// # Errors
180///
181/// Fails closed with one of the `BootstrapErrorKind::Extension*` kinds; see
182/// the crate documentation for the full table. Nothing is compiled and the
183/// server is not started when an error is returned.
184///
185/// # Examples
186///
187/// ```no_run
188/// use camino::{Utf8Path, Utf8PathBuf};
189/// use pg_embedded_setup_unpriv::extensions::{
190///     ExtensionName,
191///     ExtensionRequest,
192///     ManifestSource,
193///     install_extensions,
194/// };
195///
196/// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
197/// let request = ExtensionRequest {
198///     names: vec![ExtensionName::new("vector")?],
199///     manifest: ManifestSource::Path {
200///         path: Utf8PathBuf::from("/srv/extensions/manifest.json"),
201///         sha256: None,
202///     },
203///     cache_dir: Utf8PathBuf::from("/var/cache/pg-embedded/extensions"),
204/// };
205/// let installed = install_extensions(&request, Utf8Path::new("/var/tmp/pg/install/17.11.0"))?;
206/// assert_eq!(installed.len(), 1);
207/// # Ok(())
208/// # }
209/// ```
210pub fn install_extensions(
211    request: &ExtensionRequest,
212    install_dir: &Utf8Path,
213) -> BootstrapResult<Vec<InstalledExtension>> {
214    if tokio::runtime::Handle::try_current().is_ok() {
215        std::thread::scope(|scope| {
216            scope
217                .spawn(|| install_extensions_blocking(request, install_dir))
218                .join()
219                .unwrap_or_else(|payload| Err(install_thread_panic(payload)))
220        })
221    } else {
222        install_extensions_blocking(request, install_dir)
223    }
224}
225
226/// Async form of [`install_extensions`] that runs the work on the Tokio
227/// blocking pool.
228///
229/// The work is the same blocking install; only the caller's context differs.
230/// It takes owned arguments because the closure outlives the call.
231///
232/// # Errors
233///
234/// Returns the same errors as [`install_extensions`], plus
235/// `ExtensionInstallFailed` when the blocking task cannot be joined.
236///
237/// # Examples
238///
239/// ```no_run
240/// use camino::Utf8PathBuf;
241/// use pg_embedded_setup_unpriv::{
242///     PgEnvCfg,
243///     extensions::{ExtensionRequest, install_extensions_async},
244/// };
245///
246/// # async fn run() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
247/// let cfg = PgEnvCfg::load()?;
248/// if let Some(request) = ExtensionRequest::from_config(&cfg)? {
249///     let installed =
250///         install_extensions_async(request, Utf8PathBuf::from("/var/tmp/pg/install/17.11.0"))
251///             .await?;
252///     println!("installed {} extensions", installed.len());
253/// }
254/// # Ok(())
255/// # }
256/// ```
257pub async fn install_extensions_async(
258    request: ExtensionRequest,
259    install_dir: Utf8PathBuf,
260) -> BootstrapResult<Vec<InstalledExtension>> {
261    tokio::task::spawn_blocking(move || install_extensions_blocking(&request, &install_dir))
262        .await
263        .unwrap_or_else(|err| {
264            Err(extension_error(
265                BootstrapErrorKind::ExtensionInstallFailed,
266                Report::new(err).wrap_err("extension install task failed"),
267            ))
268        })
269}
270
271/// Turns a panic in the blocking install thread into an
272/// `ExtensionInstallFailed` error rather than letting it cross the join.
273fn install_thread_panic(payload: Box<dyn std::any::Any + Send>) -> BootstrapError {
274    let message = crate::cluster::panic_utils::panic_payload_to_string(payload);
275    extension_error(
276        BootstrapErrorKind::ExtensionInstallFailed,
277        Report::msg(format!("extension install thread panicked: {message}")),
278    )
279}
280
281/// Runs the install pipeline on the current thread.
282fn install_extensions_blocking(
283    request: &ExtensionRequest,
284    install_dir: &Utf8Path,
285) -> BootstrapResult<Vec<InstalledExtension>> {
286    let span = tracing::info_span!(
287        target: LOG_TARGET,
288        "install_extensions",
289        install_dir = %install_dir,
290        names = request.names.len()
291    );
292    let _entered = span.enter();
293    let manifest = manifest::load(&request.manifest)?;
294    let running = running_version(install_dir)?;
295    let selections = select_all(request, &manifest, &running)?;
296    request
297        .names
298        .iter()
299        .zip(selections)
300        .map(|(name, selection)| install_one(request, install_dir, name, selection))
301        .collect()
302}
303
304/// Resolves every requested name before anything is acquired or written.
305fn select_all<'a>(
306    request: &ExtensionRequest,
307    manifest: &'a Manifest,
308    running: &postgresql_embedded::Version,
309) -> BootstrapResult<Vec<manifest::Selection<'a>>> {
310    request
311        .names
312        .iter()
313        .map(|name| {
314            let query = ArtifactQuery {
315                name,
316                running,
317                target: compile_target(),
318            };
319            manifest.select(query, &request.manifest)
320        })
321        .collect()
322}
323
324/// Acquires and installs one already-selected extension.
325fn install_one(
326    request: &ExtensionRequest,
327    install_dir: &Utf8Path,
328    name: &ExtensionName,
329    selection: manifest::Selection<'_>,
330) -> BootstrapResult<InstalledExtension> {
331    tracing::debug!(
332        target: LOG_TARGET,
333        name = %name,
334        version = %selection.extension.version,
335        postgresql = %selection.artifact.postgresql,
336        target = %selection.artifact.target,
337        file = %selection.artifact.file,
338        "selected extension artefact"
339    );
340    let acquired = archive::acquire(&request.cache_dir, selection.artifact)?;
341    let files = install::install_archive(&acquired.path, selection.artifact, install_dir)?;
342    let installed = InstalledExtension {
343        name: name.clone(),
344        version: selection.extension.version.clone(),
345        postgresql: selection.artifact.postgresql.clone(),
346        target: selection.artifact.target.clone(),
347        archive_sha256: selection.artifact.sha256.clone(),
348        origin: acquired.origin,
349        files,
350    };
351    log_installed(&installed);
352    Ok(installed)
353}
354
355/// Records one installed extension: what it is, what it was built for, and
356/// whether it came from the cache or the network.
357fn log_installed(installed: &InstalledExtension) {
358    info!(
359        target: LOG_TARGET,
360        name = %installed.name,
361        version = %installed.version,
362        postgresql = %installed.postgresql,
363        target = %installed.target,
364        origin = ?installed.origin,
365        files = installed.files.len(),
366        "installed extension"
367    );
368}