Line data Source code
1 : // Download extension files from the extension store
2 : // and put them in the right place in the postgres directory (share / lib)
3 : /*
4 : The layout of the S3 bucket is as follows:
5 : 5615610098 // this is an extension build number
6 : ├── v14
7 : │ ├── extensions
8 : │ │ ├── anon.tar.zst
9 : │ │ └── embedding.tar.zst
10 : │ └── ext_index.json
11 : └── v15
12 : ├── extensions
13 : │ ├── anon.tar.zst
14 : │ └── embedding.tar.zst
15 : └── ext_index.json
16 : 5615261079
17 : ├── v14
18 : │ ├── extensions
19 : │ │ └── anon.tar.zst
20 : │ └── ext_index.json
21 : └── v15
22 : ├── extensions
23 : │ └── anon.tar.zst
24 : └── ext_index.json
25 : 5623261088
26 : ├── v14
27 : │ ├── extensions
28 : │ │ └── embedding.tar.zst
29 : │ └── ext_index.json
30 : └── v15
31 : ├── extensions
32 : │ └── embedding.tar.zst
33 : └── ext_index.json
34 :
35 : Note that build number cannot be part of prefix because we might need extensions
36 : from other build numbers.
37 :
38 : ext_index.json stores the control files and location of extension archives
39 : It also stores a list of public extensions and a library_index
40 :
41 : We don't need to duplicate extension.tar.zst files.
42 : We only need to upload a new one if it is updated.
43 : (Although currently we just upload every time anyways, hopefully will change
44 : this sometime)
45 :
46 : *access* is controlled by spec
47 :
48 : More specifically, here is an example ext_index.json
49 : {
50 : "public_extensions": [
51 : "anon",
52 : "pg_buffercache"
53 : ],
54 : "library_index": {
55 : "anon": "anon",
56 : "pg_buffercache": "pg_buffercache"
57 : },
58 : "extension_data": {
59 : "pg_buffercache": {
60 : "control_data": {
61 : "pg_buffercache.control": "# pg_buffercache extension \ncomment = 'examine the shared buffer cache' \ndefault_version = '1.3' \nmodule_pathname = '$libdir/pg_buffercache' \nrelocatable = true \ntrusted=true"
62 : },
63 : "archive_path": "5670669815/v14/extensions/pg_buffercache.tar.zst"
64 : },
65 : "anon": {
66 : "control_data": {
67 : "anon.control": "# PostgreSQL Anonymizer (anon) extension \ncomment = 'Data anonymization tools' \ndefault_version = '1.1.0' \ndirectory='extension/anon' \nrelocatable = false \nrequires = 'pgcrypto' \nsuperuser = false \nmodule_pathname = '$libdir/anon' \ntrusted = true \n"
68 : },
69 : "archive_path": "5670669815/v14/extensions/anon.tar.zst"
70 : }
71 : }
72 : }
73 : */
74 : use std::path::Path;
75 : use std::str;
76 :
77 : use anyhow::{Context, Result, bail};
78 : use bytes::Bytes;
79 : use compute_api::spec::RemoteExtSpec;
80 : use regex::Regex;
81 : use remote_storage::*;
82 : use reqwest::StatusCode;
83 : use tar::Archive;
84 : use tracing::info;
85 : use tracing::log::warn;
86 : use zstd::stream::read::Decoder;
87 :
88 : use crate::metrics::{REMOTE_EXT_REQUESTS_TOTAL, UNKNOWN_HTTP_STATUS};
89 :
90 0 : fn get_pg_config(argument: &str, pgbin: &str) -> String {
91 0 : // gives the result of `pg_config [argument]`
92 0 : // where argument is a flag like `--version` or `--sharedir`
93 0 : let pgconfig = pgbin
94 0 : .strip_suffix("postgres")
95 0 : .expect("bad pgbin")
96 0 : .to_owned()
97 0 : + "/pg_config";
98 0 : let config_output = std::process::Command::new(pgconfig)
99 0 : .arg(argument)
100 0 : .output()
101 0 : .expect("pg_config error");
102 0 : std::str::from_utf8(&config_output.stdout)
103 0 : .expect("pg_config error")
104 0 : .trim()
105 0 : .to_string()
106 0 : }
107 :
108 0 : pub fn get_pg_version(pgbin: &str) -> PostgresMajorVersion {
109 0 : // pg_config --version returns a (platform specific) human readable string
110 0 : // such as "PostgreSQL 15.4". We parse this to v14/v15/v16 etc.
111 0 : let human_version = get_pg_config("--version", pgbin);
112 0 : parse_pg_version(&human_version)
113 0 : }
114 :
115 0 : pub fn get_pg_version_string(pgbin: &str) -> String {
116 0 : match get_pg_version(pgbin) {
117 0 : PostgresMajorVersion::V14 => "v14",
118 0 : PostgresMajorVersion::V15 => "v15",
119 0 : PostgresMajorVersion::V16 => "v16",
120 0 : PostgresMajorVersion::V17 => "v17",
121 : }
122 0 : .to_owned()
123 0 : }
124 :
125 : #[derive(Copy, Clone, Debug, PartialEq, Eq)]
126 : pub enum PostgresMajorVersion {
127 : V14,
128 : V15,
129 : V16,
130 : V17,
131 : }
132 :
133 12 : fn parse_pg_version(human_version: &str) -> PostgresMajorVersion {
134 : use PostgresMajorVersion::*;
135 : // Normal releases have version strings like "PostgreSQL 15.4". But there
136 : // are also pre-release versions like "PostgreSQL 17devel" or "PostgreSQL
137 : // 16beta2" or "PostgreSQL 17rc1". And with the --with-extra-version
138 : // configure option, you can tack any string to the version number,
139 : // e.g. "PostgreSQL 15.4foobar".
140 12 : match Regex::new(r"^PostgreSQL (?<major>\d+).+")
141 12 : .unwrap()
142 12 : .captures(human_version)
143 : {
144 12 : Some(captures) if captures.len() == 2 => match &captures["major"] {
145 12 : "14" => return V14,
146 9 : "15" => return V15,
147 6 : "16" => return V16,
148 2 : "17" => return V17,
149 2 : _ => {}
150 : },
151 0 : _ => {}
152 : }
153 2 : panic!("Unsuported postgres version {human_version}");
154 10 : }
155 :
156 : // download the archive for a given extension,
157 : // unzip it, and place files in the appropriate locations (share/lib)
158 0 : pub async fn download_extension(
159 0 : ext_name: &str,
160 0 : ext_path: &RemotePath,
161 0 : ext_remote_storage: &str,
162 0 : pgbin: &str,
163 0 : ) -> Result<u64> {
164 0 : info!("Download extension {:?} from {:?}", ext_name, ext_path);
165 :
166 : // TODO add retry logic
167 0 : let download_buffer =
168 0 : match download_extension_tar(ext_remote_storage, &ext_path.to_string()).await {
169 0 : Ok(buffer) => buffer,
170 0 : Err(error_message) => {
171 0 : return Err(anyhow::anyhow!(
172 0 : "error downloading extension {:?}: {:?}",
173 0 : ext_name,
174 0 : error_message
175 0 : ));
176 : }
177 : };
178 :
179 0 : let download_size = download_buffer.len() as u64;
180 0 : info!("Download size {:?}", download_size);
181 : // it's unclear whether it is more performant to decompress into memory or not
182 : // TODO: decompressing into memory can be avoided
183 0 : let decoder = Decoder::new(download_buffer.as_ref())?;
184 0 : let mut archive = Archive::new(decoder);
185 0 :
186 0 : let unzip_dest = pgbin
187 0 : .strip_suffix("/bin/postgres")
188 0 : .expect("bad pgbin")
189 0 : .to_string()
190 0 : + "/download_extensions";
191 0 : archive.unpack(&unzip_dest)?;
192 0 : info!("Download + unzip {:?} completed successfully", &ext_path);
193 :
194 0 : let sharedir_paths = (
195 0 : unzip_dest.to_string() + "/share/extension",
196 0 : Path::new(&get_pg_config("--sharedir", pgbin)).join("extension"),
197 0 : );
198 0 : let libdir_paths = (
199 0 : unzip_dest.to_string() + "/lib",
200 0 : Path::new(&get_pg_config("--pkglibdir", pgbin)).to_path_buf(),
201 0 : );
202 : // move contents of the libdir / sharedir in unzipped archive to the correct local paths
203 0 : for paths in [sharedir_paths, libdir_paths] {
204 0 : let (zip_dir, real_dir) = paths;
205 :
206 0 : let dir = match std::fs::read_dir(&zip_dir) {
207 0 : Ok(dir) => dir,
208 0 : Err(e) => match e.kind() {
209 : // In the event of a SQL-only extension, there would be nothing
210 : // to move from the lib/ directory, so note that in the log and
211 : // move on.
212 : std::io::ErrorKind::NotFound => {
213 0 : info!("nothing to move from {}", zip_dir);
214 0 : continue;
215 : }
216 0 : _ => return Err(anyhow::anyhow!(e)),
217 : },
218 : };
219 :
220 0 : info!("mv {zip_dir:?}/* {real_dir:?}");
221 :
222 0 : for file in dir {
223 0 : let old_file = file?.path();
224 0 : let new_file =
225 0 : Path::new(&real_dir).join(old_file.file_name().context("error parsing file")?);
226 0 : info!("moving {old_file:?} to {new_file:?}");
227 :
228 : // extension download failed: Directory not empty (os error 39)
229 0 : match std::fs::rename(old_file, new_file) {
230 0 : Ok(()) => info!("move succeeded"),
231 0 : Err(e) => {
232 0 : warn!("move failed, probably because the extension already exists: {e}")
233 : }
234 : }
235 : }
236 : }
237 0 : info!("done moving extension {ext_name}");
238 0 : Ok(download_size)
239 0 : }
240 :
241 : // Create extension control files from spec
242 0 : pub fn create_control_files(remote_extensions: &RemoteExtSpec, pgbin: &str) {
243 0 : let local_sharedir = Path::new(&get_pg_config("--sharedir", pgbin)).join("extension");
244 0 : for (ext_name, ext_data) in remote_extensions.extension_data.iter() {
245 : // Check if extension is present in public or custom.
246 : // If not, then it is not allowed to be used by this compute.
247 0 : if let Some(public_extensions) = &remote_extensions.public_extensions {
248 0 : if !public_extensions.contains(ext_name) {
249 0 : if let Some(custom_extensions) = &remote_extensions.custom_extensions {
250 0 : if !custom_extensions.contains(ext_name) {
251 0 : continue; // skip this extension, it is not allowed
252 0 : }
253 0 : }
254 0 : }
255 0 : }
256 :
257 0 : for (control_name, control_content) in &ext_data.control_data {
258 0 : let control_path = local_sharedir.join(control_name);
259 0 : if !control_path.exists() {
260 0 : info!("writing file {:?}{:?}", control_path, control_content);
261 0 : std::fs::write(control_path, control_content).unwrap();
262 : } else {
263 0 : warn!(
264 0 : "control file {:?} exists both locally and remotely. ignoring the remote version.",
265 : control_path
266 : );
267 : }
268 : }
269 : }
270 0 : }
271 :
272 : // Do request to extension storage proxy, e.g.,
273 : // curl http://pg-ext-s3-gateway/latest/v15/extensions/anon.tar.zst
274 : // using HTTP GET and return the response body as bytes.
275 0 : async fn download_extension_tar(ext_remote_storage: &str, ext_path: &str) -> Result<Bytes> {
276 0 : let uri = format!("{}/{}", ext_remote_storage, ext_path);
277 0 : let filename = Path::new(ext_path)
278 0 : .file_name()
279 0 : .unwrap_or_else(|| std::ffi::OsStr::new("unknown"))
280 0 : .to_str()
281 0 : .unwrap_or("unknown")
282 0 : .to_string();
283 0 :
284 0 : info!("Downloading extension file '{}' from uri {}", filename, uri);
285 :
286 0 : match do_extension_server_request(&uri).await {
287 0 : Ok(resp) => {
288 0 : info!("Successfully downloaded remote extension data {}", ext_path);
289 0 : REMOTE_EXT_REQUESTS_TOTAL
290 0 : .with_label_values(&[&StatusCode::OK.to_string(), &filename])
291 0 : .inc();
292 0 : Ok(resp)
293 : }
294 0 : Err((msg, status)) => {
295 0 : REMOTE_EXT_REQUESTS_TOTAL
296 0 : .with_label_values(&[&status, &filename])
297 0 : .inc();
298 0 : bail!(msg);
299 : }
300 : }
301 0 : }
302 :
303 : // Do a single remote extensions server request.
304 : // Return result or (error message + stringified status code) in case of any failures.
305 0 : async fn do_extension_server_request(uri: &str) -> Result<Bytes, (String, String)> {
306 0 : let resp = reqwest::get(uri).await.map_err(|e| {
307 0 : (
308 0 : format!(
309 0 : "could not perform remote extensions server request: {:?}",
310 0 : e
311 0 : ),
312 0 : UNKNOWN_HTTP_STATUS.to_string(),
313 0 : )
314 0 : })?;
315 0 : let status = resp.status();
316 0 :
317 0 : match status {
318 0 : StatusCode::OK => match resp.bytes().await {
319 0 : Ok(resp) => Ok(resp),
320 0 : Err(e) => Err((
321 0 : format!("could not read remote extensions server response: {:?}", e),
322 0 : // It's fine to return and report error with status as 200 OK,
323 0 : // because we still failed to read the response.
324 0 : status.to_string(),
325 0 : )),
326 : },
327 0 : StatusCode::SERVICE_UNAVAILABLE => Err((
328 0 : "remote extensions server is temporarily unavailable".to_string(),
329 0 : status.to_string(),
330 0 : )),
331 0 : _ => Err((
332 0 : format!(
333 0 : "unexpected remote extensions server response status code: {}",
334 0 : status
335 0 : ),
336 0 : status.to_string(),
337 0 : )),
338 : }
339 0 : }
340 :
341 : #[cfg(test)]
342 : mod tests {
343 : use super::parse_pg_version;
344 :
345 : #[test]
346 1 : fn test_parse_pg_version() {
347 : use super::PostgresMajorVersion::*;
348 1 : assert_eq!(parse_pg_version("PostgreSQL 15.4"), V15);
349 1 : assert_eq!(parse_pg_version("PostgreSQL 15.14"), V15);
350 1 : assert_eq!(
351 1 : parse_pg_version("PostgreSQL 15.4 (Ubuntu 15.4-0ubuntu0.23.04.1)"),
352 1 : V15
353 1 : );
354 :
355 1 : assert_eq!(parse_pg_version("PostgreSQL 14.15"), V14);
356 1 : assert_eq!(parse_pg_version("PostgreSQL 14.0"), V14);
357 1 : assert_eq!(
358 1 : parse_pg_version("PostgreSQL 14.9 (Debian 14.9-1.pgdg120+1"),
359 1 : V14
360 1 : );
361 :
362 1 : assert_eq!(parse_pg_version("PostgreSQL 16devel"), V16);
363 1 : assert_eq!(parse_pg_version("PostgreSQL 16beta1"), V16);
364 1 : assert_eq!(parse_pg_version("PostgreSQL 16rc2"), V16);
365 1 : assert_eq!(parse_pg_version("PostgreSQL 16extra"), V16);
366 1 : }
367 :
368 : #[test]
369 : #[should_panic]
370 1 : fn test_parse_pg_unsupported_version() {
371 1 : parse_pg_version("PostgreSQL 13.14");
372 1 : }
373 :
374 : #[test]
375 : #[should_panic]
376 1 : fn test_parse_pg_incorrect_version_format() {
377 1 : parse_pg_version("PostgreSQL 14");
378 1 : }
379 : }
|