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 : }
|