LCOV - code coverage report
Current view: top level - libs/compute_api/src - spec.rs (source / functions) Coverage Total Hit
Test: 71d97c4519b9017c5903db4dfe4edf4a84645500.info Lines: 57.0 % 107 61
Test Date: 2024-12-19 16:48:20 Functions: 18.9 % 254 48

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

Generated by: LCOV version 2.1-beta