Line data Source code
1 : //! `ComputeSpec` represents the contents of the spec.json file.
2 : //!
3 : //! The spec.json file is used to pass information to 'compute_ctl'. It contains
4 : //! all the information needed to start up the right version of PostgreSQL,
5 : //! and connect it to the storage nodes.
6 : use std::collections::HashMap;
7 :
8 : use regex::Regex;
9 : use remote_storage::RemotePath;
10 : use serde::{Deserialize, Serialize};
11 : use utils::id::{TenantId, TimelineId};
12 : use utils::lsn::Lsn;
13 :
14 : /// String type alias representing Postgres identifier and
15 : /// intended to be used for DB / role names.
16 : pub type PgIdent = String;
17 :
18 : /// String type alias representing Postgres extension version
19 : pub type ExtVersion = String;
20 :
21 6 : fn default_reconfigure_concurrency() -> usize {
22 6 : 1
23 6 : }
24 :
25 : /// Cluster spec or configuration represented as an optional number of
26 : /// delta operations + final cluster state description.
27 45 : #[derive(Clone, Debug, Default, Deserialize, Serialize)]
28 : pub struct ComputeSpec {
29 : pub format_version: f32,
30 :
31 : // The control plane also includes a 'timestamp' field in the JSON document,
32 : // but we don't use it for anything. Serde will ignore missing fields when
33 : // deserializing it.
34 : pub operation_uuid: Option<String>,
35 :
36 : /// Compute features to enable. These feature flags are provided, when we
37 : /// know all the details about client's compute, so they cannot be used
38 : /// to change `Empty` compute behavior.
39 : #[serde(default)]
40 : pub features: Vec<ComputeFeature>,
41 :
42 : /// If compute_ctl was passed `--resize-swap-on-bind`, a value of `Some(_)` instructs
43 : /// compute_ctl to `/neonvm/bin/resize-swap` with the given size, when the spec is first
44 : /// received.
45 : ///
46 : /// Both this field and `--resize-swap-on-bind` are required, so that the control plane's
47 : /// spec generation doesn't need to be aware of the actual compute it's running on, while
48 : /// guaranteeing gradual rollout of swap. Otherwise, without `--resize-swap-on-bind`, we could
49 : /// end up trying to resize swap in VMs without it -- or end up *not* resizing swap, thus
50 : /// giving every VM much more swap than it should have (32GiB).
51 : ///
52 : /// Eventually we may remove `--resize-swap-on-bind` and exclusively use `swap_size_bytes` for
53 : /// enabling the swap resizing behavior once rollout is complete.
54 : ///
55 : /// See neondatabase/cloud#12047 for more.
56 : #[serde(default)]
57 : pub swap_size_bytes: Option<u64>,
58 :
59 : /// If compute_ctl was passed `--set-disk-quota-for-fs`, a value of `Some(_)` instructs
60 : /// compute_ctl to run `/neonvm/bin/set-disk-quota` with the given size and fs, when the
61 : /// spec is first received.
62 : ///
63 : /// Both this field and `--set-disk-quota-for-fs` are required, so that the control plane's
64 : /// spec generation doesn't need to be aware of the actual compute it's running on, while
65 : /// guaranteeing gradual rollout of disk quota.
66 : #[serde(default)]
67 : pub disk_quota_bytes: Option<u64>,
68 :
69 : /// Disables the vm-monitor behavior that resizes LFC on upscale/downscale, instead relying on
70 : /// the initial size of LFC.
71 : ///
72 : /// This is intended for use when the LFC size is being overridden from the default but
73 : /// autoscaling is still enabled, and we don't want the vm-monitor to interfere with the custom
74 : /// LFC sizing.
75 : #[serde(default)]
76 : pub disable_lfc_resizing: Option<bool>,
77 :
78 : /// Expected cluster state at the end of transition process.
79 : pub cluster: Cluster,
80 : pub delta_operations: Option<Vec<DeltaOp>>,
81 :
82 : /// An optional hint that can be passed to speed up startup time if we know
83 : /// that no pg catalog mutations (like role creation, database creation,
84 : /// extension creation) need to be done on the actual database to start.
85 : #[serde(default)] // Default false
86 : pub skip_pg_catalog_updates: bool,
87 :
88 : // Information needed to connect to the storage layer.
89 : //
90 : // `tenant_id`, `timeline_id` and `pageserver_connstring` are always needed.
91 : //
92 : // Depending on `mode`, this can be a primary read-write node, a read-only
93 : // replica, or a read-only node pinned at an older LSN.
94 : // `safekeeper_connstrings` must be set for a primary.
95 : //
96 : // For backwards compatibility, the control plane may leave out all of
97 : // these, and instead set the "neon.tenant_id", "neon.timeline_id",
98 : // etc. GUCs in cluster.settings. TODO: Once the control plane has been
99 : // updated to fill these fields, we can make these non optional.
100 : pub tenant_id: Option<TenantId>,
101 : pub timeline_id: Option<TimelineId>,
102 : pub pageserver_connstring: Option<String>,
103 :
104 : /// Safekeeper membership config generation. It is put in
105 : /// neon.safekeepers GUC and serves two purposes:
106 : /// 1) Non zero value forces walproposer to use membership configurations.
107 : /// 2) If walproposer wants to update list of safekeepers to connect to
108 : /// taking them from some safekeeper mconf, it should check what value
109 : /// is newer by comparing the generation.
110 : ///
111 : /// Note: it could be SafekeeperGeneration, but this needs linking
112 : /// compute_ctl with postgres_ffi.
113 : #[serde(default)]
114 : pub safekeepers_generation: Option<u32>,
115 : #[serde(default)]
116 : pub safekeeper_connstrings: Vec<String>,
117 :
118 : #[serde(default)]
119 : pub mode: ComputeMode,
120 :
121 : /// If set, 'storage_auth_token' is used as the password to authenticate to
122 : /// the pageserver and safekeepers.
123 : pub storage_auth_token: Option<String>,
124 :
125 : // information about available remote extensions
126 : pub remote_extensions: Option<RemoteExtSpec>,
127 :
128 : pub pgbouncer_settings: Option<HashMap<String, String>>,
129 :
130 : // Stripe size for pageserver sharding, in pages
131 : #[serde(default)]
132 : pub shard_stripe_size: Option<usize>,
133 :
134 : /// Local Proxy configuration used for JWT authentication
135 : #[serde(default)]
136 : pub local_proxy_config: Option<LocalProxySpec>,
137 :
138 : /// Number of concurrent connections during the parallel RunInEachDatabase
139 : /// phase of the apply config process.
140 : ///
141 : /// We need a higher concurrency during reconfiguration in case of many DBs,
142 : /// but instance is already running and used by client. We can easily get out of
143 : /// `max_connections` limit, and the current code won't handle that.
144 : ///
145 : /// Default is 1, but also allow control plane to override this value for specific
146 : /// projects. It's also recommended to bump `superuser_reserved_connections` +=
147 : /// `reconfigure_concurrency` for such projects to ensure that we always have
148 : /// enough spare connections for reconfiguration process to succeed.
149 : #[serde(default = "default_reconfigure_concurrency")]
150 : pub reconfigure_concurrency: usize,
151 :
152 : /// If set to true, the compute_ctl will drop all subscriptions before starting the
153 : /// compute. This is needed when we start an endpoint on a branch, so that child
154 : /// would not compete with parent branch subscriptions
155 : /// over the same replication content from publisher.
156 : #[serde(default)] // Default false
157 : pub drop_subscriptions_before_start: bool,
158 :
159 : /// Log level for audit logging:
160 : ///
161 : /// Disabled - no audit logging. This is the default.
162 : /// log - log masked statements to the postgres log using pgaudit extension
163 : /// hipaa - log unmasked statements to the file using pgaudit and pgauditlogtofile extension
164 : ///
165 : /// Extensions should be present in shared_preload_libraries
166 : #[serde(default)]
167 : pub audit_log_level: ComputeAudit,
168 : }
169 :
170 : /// Feature flag to signal `compute_ctl` to enable certain experimental functionality.
171 3 : #[derive(Serialize, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
172 : #[serde(rename_all = "snake_case")]
173 : pub enum ComputeFeature {
174 : // XXX: Add more feature flags here.
175 : /// Enable the experimental activity monitor logic, which uses `pg_stat_database` to
176 : /// track short-lived connections as user activity.
177 : ActivityMonitorExperimental,
178 :
179 : /// Pre-install and initialize anon extension for every database in the cluster
180 : AnonExtension,
181 :
182 : /// This is a special feature flag that is used to represent unknown feature flags.
183 : /// Basically all unknown to enum flags are represented as this one. See unit test
184 : /// `parse_unknown_features()` for more details.
185 : #[serde(other)]
186 : UnknownFeature,
187 : }
188 :
189 44 : #[derive(Clone, Debug, Default, Deserialize, Serialize)]
190 : pub struct RemoteExtSpec {
191 : pub public_extensions: Option<Vec<String>>,
192 : pub custom_extensions: Option<Vec<String>>,
193 : pub library_index: HashMap<String, String>,
194 : pub extension_data: HashMap<String, ExtensionData>,
195 : }
196 :
197 30 : #[derive(Clone, Debug, Serialize, Deserialize)]
198 : pub struct ExtensionData {
199 : pub control_data: HashMap<String, String>,
200 : pub archive_path: String,
201 : }
202 :
203 : impl RemoteExtSpec {
204 6 : pub fn get_ext(
205 6 : &self,
206 6 : ext_name: &str,
207 6 : is_library: bool,
208 6 : build_tag: &str,
209 6 : pg_major_version: &str,
210 6 : ) -> anyhow::Result<(String, RemotePath)> {
211 6 : let mut real_ext_name = ext_name;
212 6 : if is_library {
213 : // sometimes library names might have a suffix like
214 : // library.so or library.so.3. We strip this off
215 : // because library_index is based on the name without the file extension
216 1 : let strip_lib_suffix = Regex::new(r"\.so.*").unwrap();
217 1 : let lib_raw_name = strip_lib_suffix.replace(real_ext_name, "").to_string();
218 1 :
219 1 : real_ext_name = self
220 1 : .library_index
221 1 : .get(&lib_raw_name)
222 1 : .ok_or(anyhow::anyhow!("library {} is not found", lib_raw_name))?;
223 5 : }
224 :
225 : // Check if extension is present in public or custom.
226 : // If not, then it is not allowed to be used by this compute.
227 6 : if !self
228 6 : .public_extensions
229 6 : .as_ref()
230 6 : .is_some_and(|exts| exts.iter().any(|e| e == real_ext_name))
231 4 : && !self
232 4 : .custom_extensions
233 4 : .as_ref()
234 4 : .is_some_and(|exts| exts.iter().any(|e| e == real_ext_name))
235 : {
236 3 : return Err(anyhow::anyhow!("extension {} is not found", real_ext_name));
237 3 : }
238 3 :
239 3 : match self.extension_data.get(real_ext_name) {
240 3 : Some(_ext_data) => {
241 3 : // Construct the path to the extension archive
242 3 : // BUILD_TAG/PG_MAJOR_VERSION/extensions/EXTENSION_NAME.tar.zst
243 3 : //
244 3 : // Keep it in sync with path generation in
245 3 : // https://github.com/neondatabase/build-custom-extensions/tree/main
246 3 : let archive_path_str =
247 3 : format!("{build_tag}/{pg_major_version}/extensions/{real_ext_name}.tar.zst");
248 3 : Ok((
249 3 : real_ext_name.to_string(),
250 3 : RemotePath::from_string(&archive_path_str)?,
251 : ))
252 : }
253 0 : None => Err(anyhow::anyhow!(
254 0 : "real_ext_name {} is not found",
255 0 : real_ext_name
256 0 : )),
257 : }
258 6 : }
259 : }
260 :
261 0 : #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
262 : pub enum ComputeMode {
263 : /// A read-write node
264 : #[default]
265 : Primary,
266 : /// A read-only node, pinned at a particular LSN
267 : Static(Lsn),
268 : /// A read-only node that follows the tip of the branch in hot standby mode
269 : ///
270 : /// Future versions may want to distinguish between replicas with hot standby
271 : /// feedback and other kinds of replication configurations.
272 : Replica,
273 : }
274 :
275 : /// Log level for audit logging
276 : /// Disabled, log, hipaa
277 : /// Default is Disabled
278 0 : #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
279 : pub enum ComputeAudit {
280 : #[default]
281 : Disabled,
282 : Log,
283 : Hipaa,
284 : }
285 :
286 36 : #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
287 : pub struct Cluster {
288 : pub cluster_id: Option<String>,
289 : pub name: Option<String>,
290 : pub state: Option<String>,
291 : pub roles: Vec<Role>,
292 : pub databases: Vec<Database>,
293 :
294 : /// Desired contents of 'postgresql.conf' file. (The 'compute_ctl'
295 : /// tool may add additional settings to the final file.)
296 : pub postgresql_conf: Option<String>,
297 :
298 : /// Additional settings that will be appended to the 'postgresql.conf' file.
299 : pub settings: GenericOptions,
300 : }
301 :
302 : /// Single cluster state changing operation that could not be represented as
303 : /// a static `Cluster` structure. For example:
304 : /// - DROP DATABASE
305 : /// - DROP ROLE
306 : /// - ALTER ROLE name RENAME TO new_name
307 : /// - ALTER DATABASE name RENAME TO new_name
308 60 : #[derive(Clone, Debug, Deserialize, Serialize)]
309 : pub struct DeltaOp {
310 : pub action: String,
311 : pub name: PgIdent,
312 : pub new_name: Option<PgIdent>,
313 : }
314 :
315 : /// Rust representation of Postgres role info with only those fields
316 : /// that matter for us.
317 90 : #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
318 : pub struct Role {
319 : pub name: PgIdent,
320 : pub encrypted_password: Option<String>,
321 : pub options: GenericOptions,
322 : }
323 :
324 : /// Rust representation of Postgres database info with only those fields
325 : /// that matter for us.
326 42 : #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
327 : pub struct Database {
328 : pub name: PgIdent,
329 : pub owner: PgIdent,
330 : pub options: GenericOptions,
331 : // These are derived flags, not present in the spec file.
332 : // They are never set by the control plane.
333 : #[serde(skip_deserializing, default)]
334 : pub restrict_conn: bool,
335 : #[serde(skip_deserializing, default)]
336 : pub invalid: bool,
337 : }
338 :
339 : /// Common type representing both SQL statement params with or without value,
340 : /// like `LOGIN` or `OWNER username` in the `CREATE/ALTER ROLE`, and config
341 : /// options like `wal_level = logical`.
342 468 : #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
343 : pub struct GenericOption {
344 : pub name: String,
345 : pub value: Option<String>,
346 : pub vartype: String,
347 : }
348 :
349 : /// Optional collection of `GenericOption`'s. Type alias allows us to
350 : /// declare a `trait` on it.
351 : pub type GenericOptions = Option<Vec<GenericOption>>;
352 :
353 : /// Configured the local_proxy application with the relevant JWKS and roles it should
354 : /// use for authorizing connect requests using JWT.
355 0 : #[derive(Clone, Debug, Deserialize, Serialize)]
356 : pub struct LocalProxySpec {
357 : #[serde(default)]
358 : #[serde(skip_serializing_if = "Option::is_none")]
359 : pub jwks: Option<Vec<JwksSettings>>,
360 : }
361 :
362 0 : #[derive(Clone, Debug, Deserialize, Serialize)]
363 : pub struct JwksSettings {
364 : pub id: String,
365 : pub role_names: Vec<String>,
366 : pub jwks_url: String,
367 : pub provider_name: String,
368 : pub jwt_audience: Option<String>,
369 : }
370 :
371 : #[cfg(test)]
372 : mod tests {
373 : use std::fs::File;
374 :
375 : use super::*;
376 :
377 : #[test]
378 1 : fn allow_installing_remote_extensions() {
379 1 : let rspec: RemoteExtSpec = serde_json::from_value(serde_json::json!({
380 1 : "public_extensions": null,
381 1 : "custom_extensions": null,
382 1 : "library_index": {},
383 1 : "extension_data": {},
384 1 : }))
385 1 : .unwrap();
386 1 :
387 1 : rspec
388 1 : .get_ext("ext", false, "latest", "v17")
389 1 : .expect_err("Extension should not be found");
390 1 :
391 1 : let rspec: RemoteExtSpec = serde_json::from_value(serde_json::json!({
392 1 : "public_extensions": [],
393 1 : "custom_extensions": null,
394 1 : "library_index": {},
395 1 : "extension_data": {},
396 1 : }))
397 1 : .unwrap();
398 1 :
399 1 : rspec
400 1 : .get_ext("ext", false, "latest", "v17")
401 1 : .expect_err("Extension should not be found");
402 1 :
403 1 : let rspec: RemoteExtSpec = serde_json::from_value(serde_json::json!({
404 1 : "public_extensions": [],
405 1 : "custom_extensions": [],
406 1 : "library_index": {
407 1 : "ext": "ext"
408 1 : },
409 1 : "extension_data": {
410 1 : "ext": {
411 1 : "control_data": {
412 1 : "ext.control": ""
413 1 : },
414 1 : "archive_path": ""
415 1 : }
416 1 : },
417 1 : }))
418 1 : .unwrap();
419 1 :
420 1 : rspec
421 1 : .get_ext("ext", false, "latest", "v17")
422 1 : .expect_err("Extension should not be found");
423 1 :
424 1 : let rspec: RemoteExtSpec = serde_json::from_value(serde_json::json!({
425 1 : "public_extensions": [],
426 1 : "custom_extensions": ["ext"],
427 1 : "library_index": {
428 1 : "ext": "ext"
429 1 : },
430 1 : "extension_data": {
431 1 : "ext": {
432 1 : "control_data": {
433 1 : "ext.control": ""
434 1 : },
435 1 : "archive_path": ""
436 1 : }
437 1 : },
438 1 : }))
439 1 : .unwrap();
440 1 :
441 1 : rspec
442 1 : .get_ext("ext", false, "latest", "v17")
443 1 : .expect("Extension should be found");
444 1 :
445 1 : let rspec: RemoteExtSpec = serde_json::from_value(serde_json::json!({
446 1 : "public_extensions": ["ext"],
447 1 : "custom_extensions": [],
448 1 : "library_index": {
449 1 : "extlib": "ext",
450 1 : },
451 1 : "extension_data": {
452 1 : "ext": {
453 1 : "control_data": {
454 1 : "ext.control": ""
455 1 : },
456 1 : "archive_path": ""
457 1 : }
458 1 : },
459 1 : }))
460 1 : .unwrap();
461 1 :
462 1 : rspec
463 1 : .get_ext("ext", false, "latest", "v17")
464 1 : .expect("Extension should be found");
465 1 :
466 1 : // test library index for the case when library name
467 1 : // doesn't match the extension name
468 1 : rspec
469 1 : .get_ext("extlib", true, "latest", "v17")
470 1 : .expect("Library should be found");
471 1 : }
472 :
473 : #[test]
474 1 : fn parse_spec_file() {
475 1 : let file = File::open("tests/cluster_spec.json").unwrap();
476 1 : let spec: ComputeSpec = serde_json::from_reader(file).unwrap();
477 1 :
478 1 : // Features list defaults to empty vector.
479 1 : assert!(spec.features.is_empty());
480 :
481 : // Reconfigure concurrency defaults to 1.
482 1 : assert_eq!(spec.reconfigure_concurrency, 1);
483 1 : }
484 :
485 : #[test]
486 1 : fn parse_unknown_fields() {
487 1 : // Forward compatibility test
488 1 : let file = File::open("tests/cluster_spec.json").unwrap();
489 1 : let mut json: serde_json::Value = serde_json::from_reader(file).unwrap();
490 1 : let ob = json.as_object_mut().unwrap();
491 1 : ob.insert("unknown_field_123123123".into(), "hello".into());
492 1 : let _spec: ComputeSpec = serde_json::from_value(json).unwrap();
493 1 : }
494 :
495 : #[test]
496 1 : fn parse_unknown_features() {
497 1 : // Test that unknown feature flags do not cause any errors.
498 1 : let file = File::open("tests/cluster_spec.json").unwrap();
499 1 : let mut json: serde_json::Value = serde_json::from_reader(file).unwrap();
500 1 : let ob = json.as_object_mut().unwrap();
501 1 :
502 1 : // Add unknown feature flags.
503 1 : let features = vec!["foo_bar_feature", "baz_feature"];
504 1 : ob.insert("features".into(), features.into());
505 1 :
506 1 : let spec: ComputeSpec = serde_json::from_value(json).unwrap();
507 1 :
508 1 : assert!(spec.features.len() == 2);
509 1 : assert!(spec.features.contains(&ComputeFeature::UnknownFeature));
510 1 : assert_eq!(spec.features, vec![ComputeFeature::UnknownFeature; 2]);
511 1 : }
512 :
513 : #[test]
514 1 : fn parse_known_features() {
515 1 : // Test that we can properly parse known feature flags.
516 1 : let file = File::open("tests/cluster_spec.json").unwrap();
517 1 : let mut json: serde_json::Value = serde_json::from_reader(file).unwrap();
518 1 : let ob = json.as_object_mut().unwrap();
519 1 :
520 1 : // Add known feature flags.
521 1 : let features = vec!["activity_monitor_experimental"];
522 1 : ob.insert("features".into(), features.into());
523 1 :
524 1 : let spec: ComputeSpec = serde_json::from_value(json).unwrap();
525 1 :
526 1 : assert_eq!(
527 1 : spec.features,
528 1 : vec![ComputeFeature::ActivityMonitorExperimental]
529 1 : );
530 1 : }
531 : }
|