LCOV - code coverage report
Current view: top level - libs/compute_api/src - spec.rs (source / functions) Coverage Total Hit
Test: 32f4a56327bc9da697706839ed4836b2a00a408f.info Lines: 82.4 % 102 84
Test Date: 2024-02-07 07:37:29 Functions: 36.4 % 316 115

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

Generated by: LCOV version 2.1-beta