LCOV - code coverage report
Current view: top level - compute_tools/src - spec.rs (source / functions) Coverage Total Hit
Test: a2f0f8a80fbf1089336086fa360ce27fa555cb1a.info Lines: 0.0 % 85 0
Test Date: 2024-11-20 17:59:39 Functions: 0.0 % 8 0

            Line data    Source code
       1              : use anyhow::{anyhow, bail, Result};
       2              : use postgres::Client;
       3              : use reqwest::StatusCode;
       4              : use std::fs::File;
       5              : use std::path::Path;
       6              : use tracing::{error, info, instrument, warn};
       7              : 
       8              : use crate::config;
       9              : use crate::migration::MigrationRunner;
      10              : use crate::params::PG_HBA_ALL_MD5;
      11              : use crate::pg_helpers::*;
      12              : 
      13              : use compute_api::responses::{ControlPlaneComputeStatus, ControlPlaneSpecResponse};
      14              : use compute_api::spec::ComputeSpec;
      15              : 
      16              : // Do control plane request and return response if any. In case of error it
      17              : // returns a bool flag indicating whether it makes sense to retry the request
      18              : // and a string with error message.
      19            0 : fn do_control_plane_request(
      20            0 :     uri: &str,
      21            0 :     jwt: &str,
      22            0 : ) -> Result<ControlPlaneSpecResponse, (bool, String)> {
      23            0 :     let resp = reqwest::blocking::Client::new()
      24            0 :         .get(uri)
      25            0 :         .header("Authorization", format!("Bearer {}", jwt))
      26            0 :         .send()
      27            0 :         .map_err(|e| {
      28            0 :             (
      29            0 :                 true,
      30            0 :                 format!("could not perform spec request to control plane: {}", e),
      31            0 :             )
      32            0 :         })?;
      33              : 
      34            0 :     match resp.status() {
      35            0 :         StatusCode::OK => match resp.json::<ControlPlaneSpecResponse>() {
      36            0 :             Ok(spec_resp) => Ok(spec_resp),
      37            0 :             Err(e) => Err((
      38            0 :                 true,
      39            0 :                 format!("could not deserialize control plane response: {}", e),
      40            0 :             )),
      41              :         },
      42              :         StatusCode::SERVICE_UNAVAILABLE => {
      43            0 :             Err((true, "control plane is temporarily unavailable".to_string()))
      44              :         }
      45              :         StatusCode::BAD_GATEWAY => {
      46              :             // We have a problem with intermittent 502 errors now
      47              :             // https://github.com/neondatabase/cloud/issues/2353
      48              :             // It's fine to retry GET request in this case.
      49            0 :             Err((true, "control plane request failed with 502".to_string()))
      50              :         }
      51              :         // Another code, likely 500 or 404, means that compute is unknown to the control plane
      52              :         // or some internal failure happened. Doesn't make much sense to retry in this case.
      53            0 :         _ => Err((
      54            0 :             false,
      55            0 :             format!(
      56            0 :                 "unexpected control plane response status code: {}",
      57            0 :                 resp.status()
      58            0 :             ),
      59            0 :         )),
      60              :     }
      61            0 : }
      62              : 
      63              : /// Request spec from the control-plane by compute_id. If `NEON_CONTROL_PLANE_TOKEN`
      64              : /// env variable is set, it will be used for authorization.
      65            0 : pub fn get_spec_from_control_plane(
      66            0 :     base_uri: &str,
      67            0 :     compute_id: &str,
      68            0 : ) -> Result<Option<ComputeSpec>> {
      69            0 :     let cp_uri = format!("{base_uri}/compute/api/v2/computes/{compute_id}/spec");
      70            0 :     let jwt: String = match std::env::var("NEON_CONTROL_PLANE_TOKEN") {
      71            0 :         Ok(v) => v,
      72            0 :         Err(_) => "".to_string(),
      73              :     };
      74            0 :     let mut attempt = 1;
      75            0 :     let mut spec: Result<Option<ComputeSpec>> = Ok(None);
      76            0 : 
      77            0 :     info!("getting spec from control plane: {}", cp_uri);
      78              : 
      79              :     // Do 3 attempts to get spec from the control plane using the following logic:
      80              :     // - network error -> then retry
      81              :     // - compute id is unknown or any other error -> bail out
      82              :     // - no spec for compute yet (Empty state) -> return Ok(None)
      83              :     // - got spec -> return Ok(Some(spec))
      84            0 :     while attempt < 4 {
      85            0 :         spec = match do_control_plane_request(&cp_uri, &jwt) {
      86            0 :             Ok(spec_resp) => match spec_resp.status {
      87            0 :                 ControlPlaneComputeStatus::Empty => Ok(None),
      88              :                 ControlPlaneComputeStatus::Attached => {
      89            0 :                     if let Some(spec) = spec_resp.spec {
      90            0 :                         Ok(Some(spec))
      91              :                     } else {
      92            0 :                         bail!("compute is attached, but spec is empty")
      93              :                     }
      94              :                 }
      95              :             },
      96            0 :             Err((retry, msg)) => {
      97            0 :                 if retry {
      98            0 :                     Err(anyhow!(msg))
      99              :                 } else {
     100            0 :                     bail!(msg);
     101              :                 }
     102              :             }
     103              :         };
     104              : 
     105            0 :         if let Err(e) = &spec {
     106            0 :             error!("attempt {} to get spec failed with: {}", attempt, e);
     107              :         } else {
     108            0 :             return spec;
     109              :         }
     110              : 
     111            0 :         attempt += 1;
     112            0 :         std::thread::sleep(std::time::Duration::from_millis(100));
     113              :     }
     114              : 
     115              :     // All attempts failed, return error.
     116            0 :     spec
     117            0 : }
     118              : 
     119              : /// Check `pg_hba.conf` and update if needed to allow external connections.
     120            0 : pub fn update_pg_hba(pgdata_path: &Path) -> Result<()> {
     121            0 :     // XXX: consider making it a part of spec.json
     122            0 :     info!("checking pg_hba.conf");
     123            0 :     let pghba_path = pgdata_path.join("pg_hba.conf");
     124            0 : 
     125            0 :     if config::line_in_file(&pghba_path, PG_HBA_ALL_MD5)? {
     126            0 :         info!("updated pg_hba.conf to allow external connections");
     127              :     } else {
     128            0 :         info!("pg_hba.conf is up-to-date");
     129              :     }
     130              : 
     131            0 :     Ok(())
     132            0 : }
     133              : 
     134              : /// Create a standby.signal file
     135            0 : pub fn add_standby_signal(pgdata_path: &Path) -> Result<()> {
     136            0 :     // XXX: consider making it a part of spec.json
     137            0 :     info!("adding standby.signal");
     138            0 :     let signalfile = pgdata_path.join("standby.signal");
     139            0 : 
     140            0 :     if !signalfile.exists() {
     141            0 :         info!("created standby.signal");
     142            0 :         File::create(signalfile)?;
     143              :     } else {
     144            0 :         info!("reused pre-existing standby.signal");
     145              :     }
     146            0 :     Ok(())
     147            0 : }
     148              : 
     149            0 : #[instrument(skip_all)]
     150              : pub fn handle_neon_extension_upgrade(client: &mut Client) -> Result<()> {
     151              :     info!("handle neon extension upgrade");
     152              :     let query = "ALTER EXTENSION neon UPDATE";
     153              :     info!("update neon extension version with query: {}", query);
     154              :     client.simple_query(query)?;
     155              : 
     156              :     Ok(())
     157              : }
     158              : 
     159            0 : #[instrument(skip_all)]
     160              : pub fn handle_migrations(client: &mut Client) -> Result<()> {
     161              :     info!("handle migrations");
     162              : 
     163              :     // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
     164              :     // !BE SURE TO ONLY ADD MIGRATIONS TO THE END OF THIS ARRAY. IF YOU DO NOT, VERY VERY BAD THINGS MAY HAPPEN!
     165              :     // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
     166              : 
     167              :     // Add new migrations in numerical order.
     168              :     let migrations = [
     169              :         include_str!("./migrations/0001-neon_superuser_bypass_rls.sql"),
     170              :         include_str!("./migrations/0002-alter_roles.sql"),
     171              :         include_str!("./migrations/0003-grant_pg_create_subscription_to_neon_superuser.sql"),
     172              :         include_str!("./migrations/0004-grant_pg_monitor_to_neon_superuser.sql"),
     173              :         include_str!("./migrations/0005-grant_all_on_tables_to_neon_superuser.sql"),
     174              :         include_str!("./migrations/0006-grant_all_on_sequences_to_neon_superuser.sql"),
     175              :         include_str!(
     176              :             "./migrations/0007-grant_all_on_tables_to_neon_superuser_with_grant_option.sql"
     177              :         ),
     178              :         include_str!(
     179              :             "./migrations/0008-grant_all_on_sequences_to_neon_superuser_with_grant_option.sql"
     180              :         ),
     181              :         include_str!("./migrations/0009-revoke_replication_for_previously_allowed_roles.sql"),
     182              :         include_str!(
     183              :             "./migrations/0010-grant_snapshot_synchronization_funcs_to_neon_superuser.sql"
     184              :         ),
     185              :         include_str!(
     186              :             "./migrations/0011-grant_pg_show_replication_origin_status_to_neon_superuser.sql"
     187              :         ),
     188              :     ];
     189              : 
     190              :     MigrationRunner::new(client, &migrations).run_migrations()?;
     191              : 
     192              :     Ok(())
     193              : }
     194              : 
     195              : /// Connect to the database as superuser and pre-create anon extension
     196              : /// if it is present in shared_preload_libraries
     197            0 : #[instrument(skip_all)]
     198              : pub fn handle_extension_anon(
     199              :     spec: &ComputeSpec,
     200              :     db_owner: &str,
     201              :     db_client: &mut Client,
     202              :     grants_only: bool,
     203              : ) -> Result<()> {
     204              :     info!("handle extension anon");
     205              : 
     206              :     if let Some(libs) = spec.cluster.settings.find("shared_preload_libraries") {
     207              :         if libs.contains("anon") {
     208              :             if !grants_only {
     209              :                 // check if extension is already initialized using anon.is_initialized()
     210              :                 let query = "SELECT anon.is_initialized()";
     211              :                 match db_client.query(query, &[]) {
     212              :                     Ok(rows) => {
     213              :                         if !rows.is_empty() {
     214              :                             let is_initialized: bool = rows[0].get(0);
     215              :                             if is_initialized {
     216              :                                 info!("anon extension is already initialized");
     217              :                                 return Ok(());
     218              :                             }
     219              :                         }
     220              :                     }
     221              :                     Err(e) => {
     222              :                         warn!(
     223              :                             "anon extension is_installed check failed with expected error: {}",
     224              :                             e
     225              :                         );
     226              :                     }
     227              :                 };
     228              : 
     229              :                 // Create anon extension if this compute needs it
     230              :                 // Users cannot create it themselves, because superuser is required.
     231              :                 let mut query = "CREATE EXTENSION IF NOT EXISTS anon CASCADE";
     232              :                 info!("creating anon extension with query: {}", query);
     233              :                 match db_client.query(query, &[]) {
     234              :                     Ok(_) => {}
     235              :                     Err(e) => {
     236              :                         error!("anon extension creation failed with error: {}", e);
     237              :                         return Ok(());
     238              :                     }
     239              :                 }
     240              : 
     241              :                 // check that extension is installed
     242              :                 query = "SELECT extname FROM pg_extension WHERE extname = 'anon'";
     243              :                 let rows = db_client.query(query, &[])?;
     244              :                 if rows.is_empty() {
     245              :                     error!("anon extension is not installed");
     246              :                     return Ok(());
     247              :                 }
     248              : 
     249              :                 // Initialize anon extension
     250              :                 // This also requires superuser privileges, so users cannot do it themselves.
     251              :                 query = "SELECT anon.init()";
     252              :                 match db_client.query(query, &[]) {
     253              :                     Ok(_) => {}
     254              :                     Err(e) => {
     255              :                         error!("anon.init() failed with error: {}", e);
     256              :                         return Ok(());
     257              :                     }
     258              :                 }
     259              :             }
     260              : 
     261              :             // check that extension is installed, if not bail early
     262              :             let query = "SELECT extname FROM pg_extension WHERE extname = 'anon'";
     263              :             match db_client.query(query, &[]) {
     264              :                 Ok(rows) => {
     265              :                     if rows.is_empty() {
     266              :                         error!("anon extension is not installed");
     267              :                         return Ok(());
     268              :                     }
     269              :                 }
     270              :                 Err(e) => {
     271              :                     error!("anon extension check failed with error: {}", e);
     272              :                     return Ok(());
     273              :                 }
     274              :             };
     275              : 
     276              :             let query = format!("GRANT ALL ON SCHEMA anon TO {}", db_owner);
     277              :             info!("granting anon extension permissions with query: {}", query);
     278              :             db_client.simple_query(&query)?;
     279              : 
     280              :             // Grant permissions to db_owner to use anon extension functions
     281              :             let query = format!("GRANT ALL ON ALL FUNCTIONS IN SCHEMA anon TO {}", db_owner);
     282              :             info!("granting anon extension permissions with query: {}", query);
     283              :             db_client.simple_query(&query)?;
     284              : 
     285              :             // This is needed, because some functions are defined as SECURITY DEFINER.
     286              :             // In Postgres SECURITY DEFINER functions are executed with the privileges
     287              :             // of the owner.
     288              :             // In anon extension this it is needed to access some GUCs, which are only accessible to
     289              :             // superuser. But we've patched postgres to allow db_owner to access them as well.
     290              :             // So we need to change owner of these functions to db_owner.
     291              :             let query = format!("
     292              :                 SELECT 'ALTER FUNCTION '||nsp.nspname||'.'||p.proname||'('||pg_get_function_identity_arguments(p.oid)||') OWNER TO {};'
     293              :                 from pg_proc p
     294              :                 join pg_namespace nsp ON p.pronamespace = nsp.oid
     295              :                 where nsp.nspname = 'anon';", db_owner);
     296              : 
     297              :             info!("change anon extension functions owner to db owner");
     298              :             db_client.simple_query(&query)?;
     299              : 
     300              :             //  affects views as well
     301              :             let query = format!("GRANT ALL ON ALL TABLES IN SCHEMA anon TO {}", db_owner);
     302              :             info!("granting anon extension permissions with query: {}", query);
     303              :             db_client.simple_query(&query)?;
     304              : 
     305              :             let query = format!("GRANT ALL ON ALL SEQUENCES IN SCHEMA anon TO {}", db_owner);
     306              :             info!("granting anon extension permissions with query: {}", query);
     307              :             db_client.simple_query(&query)?;
     308              :         }
     309              :     }
     310              : 
     311              :     Ok(())
     312              : }
        

Generated by: LCOV version 2.1-beta