LCOV - code coverage report
Current view: top level - control_plane/src - local_env.rs (source / functions) Coverage Total Hit
Test: 691a4c28fe7169edd60b367c52d448a0a6605f1f.info Lines: 2.9 % 445 13
Test Date: 2024-05-10 13:18:37 Functions: 1.2 % 165 2

            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, 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 std::time::Duration;
      21              : use utils::{
      22              :     auth::{encode_from_key_file, Claims},
      23              :     id::{NodeId, TenantId, TenantTimelineId, TimelineId},
      24              : };
      25              : 
      26              : use crate::pageserver::PageServerNode;
      27              : use crate::pageserver::PAGESERVER_REMOTE_STORAGE_DIR;
      28              : use crate::safekeeper::SafekeeperNode;
      29              : 
      30              : pub const DEFAULT_PG_VERSION: u32 = 15;
      31              : 
      32              : //
      33              : // This data structures represents neon_local CLI config
      34              : //
      35              : // It is deserialized from the .neon/config file, or the config file passed
      36              : // to 'neon_local init --config=<path>' option. See control_plane/simple.conf for
      37              : // an example.
      38              : //
      39              : #[derive(PartialEq, Eq, Clone, Debug)]
      40              : pub struct LocalEnv {
      41              :     // Base directory for all the nodes (the pageserver, safekeepers and
      42              :     // compute endpoints).
      43              :     //
      44              :     // This is not stored in the config file. Rather, this is the path where the
      45              :     // config file itself is. It is read from the NEON_REPO_DIR env variable or
      46              :     // '.neon' if not given.
      47              :     pub base_data_dir: PathBuf,
      48              : 
      49              :     // Path to postgres distribution. It's expected that "bin", "include",
      50              :     // "lib", "share" from postgres distribution are there. If at some point
      51              :     // in time we will be able to run against vanilla postgres we may split that
      52              :     // to four separate paths and match OS-specific installation layout.
      53              :     pub pg_distrib_dir: PathBuf,
      54              : 
      55              :     // Path to pageserver binary.
      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              :     pub default_tenant_id: Option<TenantId>,
      61              : 
      62              :     // used to issue tokens during e.g pg start
      63              :     pub private_key_path: PathBuf,
      64              : 
      65              :     pub broker: NeonBroker,
      66              : 
      67              :     // Configuration for the storage controller (1 per neon_local environment)
      68              :     pub storage_controller: NeonStorageControllerConf,
      69              : 
      70              :     /// This Vec must always contain at least one pageserver
      71              :     /// Populdated by [`Self::load_config`] from the individual `pageserver.toml`s.
      72              :     /// NB: not used anymore except for informing users that they need to change their `.neon/config`.
      73              :     pub pageservers: Vec<PageServerConf>,
      74              : 
      75              :     pub safekeepers: Vec<SafekeeperConf>,
      76              : 
      77              :     // Control plane upcall API for pageserver: if None, we will not run storage_controller  If set, this will
      78              :     // be propagated into each pageserver's configuration.
      79              :     pub control_plane_api: Option<Url>,
      80              : 
      81              :     // Control plane upcall API for storage controller.  If set, this will be propagated into the
      82              :     // storage controller's configuration.
      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              :     // A `HashMap<String, HashMap<TenantId, TimelineId>>` would be more appropriate here,
      87              :     // but deserialization into a generic toml object as `toml::Value::try_from` fails with an error.
      88              :     // https://toml.io/en/v1.0.0 does not contain a concept of "a table inside another table".
      89              :     pub branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
      90              : }
      91              : 
      92              : /// On-disk state stored in `.neon/config`.
      93            0 : #[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize)]
      94              : #[serde(default, deny_unknown_fields)]
      95              : pub struct OnDiskConfig {
      96              :     pub pg_distrib_dir: PathBuf,
      97              :     pub neon_distrib_dir: PathBuf,
      98              :     pub default_tenant_id: Option<TenantId>,
      99              :     pub private_key_path: PathBuf,
     100              :     pub broker: NeonBroker,
     101              :     pub storage_controller: NeonStorageControllerConf,
     102              :     #[serde(
     103              :         skip_serializing,
     104              :         deserialize_with = "fail_if_pageservers_field_specified"
     105              :     )]
     106              :     pub pageservers: Vec<PageServerConf>,
     107              :     pub safekeepers: Vec<SafekeeperConf>,
     108              :     pub control_plane_api: Option<Url>,
     109              :     pub control_plane_compute_hook_api: Option<Url>,
     110              :     branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
     111              : }
     112              : 
     113            0 : fn fail_if_pageservers_field_specified<'de, D>(_: D) -> Result<Vec<PageServerConf>, D::Error>
     114            0 : where
     115            0 :     D: serde::Deserializer<'de>,
     116            0 : {
     117            0 :     Err(serde::de::Error::custom(
     118            0 :         "The 'pageservers' field is no longer used; pageserver.toml is now authoritative; \
     119            0 :          Please remove the `pageservers` from your .neon/config.",
     120            0 :     ))
     121            0 : }
     122              : 
     123              : /// The description of the neon_local env to be initialized by `neon_local init --config`.
     124            0 : #[derive(Clone, Debug, Deserialize)]
     125              : #[serde(deny_unknown_fields)]
     126              : pub struct NeonLocalInitConf {
     127              :     // TODO: do we need this? Seems unused
     128              :     pub pg_distrib_dir: Option<PathBuf>,
     129              :     // TODO: do we need this? Seems unused
     130              :     pub neon_distrib_dir: Option<PathBuf>,
     131              :     pub default_tenant_id: TenantId,
     132              :     pub broker: NeonBroker,
     133              :     pub storage_controller: Option<NeonStorageControllerConf>,
     134              :     pub pageservers: Vec<NeonLocalInitPageserverConf>,
     135              :     pub safekeepers: Vec<SafekeeperConf>,
     136              :     pub control_plane_api: Option<Option<Url>>,
     137              :     pub control_plane_compute_hook_api: Option<Option<Url>>,
     138              : }
     139              : 
     140              : /// Broker config for cluster internal communication.
     141            0 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     142              : #[serde(default)]
     143              : pub struct NeonBroker {
     144              :     /// Broker listen address for storage nodes coordination, e.g. '127.0.0.1:50051'.
     145              :     pub listen_addr: SocketAddr,
     146              : }
     147              : 
     148              : /// Broker config for cluster internal communication.
     149            0 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     150              : #[serde(default)]
     151              : pub struct NeonStorageControllerConf {
     152              :     /// Heartbeat timeout before marking a node offline
     153              :     #[serde(with = "humantime_serde")]
     154              :     pub max_unavailable: Duration,
     155              : }
     156              : 
     157              : impl NeonStorageControllerConf {
     158              :     // Use a shorter pageserver unavailability interval than the default to speed up tests.
     159              :     const DEFAULT_MAX_UNAVAILABLE_INTERVAL: std::time::Duration =
     160              :         std::time::Duration::from_secs(10);
     161              : }
     162              : 
     163              : impl Default for NeonStorageControllerConf {
     164            0 :     fn default() -> Self {
     165            0 :         Self {
     166            0 :             max_unavailable: Self::DEFAULT_MAX_UNAVAILABLE_INTERVAL,
     167            0 :         }
     168            0 :     }
     169              : }
     170              : 
     171              : // Dummy Default impl to satisfy Deserialize derive.
     172              : impl Default for NeonBroker {
     173            0 :     fn default() -> Self {
     174            0 :         NeonBroker {
     175            0 :             listen_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
     176            0 :         }
     177            0 :     }
     178              : }
     179              : 
     180              : impl NeonBroker {
     181            0 :     pub fn client_url(&self) -> Url {
     182            0 :         Url::parse(&format!("http://{}", self.listen_addr)).expect("failed to construct url")
     183            0 :     }
     184              : }
     185              : 
     186              : // neon_local needs to know this subset of pageserver configuration.
     187              : // For legacy reasons, this information is duplicated from `pageserver.toml` into `.neon/config`.
     188              : // It can get stale if `pageserver.toml` is changed.
     189              : // TODO(christian): don't store this at all in `.neon/config`, always load it from `pageserver.toml`
     190            0 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     191              : #[serde(default, deny_unknown_fields)]
     192              : pub struct PageServerConf {
     193              :     pub id: NodeId,
     194              :     pub listen_pg_addr: String,
     195              :     pub listen_http_addr: String,
     196              :     pub pg_auth_type: AuthType,
     197              :     pub http_auth_type: AuthType,
     198              : }
     199              : 
     200              : impl Default for PageServerConf {
     201            0 :     fn default() -> Self {
     202            0 :         Self {
     203            0 :             id: NodeId(0),
     204            0 :             listen_pg_addr: String::new(),
     205            0 :             listen_http_addr: String::new(),
     206            0 :             pg_auth_type: AuthType::Trust,
     207            0 :             http_auth_type: AuthType::Trust,
     208            0 :         }
     209            0 :     }
     210              : }
     211              : 
     212              : /// The toml that can be passed to `neon_local init --config`.
     213              : /// This is a subset of the `pageserver.toml` configuration.
     214              : // TODO(christian): use pageserver_api::config::ConfigToml (PR #7656)
     215            0 : #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
     216              : pub struct NeonLocalInitPageserverConf {
     217              :     pub id: NodeId,
     218              :     pub listen_pg_addr: String,
     219              :     pub listen_http_addr: String,
     220              :     pub pg_auth_type: AuthType,
     221              :     pub http_auth_type: AuthType,
     222              :     #[serde(flatten)]
     223              :     pub other: HashMap<String, toml::Value>,
     224              : }
     225              : 
     226              : impl From<&NeonLocalInitPageserverConf> for PageServerConf {
     227            0 :     fn from(conf: &NeonLocalInitPageserverConf) -> Self {
     228            0 :         let NeonLocalInitPageserverConf {
     229            0 :             id,
     230            0 :             listen_pg_addr,
     231            0 :             listen_http_addr,
     232            0 :             pg_auth_type,
     233            0 :             http_auth_type,
     234            0 :             other: _,
     235            0 :         } = conf;
     236            0 :         Self {
     237            0 :             id: *id,
     238            0 :             listen_pg_addr: listen_pg_addr.clone(),
     239            0 :             listen_http_addr: listen_http_addr.clone(),
     240            0 :             pg_auth_type: *pg_auth_type,
     241            0 :             http_auth_type: *http_auth_type,
     242            0 :         }
     243            0 :     }
     244              : }
     245              : 
     246            0 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     247              : #[serde(default)]
     248              : pub struct SafekeeperConf {
     249              :     pub id: NodeId,
     250              :     pub pg_port: u16,
     251              :     pub pg_tenant_only_port: Option<u16>,
     252              :     pub http_port: u16,
     253              :     pub sync: bool,
     254              :     pub remote_storage: Option<String>,
     255              :     pub backup_threads: Option<u32>,
     256              :     pub auth_enabled: bool,
     257              :     pub listen_addr: Option<String>,
     258              : }
     259              : 
     260              : impl Default for SafekeeperConf {
     261            0 :     fn default() -> Self {
     262            0 :         Self {
     263            0 :             id: NodeId(0),
     264            0 :             pg_port: 0,
     265            0 :             pg_tenant_only_port: None,
     266            0 :             http_port: 0,
     267            0 :             sync: true,
     268            0 :             remote_storage: None,
     269            0 :             backup_threads: None,
     270            0 :             auth_enabled: false,
     271            0 :             listen_addr: None,
     272            0 :         }
     273            0 :     }
     274              : }
     275              : 
     276              : #[derive(Clone, Copy)]
     277              : pub enum InitForceMode {
     278              :     MustNotExist,
     279              :     EmptyDirOk,
     280              :     RemoveAllContents,
     281              : }
     282              : 
     283              : impl ValueEnum for InitForceMode {
     284            4 :     fn value_variants<'a>() -> &'a [Self] {
     285            4 :         &[
     286            4 :             Self::MustNotExist,
     287            4 :             Self::EmptyDirOk,
     288            4 :             Self::RemoveAllContents,
     289            4 :         ]
     290            4 :     }
     291              : 
     292           10 :     fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
     293           10 :         Some(clap::builder::PossibleValue::new(match self {
     294            6 :             InitForceMode::MustNotExist => "must-not-exist",
     295            2 :             InitForceMode::EmptyDirOk => "empty-dir-ok",
     296            2 :             InitForceMode::RemoveAllContents => "remove-all-contents",
     297              :         }))
     298           10 :     }
     299              : }
     300              : 
     301              : impl SafekeeperConf {
     302              :     /// Compute is served by port on which only tenant scoped tokens allowed, if
     303              :     /// it is configured.
     304            0 :     pub fn get_compute_port(&self) -> u16 {
     305            0 :         self.pg_tenant_only_port.unwrap_or(self.pg_port)
     306            0 :     }
     307              : }
     308              : 
     309              : impl LocalEnv {
     310            0 :     pub fn pg_distrib_dir_raw(&self) -> PathBuf {
     311            0 :         self.pg_distrib_dir.clone()
     312            0 :     }
     313              : 
     314            0 :     pub fn pg_distrib_dir(&self, pg_version: u32) -> anyhow::Result<PathBuf> {
     315            0 :         let path = self.pg_distrib_dir.clone();
     316            0 : 
     317            0 :         #[allow(clippy::manual_range_patterns)]
     318            0 :         match pg_version {
     319            0 :             14 | 15 | 16 => Ok(path.join(format!("v{pg_version}"))),
     320            0 :             _ => bail!("Unsupported postgres version: {}", pg_version),
     321              :         }
     322            0 :     }
     323              : 
     324            0 :     pub fn pg_bin_dir(&self, pg_version: u32) -> anyhow::Result<PathBuf> {
     325            0 :         Ok(self.pg_distrib_dir(pg_version)?.join("bin"))
     326            0 :     }
     327            0 :     pub fn pg_lib_dir(&self, pg_version: u32) -> anyhow::Result<PathBuf> {
     328            0 :         Ok(self.pg_distrib_dir(pg_version)?.join("lib"))
     329            0 :     }
     330              : 
     331            0 :     pub fn pageserver_bin(&self) -> PathBuf {
     332            0 :         self.neon_distrib_dir.join("pageserver")
     333            0 :     }
     334              : 
     335            0 :     pub fn storage_controller_bin(&self) -> PathBuf {
     336            0 :         // Irrespective of configuration, storage controller binary is always
     337            0 :         // run from the same location as neon_local.  This means that for compatibility
     338            0 :         // tests that run old pageserver/safekeeper, they still run latest storage controller.
     339            0 :         let neon_local_bin_dir = env::current_exe().unwrap().parent().unwrap().to_owned();
     340            0 :         neon_local_bin_dir.join("storage_controller")
     341            0 :     }
     342              : 
     343            0 :     pub fn safekeeper_bin(&self) -> PathBuf {
     344            0 :         self.neon_distrib_dir.join("safekeeper")
     345            0 :     }
     346              : 
     347            0 :     pub fn storage_broker_bin(&self) -> PathBuf {
     348            0 :         self.neon_distrib_dir.join("storage_broker")
     349            0 :     }
     350              : 
     351            0 :     pub fn endpoints_path(&self) -> PathBuf {
     352            0 :         self.base_data_dir.join("endpoints")
     353            0 :     }
     354              : 
     355            0 :     pub fn pageserver_data_dir(&self, pageserver_id: NodeId) -> PathBuf {
     356            0 :         self.base_data_dir
     357            0 :             .join(format!("pageserver_{pageserver_id}"))
     358            0 :     }
     359              : 
     360            0 :     pub fn safekeeper_data_dir(&self, data_dir_name: &str) -> PathBuf {
     361            0 :         self.base_data_dir.join("safekeepers").join(data_dir_name)
     362            0 :     }
     363              : 
     364            0 :     pub fn get_pageserver_conf(&self, id: NodeId) -> anyhow::Result<&PageServerConf> {
     365            0 :         if let Some(conf) = self.pageservers.iter().find(|node| node.id == id) {
     366            0 :             Ok(conf)
     367              :         } else {
     368            0 :             let have_ids = self
     369            0 :                 .pageservers
     370            0 :                 .iter()
     371            0 :                 .map(|node| format!("{}:{}", node.id, node.listen_http_addr))
     372            0 :                 .collect::<Vec<_>>();
     373            0 :             let joined = have_ids.join(",");
     374            0 :             bail!("could not find pageserver {id}, have ids {joined}")
     375              :         }
     376            0 :     }
     377              : 
     378            0 :     pub fn register_branch_mapping(
     379            0 :         &mut self,
     380            0 :         branch_name: String,
     381            0 :         tenant_id: TenantId,
     382            0 :         timeline_id: TimelineId,
     383            0 :     ) -> anyhow::Result<()> {
     384            0 :         let existing_values = self
     385            0 :             .branch_name_mappings
     386            0 :             .entry(branch_name.clone())
     387            0 :             .or_default();
     388            0 : 
     389            0 :         let existing_ids = existing_values
     390            0 :             .iter()
     391            0 :             .find(|(existing_tenant_id, _)| existing_tenant_id == &tenant_id);
     392              : 
     393            0 :         if let Some((_, old_timeline_id)) = existing_ids {
     394            0 :             if old_timeline_id == &timeline_id {
     395            0 :                 Ok(())
     396              :             } else {
     397            0 :                 bail!("branch '{branch_name}' is already mapped to timeline {old_timeline_id}, cannot map to another timeline {timeline_id}");
     398              :             }
     399              :         } else {
     400            0 :             existing_values.push((tenant_id, timeline_id));
     401            0 :             Ok(())
     402              :         }
     403            0 :     }
     404              : 
     405            0 :     pub fn get_branch_timeline_id(
     406            0 :         &self,
     407            0 :         branch_name: &str,
     408            0 :         tenant_id: TenantId,
     409            0 :     ) -> Option<TimelineId> {
     410            0 :         self.branch_name_mappings
     411            0 :             .get(branch_name)?
     412            0 :             .iter()
     413            0 :             .find(|(mapped_tenant_id, _)| mapped_tenant_id == &tenant_id)
     414            0 :             .map(|&(_, timeline_id)| timeline_id)
     415            0 :             .map(TimelineId::from)
     416            0 :     }
     417              : 
     418            0 :     pub fn timeline_name_mappings(&self) -> HashMap<TenantTimelineId, String> {
     419            0 :         self.branch_name_mappings
     420            0 :             .iter()
     421            0 :             .flat_map(|(name, tenant_timelines)| {
     422            0 :                 tenant_timelines.iter().map(|&(tenant_id, timeline_id)| {
     423            0 :                     (TenantTimelineId::new(tenant_id, timeline_id), name.clone())
     424            0 :                 })
     425            0 :             })
     426            0 :             .collect()
     427            0 :     }
     428              : 
     429              :     ///  Construct `Self` from on-disk state.
     430            0 :     pub fn load_config() -> anyhow::Result<Self> {
     431            0 :         let repopath = base_path();
     432            0 : 
     433            0 :         if !repopath.exists() {
     434            0 :             bail!(
     435            0 :                 "Neon config is not found in {}. You need to run 'neon_local init' first",
     436            0 :                 repopath.to_str().unwrap()
     437            0 :             );
     438            0 :         }
     439              : 
     440              :         // TODO: check that it looks like a neon repository
     441              : 
     442              :         // load and parse file
     443            0 :         let config_file_contents = fs::read_to_string(repopath.join("config"))?;
     444            0 :         let on_disk_config: OnDiskConfig = toml::from_str(config_file_contents.as_str())?;
     445            0 :         let mut env = {
     446            0 :             let OnDiskConfig {
     447            0 :                 pg_distrib_dir,
     448            0 :                 neon_distrib_dir,
     449            0 :                 default_tenant_id,
     450            0 :                 private_key_path,
     451            0 :                 broker,
     452            0 :                 storage_controller,
     453            0 :                 pageservers,
     454            0 :                 safekeepers,
     455            0 :                 control_plane_api,
     456            0 :                 control_plane_compute_hook_api,
     457            0 :                 branch_name_mappings,
     458            0 :             } = on_disk_config;
     459            0 :             LocalEnv {
     460            0 :                 base_data_dir: repopath.clone(),
     461            0 :                 pg_distrib_dir,
     462            0 :                 neon_distrib_dir,
     463            0 :                 default_tenant_id,
     464            0 :                 private_key_path,
     465            0 :                 broker,
     466            0 :                 storage_controller,
     467            0 :                 pageservers,
     468            0 :                 safekeepers,
     469            0 :                 control_plane_api,
     470            0 :                 control_plane_compute_hook_api,
     471            0 :                 branch_name_mappings,
     472            0 :             }
     473            0 :         };
     474            0 : 
     475            0 :         // The source of truth for pageserver configuration is the pageserver.toml.
     476            0 :         assert!(
     477            0 :             env.pageservers.is_empty(),
     478            0 :             "we ensure this during deserialization"
     479              :         );
     480            0 :         env.pageservers = {
     481            0 :             let iter = std::fs::read_dir(&repopath).context("open dir")?;
     482            0 :             let mut pageservers = Vec::new();
     483            0 :             for res in iter {
     484            0 :                 let dentry = res?;
     485              :                 const PREFIX: &str = "pageserver_";
     486            0 :                 let dentry_name = dentry
     487            0 :                     .file_name()
     488            0 :                     .into_string()
     489            0 :                     .ok()
     490            0 :                     .with_context(|| format!("non-utf8 dentry: {:?}", dentry.path()))
     491            0 :                     .unwrap();
     492            0 :                 if !dentry_name.starts_with(PREFIX) {
     493            0 :                     continue;
     494            0 :                 }
     495            0 :                 if !dentry.file_type().context("determine file type")?.is_dir() {
     496            0 :                     anyhow::bail!("expected a directory, got {:?}", dentry.path());
     497            0 :                 }
     498            0 :                 let id = dentry_name[PREFIX.len()..]
     499            0 :                     .parse::<NodeId>()
     500            0 :                     .with_context(|| format!("parse id from {:?}", dentry.path()))?;
     501              :                 // TODO(christian): use pageserver_api::config::ConfigToml (PR #7656)
     502            0 :                 #[derive(serde::Serialize, serde::Deserialize)]
     503              :                 // (allow unknown fields, unlike PageServerConf)
     504              :                 struct PageserverConfigTomlSubset {
     505              :                     id: NodeId,
     506              :                     listen_pg_addr: String,
     507              :                     listen_http_addr: String,
     508              :                     pg_auth_type: AuthType,
     509              :                     http_auth_type: AuthType,
     510              :                 }
     511            0 :                 let config_toml_path = dentry.path().join("pageserver.toml");
     512            0 :                 let config_toml: PageserverConfigTomlSubset = toml_edit::de::from_str(
     513            0 :                     &std::fs::read_to_string(&config_toml_path)
     514            0 :                         .with_context(|| format!("read {:?}", config_toml_path))?,
     515              :                 )
     516            0 :                 .context("parse pageserver.toml")?;
     517              :                 let PageserverConfigTomlSubset {
     518            0 :                     id: config_toml_id,
     519            0 :                     listen_pg_addr,
     520            0 :                     listen_http_addr,
     521            0 :                     pg_auth_type,
     522            0 :                     http_auth_type,
     523            0 :                 } = config_toml;
     524            0 :                 let conf = PageServerConf {
     525              :                     id: {
     526            0 :                         anyhow::ensure!(
     527            0 :                             config_toml_id == id,
     528            0 :                             "id mismatch: config_toml.id={config_toml_id} id={id}",
     529              :                         );
     530            0 :                         id
     531            0 :                     },
     532            0 :                     listen_pg_addr,
     533            0 :                     listen_http_addr,
     534            0 :                     pg_auth_type,
     535            0 :                     http_auth_type,
     536            0 :                 };
     537            0 :                 pageservers.push(conf);
     538              :             }
     539            0 :             pageservers
     540            0 :         };
     541            0 : 
     542            0 :         Ok(env)
     543            0 :     }
     544              : 
     545            0 :     pub fn persist_config(&self) -> anyhow::Result<()> {
     546            0 :         Self::persist_config_impl(
     547            0 :             &self.base_data_dir,
     548            0 :             &OnDiskConfig {
     549            0 :                 pg_distrib_dir: self.pg_distrib_dir.clone(),
     550            0 :                 neon_distrib_dir: self.neon_distrib_dir.clone(),
     551            0 :                 default_tenant_id: self.default_tenant_id,
     552            0 :                 private_key_path: self.private_key_path.clone(),
     553            0 :                 broker: self.broker.clone(),
     554            0 :                 storage_controller: self.storage_controller.clone(),
     555            0 :                 pageservers: vec![], // it's skip_serializing anyway
     556            0 :                 safekeepers: self.safekeepers.clone(),
     557            0 :                 control_plane_api: self.control_plane_api.clone(),
     558            0 :                 control_plane_compute_hook_api: self.control_plane_compute_hook_api.clone(),
     559            0 :                 branch_name_mappings: self.branch_name_mappings.clone(),
     560            0 :             },
     561            0 :         )
     562            0 :     }
     563              : 
     564            0 :     pub fn persist_config_impl(base_path: &Path, config: &OnDiskConfig) -> anyhow::Result<()> {
     565            0 :         let conf_content = &toml::to_string_pretty(config)?;
     566            0 :         let target_config_path = base_path.join("config");
     567            0 :         fs::write(&target_config_path, conf_content).with_context(|| {
     568            0 :             format!(
     569            0 :                 "Failed to write config file into path '{}'",
     570            0 :                 target_config_path.display()
     571            0 :             )
     572            0 :         })
     573            0 :     }
     574              : 
     575              :     // this function is used only for testing purposes in CLI e g generate tokens during init
     576            0 :     pub fn generate_auth_token(&self, claims: &Claims) -> anyhow::Result<String> {
     577            0 :         let private_key_path = self.get_private_key_path();
     578            0 :         let key_data = fs::read(private_key_path)?;
     579            0 :         encode_from_key_file(claims, &key_data)
     580            0 :     }
     581              : 
     582            0 :     pub fn get_private_key_path(&self) -> PathBuf {
     583            0 :         if self.private_key_path.is_absolute() {
     584            0 :             self.private_key_path.to_path_buf()
     585              :         } else {
     586            0 :             self.base_data_dir.join(&self.private_key_path)
     587              :         }
     588            0 :     }
     589              : 
     590              :     /// Materialize the [`NeonLocalInitConf`] to disk. Called during [`neon_local init`].
     591            0 :     pub fn init(conf: NeonLocalInitConf, force: &InitForceMode) -> anyhow::Result<()> {
     592            0 :         let base_path = base_path();
     593            0 :         assert_ne!(base_path, Path::new(""));
     594            0 :         let base_path = &base_path;
     595            0 : 
     596            0 :         // create base_path dir
     597            0 :         if base_path.exists() {
     598            0 :             match force {
     599              :                 InitForceMode::MustNotExist => {
     600            0 :                     bail!(
     601            0 :                         "directory '{}' already exists. Perhaps already initialized?",
     602            0 :                         base_path.display()
     603            0 :                     );
     604              :                 }
     605              :                 InitForceMode::EmptyDirOk => {
     606            0 :                     if let Some(res) = std::fs::read_dir(base_path)?.next() {
     607            0 :                         res.context("check if directory is empty")?;
     608            0 :                         anyhow::bail!("directory not empty: {base_path:?}");
     609            0 :                     }
     610              :                 }
     611              :                 InitForceMode::RemoveAllContents => {
     612            0 :                     println!("removing all contents of '{}'", base_path.display());
     613              :                     // instead of directly calling `remove_dir_all`, we keep the original dir but removing
     614              :                     // all contents inside. This helps if the developer symbol links another directory (i.e.,
     615              :                     // S3 local SSD) to the `.neon` base directory.
     616            0 :                     for entry in std::fs::read_dir(base_path)? {
     617            0 :                         let entry = entry?;
     618            0 :                         let path = entry.path();
     619            0 :                         if path.is_dir() {
     620            0 :                             fs::remove_dir_all(&path)?;
     621              :                         } else {
     622            0 :                             fs::remove_file(&path)?;
     623              :                         }
     624              :                     }
     625              :                 }
     626              :             }
     627            0 :         }
     628            0 :         if !base_path.exists() {
     629            0 :             fs::create_dir(base_path)?;
     630            0 :         }
     631              : 
     632              :         let NeonLocalInitConf {
     633            0 :             pg_distrib_dir,
     634            0 :             neon_distrib_dir,
     635            0 :             default_tenant_id,
     636            0 :             broker,
     637            0 :             storage_controller,
     638            0 :             pageservers,
     639            0 :             safekeepers,
     640            0 :             control_plane_api,
     641            0 :             control_plane_compute_hook_api,
     642            0 :         } = conf;
     643            0 : 
     644            0 :         // Find postgres binaries.
     645            0 :         // Follow POSTGRES_DISTRIB_DIR if set, otherwise look in "pg_install".
     646            0 :         // Note that later in the code we assume, that distrib dirs follow the same pattern
     647            0 :         // for all postgres versions.
     648            0 :         let pg_distrib_dir = pg_distrib_dir.unwrap_or_else(|| {
     649            0 :             if let Some(postgres_bin) = env::var_os("POSTGRES_DISTRIB_DIR") {
     650            0 :                 postgres_bin.into()
     651              :             } else {
     652            0 :                 let cwd = env::current_dir().unwrap();
     653            0 :                 cwd.join("pg_install")
     654              :             }
     655            0 :         });
     656            0 : 
     657            0 :         // Find neon binaries.
     658            0 :         let neon_distrib_dir = neon_distrib_dir
     659            0 :             .unwrap_or_else(|| env::current_exe().unwrap().parent().unwrap().to_owned());
     660            0 : 
     661            0 :         // Generate keypair for JWT.
     662            0 :         //
     663            0 :         // The keypair is only needed if authentication is enabled in any of the
     664            0 :         // components. For convenience, we generate the keypair even if authentication
     665            0 :         // is not enabled, so that you can easily enable it after the initialization
     666            0 :         // step.
     667            0 :         generate_auth_keys(
     668            0 :             base_path.join("auth_private_key.pem").as_path(),
     669            0 :             base_path.join("auth_public_key.pem").as_path(),
     670            0 :         )
     671            0 :         .context("generate auth keys")?;
     672            0 :         let private_key_path = PathBuf::from("auth_private_key.pem");
     673            0 : 
     674            0 :         // create the runtime type because the remaining initialization code below needs
     675            0 :         // a LocalEnv instance op operation
     676            0 :         // TODO: refactor to avoid this, LocalEnv should only be constructed from on-disk state
     677            0 :         let env = LocalEnv {
     678            0 :             base_data_dir: base_path.clone(),
     679            0 :             pg_distrib_dir,
     680            0 :             neon_distrib_dir,
     681            0 :             default_tenant_id: Some(default_tenant_id),
     682            0 :             private_key_path,
     683            0 :             broker,
     684            0 :             storage_controller: storage_controller.unwrap_or_default(),
     685            0 :             pageservers: pageservers.iter().map(Into::into).collect(),
     686            0 :             safekeepers,
     687            0 :             control_plane_api: control_plane_api.unwrap_or_default(),
     688            0 :             control_plane_compute_hook_api: control_plane_compute_hook_api.unwrap_or_default(),
     689            0 :             branch_name_mappings: Default::default(),
     690            0 :         };
     691            0 : 
     692            0 :         // create endpoints dir
     693            0 :         fs::create_dir_all(env.endpoints_path())?;
     694              : 
     695              :         // create safekeeper dirs
     696            0 :         for safekeeper in &env.safekeepers {
     697            0 :             fs::create_dir_all(SafekeeperNode::datadir_path_by_id(&env, safekeeper.id))?;
     698              :         }
     699              : 
     700              :         // initialize pageserver state
     701            0 :         for (i, ps) in pageservers.into_iter().enumerate() {
     702            0 :             let runtime_ps = &env.pageservers[i];
     703            0 :             assert_eq!(&PageServerConf::from(&ps), runtime_ps);
     704            0 :             fs::create_dir(env.pageserver_data_dir(ps.id))?;
     705            0 :             PageServerNode::from_env(&env, runtime_ps)
     706            0 :                 .initialize(ps)
     707            0 :                 .context("pageserver init failed")?;
     708              :         }
     709              : 
     710              :         // setup remote remote location for default LocalFs remote storage
     711            0 :         std::fs::create_dir_all(env.base_data_dir.join(PAGESERVER_REMOTE_STORAGE_DIR))?;
     712              : 
     713            0 :         env.persist_config()
     714            0 :     }
     715              : }
     716              : 
     717            0 : pub fn base_path() -> PathBuf {
     718            0 :     match std::env::var_os("NEON_REPO_DIR") {
     719            0 :         Some(val) => PathBuf::from(val),
     720            0 :         None => PathBuf::from(".neon"),
     721              :     }
     722            0 : }
     723              : 
     724              : /// Generate a public/private key pair for JWT authentication
     725            0 : fn generate_auth_keys(private_key_path: &Path, public_key_path: &Path) -> anyhow::Result<()> {
     726              :     // Generate the key pair
     727              :     //
     728              :     // openssl genpkey -algorithm ed25519 -out auth_private_key.pem
     729            0 :     let keygen_output = Command::new("openssl")
     730            0 :         .arg("genpkey")
     731            0 :         .args(["-algorithm", "ed25519"])
     732            0 :         .args(["-out", private_key_path.to_str().unwrap()])
     733            0 :         .stdout(Stdio::null())
     734            0 :         .output()
     735            0 :         .context("failed to generate auth private key")?;
     736            0 :     if !keygen_output.status.success() {
     737            0 :         bail!(
     738            0 :             "openssl failed: '{}'",
     739            0 :             String::from_utf8_lossy(&keygen_output.stderr)
     740            0 :         );
     741            0 :     }
     742              :     // Extract the public key from the private key file
     743              :     //
     744              :     // openssl pkey -in auth_private_key.pem -pubout -out auth_public_key.pem
     745            0 :     let keygen_output = Command::new("openssl")
     746            0 :         .arg("pkey")
     747            0 :         .args(["-in", private_key_path.to_str().unwrap()])
     748            0 :         .arg("-pubout")
     749            0 :         .args(["-out", public_key_path.to_str().unwrap()])
     750            0 :         .output()
     751            0 :         .context("failed to extract public key from private key")?;
     752            0 :     if !keygen_output.status.success() {
     753            0 :         bail!(
     754            0 :             "openssl failed: '{}'",
     755            0 :             String::from_utf8_lossy(&keygen_output.stderr)
     756            0 :         );
     757            0 :     }
     758            0 :     Ok(())
     759            0 : }
        

Generated by: LCOV version 2.1-beta