LCOV - code coverage report
Current view: top level - control_plane/src - local_env.rs (source / functions) Coverage Total Hit
Test: a1cc1f33dc9899e4da66eb51e44e911a4b3bd648.info Lines: 0.0 % 661 0
Test Date: 2025-07-31 11:35:14 Functions: 0.0 % 98 0

            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 std::collections::HashMap;
       7              : use std::net::SocketAddr;
       8              : use std::path::{Path, PathBuf};
       9              : use std::process::{Command, Stdio};
      10              : use std::time::Duration;
      11              : use std::{env, fs};
      12              : 
      13              : use anyhow::{Context, bail};
      14              : use clap::ValueEnum;
      15              : use pageserver_api::config::PostHogConfig;
      16              : use pem::Pem;
      17              : use postgres_backend::AuthType;
      18              : use reqwest::{Certificate, Url};
      19              : use safekeeper_api::PgMajorVersion;
      20              : use serde::{Deserialize, Serialize};
      21              : use utils::auth::{encode_from_key_file, encode_hadron_token};
      22              : use utils::id::{NodeId, TenantId, TenantTimelineId, TimelineId};
      23              : 
      24              : use crate::broker::StorageBroker;
      25              : use crate::endpoint_storage::{
      26              :     ENDPOINT_STORAGE_DEFAULT_ADDR, ENDPOINT_STORAGE_REMOTE_STORAGE_DIR, EndpointStorage,
      27              : };
      28              : use crate::pageserver::{PAGESERVER_REMOTE_STORAGE_DIR, PageServerNode};
      29              : use crate::safekeeper::SafekeeperNode;
      30              : 
      31              : pub const DEFAULT_PG_VERSION: u32 = 17;
      32              : 
      33              : //
      34              : // This data structures represents neon_local CLI config
      35              : //
      36              : // It is deserialized from the .neon/config file, or the config file passed
      37              : // to 'neon_local init --config=<path>' option. See control_plane/simple.conf for
      38              : // an example.
      39              : //
      40              : #[derive(PartialEq, Eq, Clone, Debug)]
      41              : pub struct LocalEnv {
      42              :     // Base directory for all the nodes (the pageserver, safekeepers and
      43              :     // compute endpoints).
      44              :     //
      45              :     // This is not stored in the config file. Rather, this is the path where the
      46              :     // config file itself is. It is read from the NEON_REPO_DIR env variable which
      47              :     // must be an absolute path. If the env var is not set, $PWD/.neon is used.
      48              :     pub base_data_dir: PathBuf,
      49              : 
      50              :     // Path to postgres distribution. It's expected that "bin", "include",
      51              :     // "lib", "share" from postgres distribution are there. If at some point
      52              :     // in time we will be able to run against vanilla postgres we may split that
      53              :     // to four separate paths and match OS-specific installation layout.
      54              :     pub pg_distrib_dir: PathBuf,
      55              : 
      56              :     // Path to pageserver binary.
      57              :     pub neon_distrib_dir: PathBuf,
      58              : 
      59              :     // Default tenant ID to use with the 'neon_local' command line utility, when
      60              :     // --tenant_id is not explicitly specified.
      61              :     pub default_tenant_id: Option<TenantId>,
      62              : 
      63              :     // The type of tokens to use for authentication in the test environment. Determines
      64              :     // the type of key pairs and tokens generated in the test.
      65              :     pub token_auth_type: AuthType,
      66              :     // used to issue tokens during e.g pg start
      67              :     pub private_key_path: PathBuf,
      68              :     /// Path to environment's public key
      69              :     pub public_key_path: PathBuf,
      70              : 
      71              :     pub broker: NeonBroker,
      72              : 
      73              :     // Configuration for the storage controller (1 per neon_local environment)
      74              :     pub storage_controller: NeonStorageControllerConf,
      75              : 
      76              :     /// This Vec must always contain at least one pageserver
      77              :     /// Populdated by [`Self::load_config`] from the individual `pageserver.toml`s.
      78              :     /// NB: not used anymore except for informing users that they need to change their `.neon/config`.
      79              :     pub pageservers: Vec<PageServerConf>,
      80              : 
      81              :     pub safekeepers: Vec<SafekeeperConf>,
      82              : 
      83              :     pub endpoint_storage: EndpointStorageConf,
      84              : 
      85              :     // Control plane upcall API for pageserver: if None, we will not run storage_controller  If set, this will
      86              :     // be propagated into each pageserver's configuration.
      87              :     pub control_plane_api: Url,
      88              : 
      89              :     // Control plane upcall APIs for storage controller.  If set, this will be propagated into the
      90              :     // storage controller's configuration.
      91              :     pub control_plane_hooks_api: Option<Url>,
      92              : 
      93              :     /// Keep human-readable aliases in memory (and persist them to config), to hide ZId hex strings from the user.
      94              :     // A `HashMap<String, HashMap<TenantId, TimelineId>>` would be more appropriate here,
      95              :     // but deserialization into a generic toml object as `toml::Value::try_from` fails with an error.
      96              :     // https://toml.io/en/v1.0.0 does not contain a concept of "a table inside another table".
      97              :     pub branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
      98              : 
      99              :     /// Flag to generate SSL certificates for components that need it.
     100              :     /// Also generates root CA certificate that is used to sign all other certificates.
     101              :     pub generate_local_ssl_certs: bool,
     102              : }
     103              : 
     104              : /// On-disk state stored in `.neon/config`.
     105              : #[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize)]
     106              : #[serde(default, deny_unknown_fields)]
     107              : pub struct OnDiskConfig {
     108              :     pub pg_distrib_dir: PathBuf,
     109              :     pub neon_distrib_dir: PathBuf,
     110              :     pub default_tenant_id: Option<TenantId>,
     111              :     pub token_auth_type: Option<AuthType>,
     112              :     pub private_key_path: PathBuf,
     113              :     pub public_key_path: PathBuf,
     114              :     pub broker: NeonBroker,
     115              :     pub storage_controller: NeonStorageControllerConf,
     116              :     #[serde(
     117              :         skip_serializing,
     118              :         deserialize_with = "fail_if_pageservers_field_specified"
     119              :     )]
     120              :     pub pageservers: Vec<PageServerConf>,
     121              :     pub safekeepers: Vec<SafekeeperConf>,
     122              :     pub endpoint_storage: EndpointStorageConf,
     123              :     pub control_plane_api: Option<Url>,
     124              :     pub control_plane_hooks_api: Option<Url>,
     125              :     pub control_plane_compute_hook_api: Option<Url>,
     126              :     branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
     127              :     // Note: skip serializing because in compat tests old storage controller fails
     128              :     // to load new config file. May be removed after this field is in release branch.
     129              :     #[serde(skip_serializing_if = "std::ops::Not::not")]
     130              :     pub generate_local_ssl_certs: bool,
     131              : }
     132              : 
     133            0 : fn fail_if_pageservers_field_specified<'de, D>(_: D) -> Result<Vec<PageServerConf>, D::Error>
     134            0 : where
     135            0 :     D: serde::Deserializer<'de>,
     136              : {
     137            0 :     Err(serde::de::Error::custom(
     138            0 :         "The 'pageservers' field is no longer used; pageserver.toml is now authoritative; \
     139            0 :          Please remove the `pageservers` from your .neon/config.",
     140            0 :     ))
     141            0 : }
     142              : 
     143              : /// The description of the neon_local env to be initialized by `neon_local init --config`.
     144            0 : #[derive(Clone, Debug, Deserialize)]
     145              : #[serde(deny_unknown_fields)]
     146              : pub struct NeonLocalInitConf {
     147              :     // TODO: do we need this? Seems unused
     148              :     pub pg_distrib_dir: Option<PathBuf>,
     149              :     // TODO: do we need this? Seems unused
     150              :     pub neon_distrib_dir: Option<PathBuf>,
     151              :     pub default_tenant_id: TenantId,
     152              :     pub broker: NeonBroker,
     153              :     pub storage_controller: Option<NeonStorageControllerConf>,
     154              :     pub pageservers: Vec<NeonLocalInitPageserverConf>,
     155              :     pub safekeepers: Vec<SafekeeperConf>,
     156              :     pub endpoint_storage: EndpointStorageConf,
     157              :     pub control_plane_api: Option<Url>,
     158              :     pub control_plane_hooks_api: Option<Url>,
     159              :     pub generate_local_ssl_certs: bool,
     160              :     pub auth_token_type: AuthType,
     161              : }
     162              : 
     163            0 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     164              : #[serde(default)]
     165              : pub struct EndpointStorageConf {
     166              :     pub listen_addr: SocketAddr,
     167              : }
     168              : 
     169              : /// Broker config for cluster internal communication.
     170            0 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Default)]
     171              : #[serde(default)]
     172              : pub struct NeonBroker {
     173              :     /// Broker listen HTTP address for storage nodes coordination, e.g. '127.0.0.1:50051'.
     174              :     /// At least one of listen_addr or listen_https_addr must be set.
     175              :     pub listen_addr: Option<SocketAddr>,
     176              :     /// Broker listen HTTPS address for storage nodes coordination, e.g. '127.0.0.1:50051'.
     177              :     /// At least one of listen_addr or listen_https_addr must be set.
     178              :     /// listen_https_addr is preferred over listen_addr in neon_local.
     179              :     pub listen_https_addr: Option<SocketAddr>,
     180              : }
     181              : 
     182              : /// A part of storage controller's config the neon_local knows about.
     183              : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     184              : #[serde(default)]
     185              : pub struct NeonStorageControllerConf {
     186              :     /// Heartbeat timeout before marking a node offline
     187              :     #[serde(with = "humantime_serde")]
     188              :     pub max_offline: Duration,
     189              : 
     190              :     #[serde(with = "humantime_serde")]
     191              :     pub max_warming_up: Duration,
     192              : 
     193              :     pub start_as_candidate: bool,
     194              : 
     195              :     /// Database url used when running multiple storage controller instances
     196              :     pub database_url: Option<SocketAddr>,
     197              : 
     198              :     /// Thresholds for auto-splitting a tenant into shards.
     199              :     pub split_threshold: Option<u64>,
     200              :     pub max_split_shards: Option<u8>,
     201              :     pub initial_split_threshold: Option<u64>,
     202              :     pub initial_split_shards: Option<u8>,
     203              : 
     204              :     pub max_secondary_lag_bytes: Option<u64>,
     205              : 
     206              :     #[serde(with = "humantime_serde")]
     207              :     pub heartbeat_interval: Duration,
     208              : 
     209              :     #[serde(with = "humantime_serde")]
     210              :     pub long_reconcile_threshold: Option<Duration>,
     211              : 
     212              :     pub use_https_pageserver_api: bool,
     213              : 
     214              :     pub timelines_onto_safekeepers: bool,
     215              : 
     216              :     pub use_https_safekeeper_api: bool,
     217              : 
     218              :     pub use_local_compute_notifications: bool,
     219              : 
     220              :     pub timeline_safekeeper_count: Option<usize>,
     221              : 
     222              :     pub posthog_config: Option<PostHogConfig>,
     223              : 
     224              :     pub kick_secondary_downloads: Option<bool>,
     225              : 
     226              :     #[serde(with = "humantime_serde")]
     227              :     pub shard_split_request_timeout: Option<Duration>,
     228              : }
     229              : 
     230              : impl NeonStorageControllerConf {
     231              :     // Use a shorter pageserver unavailability interval than the default to speed up tests.
     232              :     const DEFAULT_MAX_OFFLINE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(10);
     233              : 
     234              :     const DEFAULT_MAX_WARMING_UP_INTERVAL: std::time::Duration = std::time::Duration::from_secs(30);
     235              : 
     236              :     // Very tight heartbeat interval to speed up tests
     237              :     const DEFAULT_HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(1000);
     238              : }
     239              : 
     240              : impl Default for NeonStorageControllerConf {
     241            0 :     fn default() -> Self {
     242            0 :         Self {
     243            0 :             max_offline: Self::DEFAULT_MAX_OFFLINE_INTERVAL,
     244            0 :             max_warming_up: Self::DEFAULT_MAX_WARMING_UP_INTERVAL,
     245            0 :             start_as_candidate: false,
     246            0 :             database_url: None,
     247            0 :             split_threshold: None,
     248            0 :             max_split_shards: None,
     249            0 :             initial_split_threshold: None,
     250            0 :             initial_split_shards: None,
     251            0 :             max_secondary_lag_bytes: None,
     252            0 :             heartbeat_interval: Self::DEFAULT_HEARTBEAT_INTERVAL,
     253            0 :             long_reconcile_threshold: None,
     254            0 :             use_https_pageserver_api: false,
     255            0 :             timelines_onto_safekeepers: true,
     256            0 :             use_https_safekeeper_api: false,
     257            0 :             use_local_compute_notifications: true,
     258            0 :             timeline_safekeeper_count: None,
     259            0 :             posthog_config: None,
     260            0 :             kick_secondary_downloads: None,
     261            0 :             shard_split_request_timeout: None,
     262            0 :         }
     263            0 :     }
     264              : }
     265              : 
     266              : impl Default for EndpointStorageConf {
     267            0 :     fn default() -> Self {
     268            0 :         Self {
     269            0 :             listen_addr: ENDPOINT_STORAGE_DEFAULT_ADDR,
     270            0 :         }
     271            0 :     }
     272              : }
     273              : 
     274              : impl NeonBroker {
     275            0 :     pub fn client_url(&self) -> Url {
     276            0 :         let url = if let Some(addr) = self.listen_https_addr {
     277            0 :             format!("https://{addr}")
     278              :         } else {
     279            0 :             format!(
     280            0 :                 "http://{}",
     281            0 :                 self.listen_addr
     282            0 :                     .expect("at least one address should be set")
     283              :             )
     284              :         };
     285              : 
     286            0 :         Url::parse(&url).expect("failed to construct url")
     287            0 :     }
     288              : }
     289              : 
     290              : // neon_local needs to know this subset of pageserver configuration.
     291              : // For legacy reasons, this information is duplicated from `pageserver.toml` into `.neon/config`.
     292              : // It can get stale if `pageserver.toml` is changed.
     293              : // TODO(christian): don't store this at all in `.neon/config`, always load it from `pageserver.toml`
     294            0 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     295              : #[serde(default, deny_unknown_fields)]
     296              : pub struct PageServerConf {
     297              :     pub id: NodeId,
     298              :     pub listen_pg_addr: String,
     299              :     pub listen_http_addr: String,
     300              :     pub listen_https_addr: Option<String>,
     301              :     pub listen_grpc_addr: Option<String>,
     302              :     pub pg_auth_type: AuthType,
     303              :     pub http_auth_type: AuthType,
     304              :     pub grpc_auth_type: AuthType,
     305              :     pub no_sync: bool,
     306              : }
     307              : 
     308              : impl Default for PageServerConf {
     309            0 :     fn default() -> Self {
     310            0 :         Self {
     311            0 :             id: NodeId(0),
     312            0 :             listen_pg_addr: String::new(),
     313            0 :             listen_http_addr: String::new(),
     314            0 :             listen_https_addr: None,
     315            0 :             listen_grpc_addr: None,
     316            0 :             pg_auth_type: AuthType::Trust,
     317            0 :             http_auth_type: AuthType::Trust,
     318            0 :             grpc_auth_type: AuthType::Trust,
     319            0 :             no_sync: false,
     320            0 :         }
     321            0 :     }
     322              : }
     323              : 
     324              : /// The toml that can be passed to `neon_local init --config`.
     325              : /// This is a subset of the `pageserver.toml` configuration.
     326              : // TODO(christian): use pageserver_api::config::ConfigToml (PR #7656)
     327            0 : #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
     328              : pub struct NeonLocalInitPageserverConf {
     329              :     pub id: NodeId,
     330              :     pub listen_pg_addr: String,
     331              :     pub listen_http_addr: String,
     332              :     pub listen_https_addr: Option<String>,
     333              :     pub listen_grpc_addr: Option<String>,
     334              :     pub pg_auth_type: AuthType,
     335              :     pub http_auth_type: AuthType,
     336              :     pub grpc_auth_type: AuthType,
     337              :     #[serde(default, skip_serializing_if = "std::ops::Not::not")]
     338              :     pub no_sync: bool,
     339              :     #[serde(flatten)]
     340              :     pub other: HashMap<String, toml::Value>,
     341              : }
     342              : 
     343              : impl From<&NeonLocalInitPageserverConf> for PageServerConf {
     344            0 :     fn from(conf: &NeonLocalInitPageserverConf) -> Self {
     345              :         let NeonLocalInitPageserverConf {
     346            0 :             id,
     347            0 :             listen_pg_addr,
     348            0 :             listen_http_addr,
     349            0 :             listen_https_addr,
     350            0 :             listen_grpc_addr,
     351            0 :             pg_auth_type,
     352            0 :             http_auth_type,
     353            0 :             grpc_auth_type,
     354            0 :             no_sync,
     355              :             other: _,
     356            0 :         } = conf;
     357            0 :         Self {
     358            0 :             id: *id,
     359            0 :             listen_pg_addr: listen_pg_addr.clone(),
     360            0 :             listen_http_addr: listen_http_addr.clone(),
     361            0 :             listen_https_addr: listen_https_addr.clone(),
     362            0 :             listen_grpc_addr: listen_grpc_addr.clone(),
     363            0 :             pg_auth_type: *pg_auth_type,
     364            0 :             grpc_auth_type: *grpc_auth_type,
     365            0 :             http_auth_type: *http_auth_type,
     366            0 :             no_sync: *no_sync,
     367            0 :         }
     368            0 :     }
     369              : }
     370              : 
     371            0 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
     372              : #[serde(default)]
     373              : pub struct SafekeeperConf {
     374              :     pub id: NodeId,
     375              :     pub pg_port: u16,
     376              :     pub pg_tenant_only_port: Option<u16>,
     377              :     pub http_port: u16,
     378              :     pub https_port: Option<u16>,
     379              :     pub sync: bool,
     380              :     pub remote_storage: Option<String>,
     381              :     pub backup_threads: Option<u32>,
     382              :     pub auth_type: AuthType,
     383              :     pub listen_addr: Option<String>,
     384              : }
     385              : 
     386              : impl Default for SafekeeperConf {
     387            0 :     fn default() -> Self {
     388            0 :         Self {
     389            0 :             id: NodeId(0),
     390            0 :             pg_port: 0,
     391            0 :             pg_tenant_only_port: None,
     392            0 :             http_port: 0,
     393            0 :             https_port: None,
     394            0 :             sync: true,
     395            0 :             remote_storage: None,
     396            0 :             backup_threads: None,
     397            0 :             auth_type: AuthType::Trust,
     398            0 :             listen_addr: None,
     399            0 :         }
     400            0 :     }
     401              : }
     402              : 
     403              : #[derive(Clone, Copy)]
     404              : pub enum InitForceMode {
     405              :     MustNotExist,
     406              :     EmptyDirOk,
     407              :     RemoveAllContents,
     408              : }
     409              : 
     410              : impl ValueEnum for InitForceMode {
     411            0 :     fn value_variants<'a>() -> &'a [Self] {
     412            0 :         &[
     413            0 :             Self::MustNotExist,
     414            0 :             Self::EmptyDirOk,
     415            0 :             Self::RemoveAllContents,
     416            0 :         ]
     417            0 :     }
     418              : 
     419            0 :     fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
     420            0 :         Some(clap::builder::PossibleValue::new(match self {
     421            0 :             InitForceMode::MustNotExist => "must-not-exist",
     422            0 :             InitForceMode::EmptyDirOk => "empty-dir-ok",
     423            0 :             InitForceMode::RemoveAllContents => "remove-all-contents",
     424              :         }))
     425            0 :     }
     426              : }
     427              : 
     428              : impl SafekeeperConf {
     429              :     /// Compute is served by port on which only tenant scoped tokens allowed, if
     430              :     /// it is configured.
     431            0 :     pub fn get_compute_port(&self) -> u16 {
     432            0 :         self.pg_tenant_only_port.unwrap_or(self.pg_port)
     433            0 :     }
     434              : }
     435              : 
     436              : impl LocalEnv {
     437            0 :     pub fn pg_distrib_dir_raw(&self) -> PathBuf {
     438            0 :         self.pg_distrib_dir.clone()
     439            0 :     }
     440              : 
     441            0 :     pub fn pg_distrib_dir(&self, pg_version: PgMajorVersion) -> anyhow::Result<PathBuf> {
     442            0 :         let path = self.pg_distrib_dir.clone();
     443              : 
     444            0 :         Ok(path.join(pg_version.v_str()))
     445            0 :     }
     446              : 
     447            0 :     pub fn pg_dir(&self, pg_version: PgMajorVersion, dir_name: &str) -> anyhow::Result<PathBuf> {
     448            0 :         Ok(self.pg_distrib_dir(pg_version)?.join(dir_name))
     449            0 :     }
     450              : 
     451            0 :     pub fn pg_bin_dir(&self, pg_version: PgMajorVersion) -> anyhow::Result<PathBuf> {
     452            0 :         self.pg_dir(pg_version, "bin")
     453            0 :     }
     454              : 
     455            0 :     pub fn pg_lib_dir(&self, pg_version: PgMajorVersion) -> anyhow::Result<PathBuf> {
     456            0 :         self.pg_dir(pg_version, "lib")
     457            0 :     }
     458              : 
     459            0 :     pub fn endpoint_storage_bin(&self) -> PathBuf {
     460            0 :         self.neon_distrib_dir.join("endpoint_storage")
     461            0 :     }
     462              : 
     463            0 :     pub fn pageserver_bin(&self) -> PathBuf {
     464            0 :         self.neon_distrib_dir.join("pageserver")
     465            0 :     }
     466              : 
     467            0 :     pub fn storage_controller_bin(&self) -> PathBuf {
     468              :         // Irrespective of configuration, storage controller binary is always
     469              :         // run from the same location as neon_local.  This means that for compatibility
     470              :         // tests that run old pageserver/safekeeper, they still run latest storage controller.
     471            0 :         let neon_local_bin_dir = env::current_exe().unwrap().parent().unwrap().to_owned();
     472            0 :         neon_local_bin_dir.join("storage_controller")
     473            0 :     }
     474              : 
     475            0 :     pub fn safekeeper_bin(&self) -> PathBuf {
     476            0 :         self.neon_distrib_dir.join("safekeeper")
     477            0 :     }
     478              : 
     479            0 :     pub fn storage_broker_bin(&self) -> PathBuf {
     480            0 :         self.neon_distrib_dir.join("storage_broker")
     481            0 :     }
     482              : 
     483            0 :     pub fn endpoints_path(&self) -> PathBuf {
     484            0 :         self.base_data_dir.join("endpoints")
     485            0 :     }
     486              : 
     487            0 :     pub fn storage_broker_data_dir(&self) -> PathBuf {
     488            0 :         self.base_data_dir.join("storage_broker")
     489            0 :     }
     490              : 
     491            0 :     pub fn pageserver_data_dir(&self, pageserver_id: NodeId) -> PathBuf {
     492            0 :         self.base_data_dir
     493            0 :             .join(format!("pageserver_{pageserver_id}"))
     494            0 :     }
     495              : 
     496            0 :     pub fn safekeeper_data_dir(&self, data_dir_name: &str) -> PathBuf {
     497            0 :         self.base_data_dir.join("safekeepers").join(data_dir_name)
     498            0 :     }
     499              : 
     500            0 :     pub fn endpoint_storage_data_dir(&self) -> PathBuf {
     501            0 :         self.base_data_dir.join("endpoint_storage")
     502            0 :     }
     503              : 
     504            0 :     pub fn get_pageserver_conf(&self, id: NodeId) -> anyhow::Result<&PageServerConf> {
     505            0 :         if let Some(conf) = self.pageservers.iter().find(|node| node.id == id) {
     506            0 :             Ok(conf)
     507              :         } else {
     508            0 :             let have_ids = self
     509            0 :                 .pageservers
     510            0 :                 .iter()
     511            0 :                 .map(|node| format!("{}:{}", node.id, node.listen_http_addr))
     512            0 :                 .collect::<Vec<_>>();
     513            0 :             let joined = have_ids.join(",");
     514            0 :             bail!("could not find pageserver {id}, have ids {joined}")
     515              :         }
     516            0 :     }
     517              : 
     518            0 :     pub fn ssl_ca_cert_path(&self) -> Option<PathBuf> {
     519            0 :         if self.generate_local_ssl_certs {
     520            0 :             Some(self.base_data_dir.join("rootCA.crt"))
     521              :         } else {
     522            0 :             None
     523              :         }
     524            0 :     }
     525              : 
     526            0 :     pub fn ssl_ca_key_path(&self) -> Option<PathBuf> {
     527            0 :         if self.generate_local_ssl_certs {
     528            0 :             Some(self.base_data_dir.join("rootCA.key"))
     529              :         } else {
     530            0 :             None
     531              :         }
     532            0 :     }
     533              : 
     534            0 :     pub fn generate_ssl_ca_cert(&self) -> anyhow::Result<()> {
     535            0 :         let cert_path = self.ssl_ca_cert_path().unwrap();
     536            0 :         let key_path = self.ssl_ca_key_path().unwrap();
     537            0 :         if !fs::exists(cert_path.as_path())? {
     538            0 :             generate_ssl_ca_cert(cert_path.as_path(), key_path.as_path())?;
     539            0 :         }
     540            0 :         Ok(())
     541            0 :     }
     542              : 
     543            0 :     pub fn generate_ssl_cert(&self, cert_path: &Path, key_path: &Path) -> anyhow::Result<()> {
     544            0 :         self.generate_ssl_ca_cert()?;
     545            0 :         generate_ssl_cert(
     546            0 :             cert_path,
     547            0 :             key_path,
     548            0 :             self.ssl_ca_cert_path().unwrap().as_path(),
     549            0 :             self.ssl_ca_key_path().unwrap().as_path(),
     550              :         )
     551            0 :     }
     552              : 
     553              :     /// Creates HTTP client with local SSL CA certificates.
     554            0 :     pub fn create_http_client(&self) -> reqwest::Client {
     555            0 :         let ssl_ca_certs = self.ssl_ca_cert_path().map(|ssl_ca_file| {
     556            0 :             let buf = std::fs::read(ssl_ca_file).expect("SSL CA file should exist");
     557            0 :             Certificate::from_pem_bundle(&buf).expect("SSL CA file should be valid")
     558            0 :         });
     559              : 
     560            0 :         let mut http_client = reqwest::Client::builder();
     561            0 :         for ssl_ca_cert in ssl_ca_certs.unwrap_or_default() {
     562            0 :             http_client = http_client.add_root_certificate(ssl_ca_cert);
     563            0 :         }
     564              : 
     565            0 :         http_client
     566            0 :             .build()
     567            0 :             .expect("HTTP client should construct with no error")
     568            0 :     }
     569              : 
     570              :     /// Inspect the base data directory and extract the instance id and instance directory path
     571              :     /// for all storage controller instances
     572            0 :     pub async fn storage_controller_instances(&self) -> std::io::Result<Vec<(u8, PathBuf)>> {
     573            0 :         let mut instances = Vec::default();
     574              : 
     575            0 :         let dir = std::fs::read_dir(self.base_data_dir.clone())?;
     576            0 :         for dentry in dir {
     577            0 :             let dentry = dentry?;
     578            0 :             let is_dir = dentry.metadata()?.is_dir();
     579            0 :             let filename = dentry.file_name().into_string().unwrap();
     580            0 :             let parsed_instance_id = match filename.strip_prefix("storage_controller_") {
     581            0 :                 Some(suffix) => suffix.parse::<u8>().ok(),
     582            0 :                 None => None,
     583              :             };
     584              : 
     585            0 :             let is_instance_dir = is_dir && parsed_instance_id.is_some();
     586              : 
     587            0 :             if !is_instance_dir {
     588            0 :                 continue;
     589            0 :             }
     590              : 
     591            0 :             instances.push((
     592            0 :                 parsed_instance_id.expect("Checked previously"),
     593            0 :                 dentry.path(),
     594            0 :             ));
     595              :         }
     596              : 
     597            0 :         Ok(instances)
     598            0 :     }
     599              : 
     600            0 :     pub fn register_branch_mapping(
     601            0 :         &mut self,
     602            0 :         branch_name: String,
     603            0 :         tenant_id: TenantId,
     604            0 :         timeline_id: TimelineId,
     605            0 :     ) -> anyhow::Result<()> {
     606            0 :         let existing_values = self
     607            0 :             .branch_name_mappings
     608            0 :             .entry(branch_name.clone())
     609            0 :             .or_default();
     610              : 
     611            0 :         let existing_ids = existing_values
     612            0 :             .iter()
     613            0 :             .find(|(existing_tenant_id, _)| existing_tenant_id == &tenant_id);
     614              : 
     615            0 :         if let Some((_, old_timeline_id)) = existing_ids {
     616            0 :             if old_timeline_id == &timeline_id {
     617            0 :                 Ok(())
     618              :             } else {
     619            0 :                 bail!(
     620            0 :                     "branch '{branch_name}' is already mapped to timeline {old_timeline_id}, cannot map to another timeline {timeline_id}"
     621              :                 );
     622              :             }
     623              :         } else {
     624            0 :             existing_values.push((tenant_id, timeline_id));
     625            0 :             Ok(())
     626              :         }
     627            0 :     }
     628              : 
     629            0 :     pub fn get_branch_timeline_id(
     630            0 :         &self,
     631            0 :         branch_name: &str,
     632            0 :         tenant_id: TenantId,
     633            0 :     ) -> Option<TimelineId> {
     634            0 :         self.branch_name_mappings
     635            0 :             .get(branch_name)?
     636            0 :             .iter()
     637            0 :             .find(|(mapped_tenant_id, _)| mapped_tenant_id == &tenant_id)
     638            0 :             .map(|&(_, timeline_id)| timeline_id)
     639            0 :     }
     640              : 
     641            0 :     pub fn timeline_name_mappings(&self) -> HashMap<TenantTimelineId, String> {
     642            0 :         self.branch_name_mappings
     643            0 :             .iter()
     644            0 :             .flat_map(|(name, tenant_timelines)| {
     645            0 :                 tenant_timelines.iter().map(|&(tenant_id, timeline_id)| {
     646            0 :                     (TenantTimelineId::new(tenant_id, timeline_id), name.clone())
     647            0 :                 })
     648            0 :             })
     649            0 :             .collect()
     650            0 :     }
     651              : 
     652              :     ///  Construct `Self` from on-disk state.
     653            0 :     pub fn load_config(repopath: &Path) -> anyhow::Result<Self> {
     654            0 :         if !repopath.exists() {
     655            0 :             bail!(
     656            0 :                 "Neon config is not found in {}. You need to run 'neon_local init' first",
     657            0 :                 repopath.to_str().unwrap()
     658              :             );
     659            0 :         }
     660              : 
     661              :         // TODO: check that it looks like a neon repository
     662              : 
     663              :         // load and parse file
     664            0 :         let config_file_contents = fs::read_to_string(repopath.join("config"))?;
     665            0 :         let on_disk_config: OnDiskConfig = toml::from_str(config_file_contents.as_str())?;
     666            0 :         let mut env = {
     667              :             let OnDiskConfig {
     668            0 :                 pg_distrib_dir,
     669            0 :                 neon_distrib_dir,
     670            0 :                 default_tenant_id,
     671            0 :                 token_auth_type,
     672            0 :                 private_key_path,
     673            0 :                 public_key_path,
     674            0 :                 broker,
     675            0 :                 storage_controller,
     676            0 :                 pageservers,
     677            0 :                 safekeepers,
     678            0 :                 control_plane_api,
     679            0 :                 control_plane_hooks_api,
     680              :                 control_plane_compute_hook_api: _,
     681            0 :                 branch_name_mappings,
     682            0 :                 generate_local_ssl_certs,
     683            0 :                 endpoint_storage,
     684            0 :             } = on_disk_config;
     685            0 :             LocalEnv {
     686            0 :                 base_data_dir: repopath.to_owned(),
     687            0 :                 pg_distrib_dir,
     688            0 :                 neon_distrib_dir,
     689            0 :                 default_tenant_id,
     690            0 :                 token_auth_type: token_auth_type.unwrap_or(AuthType::NeonJWT),
     691            0 :                 private_key_path,
     692            0 :                 public_key_path,
     693            0 :                 broker,
     694            0 :                 storage_controller,
     695            0 :                 pageservers,
     696            0 :                 safekeepers,
     697            0 :                 control_plane_api: control_plane_api.unwrap(),
     698            0 :                 control_plane_hooks_api,
     699            0 :                 branch_name_mappings,
     700            0 :                 generate_local_ssl_certs,
     701            0 :                 endpoint_storage,
     702            0 :             }
     703              :         };
     704              : 
     705              :         // The source of truth for pageserver configuration is the pageserver.toml.
     706            0 :         assert!(
     707            0 :             env.pageservers.is_empty(),
     708            0 :             "we ensure this during deserialization"
     709              :         );
     710            0 :         env.pageservers = {
     711            0 :             let iter = std::fs::read_dir(repopath).context("open dir")?;
     712            0 :             let mut pageservers = Vec::new();
     713            0 :             for res in iter {
     714            0 :                 let dentry = res?;
     715              :                 const PREFIX: &str = "pageserver_";
     716            0 :                 let dentry_name = dentry
     717            0 :                     .file_name()
     718            0 :                     .into_string()
     719            0 :                     .ok()
     720            0 :                     .with_context(|| format!("non-utf8 dentry: {:?}", dentry.path()))
     721            0 :                     .unwrap();
     722            0 :                 if !dentry_name.starts_with(PREFIX) {
     723            0 :                     continue;
     724            0 :                 }
     725            0 :                 if !dentry.file_type().context("determine file type")?.is_dir() {
     726            0 :                     anyhow::bail!("expected a directory, got {:?}", dentry.path());
     727            0 :                 }
     728            0 :                 let id = dentry_name[PREFIX.len()..]
     729            0 :                     .parse::<NodeId>()
     730            0 :                     .with_context(|| format!("parse id from {:?}", dentry.path()))?;
     731              :                 // TODO(christian): use pageserver_api::config::ConfigToml (PR #7656)
     732            0 :                 #[derive(serde::Serialize, serde::Deserialize)]
     733              :                 // (allow unknown fields, unlike PageServerConf)
     734              :                 struct PageserverConfigTomlSubset {
     735              :                     listen_pg_addr: String,
     736              :                     listen_http_addr: String,
     737              :                     listen_https_addr: Option<String>,
     738              :                     listen_grpc_addr: Option<String>,
     739              :                     pg_auth_type: AuthType,
     740              :                     http_auth_type: AuthType,
     741              :                     grpc_auth_type: AuthType,
     742              :                     #[serde(default)]
     743              :                     no_sync: bool,
     744              :                 }
     745            0 :                 let config_toml_path = dentry.path().join("pageserver.toml");
     746            0 :                 let config_toml: PageserverConfigTomlSubset = toml_edit::de::from_str(
     747            0 :                     &std::fs::read_to_string(&config_toml_path)
     748            0 :                         .with_context(|| format!("read {config_toml_path:?}"))?,
     749              :                 )
     750            0 :                 .context("parse pageserver.toml")?;
     751            0 :                 let identity_toml_path = dentry.path().join("identity.toml");
     752            0 :                 #[derive(serde::Serialize, serde::Deserialize)]
     753              :                 struct IdentityTomlSubset {
     754              :                     id: NodeId,
     755              :                 }
     756            0 :                 let identity_toml: IdentityTomlSubset = toml_edit::de::from_str(
     757            0 :                     &std::fs::read_to_string(&identity_toml_path)
     758            0 :                         .with_context(|| format!("read {identity_toml_path:?}"))?,
     759              :                 )
     760            0 :                 .context("parse identity.toml")?;
     761              :                 let PageserverConfigTomlSubset {
     762            0 :                     listen_pg_addr,
     763            0 :                     listen_http_addr,
     764            0 :                     listen_https_addr,
     765            0 :                     listen_grpc_addr,
     766            0 :                     pg_auth_type,
     767            0 :                     http_auth_type,
     768            0 :                     grpc_auth_type,
     769            0 :                     no_sync,
     770            0 :                 } = config_toml;
     771              :                 let IdentityTomlSubset {
     772            0 :                     id: identity_toml_id,
     773            0 :                 } = identity_toml;
     774            0 :                 let conf = PageServerConf {
     775              :                     id: {
     776            0 :                         anyhow::ensure!(
     777            0 :                             identity_toml_id == id,
     778            0 :                             "id mismatch: identity.toml:id={identity_toml_id} pageserver_(.*) id={id}",
     779              :                         );
     780            0 :                         id
     781              :                     },
     782            0 :                     listen_pg_addr,
     783            0 :                     listen_http_addr,
     784            0 :                     listen_https_addr,
     785            0 :                     listen_grpc_addr,
     786            0 :                     pg_auth_type,
     787            0 :                     http_auth_type,
     788            0 :                     grpc_auth_type,
     789            0 :                     no_sync,
     790              :                 };
     791            0 :                 pageservers.push(conf);
     792              :             }
     793            0 :             pageservers
     794              :         };
     795              : 
     796            0 :         Ok(env)
     797            0 :     }
     798              : 
     799            0 :     pub fn persist_config(&self) -> anyhow::Result<()> {
     800            0 :         Self::persist_config_impl(
     801            0 :             &self.base_data_dir,
     802            0 :             &OnDiskConfig {
     803            0 :                 pg_distrib_dir: self.pg_distrib_dir.clone(),
     804            0 :                 neon_distrib_dir: self.neon_distrib_dir.clone(),
     805            0 :                 default_tenant_id: self.default_tenant_id,
     806            0 :                 token_auth_type: Some(self.token_auth_type),
     807            0 :                 private_key_path: self.private_key_path.clone(),
     808            0 :                 public_key_path: self.public_key_path.clone(),
     809            0 :                 broker: self.broker.clone(),
     810            0 :                 storage_controller: self.storage_controller.clone(),
     811            0 :                 pageservers: vec![], // it's skip_serializing anyway
     812            0 :                 safekeepers: self.safekeepers.clone(),
     813            0 :                 control_plane_api: Some(self.control_plane_api.clone()),
     814            0 :                 control_plane_hooks_api: self.control_plane_hooks_api.clone(),
     815            0 :                 control_plane_compute_hook_api: None,
     816            0 :                 branch_name_mappings: self.branch_name_mappings.clone(),
     817            0 :                 generate_local_ssl_certs: self.generate_local_ssl_certs,
     818            0 :                 endpoint_storage: self.endpoint_storage.clone(),
     819            0 :             },
     820              :         )
     821            0 :     }
     822              : 
     823            0 :     pub fn persist_config_impl(base_path: &Path, config: &OnDiskConfig) -> anyhow::Result<()> {
     824            0 :         let conf_content = &toml::to_string_pretty(config)?;
     825            0 :         let target_config_path = base_path.join("config");
     826            0 :         fs::write(&target_config_path, conf_content).with_context(|| {
     827            0 :             format!(
     828            0 :                 "Failed to write config file into path '{}'",
     829            0 :                 target_config_path.display()
     830              :             )
     831            0 :         })
     832            0 :     }
     833              : 
     834              :     // this function is used only for testing purposes in CLI e g generate tokens during init
     835            0 :     pub fn generate_auth_token<S: Serialize>(&self, claims: &S) -> anyhow::Result<String> {
     836            0 :         match self.token_auth_type {
     837              :             AuthType::NeonJWT => {
     838            0 :                 let key_data = self.read_private_key()?;
     839            0 :                 encode_from_key_file(claims, &key_data)
     840              :             }
     841              :             AuthType::HadronJWT => {
     842            0 :                 let private_key_path = self.get_private_key_path();
     843            0 :                 let key_data = fs::read(private_key_path)?;
     844            0 :                 encode_hadron_token(claims, &key_data)
     845              :             }
     846            0 :             _ => panic!("unsupported token auth type {:?}", self.token_auth_type),
     847              :         }
     848            0 :     }
     849              : 
     850              :     /// Get the path to the private key.
     851            0 :     pub fn get_private_key_path(&self) -> PathBuf {
     852            0 :         if self.private_key_path.is_absolute() {
     853            0 :             self.private_key_path.to_path_buf()
     854              :         } else {
     855            0 :             self.base_data_dir.join(&self.private_key_path)
     856              :         }
     857            0 :     }
     858              : 
     859              :     /// Get the path to the public key.
     860            0 :     pub fn get_public_key_path(&self) -> PathBuf {
     861            0 :         if self.public_key_path.is_absolute() {
     862            0 :             self.public_key_path.to_path_buf()
     863              :         } else {
     864            0 :             self.base_data_dir.join(&self.public_key_path)
     865              :         }
     866            0 :     }
     867              : 
     868              :     /// Read the contents of the private key file.
     869            0 :     pub fn read_private_key(&self) -> anyhow::Result<Pem> {
     870            0 :         let private_key_path = self.get_private_key_path();
     871            0 :         let pem = pem::parse(fs::read(private_key_path)?)?;
     872            0 :         Ok(pem)
     873            0 :     }
     874              : 
     875              :     /// Read the contents of the public key file.
     876            0 :     pub fn read_public_key(&self) -> anyhow::Result<Pem> {
     877            0 :         let public_key_path = self.get_public_key_path();
     878            0 :         let pem = pem::parse(fs::read(public_key_path)?)?;
     879            0 :         Ok(pem)
     880            0 :     }
     881              : 
     882              :     /// Materialize the [`NeonLocalInitConf`] to disk. Called during [`neon_local init`].
     883            0 :     pub fn init(conf: NeonLocalInitConf, force: &InitForceMode) -> anyhow::Result<()> {
     884            0 :         let base_path = base_path();
     885            0 :         assert_ne!(base_path, Path::new(""));
     886            0 :         let base_path = &base_path;
     887              : 
     888              :         // create base_path dir
     889            0 :         if base_path.exists() {
     890            0 :             match force {
     891              :                 InitForceMode::MustNotExist => {
     892            0 :                     bail!(
     893            0 :                         "directory '{}' already exists. Perhaps already initialized?",
     894            0 :                         base_path.display()
     895              :                     );
     896              :                 }
     897              :                 InitForceMode::EmptyDirOk => {
     898            0 :                     if let Some(res) = std::fs::read_dir(base_path)?.next() {
     899            0 :                         res.context("check if directory is empty")?;
     900            0 :                         anyhow::bail!("directory not empty: {base_path:?}");
     901            0 :                     }
     902              :                 }
     903              :                 InitForceMode::RemoveAllContents => {
     904            0 :                     println!("removing all contents of '{}'", base_path.display());
     905              :                     // instead of directly calling `remove_dir_all`, we keep the original dir but removing
     906              :                     // all contents inside. This helps if the developer symbol links another directory (i.e.,
     907              :                     // S3 local SSD) to the `.neon` base directory.
     908            0 :                     for entry in std::fs::read_dir(base_path)? {
     909            0 :                         let entry = entry?;
     910            0 :                         let path = entry.path();
     911            0 :                         if path.is_dir() {
     912            0 :                             fs::remove_dir_all(&path)?;
     913              :                         } else {
     914            0 :                             fs::remove_file(&path)?;
     915              :                         }
     916              :                     }
     917              :                 }
     918              :             }
     919            0 :         }
     920            0 :         if !base_path.exists() {
     921            0 :             fs::create_dir(base_path)?;
     922            0 :         }
     923              : 
     924              :         let NeonLocalInitConf {
     925            0 :             pg_distrib_dir,
     926            0 :             neon_distrib_dir,
     927            0 :             default_tenant_id,
     928            0 :             broker,
     929            0 :             storage_controller,
     930            0 :             pageservers,
     931            0 :             safekeepers,
     932            0 :             control_plane_api,
     933            0 :             generate_local_ssl_certs,
     934            0 :             control_plane_hooks_api,
     935            0 :             endpoint_storage,
     936            0 :             auth_token_type,
     937            0 :         } = conf;
     938              : 
     939              :         // Find postgres binaries.
     940              :         // Follow POSTGRES_DISTRIB_DIR if set, otherwise look in "pg_install".
     941              :         // Note that later in the code we assume, that distrib dirs follow the same pattern
     942              :         // for all postgres versions.
     943            0 :         let pg_distrib_dir = pg_distrib_dir.unwrap_or_else(|| {
     944            0 :             if let Some(postgres_bin) = env::var_os("POSTGRES_DISTRIB_DIR") {
     945            0 :                 postgres_bin.into()
     946              :             } else {
     947            0 :                 let cwd = env::current_dir().unwrap();
     948            0 :                 cwd.join("pg_install")
     949              :             }
     950            0 :         });
     951              : 
     952              :         // Find neon binaries.
     953            0 :         let neon_distrib_dir = neon_distrib_dir
     954            0 :             .unwrap_or_else(|| env::current_exe().unwrap().parent().unwrap().to_owned());
     955              : 
     956              :         // Generate keypair for JWT.
     957              :         //
     958              :         // The keypair is only needed if authentication is enabled in any of the
     959              :         // components. For convenience, we generate the keypair even if authentication
     960              :         // is not enabled, so that you can easily enable it after the initialization
     961              :         // step.
     962            0 :         generate_auth_keys(
     963            0 :             base_path.join("auth_private_key.pem").as_path(),
     964            0 :             base_path.join("auth_public_key.pem").as_path(),
     965            0 :             auth_token_type,
     966              :         )
     967            0 :         .context("generate auth keys")?;
     968            0 :         let private_key_path = PathBuf::from("auth_private_key.pem");
     969            0 :         let public_key_path = PathBuf::from("auth_public_key.pem");
     970              : 
     971              :         // create the runtime type because the remaining initialization code below needs
     972              :         // a LocalEnv instance op operation
     973              :         // TODO: refactor to avoid this, LocalEnv should only be constructed from on-disk state
     974            0 :         let env = LocalEnv {
     975            0 :             base_data_dir: base_path.clone(),
     976            0 :             pg_distrib_dir,
     977            0 :             neon_distrib_dir,
     978            0 :             default_tenant_id: Some(default_tenant_id),
     979            0 :             token_auth_type: auth_token_type,
     980            0 :             private_key_path,
     981            0 :             public_key_path,
     982            0 :             broker,
     983            0 :             storage_controller: storage_controller.unwrap_or_default(),
     984            0 :             pageservers: pageservers.iter().map(Into::into).collect(),
     985            0 :             safekeepers,
     986            0 :             control_plane_api: control_plane_api.unwrap(),
     987            0 :             control_plane_hooks_api,
     988            0 :             branch_name_mappings: Default::default(),
     989            0 :             generate_local_ssl_certs,
     990            0 :             endpoint_storage,
     991            0 :         };
     992              : 
     993            0 :         if generate_local_ssl_certs {
     994            0 :             env.generate_ssl_ca_cert()?;
     995            0 :         }
     996              : 
     997              :         // create endpoints dir
     998            0 :         fs::create_dir_all(env.endpoints_path())?;
     999              : 
    1000              :         // create storage broker dir
    1001            0 :         fs::create_dir_all(env.storage_broker_data_dir())?;
    1002            0 :         StorageBroker::from_env(&env)
    1003            0 :             .initialize()
    1004            0 :             .context("storage broker init failed")?;
    1005              : 
    1006              :         // create safekeeper dirs
    1007            0 :         for safekeeper in &env.safekeepers {
    1008            0 :             fs::create_dir_all(SafekeeperNode::datadir_path_by_id(&env, safekeeper.id))?;
    1009            0 :             SafekeeperNode::from_env(&env, safekeeper)
    1010            0 :                 .initialize()
    1011            0 :                 .context("safekeeper init failed")?;
    1012              :         }
    1013              : 
    1014              :         // initialize pageserver state
    1015            0 :         for (i, ps) in pageservers.into_iter().enumerate() {
    1016            0 :             let runtime_ps = &env.pageservers[i];
    1017            0 :             assert_eq!(&PageServerConf::from(&ps), runtime_ps);
    1018            0 :             fs::create_dir(env.pageserver_data_dir(ps.id))?;
    1019            0 :             PageServerNode::from_env(&env, runtime_ps)
    1020            0 :                 .initialize(ps)
    1021            0 :                 .context("pageserver init failed")?;
    1022              :         }
    1023              : 
    1024            0 :         EndpointStorage::from_env(&env)
    1025            0 :             .init()
    1026            0 :             .context("object storage init failed")?;
    1027              : 
    1028              :         // setup remote remote location for default LocalFs remote storage
    1029            0 :         std::fs::create_dir_all(env.base_data_dir.join(PAGESERVER_REMOTE_STORAGE_DIR))?;
    1030            0 :         std::fs::create_dir_all(env.base_data_dir.join(ENDPOINT_STORAGE_REMOTE_STORAGE_DIR))?;
    1031              : 
    1032            0 :         env.persist_config()
    1033            0 :     }
    1034              : }
    1035              : 
    1036            0 : pub fn base_path() -> PathBuf {
    1037            0 :     let path = match std::env::var_os("NEON_REPO_DIR") {
    1038            0 :         Some(val) => {
    1039            0 :             let path = PathBuf::from(val);
    1040            0 :             if !path.is_absolute() {
    1041              :                 // repeat the env var in the error because our default is always absolute
    1042            0 :                 panic!("NEON_REPO_DIR must be an absolute path, got {path:?}");
    1043            0 :             }
    1044            0 :             path
    1045              :         }
    1046              :         None => {
    1047            0 :             let pwd = std::env::current_dir()
    1048              :                 // technically this can fail but it's quite unlikeley
    1049            0 :                 .expect("determine current directory");
    1050            0 :             let pwd_abs = pwd.canonicalize().expect("canonicalize current directory");
    1051            0 :             pwd_abs.join(".neon")
    1052              :         }
    1053              :     };
    1054            0 :     assert!(path.is_absolute());
    1055            0 :     path
    1056            0 : }
    1057              : 
    1058              : /// Generate a public/private key pair for JWT authentication
    1059            0 : fn generate_auth_keys(
    1060            0 :     private_key_path: &Path,
    1061            0 :     public_key_path: &Path,
    1062            0 :     auth_type: AuthType,
    1063            0 : ) -> anyhow::Result<()> {
    1064            0 :     if auth_type == AuthType::NeonJWT {
    1065              :         // Generate the key pair
    1066              :         //
    1067              :         // openssl genpkey -algorithm ed25519 -out auth_private_key.pem
    1068            0 :         let keygen_output = Command::new("openssl")
    1069            0 :             .arg("genpkey")
    1070            0 :             .args(["-algorithm", "ed25519"])
    1071            0 :             .args(["-out", private_key_path.to_str().unwrap()])
    1072            0 :             .stdout(Stdio::null())
    1073            0 :             .output()
    1074            0 :             .context("failed to generate auth private key")?;
    1075            0 :         if !keygen_output.status.success() {
    1076            0 :             bail!(
    1077            0 :                 "openssl failed: '{}'",
    1078            0 :                 String::from_utf8_lossy(&keygen_output.stderr)
    1079              :             );
    1080            0 :         }
    1081              :         // Extract the public key from the private key file
    1082              :         //
    1083              :         // openssl pkey -in auth_private_key.pem -pubout -out auth_public_key.pem
    1084            0 :         let keygen_output = Command::new("openssl")
    1085            0 :             .arg("pkey")
    1086            0 :             .args(["-in", private_key_path.to_str().unwrap()])
    1087            0 :             .arg("-pubout")
    1088            0 :             .args(["-out", public_key_path.to_str().unwrap()])
    1089            0 :             .output()
    1090            0 :             .context("failed to extract public key from private key")?;
    1091            0 :         if !keygen_output.status.success() {
    1092            0 :             bail!(
    1093            0 :                 "openssl failed: '{}'",
    1094            0 :                 String::from_utf8_lossy(&keygen_output.stderr)
    1095              :             );
    1096            0 :         }
    1097            0 :     } else if auth_type == AuthType::HadronJWT {
    1098              :         // Generate the RSA key pair. Note that the public key is embedded in an X509 certificate.
    1099              :         //
    1100              :         // openssl req -x509 -newkey rsa:4096 -keyout auth_private_key.pem -out auth_public_key.pem -nodes -subj "/CN=eng-brickstore@databricks.com"
    1101            0 :         let keygen_output = Command::new("openssl")
    1102            0 :             .arg("req")
    1103            0 :             .args(["-x509", "-newkey", "rsa:4096", "-sha256"])
    1104            0 :             .args(["-keyout", private_key_path.to_str().unwrap()])
    1105            0 :             .args(["-out", public_key_path.to_str().unwrap()])
    1106            0 :             .args(["-nodes"])
    1107            0 :             .args(["-subj", "/CN=eng-brickstore@databricks.com"])
    1108            0 :             .output()
    1109            0 :             .context("Failed to generate RSA key pair for Hadron token auth")?;
    1110            0 :         if !keygen_output.status.success() {
    1111            0 :             bail!(
    1112            0 :                 "openssl failed: '{}'",
    1113            0 :                 String::from_utf8_lossy(&keygen_output.stderr)
    1114              :             );
    1115            0 :         }
    1116            0 :     }
    1117              : 
    1118            0 :     Ok(())
    1119            0 : }
    1120              : 
    1121            0 : fn generate_ssl_ca_cert(cert_path: &Path, key_path: &Path) -> anyhow::Result<()> {
    1122              :     // openssl req -x509 -newkey rsa:2048 -nodes -subj "/CN=Neon Local CA" -days 36500 \
    1123              :     // -out rootCA.crt -keyout rootCA.key
    1124            0 :     let keygen_output = Command::new("openssl")
    1125            0 :         .args([
    1126            0 :             "req", "-x509", "-newkey", "ed25519", "-nodes", "-days", "36500",
    1127            0 :         ])
    1128            0 :         .args(["-subj", "/CN=Neon Local CA"])
    1129            0 :         .args(["-out", cert_path.to_str().unwrap()])
    1130            0 :         .args(["-keyout", key_path.to_str().unwrap()])
    1131            0 :         .output()
    1132            0 :         .context("failed to generate CA certificate")?;
    1133            0 :     if !keygen_output.status.success() {
    1134            0 :         bail!(
    1135            0 :             "openssl failed: '{}'",
    1136            0 :             String::from_utf8_lossy(&keygen_output.stderr)
    1137              :         );
    1138            0 :     }
    1139            0 :     Ok(())
    1140            0 : }
    1141              : 
    1142            0 : fn generate_ssl_cert(
    1143            0 :     cert_path: &Path,
    1144            0 :     key_path: &Path,
    1145            0 :     ca_cert_path: &Path,
    1146            0 :     ca_key_path: &Path,
    1147            0 : ) -> anyhow::Result<()> {
    1148              :     // Generate Certificate Signing Request (CSR).
    1149            0 :     let mut csr_path = cert_path.to_path_buf();
    1150            0 :     csr_path.set_extension(".csr");
    1151              : 
    1152              :     // openssl req -new -nodes -newkey rsa:2048 -keyout server.key -out server.csr \
    1153              :     // -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
    1154            0 :     let keygen_output = Command::new("openssl")
    1155            0 :         .args(["req", "-new", "-nodes"])
    1156            0 :         .args(["-newkey", "ed25519"])
    1157            0 :         .args(["-subj", "/CN=localhost"])
    1158            0 :         .args(["-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1"])
    1159            0 :         .args(["-keyout", key_path.to_str().unwrap()])
    1160            0 :         .args(["-out", csr_path.to_str().unwrap()])
    1161            0 :         .output()
    1162            0 :         .context("failed to generate CSR")?;
    1163            0 :     if !keygen_output.status.success() {
    1164            0 :         bail!(
    1165            0 :             "openssl failed: '{}'",
    1166            0 :             String::from_utf8_lossy(&keygen_output.stderr)
    1167              :         );
    1168            0 :     }
    1169              : 
    1170              :     // Sign CSR with CA key.
    1171              :     //
    1172              :     // openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
    1173              :     // -out server.crt -days 36500 -copy_extensions copyall
    1174            0 :     let keygen_output = Command::new("openssl")
    1175            0 :         .args(["x509", "-req"])
    1176            0 :         .args(["-in", csr_path.to_str().unwrap()])
    1177            0 :         .args(["-CA", ca_cert_path.to_str().unwrap()])
    1178            0 :         .args(["-CAkey", ca_key_path.to_str().unwrap()])
    1179            0 :         .arg("-CAcreateserial")
    1180            0 :         .args(["-out", cert_path.to_str().unwrap()])
    1181            0 :         .args(["-days", "36500"])
    1182            0 :         .args(["-copy_extensions", "copyall"])
    1183            0 :         .output()
    1184            0 :         .context("failed to sign CSR")?;
    1185            0 :     if !keygen_output.status.success() {
    1186            0 :         bail!(
    1187            0 :             "openssl failed: '{}'",
    1188            0 :             String::from_utf8_lossy(&keygen_output.stderr)
    1189              :         );
    1190            0 :     }
    1191              : 
    1192              :     // Remove CSR file as it's not needed anymore.
    1193            0 :     fs::remove_file(csr_path)?;
    1194              : 
    1195            0 :     Ok(())
    1196            0 : }
        

Generated by: LCOV version 2.1-beta