LCOV - code coverage report
Current view: top level - control_plane/src - local_env.rs (source / functions) Coverage Total Hit
Test: 32f4a56327bc9da697706839ed4836b2a00a408f.info Lines: 81.3 % 337 274
Test Date: 2024-02-07 07:37:29 Functions: 60.9 % 110 67

            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       122071 : #[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 attachment_service.  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 attachment service.  If set, this will be propagated into the
      81              :     // attachment service'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        19727 : #[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         6573 :     fn default() -> Self {
     104         6573 :         NeonBroker {
     105         6573 :             listen_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
     106         6573 :         }
     107         6573 :     }
     108              : }
     109              : 
     110              : impl NeonBroker {
     111         1510 :     pub fn client_url(&self) -> Url {
     112         1510 :         Url::parse(&format!("http://{}", self.listen_addr)).expect("failed to construct url")
     113         1510 :     }
     114              : }
     115              : 
     116        85558 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     117              : #[serde(default)]
     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              : 
     131              : impl Default for PageServerConf {
     132         7778 :     fn default() -> Self {
     133         7778 :         Self {
     134         7778 :             id: NodeId(0),
     135         7778 :             listen_pg_addr: String::new(),
     136         7778 :             listen_http_addr: String::new(),
     137         7778 :             pg_auth_type: AuthType::Trust,
     138         7778 :             http_auth_type: AuthType::Trust,
     139         7778 :         }
     140         7778 :     }
     141              : }
     142              : 
     143       108569 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     144              : #[serde(default)]
     145              : pub struct SafekeeperConf {
     146              :     pub id: NodeId,
     147              :     pub pg_port: u16,
     148              :     pub pg_tenant_only_port: Option<u16>,
     149              :     pub http_port: u16,
     150              :     pub sync: bool,
     151              :     pub remote_storage: Option<String>,
     152              :     pub backup_threads: Option<u32>,
     153              :     pub auth_enabled: bool,
     154              : }
     155              : 
     156              : impl Default for SafekeeperConf {
     157         8361 :     fn default() -> Self {
     158         8361 :         Self {
     159         8361 :             id: NodeId(0),
     160         8361 :             pg_port: 0,
     161         8361 :             pg_tenant_only_port: None,
     162         8361 :             http_port: 0,
     163         8361 :             sync: true,
     164         8361 :             remote_storage: None,
     165         8361 :             backup_threads: None,
     166         8361 :             auth_enabled: false,
     167         8361 :         }
     168         8361 :     }
     169              : }
     170              : 
     171          710 : #[derive(Clone, Copy)]
     172              : pub enum InitForceMode {
     173              :     MustNotExist,
     174              :     EmptyDirOk,
     175              :     RemoveAllContents,
     176              : }
     177              : 
     178              : impl ValueEnum for InitForceMode {
     179         1066 :     fn value_variants<'a>() -> &'a [Self] {
     180         1066 :         &[
     181         1066 :             Self::MustNotExist,
     182         1066 :             Self::EmptyDirOk,
     183         1066 :             Self::RemoveAllContents,
     184         1066 :         ]
     185         1066 :     }
     186              : 
     187         7886 :     fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
     188         7886 :         Some(clap::builder::PossibleValue::new(match self {
     189         7174 :             InitForceMode::MustNotExist => "must-not-exist",
     190          356 :             InitForceMode::EmptyDirOk => "empty-dir-ok",
     191          356 :             InitForceMode::RemoveAllContents => "remove-all-contents",
     192              :         }))
     193         7886 :     }
     194              : }
     195              : 
     196              : impl SafekeeperConf {
     197              :     /// Compute is served by port on which only tenant scoped tokens allowed, if
     198              :     /// it is configured.
     199         1287 :     pub fn get_compute_port(&self) -> u16 {
     200         1287 :         self.pg_tenant_only_port.unwrap_or(self.pg_port)
     201         1287 :     }
     202              : }
     203              : 
     204              : impl LocalEnv {
     205          993 :     pub fn pg_distrib_dir_raw(&self) -> PathBuf {
     206          993 :         self.pg_distrib_dir.clone()
     207          993 :     }
     208              : 
     209         3394 :     pub fn pg_distrib_dir(&self, pg_version: u32) -> anyhow::Result<PathBuf> {
     210         3394 :         let path = self.pg_distrib_dir.clone();
     211         3394 : 
     212         3394 :         #[allow(clippy::manual_range_patterns)]
     213         3394 :         match pg_version {
     214         3394 :             14 | 15 | 16 => Ok(path.join(format!("v{pg_version}"))),
     215            0 :             _ => bail!("Unsupported postgres version: {}", pg_version),
     216              :         }
     217         3394 :     }
     218              : 
     219         2242 :     pub fn pg_bin_dir(&self, pg_version: u32) -> anyhow::Result<PathBuf> {
     220         2242 :         Ok(self.pg_distrib_dir(pg_version)?.join("bin"))
     221         2242 :     }
     222         1152 :     pub fn pg_lib_dir(&self, pg_version: u32) -> anyhow::Result<PathBuf> {
     223         1152 :         Ok(self.pg_distrib_dir(pg_version)?.join("lib"))
     224         1152 :     }
     225              : 
     226          993 :     pub fn pageserver_bin(&self) -> PathBuf {
     227          993 :         self.neon_distrib_dir.join("pageserver")
     228          993 :     }
     229              : 
     230          361 :     pub fn attachment_service_bin(&self) -> PathBuf {
     231          361 :         // Irrespective of configuration, attachment service binary is always
     232          361 :         // run from the same location as neon_local.  This means that for compatibility
     233          361 :         // tests that run old pageserver/safekeeper, they still run latest attachment service.
     234          361 :         let neon_local_bin_dir = env::current_exe().unwrap().parent().unwrap().to_owned();
     235          361 :         neon_local_bin_dir.join("attachment_service")
     236          361 :     }
     237              : 
     238          513 :     pub fn safekeeper_bin(&self) -> PathBuf {
     239          513 :         self.neon_distrib_dir.join("safekeeper")
     240          513 :     }
     241              : 
     242            3 :     pub fn storage_broker_bin(&self) -> PathBuf {
     243            3 :         self.neon_distrib_dir.join("storage_broker")
     244            3 :     }
     245              : 
     246        12244 :     pub fn endpoints_path(&self) -> PathBuf {
     247        12244 :         self.base_data_dir.join("endpoints")
     248        12244 :     }
     249              : 
     250         2203 :     pub fn pageserver_data_dir(&self, pageserver_id: NodeId) -> PathBuf {
     251         2203 :         self.base_data_dir
     252         2203 :             .join(format!("pageserver_{pageserver_id}"))
     253         2203 :     }
     254              : 
     255         2509 :     pub fn safekeeper_data_dir(&self, data_dir_name: &str) -> PathBuf {
     256         2509 :         self.base_data_dir.join("safekeepers").join(data_dir_name)
     257         2509 :     }
     258              : 
     259         2521 :     pub fn get_pageserver_conf(&self, id: NodeId) -> anyhow::Result<&PageServerConf> {
     260         2967 :         if let Some(conf) = self.pageservers.iter().find(|node| node.id == id) {
     261         2520 :             Ok(conf)
     262              :         } else {
     263            1 :             let have_ids = self
     264            1 :                 .pageservers
     265            1 :                 .iter()
     266            2 :                 .map(|node| format!("{}:{}", node.id, node.listen_http_addr))
     267            1 :                 .collect::<Vec<_>>();
     268            1 :             let joined = have_ids.join(",");
     269            1 :             bail!("could not find pageserver {id}, have ids {joined}")
     270              :         }
     271         2521 :     }
     272              : 
     273          797 :     pub fn register_branch_mapping(
     274          797 :         &mut self,
     275          797 :         branch_name: String,
     276          797 :         tenant_id: TenantId,
     277          797 :         timeline_id: TimelineId,
     278          797 :     ) -> anyhow::Result<()> {
     279          797 :         let existing_values = self
     280          797 :             .branch_name_mappings
     281          797 :             .entry(branch_name.clone())
     282          797 :             .or_default();
     283          797 : 
     284          797 :         let existing_ids = existing_values
     285          797 :             .iter()
     286          797 :             .find(|(existing_tenant_id, _)| existing_tenant_id == &tenant_id);
     287              : 
     288          797 :         if let Some((_, old_timeline_id)) = existing_ids {
     289            1 :             if old_timeline_id == &timeline_id {
     290            1 :                 Ok(())
     291              :             } else {
     292            0 :                 bail!("branch '{branch_name}' is already mapped to timeline {old_timeline_id}, cannot map to another timeline {timeline_id}");
     293              :             }
     294              :         } else {
     295          796 :             existing_values.push((tenant_id, timeline_id));
     296          796 :             Ok(())
     297              :         }
     298          797 :     }
     299              : 
     300          794 :     pub fn get_branch_timeline_id(
     301          794 :         &self,
     302          794 :         branch_name: &str,
     303          794 :         tenant_id: TenantId,
     304          794 :     ) -> Option<TimelineId> {
     305          794 :         self.branch_name_mappings
     306          794 :             .get(branch_name)?
     307          794 :             .iter()
     308          908 :             .find(|(mapped_tenant_id, _)| mapped_tenant_id == &tenant_id)
     309          794 :             .map(|&(_, timeline_id)| timeline_id)
     310          794 :             .map(TimelineId::from)
     311          794 :     }
     312              : 
     313           19 :     pub fn timeline_name_mappings(&self) -> HashMap<TenantTimelineId, String> {
     314           19 :         self.branch_name_mappings
     315           19 :             .iter()
     316           36 :             .flat_map(|(name, tenant_timelines)| {
     317           38 :                 tenant_timelines.iter().map(|&(tenant_id, timeline_id)| {
     318           38 :                     (TenantTimelineId::new(tenant_id, timeline_id), name.clone())
     319           38 :                 })
     320           36 :             })
     321           19 :             .collect()
     322           19 :     }
     323              : 
     324              :     /// Create a LocalEnv from a config file.
     325              :     ///
     326              :     /// Unlike 'load_config', this function fills in any defaults that are missing
     327              :     /// from the config file.
     328          358 :     pub fn parse_config(toml: &str) -> anyhow::Result<Self> {
     329          358 :         let mut env: LocalEnv = toml::from_str(toml)?;
     330              : 
     331              :         // Find postgres binaries.
     332              :         // Follow POSTGRES_DISTRIB_DIR if set, otherwise look in "pg_install".
     333              :         // Note that later in the code we assume, that distrib dirs follow the same pattern
     334              :         // for all postgres versions.
     335          356 :         if env.pg_distrib_dir == Path::new("") {
     336          356 :             if let Some(postgres_bin) = env::var_os("POSTGRES_DISTRIB_DIR") {
     337          354 :                 env.pg_distrib_dir = postgres_bin.into();
     338          354 :             } else {
     339            2 :                 let cwd = env::current_dir()?;
     340            2 :                 env.pg_distrib_dir = cwd.join("pg_install")
     341              :             }
     342            0 :         }
     343              : 
     344              :         // Find neon binaries.
     345          356 :         if env.neon_distrib_dir == Path::new("") {
     346          356 :             env.neon_distrib_dir = env::current_exe()?.parent().unwrap().to_owned();
     347            0 :         }
     348              : 
     349          356 :         if env.pageservers.is_empty() {
     350            0 :             anyhow::bail!("Configuration must contain at least one pageserver");
     351          356 :         }
     352          356 : 
     353          356 :         env.base_data_dir = base_path();
     354          356 : 
     355          356 :         Ok(env)
     356          358 :     }
     357              : 
     358              :     /// Locate and load config
     359         6217 :     pub fn load_config() -> anyhow::Result<Self> {
     360         6217 :         let repopath = base_path();
     361         6217 : 
     362         6217 :         if !repopath.exists() {
     363            0 :             bail!(
     364            0 :                 "Neon config is not found in {}. You need to run 'neon_local init' first",
     365            0 :                 repopath.to_str().unwrap()
     366            0 :             );
     367         6217 :         }
     368              : 
     369              :         // TODO: check that it looks like a neon repository
     370              : 
     371              :         // load and parse file
     372         6217 :         let config = fs::read_to_string(repopath.join("config"))?;
     373         6217 :         let mut env: LocalEnv = toml::from_str(config.as_str())?;
     374              : 
     375         6217 :         env.base_data_dir = repopath;
     376         6217 : 
     377         6217 :         Ok(env)
     378         6217 :     }
     379              : 
     380         1504 :     pub fn persist_config(&self, base_path: &Path) -> anyhow::Result<()> {
     381         1504 :         // Currently, the user first passes a config file with 'neon_local init --config=<path>'
     382         1504 :         // We read that in, in `create_config`, and fill any missing defaults. Then it's saved
     383         1504 :         // to .neon/config. TODO: We lose any formatting and comments along the way, which is
     384         1504 :         // a bit sad.
     385         1504 :         let mut conf_content = r#"# This file describes a local deployment of the page server
     386         1504 : # and safekeeeper node. It is read by the 'neon_local' command-line
     387         1504 : # utility.
     388         1504 : "#
     389         1504 :         .to_string();
     390         1504 : 
     391         1504 :         // Convert the LocalEnv to a toml file.
     392         1504 :         //
     393         1504 :         // This could be as simple as this:
     394         1504 :         //
     395         1504 :         // conf_content += &toml::to_string_pretty(env)?;
     396         1504 :         //
     397         1504 :         // But it results in a "values must be emitted before tables". I'm not sure
     398         1504 :         // why, AFAICS the table, i.e. 'safekeepers: Vec<SafekeeperConf>' is last.
     399         1504 :         // Maybe rust reorders the fields to squeeze avoid padding or something?
     400         1504 :         // In any case, converting to toml::Value first, and serializing that, works.
     401         1504 :         // See https://github.com/alexcrichton/toml-rs/issues/142
     402         1504 :         conf_content += &toml::to_string_pretty(&toml::Value::try_from(self)?)?;
     403              : 
     404         1504 :         let target_config_path = base_path.join("config");
     405         1504 :         fs::write(&target_config_path, conf_content).with_context(|| {
     406            0 :             format!(
     407            0 :                 "Failed to write config file into path '{}'",
     408            0 :                 target_config_path.display()
     409            0 :             )
     410         1504 :         })
     411         1504 :     }
     412              : 
     413              :     // this function is used only for testing purposes in CLI e g generate tokens during init
     414          184 :     pub fn generate_auth_token(&self, claims: &Claims) -> anyhow::Result<String> {
     415          184 :         let private_key_path = if self.private_key_path.is_absolute() {
     416            0 :             self.private_key_path.to_path_buf()
     417              :         } else {
     418          184 :             self.base_data_dir.join(&self.private_key_path)
     419              :         };
     420              : 
     421          184 :         let key_data = fs::read(private_key_path)?;
     422          184 :         encode_from_key_file(claims, &key_data)
     423          184 :     }
     424              : 
     425              :     //
     426              :     // Initialize a new Neon repository
     427              :     //
     428          354 :     pub fn init(&mut self, pg_version: u32, force: &InitForceMode) -> anyhow::Result<()> {
     429          354 :         // check if config already exists
     430          354 :         let base_path = &self.base_data_dir;
     431          354 :         ensure!(
     432          354 :             base_path != Path::new(""),
     433            0 :             "repository base path is missing"
     434              :         );
     435              : 
     436          354 :         if base_path.exists() {
     437            0 :             match force {
     438              :                 InitForceMode::MustNotExist => {
     439            0 :                     bail!(
     440            0 :                         "directory '{}' already exists. Perhaps already initialized?",
     441            0 :                         base_path.display()
     442            0 :                     );
     443              :                 }
     444              :                 InitForceMode::EmptyDirOk => {
     445            0 :                     if let Some(res) = std::fs::read_dir(base_path)?.next() {
     446            0 :                         res.context("check if directory is empty")?;
     447            0 :                         anyhow::bail!("directory not empty: {base_path:?}");
     448            0 :                     }
     449              :                 }
     450              :                 InitForceMode::RemoveAllContents => {
     451            0 :                     println!("removing all contents of '{}'", base_path.display());
     452              :                     // instead of directly calling `remove_dir_all`, we keep the original dir but removing
     453              :                     // all contents inside. This helps if the developer symbol links another directory (i.e.,
     454              :                     // S3 local SSD) to the `.neon` base directory.
     455            0 :                     for entry in std::fs::read_dir(base_path)? {
     456            0 :                         let entry = entry?;
     457            0 :                         let path = entry.path();
     458            0 :                         if path.is_dir() {
     459            0 :                             fs::remove_dir_all(&path)?;
     460              :                         } else {
     461            0 :                             fs::remove_file(&path)?;
     462              :                         }
     463              :                     }
     464              :                 }
     465              :             }
     466          354 :         }
     467              : 
     468          354 :         if !self.pg_bin_dir(pg_version)?.join("postgres").exists() {
     469            0 :             bail!(
     470            0 :                 "Can't find postgres binary at {}",
     471            0 :                 self.pg_bin_dir(pg_version)?.display()
     472              :             );
     473          354 :         }
     474          708 :         for binary in ["pageserver", "safekeeper"] {
     475          708 :             if !self.neon_distrib_dir.join(binary).exists() {
     476            0 :                 bail!(
     477            0 :                     "Can't find binary '{binary}' in neon distrib dir '{}'",
     478            0 :                     self.neon_distrib_dir.display()
     479            0 :                 );
     480          708 :             }
     481              :         }
     482              : 
     483          354 :         if !base_path.exists() {
     484          354 :             fs::create_dir(base_path)?;
     485            0 :         }
     486              : 
     487              :         // Generate keypair for JWT.
     488              :         //
     489              :         // The keypair is only needed if authentication is enabled in any of the
     490              :         // components. For convenience, we generate the keypair even if authentication
     491              :         // is not enabled, so that you can easily enable it after the initialization
     492              :         // step. However, if the key generation fails, we treat it as non-fatal if
     493              :         // authentication was not enabled.
     494          354 :         if self.private_key_path == PathBuf::new() {
     495          354 :             match generate_auth_keys(
     496          354 :                 base_path.join("auth_private_key.pem").as_path(),
     497          354 :                 base_path.join("auth_public_key.pem").as_path(),
     498          354 :             ) {
     499          354 :                 Ok(()) => {
     500          354 :                     self.private_key_path = PathBuf::from("auth_private_key.pem");
     501          354 :                 }
     502            0 :                 Err(e) => {
     503            0 :                     if !self.auth_keys_needed() {
     504            0 :                         eprintln!("Could not generate keypair for JWT authentication: {e}");
     505            0 :                         eprintln!("Continuing anyway because authentication was not enabled");
     506            0 :                         self.private_key_path = PathBuf::from("auth_private_key.pem");
     507            0 :                     } else {
     508            0 :                         return Err(e);
     509              :                     }
     510              :                 }
     511              :             }
     512            0 :         }
     513              : 
     514          354 :         fs::create_dir_all(self.endpoints_path())?;
     515              : 
     516          774 :         for safekeeper in &self.safekeepers {
     517          420 :             fs::create_dir_all(SafekeeperNode::datadir_path_by_id(self, safekeeper.id))?;
     518              :         }
     519              : 
     520          354 :         self.persist_config(base_path)
     521          354 :     }
     522              : 
     523            0 :     fn auth_keys_needed(&self) -> bool {
     524            0 :         self.pageservers.iter().any(|ps| {
     525            0 :             ps.pg_auth_type == AuthType::NeonJWT || ps.http_auth_type == AuthType::NeonJWT
     526            0 :         }) || self.safekeepers.iter().any(|sk| sk.auth_enabled)
     527            0 :     }
     528              : }
     529              : 
     530         6573 : fn base_path() -> PathBuf {
     531         6573 :     match std::env::var_os("NEON_REPO_DIR") {
     532         6571 :         Some(val) => PathBuf::from(val),
     533            2 :         None => PathBuf::from(".neon"),
     534              :     }
     535         6573 : }
     536              : 
     537              : /// Generate a public/private key pair for JWT authentication
     538          354 : fn generate_auth_keys(private_key_path: &Path, public_key_path: &Path) -> anyhow::Result<()> {
     539              :     // Generate the key pair
     540              :     //
     541              :     // openssl genpkey -algorithm ed25519 -out auth_private_key.pem
     542          354 :     let keygen_output = Command::new("openssl")
     543          354 :         .arg("genpkey")
     544          354 :         .args(["-algorithm", "ed25519"])
     545          354 :         .args(["-out", private_key_path.to_str().unwrap()])
     546          354 :         .stdout(Stdio::null())
     547          354 :         .output()
     548          354 :         .context("failed to generate auth private key")?;
     549          354 :     if !keygen_output.status.success() {
     550            0 :         bail!(
     551            0 :             "openssl failed: '{}'",
     552            0 :             String::from_utf8_lossy(&keygen_output.stderr)
     553            0 :         );
     554          354 :     }
     555              :     // Extract the public key from the private key file
     556              :     //
     557              :     // openssl pkey -in auth_private_key.pem -pubout -out auth_public_key.pem
     558          354 :     let keygen_output = Command::new("openssl")
     559          354 :         .arg("pkey")
     560          354 :         .args(["-in", private_key_path.to_str().unwrap()])
     561          354 :         .arg("-pubout")
     562          354 :         .args(["-out", public_key_path.to_str().unwrap()])
     563          354 :         .output()
     564          354 :         .context("failed to extract public key from private key")?;
     565          354 :     if !keygen_output.status.success() {
     566            0 :         bail!(
     567            0 :             "openssl failed: '{}'",
     568            0 :             String::from_utf8_lossy(&keygen_output.stderr)
     569            0 :         );
     570          354 :     }
     571          354 :     Ok(())
     572          354 : }
     573              : 
     574              : #[cfg(test)]
     575              : mod tests {
     576              :     use super::*;
     577              : 
     578            2 :     #[test]
     579            2 :     fn simple_conf_parsing() {
     580            2 :         let simple_conf_toml = include_str!("../simple.conf");
     581            2 :         let simple_conf_parse_result = LocalEnv::parse_config(simple_conf_toml);
     582            2 :         assert!(
     583            2 :             simple_conf_parse_result.is_ok(),
     584            0 :             "failed to parse simple config {simple_conf_toml}, reason: {simple_conf_parse_result:?}"
     585              :         );
     586              : 
     587            2 :         let string_to_replace = "listen_addr = '127.0.0.1:50051'";
     588            2 :         let spoiled_url_str = "listen_addr = '!@$XOXO%^&'";
     589            2 :         let spoiled_url_toml = simple_conf_toml.replace(string_to_replace, spoiled_url_str);
     590            2 :         assert!(
     591            2 :             spoiled_url_toml.contains(spoiled_url_str),
     592            0 :             "Failed to replace string {string_to_replace} in the toml file {simple_conf_toml}"
     593              :         );
     594            2 :         let spoiled_url_parse_result = LocalEnv::parse_config(&spoiled_url_toml);
     595            2 :         assert!(
     596            2 :             spoiled_url_parse_result.is_err(),
     597            0 :             "expected toml with invalid Url {spoiled_url_toml} to fail the parsing, but got {spoiled_url_parse_result:?}"
     598              :         );
     599            2 :     }
     600              : }
        

Generated by: LCOV version 2.1-beta