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 serde::{Deserialize, Serialize};
9 : use utils::id::{TenantId, TimelineId};
10 : use utils::lsn::Lsn;
11 :
12 : use regex::Regex;
13 : use remote_storage::RemotePath;
14 :
15 : /// String type alias representing Postgres identifier and
16 : /// intended to be used for DB / role names.
17 : pub type PgIdent = String;
18 :
19 : /// Cluster spec or configuration represented as an optional number of
20 : /// delta operations + final cluster state description.
21 51 : #[derive(Clone, Debug, Default, Deserialize, Serialize)]
22 : pub struct ComputeSpec {
23 : pub format_version: f32,
24 :
25 : // The control plane also includes a 'timestamp' field in the JSON document,
26 : // but we don't use it for anything. Serde will ignore missing fields when
27 : // deserializing it.
28 : pub operation_uuid: Option<String>,
29 :
30 : /// Compute features to enable. These feature flags are provided, when we
31 : /// know all the details about client's compute, so they cannot be used
32 : /// to change `Empty` compute behavior.
33 : #[serde(default)]
34 : pub features: Vec<ComputeFeature>,
35 :
36 : /// If compute_ctl was passed `--resize-swap-on-bind`, a value of `Some(_)` instructs
37 : /// compute_ctl to `/neonvm/bin/resize-swap` with the given size, when the spec is first
38 : /// received.
39 : ///
40 : /// Both this field and `--resize-swap-on-bind` are required, so that the control plane's
41 : /// spec generation doesn't need to be aware of the actual compute it's running on, while
42 : /// guaranteeing gradual rollout of swap. Otherwise, without `--resize-swap-on-bind`, we could
43 : /// end up trying to resize swap in VMs without it -- or end up *not* resizing swap, thus
44 : /// giving every VM much more swap than it should have (32GiB).
45 : ///
46 : /// Eventually we may remove `--resize-swap-on-bind` and exclusively use `swap_size_bytes` for
47 : /// enabling the swap resizing behavior once rollout is complete.
48 : ///
49 : /// See neondatabase/cloud#12047 for more.
50 : #[serde(default)]
51 : pub swap_size_bytes: Option<u64>,
52 :
53 : /// Expected cluster state at the end of transition process.
54 : pub cluster: Cluster,
55 : pub delta_operations: Option<Vec<DeltaOp>>,
56 :
57 : /// An optinal hint that can be passed to speed up startup time if we know
58 : /// that no pg catalog mutations (like role creation, database creation,
59 : /// extension creation) need to be done on the actual database to start.
60 : #[serde(default)] // Default false
61 : pub skip_pg_catalog_updates: bool,
62 :
63 : // Information needed to connect to the storage layer.
64 : //
65 : // `tenant_id`, `timeline_id` and `pageserver_connstring` are always needed.
66 : //
67 : // Depending on `mode`, this can be a primary read-write node, a read-only
68 : // replica, or a read-only node pinned at an older LSN.
69 : // `safekeeper_connstrings` must be set for a primary.
70 : //
71 : // For backwards compatibility, the control plane may leave out all of
72 : // these, and instead set the "neon.tenant_id", "neon.timeline_id",
73 : // etc. GUCs in cluster.settings. TODO: Once the control plane has been
74 : // updated to fill these fields, we can make these non optional.
75 : pub tenant_id: Option<TenantId>,
76 :
77 : pub timeline_id: Option<TimelineId>,
78 :
79 : pub pageserver_connstring: Option<String>,
80 :
81 : #[serde(default)]
82 : pub safekeeper_connstrings: Vec<String>,
83 :
84 : #[serde(default)]
85 : pub mode: ComputeMode,
86 :
87 : /// If set, 'storage_auth_token' is used as the password to authenticate to
88 : /// the pageserver and safekeepers.
89 : pub storage_auth_token: Option<String>,
90 :
91 : // information about available remote extensions
92 : pub remote_extensions: Option<RemoteExtSpec>,
93 :
94 : pub pgbouncer_settings: Option<HashMap<String, String>>,
95 :
96 : // Stripe size for pageserver sharding, in pages
97 : #[serde(default)]
98 : pub shard_stripe_size: Option<usize>,
99 : }
100 :
101 : /// Feature flag to signal `compute_ctl` to enable certain experimental functionality.
102 6 : #[derive(Serialize, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
103 : #[serde(rename_all = "snake_case")]
104 : pub enum ComputeFeature {
105 : // XXX: Add more feature flags here.
106 : /// Enable the experimental activity monitor logic, which uses `pg_stat_database` to
107 : /// track short-lived connections as user activity.
108 : ActivityMonitorExperimental,
109 :
110 : /// Pre-install and initialize anon extension for every database in the cluster
111 : AnonExtension,
112 :
113 : /// This is a special feature flag that is used to represent unknown feature flags.
114 : /// Basically all unknown to enum flags are represented as this one. See unit test
115 : /// `parse_unknown_features()` for more details.
116 : #[serde(other)]
117 : UnknownFeature,
118 : }
119 :
120 30 : #[derive(Clone, Debug, Default, Deserialize, Serialize)]
121 : pub struct RemoteExtSpec {
122 : pub public_extensions: Option<Vec<String>>,
123 : pub custom_extensions: Option<Vec<String>>,
124 : pub library_index: HashMap<String, String>,
125 : pub extension_data: HashMap<String, ExtensionData>,
126 : }
127 :
128 36 : #[derive(Clone, Debug, Serialize, Deserialize)]
129 : pub struct ExtensionData {
130 : pub control_data: HashMap<String, String>,
131 : pub archive_path: String,
132 : }
133 :
134 : impl RemoteExtSpec {
135 0 : pub fn get_ext(
136 0 : &self,
137 0 : ext_name: &str,
138 0 : is_library: bool,
139 0 : build_tag: &str,
140 0 : pg_major_version: &str,
141 0 : ) -> anyhow::Result<(String, RemotePath)> {
142 0 : let mut real_ext_name = ext_name;
143 0 : if is_library {
144 : // sometimes library names might have a suffix like
145 : // library.so or library.so.3. We strip this off
146 : // because library_index is based on the name without the file extension
147 0 : let strip_lib_suffix = Regex::new(r"\.so.*").unwrap();
148 0 : let lib_raw_name = strip_lib_suffix.replace(real_ext_name, "").to_string();
149 0 :
150 0 : real_ext_name = self
151 0 : .library_index
152 0 : .get(&lib_raw_name)
153 0 : .ok_or(anyhow::anyhow!("library {} is not found", lib_raw_name))?;
154 0 : }
155 :
156 : // Check if extension is present in public or custom.
157 : // If not, then it is not allowed to be used by this compute.
158 0 : if let Some(public_extensions) = &self.public_extensions {
159 0 : if !public_extensions.contains(&real_ext_name.to_string()) {
160 0 : if let Some(custom_extensions) = &self.custom_extensions {
161 0 : if !custom_extensions.contains(&real_ext_name.to_string()) {
162 0 : return Err(anyhow::anyhow!("extension {} is not found", real_ext_name));
163 0 : }
164 0 : }
165 0 : }
166 0 : }
167 :
168 0 : match self.extension_data.get(real_ext_name) {
169 0 : Some(_ext_data) => {
170 0 : // Construct the path to the extension archive
171 0 : // BUILD_TAG/PG_MAJOR_VERSION/extensions/EXTENSION_NAME.tar.zst
172 0 : //
173 0 : // Keep it in sync with path generation in
174 0 : // https://github.com/neondatabase/build-custom-extensions/tree/main
175 0 : let archive_path_str =
176 0 : format!("{build_tag}/{pg_major_version}/extensions/{real_ext_name}.tar.zst");
177 0 : Ok((
178 0 : real_ext_name.to_string(),
179 0 : RemotePath::from_string(&archive_path_str)?,
180 : ))
181 : }
182 0 : None => Err(anyhow::anyhow!(
183 0 : "real_ext_name {} is not found",
184 0 : real_ext_name
185 0 : )),
186 : }
187 0 : }
188 : }
189 :
190 0 : #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
191 : pub enum ComputeMode {
192 : /// A read-write node
193 : #[default]
194 : Primary,
195 : /// A read-only node, pinned at a particular LSN
196 : Static(Lsn),
197 : /// A read-only node that follows the tip of the branch in hot standby mode
198 : ///
199 : /// Future versions may want to distinguish between replicas with hot standby
200 : /// feedback and other kinds of replication configurations.
201 : Replica,
202 : }
203 :
204 42 : #[derive(Clone, Debug, Default, Deserialize, Serialize)]
205 : pub struct Cluster {
206 : pub cluster_id: Option<String>,
207 : pub name: Option<String>,
208 : pub state: Option<String>,
209 : pub roles: Vec<Role>,
210 : pub databases: Vec<Database>,
211 :
212 : /// Desired contents of 'postgresql.conf' file. (The 'compute_ctl'
213 : /// tool may add additional settings to the final file.)
214 : pub postgresql_conf: Option<String>,
215 :
216 : /// Additional settings that will be appended to the 'postgresql.conf' file.
217 : pub settings: GenericOptions,
218 : }
219 :
220 : /// Single cluster state changing operation that could not be represented as
221 : /// a static `Cluster` structure. For example:
222 : /// - DROP DATABASE
223 : /// - DROP ROLE
224 : /// - ALTER ROLE name RENAME TO new_name
225 : /// - ALTER DATABASE name RENAME TO new_name
226 84 : #[derive(Clone, Debug, Deserialize, Serialize)]
227 : pub struct DeltaOp {
228 : pub action: String,
229 : pub name: PgIdent,
230 : pub new_name: Option<PgIdent>,
231 : }
232 :
233 : /// Rust representation of Postgres role info with only those fields
234 : /// that matter for us.
235 126 : #[derive(Clone, Debug, Deserialize, Serialize)]
236 : pub struct Role {
237 : pub name: PgIdent,
238 : pub encrypted_password: Option<String>,
239 : pub options: GenericOptions,
240 : }
241 :
242 : /// Rust representation of Postgres database info with only those fields
243 : /// that matter for us.
244 60 : #[derive(Clone, Debug, Deserialize, Serialize)]
245 : pub struct Database {
246 : pub name: PgIdent,
247 : pub owner: PgIdent,
248 : pub options: GenericOptions,
249 : // These are derived flags, not present in the spec file.
250 : // They are never set by the control plane.
251 : #[serde(skip_deserializing, default)]
252 : pub restrict_conn: bool,
253 : #[serde(skip_deserializing, default)]
254 : pub invalid: bool,
255 : }
256 :
257 : /// Common type representing both SQL statement params with or without value,
258 : /// like `LOGIN` or `OWNER username` in the `CREATE/ALTER ROLE`, and config
259 : /// options like `wal_level = logical`.
260 624 : #[derive(Clone, Debug, Deserialize, Serialize)]
261 : pub struct GenericOption {
262 : pub name: String,
263 : pub value: Option<String>,
264 : pub vartype: String,
265 : }
266 :
267 : /// Optional collection of `GenericOption`'s. Type alias allows us to
268 : /// declare a `trait` on it.
269 : pub type GenericOptions = Option<Vec<GenericOption>>;
270 :
271 : #[cfg(test)]
272 : mod tests {
273 : use super::*;
274 : use std::fs::File;
275 :
276 : #[test]
277 1 : fn parse_spec_file() {
278 1 : let file = File::open("tests/cluster_spec.json").unwrap();
279 1 : let spec: ComputeSpec = serde_json::from_reader(file).unwrap();
280 1 :
281 1 : // Features list defaults to empty vector.
282 1 : assert!(spec.features.is_empty());
283 1 : }
284 :
285 : #[test]
286 1 : fn parse_unknown_fields() {
287 1 : // Forward compatibility test
288 1 : let file = File::open("tests/cluster_spec.json").unwrap();
289 1 : let mut json: serde_json::Value = serde_json::from_reader(file).unwrap();
290 1 : let ob = json.as_object_mut().unwrap();
291 1 : ob.insert("unknown_field_123123123".into(), "hello".into());
292 1 : let _spec: ComputeSpec = serde_json::from_value(json).unwrap();
293 1 : }
294 :
295 : #[test]
296 1 : fn parse_unknown_features() {
297 1 : // Test that unknown feature flags do not cause any errors.
298 1 : let file = File::open("tests/cluster_spec.json").unwrap();
299 1 : let mut json: serde_json::Value = serde_json::from_reader(file).unwrap();
300 1 : let ob = json.as_object_mut().unwrap();
301 1 :
302 1 : // Add unknown feature flags.
303 1 : let features = vec!["foo_bar_feature", "baz_feature"];
304 1 : ob.insert("features".into(), features.into());
305 1 :
306 1 : let spec: ComputeSpec = serde_json::from_value(json).unwrap();
307 1 :
308 1 : assert!(spec.features.len() == 2);
309 1 : assert!(spec.features.contains(&ComputeFeature::UnknownFeature));
310 1 : assert_eq!(spec.features, vec![ComputeFeature::UnknownFeature; 2]);
311 1 : }
312 :
313 : #[test]
314 1 : fn parse_known_features() {
315 1 : // Test that we can properly parse known feature flags.
316 1 : let file = File::open("tests/cluster_spec.json").unwrap();
317 1 : let mut json: serde_json::Value = serde_json::from_reader(file).unwrap();
318 1 : let ob = json.as_object_mut().unwrap();
319 1 :
320 1 : // Add known feature flags.
321 1 : let features = vec!["activity_monitor_experimental"];
322 1 : ob.insert("features".into(), features.into());
323 1 :
324 1 : let spec: ComputeSpec = serde_json::from_value(json).unwrap();
325 1 :
326 1 : assert_eq!(
327 1 : spec.features,
328 1 : vec![ComputeFeature::ActivityMonitorExperimental]
329 1 : );
330 1 : }
331 : }
|