LCOV - code coverage report
Current view: top level - control_plane/src - local_env.rs (source / functions) Coverage Total Hit
Test: b837401fb09d2d9818b70e630fdb67e9799b7b0d.info Lines: 23.2 % 341 79
Test Date: 2024-04-18 15:32:49 Functions: 23.3 % 86 20

            Line data    Source code
       1              : //! This module is responsible for locating and loading paths in a local setup.
       2              : //!
       3              : //! Now it also provides init method which acts like a stub for proper installation
       4              : //! script which will use local paths.
       5              : 
       6              : use anyhow::{bail, ensure, Context};
       7              : 
       8              : use clap::ValueEnum;
       9              : use postgres_backend::AuthType;
      10              : use reqwest::Url;
      11              : use serde::{Deserialize, Serialize};
      12              : use std::collections::HashMap;
      13              : use std::env;
      14              : use std::fs;
      15              : use std::net::IpAddr;
      16              : use std::net::Ipv4Addr;
      17              : use std::net::SocketAddr;
      18              : use std::path::{Path, PathBuf};
      19              : use std::process::{Command, Stdio};
      20              : use utils::{
      21              :     auth::{encode_from_key_file, Claims},
      22              :     id::{NodeId, TenantId, TenantTimelineId, TimelineId},
      23              : };
      24              : 
      25              : use crate::safekeeper::SafekeeperNode;
      26              : 
      27              : pub const DEFAULT_PG_VERSION: u32 = 15;
      28              : 
      29              : //
      30              : // This data structures represents neon_local CLI config
      31              : //
      32              : // It is deserialized from the .neon/config file, or the config file passed
      33              : // to 'neon_local init --config=<path>' option. See control_plane/simple.conf for
      34              : // an example.
      35              : //
      36           30 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
      37              : pub struct LocalEnv {
      38              :     // Base directory for all the nodes (the pageserver, safekeepers and
      39              :     // compute endpoints).
      40              :     //
      41              :     // This is not stored in the config file. Rather, this is the path where the
      42              :     // config file itself is. It is read from the NEON_REPO_DIR env variable or
      43              :     // '.neon' if not given.
      44              :     #[serde(skip)]
      45              :     pub base_data_dir: PathBuf,
      46              : 
      47              :     // Path to postgres distribution. It's expected that "bin", "include",
      48              :     // "lib", "share" from postgres distribution are there. If at some point
      49              :     // in time we will be able to run against vanilla postgres we may split that
      50              :     // to four separate paths and match OS-specific installation layout.
      51              :     #[serde(default)]
      52              :     pub pg_distrib_dir: PathBuf,
      53              : 
      54              :     // Path to pageserver binary.
      55              :     #[serde(default)]
      56              :     pub neon_distrib_dir: PathBuf,
      57              : 
      58              :     // Default tenant ID to use with the 'neon_local' command line utility, when
      59              :     // --tenant_id is not explicitly specified.
      60              :     #[serde(default)]
      61              :     pub default_tenant_id: Option<TenantId>,
      62              : 
      63              :     // used to issue tokens during e.g pg start
      64              :     #[serde(default)]
      65              :     pub private_key_path: PathBuf,
      66              : 
      67              :     pub broker: NeonBroker,
      68              : 
      69              :     /// This Vec must always contain at least one pageserver
      70              :     pub pageservers: Vec<PageServerConf>,
      71              : 
      72              :     #[serde(default)]
      73              :     pub safekeepers: Vec<SafekeeperConf>,
      74              : 
      75              :     // Control plane upcall API for pageserver: if None, we will not run storage_controller  If set, this will
      76              :     // be propagated into each pageserver's configuration.
      77              :     #[serde(default)]
      78              :     pub control_plane_api: Option<Url>,
      79              : 
      80              :     // Control plane upcall API for storage controller.  If set, this will be propagated into the
      81              :     // storage controller's configuration.
      82              :     #[serde(default)]
      83              :     pub control_plane_compute_hook_api: Option<Url>,
      84              : 
      85              :     /// Keep human-readable aliases in memory (and persist them to config), to hide ZId hex strings from the user.
      86              :     #[serde(default)]
      87              :     // A `HashMap<String, HashMap<TenantId, TimelineId>>` would be more appropriate here,
      88              :     // but deserialization into a generic toml object as `toml::Value::try_from` fails with an error.
      89              :     // https://toml.io/en/v1.0.0 does not contain a concept of "a table inside another table".
      90              :     branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
      91              : }
      92              : 
      93              : /// Broker config for cluster internal communication.
      94           14 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
      95              : #[serde(default)]
      96              : pub struct NeonBroker {
      97              :     /// Broker listen address for storage nodes coordination, e.g. '127.0.0.1:50051'.
      98              :     pub listen_addr: SocketAddr,
      99              : }
     100              : 
     101              : // Dummy Default impl to satisfy Deserialize derive.
     102              : impl Default for NeonBroker {
     103            2 :     fn default() -> Self {
     104            2 :         NeonBroker {
     105            2 :             listen_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
     106            2 :         }
     107            2 :     }
     108              : }
     109              : 
     110              : impl NeonBroker {
     111            0 :     pub fn client_url(&self) -> Url {
     112            0 :         Url::parse(&format!("http://{}", self.listen_addr)).expect("failed to construct url")
     113            0 :     }
     114              : }
     115              : 
     116           44 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     117              : #[serde(default, deny_unknown_fields)]
     118              : pub struct PageServerConf {
     119              :     // node id
     120              :     pub id: NodeId,
     121              : 
     122              :     // Pageserver connection settings
     123              :     pub listen_pg_addr: String,
     124              :     pub listen_http_addr: String,
     125              : 
     126              :     // auth type used for the PG and HTTP ports
     127              :     pub pg_auth_type: AuthType,
     128              :     pub http_auth_type: AuthType,
     129              : 
     130              :     pub(crate) virtual_file_io_engine: Option<String>,
     131              :     pub(crate) get_vectored_impl: Option<String>,
     132              : }
     133              : 
     134              : impl Default for PageServerConf {
     135            4 :     fn default() -> Self {
     136            4 :         Self {
     137            4 :             id: NodeId(0),
     138            4 :             listen_pg_addr: String::new(),
     139            4 :             listen_http_addr: String::new(),
     140            4 :             pg_auth_type: AuthType::Trust,
     141            4 :             http_auth_type: AuthType::Trust,
     142            4 :             virtual_file_io_engine: None,
     143            4 :             get_vectored_impl: None,
     144            4 :         }
     145            4 :     }
     146              : }
     147              : 
     148           28 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     149              : #[serde(default)]
     150              : pub struct SafekeeperConf {
     151              :     pub id: NodeId,
     152              :     pub pg_port: u16,
     153              :     pub pg_tenant_only_port: Option<u16>,
     154              :     pub http_port: u16,
     155              :     pub sync: bool,
     156              :     pub remote_storage: Option<String>,
     157              :     pub backup_threads: Option<u32>,
     158              :     pub auth_enabled: bool,
     159              :     pub listen_addr: Option<String>,
     160              : }
     161              : 
     162              : impl Default for SafekeeperConf {
     163            4 :     fn default() -> Self {
     164            4 :         Self {
     165            4 :             id: NodeId(0),
     166            4 :             pg_port: 0,
     167            4 :             pg_tenant_only_port: None,
     168            4 :             http_port: 0,
     169            4 :             sync: true,
     170            4 :             remote_storage: None,
     171            4 :             backup_threads: None,
     172            4 :             auth_enabled: false,
     173            4 :             listen_addr: None,
     174            4 :         }
     175            4 :     }
     176              : }
     177              : 
     178              : #[derive(Clone, Copy)]
     179              : pub enum InitForceMode {
     180              :     MustNotExist,
     181              :     EmptyDirOk,
     182              :     RemoveAllContents,
     183              : }
     184              : 
     185              : impl ValueEnum for InitForceMode {
     186            4 :     fn value_variants<'a>() -> &'a [Self] {
     187            4 :         &[
     188            4 :             Self::MustNotExist,
     189            4 :             Self::EmptyDirOk,
     190            4 :             Self::RemoveAllContents,
     191            4 :         ]
     192            4 :     }
     193              : 
     194           10 :     fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
     195           10 :         Some(clap::builder::PossibleValue::new(match self {
     196            6 :             InitForceMode::MustNotExist => "must-not-exist",
     197            2 :             InitForceMode::EmptyDirOk => "empty-dir-ok",
     198            2 :             InitForceMode::RemoveAllContents => "remove-all-contents",
     199              :         }))
     200           10 :     }
     201              : }
     202              : 
     203              : impl SafekeeperConf {
     204              :     /// Compute is served by port on which only tenant scoped tokens allowed, if
     205              :     /// it is configured.
     206            0 :     pub fn get_compute_port(&self) -> u16 {
     207            0 :         self.pg_tenant_only_port.unwrap_or(self.pg_port)
     208            0 :     }
     209              : }
     210              : 
     211              : impl LocalEnv {
     212            0 :     pub fn pg_distrib_dir_raw(&self) -> PathBuf {
     213            0 :         self.pg_distrib_dir.clone()
     214            0 :     }
     215              : 
     216            0 :     pub fn pg_distrib_dir(&self, pg_version: u32) -> anyhow::Result<PathBuf> {
     217            0 :         let path = self.pg_distrib_dir.clone();
     218            0 : 
     219            0 :         #[allow(clippy::manual_range_patterns)]
     220            0 :         match pg_version {
     221            0 :             14 | 15 | 16 => Ok(path.join(format!("v{pg_version}"))),
     222            0 :             _ => bail!("Unsupported postgres version: {}", pg_version),
     223              :         }
     224            0 :     }
     225              : 
     226            0 :     pub fn pg_bin_dir(&self, pg_version: u32) -> anyhow::Result<PathBuf> {
     227            0 :         Ok(self.pg_distrib_dir(pg_version)?.join("bin"))
     228            0 :     }
     229            0 :     pub fn pg_lib_dir(&self, pg_version: u32) -> anyhow::Result<PathBuf> {
     230            0 :         Ok(self.pg_distrib_dir(pg_version)?.join("lib"))
     231            0 :     }
     232              : 
     233            0 :     pub fn pageserver_bin(&self) -> PathBuf {
     234            0 :         self.neon_distrib_dir.join("pageserver")
     235            0 :     }
     236              : 
     237            0 :     pub fn storage_controller_bin(&self) -> PathBuf {
     238            0 :         // Irrespective of configuration, storage controller binary is always
     239            0 :         // run from the same location as neon_local.  This means that for compatibility
     240            0 :         // tests that run old pageserver/safekeeper, they still run latest storage controller.
     241            0 :         let neon_local_bin_dir = env::current_exe().unwrap().parent().unwrap().to_owned();
     242            0 :         neon_local_bin_dir.join("storage_controller")
     243            0 :     }
     244              : 
     245            0 :     pub fn safekeeper_bin(&self) -> PathBuf {
     246            0 :         self.neon_distrib_dir.join("safekeeper")
     247            0 :     }
     248              : 
     249            0 :     pub fn storage_broker_bin(&self) -> PathBuf {
     250            0 :         self.neon_distrib_dir.join("storage_broker")
     251            0 :     }
     252              : 
     253            0 :     pub fn endpoints_path(&self) -> PathBuf {
     254            0 :         self.base_data_dir.join("endpoints")
     255            0 :     }
     256              : 
     257            0 :     pub fn pageserver_data_dir(&self, pageserver_id: NodeId) -> PathBuf {
     258            0 :         self.base_data_dir
     259            0 :             .join(format!("pageserver_{pageserver_id}"))
     260            0 :     }
     261              : 
     262            0 :     pub fn safekeeper_data_dir(&self, data_dir_name: &str) -> PathBuf {
     263            0 :         self.base_data_dir.join("safekeepers").join(data_dir_name)
     264            0 :     }
     265              : 
     266            0 :     pub fn get_pageserver_conf(&self, id: NodeId) -> anyhow::Result<&PageServerConf> {
     267            0 :         if let Some(conf) = self.pageservers.iter().find(|node| node.id == id) {
     268            0 :             Ok(conf)
     269              :         } else {
     270            0 :             let have_ids = self
     271            0 :                 .pageservers
     272            0 :                 .iter()
     273            0 :                 .map(|node| format!("{}:{}", node.id, node.listen_http_addr))
     274            0 :                 .collect::<Vec<_>>();
     275            0 :             let joined = have_ids.join(",");
     276            0 :             bail!("could not find pageserver {id}, have ids {joined}")
     277              :         }
     278            0 :     }
     279              : 
     280            0 :     pub fn register_branch_mapping(
     281            0 :         &mut self,
     282            0 :         branch_name: String,
     283            0 :         tenant_id: TenantId,
     284            0 :         timeline_id: TimelineId,
     285            0 :     ) -> anyhow::Result<()> {
     286            0 :         let existing_values = self
     287            0 :             .branch_name_mappings
     288            0 :             .entry(branch_name.clone())
     289            0 :             .or_default();
     290            0 : 
     291            0 :         let existing_ids = existing_values
     292            0 :             .iter()
     293            0 :             .find(|(existing_tenant_id, _)| existing_tenant_id == &tenant_id);
     294              : 
     295            0 :         if let Some((_, old_timeline_id)) = existing_ids {
     296            0 :             if old_timeline_id == &timeline_id {
     297            0 :                 Ok(())
     298              :             } else {
     299            0 :                 bail!("branch '{branch_name}' is already mapped to timeline {old_timeline_id}, cannot map to another timeline {timeline_id}");
     300              :             }
     301              :         } else {
     302            0 :             existing_values.push((tenant_id, timeline_id));
     303            0 :             Ok(())
     304              :         }
     305            0 :     }
     306              : 
     307            0 :     pub fn get_branch_timeline_id(
     308            0 :         &self,
     309            0 :         branch_name: &str,
     310            0 :         tenant_id: TenantId,
     311            0 :     ) -> Option<TimelineId> {
     312            0 :         self.branch_name_mappings
     313            0 :             .get(branch_name)?
     314            0 :             .iter()
     315            0 :             .find(|(mapped_tenant_id, _)| mapped_tenant_id == &tenant_id)
     316            0 :             .map(|&(_, timeline_id)| timeline_id)
     317            0 :             .map(TimelineId::from)
     318            0 :     }
     319              : 
     320            0 :     pub fn timeline_name_mappings(&self) -> HashMap<TenantTimelineId, String> {
     321            0 :         self.branch_name_mappings
     322            0 :             .iter()
     323            0 :             .flat_map(|(name, tenant_timelines)| {
     324            0 :                 tenant_timelines.iter().map(|&(tenant_id, timeline_id)| {
     325            0 :                     (TenantTimelineId::new(tenant_id, timeline_id), name.clone())
     326            0 :                 })
     327            0 :             })
     328            0 :             .collect()
     329            0 :     }
     330              : 
     331              :     /// Create a LocalEnv from a config file.
     332              :     ///
     333              :     /// Unlike 'load_config', this function fills in any defaults that are missing
     334              :     /// from the config file.
     335            4 :     pub fn parse_config(toml: &str) -> anyhow::Result<Self> {
     336            4 :         let mut env: LocalEnv = toml::from_str(toml)?;
     337              : 
     338              :         // Find postgres binaries.
     339              :         // Follow POSTGRES_DISTRIB_DIR if set, otherwise look in "pg_install".
     340              :         // Note that later in the code we assume, that distrib dirs follow the same pattern
     341              :         // for all postgres versions.
     342            2 :         if env.pg_distrib_dir == Path::new("") {
     343            2 :             if let Some(postgres_bin) = env::var_os("POSTGRES_DISTRIB_DIR") {
     344            0 :                 env.pg_distrib_dir = postgres_bin.into();
     345            0 :             } else {
     346            2 :                 let cwd = env::current_dir()?;
     347            2 :                 env.pg_distrib_dir = cwd.join("pg_install")
     348              :             }
     349            0 :         }
     350              : 
     351              :         // Find neon binaries.
     352            2 :         if env.neon_distrib_dir == Path::new("") {
     353            2 :             env.neon_distrib_dir = env::current_exe()?.parent().unwrap().to_owned();
     354            0 :         }
     355              : 
     356            2 :         if env.pageservers.is_empty() {
     357            0 :             anyhow::bail!("Configuration must contain at least one pageserver");
     358            2 :         }
     359            2 : 
     360            2 :         env.base_data_dir = base_path();
     361            2 : 
     362            2 :         Ok(env)
     363            4 :     }
     364              : 
     365              :     /// Locate and load config
     366            0 :     pub fn load_config() -> anyhow::Result<Self> {
     367            0 :         let repopath = base_path();
     368            0 : 
     369            0 :         if !repopath.exists() {
     370            0 :             bail!(
     371            0 :                 "Neon config is not found in {}. You need to run 'neon_local init' first",
     372            0 :                 repopath.to_str().unwrap()
     373            0 :             );
     374            0 :         }
     375              : 
     376              :         // TODO: check that it looks like a neon repository
     377              : 
     378              :         // load and parse file
     379            0 :         let config = fs::read_to_string(repopath.join("config"))?;
     380            0 :         let mut env: LocalEnv = toml::from_str(config.as_str())?;
     381              : 
     382            0 :         env.base_data_dir = repopath;
     383            0 : 
     384            0 :         Ok(env)
     385            0 :     }
     386              : 
     387            0 :     pub fn persist_config(&self, base_path: &Path) -> anyhow::Result<()> {
     388            0 :         // Currently, the user first passes a config file with 'neon_local init --config=<path>'
     389            0 :         // We read that in, in `create_config`, and fill any missing defaults. Then it's saved
     390            0 :         // to .neon/config. TODO: We lose any formatting and comments along the way, which is
     391            0 :         // a bit sad.
     392            0 :         let mut conf_content = r#"# This file describes a local deployment of the page server
     393            0 : # and safekeeeper node. It is read by the 'neon_local' command-line
     394            0 : # utility.
     395            0 : "#
     396            0 :         .to_string();
     397            0 : 
     398            0 :         // Convert the LocalEnv to a toml file.
     399            0 :         //
     400            0 :         // This could be as simple as this:
     401            0 :         //
     402            0 :         // conf_content += &toml::to_string_pretty(env)?;
     403            0 :         //
     404            0 :         // But it results in a "values must be emitted before tables". I'm not sure
     405            0 :         // why, AFAICS the table, i.e. 'safekeepers: Vec<SafekeeperConf>' is last.
     406            0 :         // Maybe rust reorders the fields to squeeze avoid padding or something?
     407            0 :         // In any case, converting to toml::Value first, and serializing that, works.
     408            0 :         // See https://github.com/alexcrichton/toml-rs/issues/142
     409            0 :         conf_content += &toml::to_string_pretty(&toml::Value::try_from(self)?)?;
     410              : 
     411            0 :         let target_config_path = base_path.join("config");
     412            0 :         fs::write(&target_config_path, conf_content).with_context(|| {
     413            0 :             format!(
     414            0 :                 "Failed to write config file into path '{}'",
     415            0 :                 target_config_path.display()
     416            0 :             )
     417            0 :         })
     418            0 :     }
     419              : 
     420              :     // this function is used only for testing purposes in CLI e g generate tokens during init
     421            0 :     pub fn generate_auth_token(&self, claims: &Claims) -> anyhow::Result<String> {
     422            0 :         let private_key_path = self.get_private_key_path();
     423            0 :         let key_data = fs::read(private_key_path)?;
     424            0 :         encode_from_key_file(claims, &key_data)
     425            0 :     }
     426              : 
     427            0 :     pub fn get_private_key_path(&self) -> PathBuf {
     428            0 :         if self.private_key_path.is_absolute() {
     429            0 :             self.private_key_path.to_path_buf()
     430              :         } else {
     431            0 :             self.base_data_dir.join(&self.private_key_path)
     432              :         }
     433            0 :     }
     434              : 
     435              :     //
     436              :     // Initialize a new Neon repository
     437              :     //
     438            0 :     pub fn init(&mut self, pg_version: u32, force: &InitForceMode) -> anyhow::Result<()> {
     439            0 :         // check if config already exists
     440            0 :         let base_path = &self.base_data_dir;
     441            0 :         ensure!(
     442            0 :             base_path != Path::new(""),
     443            0 :             "repository base path is missing"
     444              :         );
     445              : 
     446            0 :         if base_path.exists() {
     447            0 :             match force {
     448              :                 InitForceMode::MustNotExist => {
     449            0 :                     bail!(
     450            0 :                         "directory '{}' already exists. Perhaps already initialized?",
     451            0 :                         base_path.display()
     452            0 :                     );
     453              :                 }
     454              :                 InitForceMode::EmptyDirOk => {
     455            0 :                     if let Some(res) = std::fs::read_dir(base_path)?.next() {
     456            0 :                         res.context("check if directory is empty")?;
     457            0 :                         anyhow::bail!("directory not empty: {base_path:?}");
     458            0 :                     }
     459              :                 }
     460              :                 InitForceMode::RemoveAllContents => {
     461            0 :                     println!("removing all contents of '{}'", base_path.display());
     462              :                     // instead of directly calling `remove_dir_all`, we keep the original dir but removing
     463              :                     // all contents inside. This helps if the developer symbol links another directory (i.e.,
     464              :                     // S3 local SSD) to the `.neon` base directory.
     465            0 :                     for entry in std::fs::read_dir(base_path)? {
     466            0 :                         let entry = entry?;
     467            0 :                         let path = entry.path();
     468            0 :                         if path.is_dir() {
     469            0 :                             fs::remove_dir_all(&path)?;
     470              :                         } else {
     471            0 :                             fs::remove_file(&path)?;
     472              :                         }
     473              :                     }
     474              :                 }
     475              :             }
     476            0 :         }
     477              : 
     478            0 :         if !self.pg_bin_dir(pg_version)?.join("postgres").exists() {
     479            0 :             bail!(
     480            0 :                 "Can't find postgres binary at {}",
     481            0 :                 self.pg_bin_dir(pg_version)?.display()
     482              :             );
     483            0 :         }
     484            0 :         for binary in ["pageserver", "safekeeper"] {
     485            0 :             if !self.neon_distrib_dir.join(binary).exists() {
     486            0 :                 bail!(
     487            0 :                     "Can't find binary '{binary}' in neon distrib dir '{}'",
     488            0 :                     self.neon_distrib_dir.display()
     489            0 :                 );
     490            0 :             }
     491              :         }
     492              : 
     493            0 :         if !base_path.exists() {
     494            0 :             fs::create_dir(base_path)?;
     495            0 :         }
     496              : 
     497              :         // Generate keypair for JWT.
     498              :         //
     499              :         // The keypair is only needed if authentication is enabled in any of the
     500              :         // components. For convenience, we generate the keypair even if authentication
     501              :         // is not enabled, so that you can easily enable it after the initialization
     502              :         // step. However, if the key generation fails, we treat it as non-fatal if
     503              :         // authentication was not enabled.
     504            0 :         if self.private_key_path == PathBuf::new() {
     505            0 :             match generate_auth_keys(
     506            0 :                 base_path.join("auth_private_key.pem").as_path(),
     507            0 :                 base_path.join("auth_public_key.pem").as_path(),
     508            0 :             ) {
     509            0 :                 Ok(()) => {
     510            0 :                     self.private_key_path = PathBuf::from("auth_private_key.pem");
     511            0 :                 }
     512            0 :                 Err(e) => {
     513            0 :                     if !self.auth_keys_needed() {
     514            0 :                         eprintln!("Could not generate keypair for JWT authentication: {e}");
     515            0 :                         eprintln!("Continuing anyway because authentication was not enabled");
     516            0 :                         self.private_key_path = PathBuf::from("auth_private_key.pem");
     517            0 :                     } else {
     518            0 :                         return Err(e);
     519              :                     }
     520              :                 }
     521              :             }
     522            0 :         }
     523              : 
     524            0 :         fs::create_dir_all(self.endpoints_path())?;
     525              : 
     526            0 :         for safekeeper in &self.safekeepers {
     527            0 :             fs::create_dir_all(SafekeeperNode::datadir_path_by_id(self, safekeeper.id))?;
     528              :         }
     529              : 
     530            0 :         self.persist_config(base_path)
     531            0 :     }
     532              : 
     533            0 :     fn auth_keys_needed(&self) -> bool {
     534            0 :         self.pageservers.iter().any(|ps| {
     535            0 :             ps.pg_auth_type == AuthType::NeonJWT || ps.http_auth_type == AuthType::NeonJWT
     536            0 :         }) || self.safekeepers.iter().any(|sk| sk.auth_enabled)
     537            0 :     }
     538              : }
     539              : 
     540            2 : fn base_path() -> PathBuf {
     541            2 :     match std::env::var_os("NEON_REPO_DIR") {
     542            0 :         Some(val) => PathBuf::from(val),
     543            2 :         None => PathBuf::from(".neon"),
     544              :     }
     545            2 : }
     546              : 
     547              : /// Generate a public/private key pair for JWT authentication
     548            0 : fn generate_auth_keys(private_key_path: &Path, public_key_path: &Path) -> anyhow::Result<()> {
     549              :     // Generate the key pair
     550              :     //
     551              :     // openssl genpkey -algorithm ed25519 -out auth_private_key.pem
     552            0 :     let keygen_output = Command::new("openssl")
     553            0 :         .arg("genpkey")
     554            0 :         .args(["-algorithm", "ed25519"])
     555            0 :         .args(["-out", private_key_path.to_str().unwrap()])
     556            0 :         .stdout(Stdio::null())
     557            0 :         .output()
     558            0 :         .context("failed to generate auth private key")?;
     559            0 :     if !keygen_output.status.success() {
     560            0 :         bail!(
     561            0 :             "openssl failed: '{}'",
     562            0 :             String::from_utf8_lossy(&keygen_output.stderr)
     563            0 :         );
     564            0 :     }
     565              :     // Extract the public key from the private key file
     566              :     //
     567              :     // openssl pkey -in auth_private_key.pem -pubout -out auth_public_key.pem
     568            0 :     let keygen_output = Command::new("openssl")
     569            0 :         .arg("pkey")
     570            0 :         .args(["-in", private_key_path.to_str().unwrap()])
     571            0 :         .arg("-pubout")
     572            0 :         .args(["-out", public_key_path.to_str().unwrap()])
     573            0 :         .output()
     574            0 :         .context("failed to extract public key from private key")?;
     575            0 :     if !keygen_output.status.success() {
     576            0 :         bail!(
     577            0 :             "openssl failed: '{}'",
     578            0 :             String::from_utf8_lossy(&keygen_output.stderr)
     579            0 :         );
     580            0 :     }
     581            0 :     Ok(())
     582            0 : }
     583              : 
     584              : #[cfg(test)]
     585              : mod tests {
     586              :     use super::*;
     587              : 
     588              :     #[test]
     589            2 :     fn simple_conf_parsing() {
     590            2 :         let simple_conf_toml = include_str!("../simple.conf");
     591            2 :         let simple_conf_parse_result = LocalEnv::parse_config(simple_conf_toml);
     592            2 :         assert!(
     593            2 :             simple_conf_parse_result.is_ok(),
     594            0 :             "failed to parse simple config {simple_conf_toml}, reason: {simple_conf_parse_result:?}"
     595              :         );
     596              : 
     597            2 :         let string_to_replace = "listen_addr = '127.0.0.1:50051'";
     598            2 :         let spoiled_url_str = "listen_addr = '!@$XOXO%^&'";
     599            2 :         let spoiled_url_toml = simple_conf_toml.replace(string_to_replace, spoiled_url_str);
     600            2 :         assert!(
     601            2 :             spoiled_url_toml.contains(spoiled_url_str),
     602            0 :             "Failed to replace string {string_to_replace} in the toml file {simple_conf_toml}"
     603              :         );
     604            2 :         let spoiled_url_parse_result = LocalEnv::parse_config(&spoiled_url_toml);
     605            2 :         assert!(
     606            2 :             spoiled_url_parse_result.is_err(),
     607            0 :             "expected toml with invalid Url {spoiled_url_toml} to fail the parsing, but got {spoiled_url_parse_result:?}"
     608              :         );
     609            2 :     }
     610              : }
        

Generated by: LCOV version 2.1-beta