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