Line data Source code
1 : //!
2 : //! `neon_local` is an executable that can be used to create a local
3 : //! Neon environment, for testing purposes. The local environment is
4 : //! quite different from the cloud environment with Kubernetes, but it
5 : //! easier to work with locally. The python tests in `test_runner`
6 : //! rely on `neon_local` to set up the environment for each test.
7 : //!
8 : use anyhow::{anyhow, bail, Context, Result};
9 : use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
10 : use compute_api::spec::ComputeMode;
11 : use control_plane::endpoint::ComputeControlPlane;
12 : use control_plane::local_env::LocalEnv;
13 : use control_plane::pageserver::PageServerNode;
14 : use control_plane::safekeeper::SafekeeperNode;
15 : use control_plane::{broker, local_env};
16 : use pageserver_api::models::TimelineInfo;
17 : use pageserver_api::{
18 : DEFAULT_HTTP_LISTEN_ADDR as DEFAULT_PAGESERVER_HTTP_ADDR,
19 : DEFAULT_PG_LISTEN_ADDR as DEFAULT_PAGESERVER_PG_ADDR,
20 : };
21 : use postgres_backend::AuthType;
22 : use safekeeper_api::{
23 : DEFAULT_HTTP_LISTEN_PORT as DEFAULT_SAFEKEEPER_HTTP_PORT,
24 : DEFAULT_PG_LISTEN_PORT as DEFAULT_SAFEKEEPER_PG_PORT,
25 : };
26 : use std::collections::{BTreeSet, HashMap};
27 : use std::path::PathBuf;
28 : use std::process::exit;
29 : use std::str::FromStr;
30 : use storage_broker::DEFAULT_LISTEN_ADDR as DEFAULT_BROKER_ADDR;
31 : use utils::{
32 : auth::{Claims, Scope},
33 : id::{NodeId, TenantId, TenantTimelineId, TimelineId},
34 : lsn::Lsn,
35 : project_git_version,
36 : };
37 :
38 : // Default id of a safekeeper node, if not specified on the command line.
39 : const DEFAULT_SAFEKEEPER_ID: NodeId = NodeId(1);
40 : const DEFAULT_PAGESERVER_ID: NodeId = NodeId(1);
41 : const DEFAULT_BRANCH_NAME: &str = "main";
42 : project_git_version!(GIT_VERSION);
43 :
44 : const DEFAULT_PG_VERSION: &str = "15";
45 :
46 0 : fn default_conf() -> String {
47 0 : format!(
48 0 : r#"
49 0 : # Default built-in configuration, defined in main.rs
50 0 : [broker]
51 0 : listen_addr = '{DEFAULT_BROKER_ADDR}'
52 0 :
53 0 : [pageserver]
54 0 : id = {DEFAULT_PAGESERVER_ID}
55 0 : listen_pg_addr = '{DEFAULT_PAGESERVER_PG_ADDR}'
56 0 : listen_http_addr = '{DEFAULT_PAGESERVER_HTTP_ADDR}'
57 0 : pg_auth_type = '{trust_auth}'
58 0 : http_auth_type = '{trust_auth}'
59 0 :
60 0 : [[safekeepers]]
61 0 : id = {DEFAULT_SAFEKEEPER_ID}
62 0 : pg_port = {DEFAULT_SAFEKEEPER_PG_PORT}
63 0 : http_port = {DEFAULT_SAFEKEEPER_HTTP_PORT}
64 0 : "#,
65 0 : trust_auth = AuthType::Trust,
66 0 : )
67 0 : }
68 :
69 : ///
70 : /// Timelines tree element used as a value in the HashMap.
71 : ///
72 : struct TimelineTreeEl {
73 : /// `TimelineInfo` received from the `pageserver` via the `timeline_list` http API call.
74 : pub info: TimelineInfo,
75 : /// Name, recovered from neon config mappings
76 : pub name: Option<String>,
77 : /// Holds all direct children of this timeline referenced using `timeline_id`.
78 : pub children: BTreeSet<TimelineId>,
79 : }
80 :
81 : // Main entry point for the 'neon_local' CLI utility
82 : //
83 : // This utility helps to manage neon installation. That includes following:
84 : // * Management of local postgres installations running on top of the
85 : // pageserver.
86 : // * Providing CLI api to the pageserver
87 : // * TODO: export/import to/from usual postgres
88 5409 : fn main() -> Result<()> {
89 5409 : let matches = cli().get_matches();
90 :
91 5409 : let (sub_name, sub_args) = match matches.subcommand() {
92 5409 : Some(subcommand_data) => subcommand_data,
93 0 : None => bail!("no subcommand provided"),
94 : };
95 :
96 : // Check for 'neon init' command first.
97 5409 : let subcommand_result = if sub_name == "init" {
98 369 : handle_init(sub_args).map(Some)
99 : } else {
100 : // all other commands need an existing config
101 5040 : let mut env = LocalEnv::load_config().context("Error loading config")?;
102 5040 : let original_env = env.clone();
103 :
104 5040 : let subcommand_result = match sub_name {
105 5040 : "tenant" => handle_tenant(sub_args, &mut env),
106 4548 : "timeline" => handle_timeline(sub_args, &mut env),
107 4107 : "start" => handle_start_all(sub_args, &env),
108 4103 : "stop" => handle_stop_all(sub_args, &env),
109 4099 : "pageserver" => handle_pageserver(sub_args, &env),
110 2955 : "safekeeper" => handle_safekeeper(sub_args, &env),
111 1907 : "endpoint" => handle_endpoint(sub_args, &env),
112 0 : "pg" => bail!("'pg' subcommand has been renamed to 'endpoint'"),
113 0 : _ => bail!("unexpected subcommand {sub_name}"),
114 : };
115 :
116 5040 : if original_env != env {
117 887 : subcommand_result.map(|()| Some(env))
118 : } else {
119 4153 : subcommand_result.map(|()| None)
120 : }
121 : };
122 :
123 5366 : match subcommand_result {
124 1256 : Ok(Some(updated_env)) => updated_env.persist_config(&updated_env.base_data_dir)?,
125 4110 : Ok(None) => (),
126 43 : Err(e) => {
127 43 : eprintln!("command failed: {e:?}");
128 43 : exit(1);
129 : }
130 : }
131 5366 : Ok(())
132 5366 : }
133 :
134 : ///
135 : /// Prints timelines list as a tree-like structure.
136 : ///
137 15 : fn print_timelines_tree(
138 15 : timelines: Vec<TimelineInfo>,
139 15 : mut timeline_name_mappings: HashMap<TenantTimelineId, String>,
140 15 : ) -> Result<()> {
141 15 : let mut timelines_hash = timelines
142 15 : .iter()
143 30 : .map(|t| {
144 30 : (
145 30 : t.timeline_id,
146 30 : TimelineTreeEl {
147 30 : info: t.clone(),
148 30 : children: BTreeSet::new(),
149 30 : name: timeline_name_mappings
150 30 : .remove(&TenantTimelineId::new(t.tenant_id, t.timeline_id)),
151 30 : },
152 30 : )
153 30 : })
154 15 : .collect::<HashMap<_, _>>();
155 :
156 : // Memorize all direct children of each timeline.
157 30 : for timeline in timelines.iter() {
158 30 : if let Some(ancestor_timeline_id) = timeline.ancestor_timeline_id {
159 15 : timelines_hash
160 15 : .get_mut(&ancestor_timeline_id)
161 15 : .context("missing timeline info in the HashMap")?
162 : .children
163 15 : .insert(timeline.timeline_id);
164 15 : }
165 : }
166 :
167 30 : for timeline in timelines_hash.values() {
168 : // Start with root local timelines (no ancestors) first.
169 30 : if timeline.info.ancestor_timeline_id.is_none() {
170 15 : print_timeline(0, &Vec::from([true]), timeline, &timelines_hash)?;
171 15 : }
172 : }
173 :
174 15 : Ok(())
175 15 : }
176 :
177 : ///
178 : /// Recursively prints timeline info with all its children.
179 : ///
180 30 : fn print_timeline(
181 30 : nesting_level: usize,
182 30 : is_last: &[bool],
183 30 : timeline: &TimelineTreeEl,
184 30 : timelines: &HashMap<TimelineId, TimelineTreeEl>,
185 30 : ) -> Result<()> {
186 30 : if nesting_level > 0 {
187 15 : let ancestor_lsn = match timeline.info.ancestor_lsn {
188 15 : Some(lsn) => lsn.to_string(),
189 0 : None => "Unknown Lsn".to_string(),
190 : };
191 :
192 15 : let mut br_sym = "┣━";
193 15 :
194 15 : // Draw each nesting padding with proper style
195 15 : // depending on whether its timeline ended or not.
196 15 : if nesting_level > 1 {
197 3 : for l in &is_last[1..is_last.len() - 1] {
198 3 : if *l {
199 3 : print!(" ");
200 3 : } else {
201 0 : print!("┃ ");
202 0 : }
203 : }
204 12 : }
205 :
206 : // We are the last in this sub-timeline
207 15 : if *is_last.last().unwrap() {
208 10 : br_sym = "┗━";
209 10 : }
210 :
211 15 : print!("{} @{}: ", br_sym, ancestor_lsn);
212 15 : }
213 :
214 : // Finally print a timeline id and name with new line
215 30 : println!(
216 30 : "{} [{}]",
217 30 : timeline.name.as_deref().unwrap_or("_no_name_"),
218 30 : timeline.info.timeline_id
219 30 : );
220 30 :
221 30 : let len = timeline.children.len();
222 30 : let mut i: usize = 0;
223 30 : let mut is_last_new = Vec::from(is_last);
224 30 : is_last_new.push(false);
225 :
226 45 : for child in &timeline.children {
227 15 : i += 1;
228 15 :
229 15 : // Mark that the last padding is the end of the timeline
230 15 : if i == len {
231 10 : if let Some(last) = is_last_new.last_mut() {
232 10 : *last = true;
233 10 : }
234 5 : }
235 :
236 : print_timeline(
237 15 : nesting_level + 1,
238 15 : &is_last_new,
239 15 : timelines
240 15 : .get(child)
241 15 : .context("missing timeline info in the HashMap")?,
242 15 : timelines,
243 0 : )?;
244 : }
245 :
246 30 : Ok(())
247 30 : }
248 :
249 : /// Returns a map of timeline IDs to timeline_id@lsn strings.
250 : /// Connects to the pageserver to query this information.
251 0 : fn get_timeline_infos(
252 0 : env: &local_env::LocalEnv,
253 0 : tenant_id: &TenantId,
254 0 : ) -> Result<HashMap<TimelineId, TimelineInfo>> {
255 0 : Ok(PageServerNode::from_env(env)
256 0 : .timeline_list(tenant_id)?
257 0 : .into_iter()
258 0 : .map(|timeline_info| (timeline_info.timeline_id, timeline_info))
259 0 : .collect())
260 0 : }
261 :
262 : // Helper function to parse --tenant_id option, or get the default from config file
263 : fn get_tenant_id(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> anyhow::Result<TenantId> {
264 2362 : if let Some(tenant_id_from_arguments) = parse_tenant_id(sub_match).transpose() {
265 2362 : tenant_id_from_arguments
266 0 : } else if let Some(default_id) = env.default_tenant_id {
267 0 : Ok(default_id)
268 : } else {
269 0 : anyhow::bail!("No tenant id. Use --tenant-id, or set a default tenant");
270 : }
271 2362 : }
272 :
273 2834 : fn parse_tenant_id(sub_match: &ArgMatches) -> anyhow::Result<Option<TenantId>> {
274 2834 : sub_match
275 2834 : .get_one::<String>("tenant-id")
276 2834 : .map(|tenant_id| TenantId::from_str(tenant_id))
277 2834 : .transpose()
278 2834 : .context("Failed to parse tenant id from the argument string")
279 2834 : }
280 :
281 475 : fn parse_timeline_id(sub_match: &ArgMatches) -> anyhow::Result<Option<TimelineId>> {
282 475 : sub_match
283 475 : .get_one::<String>("timeline-id")
284 475 : .map(|timeline_id| TimelineId::from_str(timeline_id))
285 475 : .transpose()
286 475 : .context("Failed to parse timeline id from the argument string")
287 475 : }
288 :
289 369 : fn handle_init(init_match: &ArgMatches) -> anyhow::Result<LocalEnv> {
290 : // Create config file
291 369 : let toml_file: String = if let Some(config_path) = init_match.get_one::<PathBuf>("config") {
292 : // load and parse the file
293 369 : std::fs::read_to_string(config_path).with_context(|| {
294 0 : format!(
295 0 : "Could not read configuration file '{}'",
296 0 : config_path.display()
297 0 : )
298 369 : })?
299 : } else {
300 : // Built-in default config
301 0 : default_conf()
302 : };
303 :
304 369 : let pg_version = init_match
305 369 : .get_one::<u32>("pg-version")
306 369 : .copied()
307 369 : .context("Failed to parse postgres version from the argument string")?;
308 :
309 369 : let mut env =
310 369 : LocalEnv::parse_config(&toml_file).context("Failed to create neon configuration")?;
311 369 : let force = init_match.get_flag("force");
312 369 : env.init(pg_version, force)
313 369 : .context("Failed to initialize neon repository")?;
314 :
315 : // Initialize pageserver, create initial tenant and timeline.
316 369 : let pageserver = PageServerNode::from_env(&env);
317 369 : pageserver
318 369 : .initialize(&pageserver_config_overrides(init_match))
319 369 : .unwrap_or_else(|e| {
320 0 : eprintln!("pageserver init failed: {e:?}");
321 0 : exit(1);
322 369 : });
323 369 :
324 369 : Ok(env)
325 369 : }
326 :
327 944 : fn pageserver_config_overrides(init_match: &ArgMatches) -> Vec<&str> {
328 944 : init_match
329 944 : .get_many::<String>("pageserver-config-override")
330 944 : .into_iter()
331 944 : .flatten()
332 944 : .map(String::as_str)
333 944 : .collect()
334 944 : }
335 :
336 492 : fn handle_tenant(tenant_match: &ArgMatches, env: &mut local_env::LocalEnv) -> anyhow::Result<()> {
337 492 : let pageserver = PageServerNode::from_env(env);
338 492 : match tenant_match.subcommand() {
339 492 : Some(("list", _)) => {
340 11 : for t in pageserver.tenant_list()? {
341 11 : println!("{} {:?}", t.id, t.state);
342 11 : }
343 : }
344 486 : Some(("create", create_match)) => {
345 472 : let initial_tenant_id = parse_tenant_id(create_match)?;
346 472 : let tenant_conf: HashMap<_, _> = create_match
347 472 : .get_many::<String>("config")
348 779 : .map(|vals| vals.flat_map(|c| c.split_once(':')).collect())
349 472 : .unwrap_or_default();
350 472 : let new_tenant_id = pageserver.tenant_create(initial_tenant_id, tenant_conf)?;
351 470 : println!("tenant {new_tenant_id} successfully created on the pageserver");
352 :
353 : // Create an initial timeline for the new tenant
354 470 : let new_timeline_id = parse_timeline_id(create_match)?;
355 470 : let pg_version = create_match
356 470 : .get_one::<u32>("pg-version")
357 470 : .copied()
358 470 : .context("Failed to parse postgres version from the argument string")?;
359 :
360 470 : let timeline_info = pageserver.timeline_create(
361 470 : new_tenant_id,
362 470 : new_timeline_id,
363 470 : None,
364 470 : None,
365 470 : Some(pg_version),
366 470 : )?;
367 470 : let new_timeline_id = timeline_info.timeline_id;
368 470 : let last_record_lsn = timeline_info.last_record_lsn;
369 470 :
370 470 : env.register_branch_mapping(
371 470 : DEFAULT_BRANCH_NAME.to_string(),
372 470 : new_tenant_id,
373 470 : new_timeline_id,
374 470 : )?;
375 :
376 470 : println!(
377 470 : "Created an initial timeline '{new_timeline_id}' at Lsn {last_record_lsn} for tenant: {new_tenant_id}",
378 470 : );
379 470 :
380 470 : if create_match.get_flag("set-default") {
381 1 : println!("Setting tenant {new_tenant_id} as a default one");
382 1 : env.default_tenant_id = Some(new_tenant_id);
383 469 : }
384 : }
385 14 : Some(("set-default", set_default_match)) => {
386 0 : let tenant_id =
387 0 : parse_tenant_id(set_default_match)?.context("No tenant id specified")?;
388 0 : println!("Setting tenant {tenant_id} as a default one");
389 0 : env.default_tenant_id = Some(tenant_id);
390 : }
391 14 : Some(("config", create_match)) => {
392 14 : let tenant_id = get_tenant_id(create_match, env)?;
393 14 : let tenant_conf: HashMap<_, _> = create_match
394 14 : .get_many::<String>("config")
395 47 : .map(|vals| vals.flat_map(|c| c.split_once(':')).collect())
396 14 : .unwrap_or_default();
397 14 :
398 14 : pageserver
399 14 : .tenant_config(tenant_id, tenant_conf)
400 14 : .with_context(|| format!("Tenant config failed for tenant with id {tenant_id}"))?;
401 14 : println!("tenant {tenant_id} successfully configured on the pageserver");
402 : }
403 0 : Some((sub_name, _)) => bail!("Unexpected tenant subcommand '{}'", sub_name),
404 0 : None => bail!("no tenant subcommand provided"),
405 : }
406 490 : Ok(())
407 492 : }
408 :
409 441 : fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::LocalEnv) -> Result<()> {
410 441 : let pageserver = PageServerNode::from_env(env);
411 441 :
412 441 : match timeline_match.subcommand() {
413 441 : Some(("list", list_match)) => {
414 15 : let tenant_id = get_tenant_id(list_match, env)?;
415 15 : let timelines = pageserver.timeline_list(&tenant_id)?;
416 15 : print_timelines_tree(timelines, env.timeline_name_mappings())?;
417 : }
418 426 : Some(("create", create_match)) => {
419 167 : let tenant_id = get_tenant_id(create_match, env)?;
420 167 : let new_branch_name = create_match
421 167 : .get_one::<String>("branch-name")
422 167 : .ok_or_else(|| anyhow!("No branch name provided"))?;
423 :
424 167 : let pg_version = create_match
425 167 : .get_one::<u32>("pg-version")
426 167 : .copied()
427 167 : .context("Failed to parse postgres version from the argument string")?;
428 :
429 165 : let timeline_info =
430 167 : pageserver.timeline_create(tenant_id, None, None, None, Some(pg_version))?;
431 165 : let new_timeline_id = timeline_info.timeline_id;
432 165 :
433 165 : let last_record_lsn = timeline_info.last_record_lsn;
434 165 : env.register_branch_mapping(new_branch_name.to_string(), tenant_id, new_timeline_id)?;
435 :
436 165 : println!(
437 165 : "Created timeline '{}' at Lsn {last_record_lsn} for tenant: {tenant_id}",
438 165 : timeline_info.timeline_id
439 165 : );
440 : }
441 259 : Some(("import", import_match)) => {
442 5 : let tenant_id = get_tenant_id(import_match, env)?;
443 5 : let timeline_id = parse_timeline_id(import_match)?.expect("No timeline id provided");
444 5 : let name = import_match
445 5 : .get_one::<String>("node-name")
446 5 : .ok_or_else(|| anyhow!("No node name provided"))?;
447 :
448 : // Parse base inputs
449 5 : let base_tarfile = import_match
450 5 : .get_one::<PathBuf>("base-tarfile")
451 5 : .ok_or_else(|| anyhow!("No base-tarfile provided"))?
452 5 : .to_owned();
453 5 : let base_lsn = Lsn::from_str(
454 5 : import_match
455 5 : .get_one::<String>("base-lsn")
456 5 : .ok_or_else(|| anyhow!("No base-lsn provided"))?,
457 0 : )?;
458 5 : let base = (base_lsn, base_tarfile);
459 5 :
460 5 : // Parse pg_wal inputs
461 5 : let wal_tarfile = import_match.get_one::<PathBuf>("wal-tarfile").cloned();
462 5 : let end_lsn = import_match
463 5 : .get_one::<String>("end-lsn")
464 5 : .map(|s| Lsn::from_str(s).unwrap());
465 5 : // TODO validate both or none are provided
466 5 : let pg_wal = end_lsn.zip(wal_tarfile);
467 :
468 5 : let pg_version = import_match
469 5 : .get_one::<u32>("pg-version")
470 5 : .copied()
471 5 : .context("Failed to parse postgres version from the argument string")?;
472 :
473 5 : let mut cplane = ComputeControlPlane::load(env.clone())?;
474 5 : println!("Importing timeline into pageserver ...");
475 5 : pageserver.timeline_import(tenant_id, timeline_id, base, pg_wal, pg_version)?;
476 3 : env.register_branch_mapping(name.to_string(), tenant_id, timeline_id)?;
477 :
478 3 : println!("Creating endpoint for imported timeline ...");
479 3 : cplane.new_endpoint(
480 3 : name,
481 3 : tenant_id,
482 3 : timeline_id,
483 3 : None,
484 3 : None,
485 3 : pg_version,
486 3 : ComputeMode::Primary,
487 3 : )?;
488 3 : println!("Done");
489 : }
490 254 : Some(("branch", branch_match)) => {
491 254 : let tenant_id = get_tenant_id(branch_match, env)?;
492 254 : let new_branch_name = branch_match
493 254 : .get_one::<String>("branch-name")
494 254 : .ok_or_else(|| anyhow!("No branch name provided"))?;
495 254 : let ancestor_branch_name = branch_match
496 254 : .get_one::<String>("ancestor-branch-name")
497 254 : .map(|s| s.as_str())
498 254 : .unwrap_or(DEFAULT_BRANCH_NAME);
499 254 : let ancestor_timeline_id = env
500 254 : .get_branch_timeline_id(ancestor_branch_name, tenant_id)
501 254 : .ok_or_else(|| {
502 0 : anyhow!("Found no timeline id for branch name '{ancestor_branch_name}'")
503 254 : })?;
504 :
505 254 : let start_lsn = branch_match
506 254 : .get_one::<String>("ancestor-start-lsn")
507 254 : .map(|lsn_str| Lsn::from_str(lsn_str))
508 254 : .transpose()
509 254 : .context("Failed to parse ancestor start Lsn from the request")?;
510 254 : let timeline_info = pageserver.timeline_create(
511 254 : tenant_id,
512 254 : None,
513 254 : start_lsn,
514 254 : Some(ancestor_timeline_id),
515 254 : None,
516 254 : )?;
517 250 : let new_timeline_id = timeline_info.timeline_id;
518 250 :
519 250 : let last_record_lsn = timeline_info.last_record_lsn;
520 250 :
521 250 : env.register_branch_mapping(new_branch_name.to_string(), tenant_id, new_timeline_id)?;
522 :
523 250 : println!(
524 250 : "Created timeline '{}' at Lsn {last_record_lsn} for tenant: {tenant_id}. Ancestor timeline: '{ancestor_branch_name}'",
525 250 : timeline_info.timeline_id
526 250 : );
527 : }
528 0 : Some((sub_name, _)) => bail!("Unexpected tenant subcommand '{sub_name}'"),
529 0 : None => bail!("no tenant subcommand provided"),
530 : }
531 :
532 433 : Ok(())
533 441 : }
534 :
535 1907 : fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> {
536 1907 : let (sub_name, sub_args) = match ep_match.subcommand() {
537 1907 : Some(ep_subcommand_data) => ep_subcommand_data,
538 0 : None => bail!("no endpoint subcommand provided"),
539 : };
540 :
541 1907 : let mut cplane = ComputeControlPlane::load(env.clone())?;
542 :
543 : // All subcommands take an optional --tenant-id option
544 1907 : let tenant_id = get_tenant_id(sub_args, env)?;
545 :
546 1907 : match sub_name {
547 1907 : "list" => {
548 0 : let timeline_infos = get_timeline_infos(env, &tenant_id).unwrap_or_else(|e| {
549 0 : eprintln!("Failed to load timeline info: {}", e);
550 0 : HashMap::new()
551 0 : });
552 0 :
553 0 : let timeline_name_mappings = env.timeline_name_mappings();
554 0 :
555 0 : let mut table = comfy_table::Table::new();
556 0 :
557 0 : table.load_preset(comfy_table::presets::NOTHING);
558 0 :
559 0 : table.set_header([
560 0 : "ENDPOINT",
561 0 : "ADDRESS",
562 0 : "TIMELINE",
563 0 : "BRANCH NAME",
564 0 : "LSN",
565 0 : "STATUS",
566 0 : ]);
567 :
568 0 : for (endpoint_id, endpoint) in cplane
569 0 : .endpoints
570 0 : .iter()
571 0 : .filter(|(_, endpoint)| endpoint.tenant_id == tenant_id)
572 0 : {
573 0 : let lsn_str = match endpoint.mode {
574 0 : ComputeMode::Static(lsn) => {
575 0 : // -> read-only endpoint
576 0 : // Use the node's LSN.
577 0 : lsn.to_string()
578 : }
579 : _ => {
580 : // -> primary endpoint or hot replica
581 : // Use the LSN at the end of the timeline.
582 0 : timeline_infos
583 0 : .get(&endpoint.timeline_id)
584 0 : .map(|bi| bi.last_record_lsn.to_string())
585 0 : .unwrap_or_else(|| "?".to_string())
586 : }
587 : };
588 :
589 0 : let branch_name = timeline_name_mappings
590 0 : .get(&TenantTimelineId::new(tenant_id, endpoint.timeline_id))
591 0 : .map(|name| name.as_str())
592 0 : .unwrap_or("?");
593 0 :
594 0 : table.add_row([
595 0 : endpoint_id.as_str(),
596 0 : &endpoint.pg_address.to_string(),
597 0 : &endpoint.timeline_id.to_string(),
598 0 : branch_name,
599 0 : lsn_str.as_str(),
600 0 : endpoint.status(),
601 0 : ]);
602 : }
603 :
604 0 : println!("{table}");
605 : }
606 1907 : "create" => {
607 592 : let branch_name = sub_args
608 592 : .get_one::<String>("branch-name")
609 592 : .map(|s| s.as_str())
610 592 : .unwrap_or(DEFAULT_BRANCH_NAME);
611 592 : let endpoint_id = sub_args
612 592 : .get_one::<String>("endpoint_id")
613 592 : .map(String::to_string)
614 592 : .unwrap_or_else(|| format!("ep-{branch_name}"));
615 :
616 592 : let lsn = sub_args
617 592 : .get_one::<String>("lsn")
618 592 : .map(|lsn_str| Lsn::from_str(lsn_str))
619 592 : .transpose()
620 592 : .context("Failed to parse Lsn from the request")?;
621 592 : let timeline_id = env
622 592 : .get_branch_timeline_id(branch_name, tenant_id)
623 592 : .ok_or_else(|| anyhow!("Found no timeline id for branch name '{branch_name}'"))?;
624 :
625 592 : let pg_port: Option<u16> = sub_args.get_one::<u16>("pg-port").copied();
626 592 : let http_port: Option<u16> = sub_args.get_one::<u16>("http-port").copied();
627 592 : let pg_version = sub_args
628 592 : .get_one::<u32>("pg-version")
629 592 : .copied()
630 592 : .context("Failed to parse postgres version from the argument string")?;
631 :
632 592 : let hot_standby = sub_args
633 592 : .get_one::<bool>("hot-standby")
634 592 : .copied()
635 592 : .unwrap_or(false);
636 :
637 592 : let mode = match (lsn, hot_standby) {
638 88 : (Some(lsn), false) => ComputeMode::Static(lsn),
639 1 : (None, true) => ComputeMode::Replica,
640 503 : (None, false) => ComputeMode::Primary,
641 0 : (Some(_), true) => anyhow::bail!("cannot specify both lsn and hot-standby"),
642 : };
643 :
644 592 : cplane.new_endpoint(
645 592 : &endpoint_id,
646 592 : tenant_id,
647 592 : timeline_id,
648 592 : pg_port,
649 592 : http_port,
650 592 : pg_version,
651 592 : mode,
652 592 : )?;
653 : }
654 1315 : "start" => {
655 663 : let pg_port: Option<u16> = sub_args.get_one::<u16>("pg-port").copied();
656 663 : let http_port: Option<u16> = sub_args.get_one::<u16>("http-port").copied();
657 663 : let endpoint_id = sub_args
658 663 : .get_one::<String>("endpoint_id")
659 663 : .ok_or_else(|| anyhow!("No endpoint ID was provided to start"))?;
660 :
661 663 : let remote_ext_config = sub_args.get_one::<String>("remote-ext-config");
662 :
663 : // If --safekeepers argument is given, use only the listed safekeeper nodes.
664 663 : let safekeepers =
665 663 : if let Some(safekeepers_str) = sub_args.get_one::<String>("safekeepers") {
666 659 : let mut safekeepers: Vec<NodeId> = Vec::new();
667 862 : for sk_id in safekeepers_str.split(',').map(str::trim) {
668 862 : let sk_id = NodeId(u64::from_str(sk_id).map_err(|_| {
669 0 : anyhow!("invalid node ID \"{sk_id}\" in --safekeepers list")
670 862 : })?);
671 862 : safekeepers.push(sk_id);
672 : }
673 659 : safekeepers
674 : } else {
675 8 : env.safekeepers.iter().map(|sk| sk.id).collect()
676 : };
677 :
678 663 : let endpoint = cplane.endpoints.get(endpoint_id.as_str());
679 :
680 663 : let auth_token = if matches!(env.pageserver.pg_auth_type, AuthType::NeonJWT) {
681 15 : let claims = Claims::new(Some(tenant_id), Scope::Tenant);
682 15 :
683 15 : Some(env.generate_auth_token(&claims)?)
684 : } else {
685 648 : None
686 : };
687 :
688 663 : let hot_standby = sub_args
689 663 : .get_one::<bool>("hot-standby")
690 663 : .copied()
691 663 : .unwrap_or(false);
692 :
693 663 : if let Some(endpoint) = endpoint {
694 659 : match (&endpoint.mode, hot_standby) {
695 : (ComputeMode::Static(_), true) => {
696 0 : bail!("Cannot start a node in hot standby mode when it is already configured as a static replica")
697 : }
698 : (ComputeMode::Primary, true) => {
699 0 : bail!("Cannot start a node as a hot standby replica, it is already configured as primary node")
700 : }
701 659 : _ => {}
702 659 : }
703 659 : println!("Starting existing endpoint {endpoint_id}...");
704 659 : endpoint.start(&auth_token, safekeepers, remote_ext_config)?;
705 : } else {
706 4 : let branch_name = sub_args
707 4 : .get_one::<String>("branch-name")
708 4 : .map(|s| s.as_str())
709 4 : .unwrap_or(DEFAULT_BRANCH_NAME);
710 4 : let timeline_id = env
711 4 : .get_branch_timeline_id(branch_name, tenant_id)
712 4 : .ok_or_else(|| {
713 0 : anyhow!("Found no timeline id for branch name '{branch_name}'")
714 4 : })?;
715 4 : let lsn = sub_args
716 4 : .get_one::<String>("lsn")
717 4 : .map(|lsn_str| Lsn::from_str(lsn_str))
718 4 : .transpose()
719 4 : .context("Failed to parse Lsn from the request")?;
720 4 : let pg_version = sub_args
721 4 : .get_one::<u32>("pg-version")
722 4 : .copied()
723 4 : .context("Failed to `pg-version` from the argument string")?;
724 :
725 4 : let mode = match (lsn, hot_standby) {
726 0 : (Some(lsn), false) => ComputeMode::Static(lsn),
727 0 : (None, true) => ComputeMode::Replica,
728 4 : (None, false) => ComputeMode::Primary,
729 0 : (Some(_), true) => anyhow::bail!("cannot specify both lsn and hot-standby"),
730 : };
731 :
732 : // when used with custom port this results in non obvious behaviour
733 : // port is remembered from first start command, i e
734 : // start --port X
735 : // stop
736 : // start <-- will also use port X even without explicit port argument
737 4 : println!("Starting new endpoint {endpoint_id} (PostgreSQL v{pg_version}) on timeline {timeline_id} ...");
738 :
739 4 : let ep = cplane.new_endpoint(
740 4 : endpoint_id,
741 4 : tenant_id,
742 4 : timeline_id,
743 4 : pg_port,
744 4 : http_port,
745 4 : pg_version,
746 4 : mode,
747 4 : )?;
748 4 : ep.start(&auth_token, safekeepers, remote_ext_config)?;
749 : }
750 : }
751 652 : "stop" => {
752 652 : let endpoint_id = sub_args
753 652 : .get_one::<String>("endpoint_id")
754 652 : .ok_or_else(|| anyhow!("No endpoint ID was provided to stop"))?;
755 652 : let destroy = sub_args.get_flag("destroy");
756 :
757 652 : let endpoint = cplane
758 652 : .endpoints
759 652 : .get(endpoint_id.as_str())
760 652 : .with_context(|| format!("postgres endpoint {endpoint_id} is not found"))?;
761 652 : endpoint.stop(destroy)?;
762 : }
763 :
764 0 : _ => bail!("Unexpected endpoint subcommand '{sub_name}'"),
765 : }
766 :
767 1874 : Ok(())
768 1907 : }
769 :
770 1144 : fn handle_pageserver(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> {
771 1144 : let pageserver = PageServerNode::from_env(env);
772 1144 :
773 1144 : match sub_match.subcommand() {
774 1144 : Some(("start", start_match)) => {
775 571 : if let Err(e) = pageserver.start(&pageserver_config_overrides(start_match)) {
776 0 : eprintln!("pageserver start failed: {e}");
777 0 : exit(1);
778 571 : }
779 : }
780 :
781 573 : Some(("stop", stop_match)) => {
782 573 : let immediate = stop_match
783 573 : .get_one::<String>("stop-mode")
784 573 : .map(|s| s.as_str())
785 573 : == Some("immediate");
786 :
787 573 : if let Err(e) = pageserver.stop(immediate) {
788 0 : eprintln!("pageserver stop failed: {}", e);
789 0 : exit(1);
790 573 : }
791 : }
792 :
793 0 : Some(("restart", restart_match)) => {
794 : //TODO what shutdown strategy should we use here?
795 0 : if let Err(e) = pageserver.stop(false) {
796 0 : eprintln!("pageserver stop failed: {}", e);
797 0 : exit(1);
798 0 : }
799 :
800 0 : if let Err(e) = pageserver.start(&pageserver_config_overrides(restart_match)) {
801 0 : eprintln!("pageserver start failed: {e}");
802 0 : exit(1);
803 0 : }
804 : }
805 :
806 0 : Some(("status", _)) => match PageServerNode::from_env(env).check_status() {
807 0 : Ok(_) => println!("Page server is up and running"),
808 0 : Err(err) => {
809 0 : eprintln!("Page server is not available: {}", err);
810 0 : exit(1);
811 : }
812 : },
813 :
814 0 : Some((sub_name, _)) => bail!("Unexpected pageserver subcommand '{}'", sub_name),
815 0 : None => bail!("no pageserver subcommand provided"),
816 : }
817 1144 : Ok(())
818 1144 : }
819 :
820 : fn get_safekeeper(env: &local_env::LocalEnv, id: NodeId) -> Result<SafekeeperNode> {
821 1378 : if let Some(node) = env.safekeepers.iter().find(|node| node.id == id) {
822 1048 : Ok(SafekeeperNode::from_env(env, node))
823 : } else {
824 0 : bail!("could not find safekeeper {id}")
825 : }
826 1048 : }
827 :
828 : // Get list of options to append to safekeeper command invocation.
829 509 : fn safekeeper_extra_opts(init_match: &ArgMatches) -> Vec<String> {
830 509 : init_match
831 509 : .get_many::<String>("safekeeper-extra-opt")
832 509 : .into_iter()
833 509 : .flatten()
834 509 : .map(|s| s.to_owned())
835 509 : .collect()
836 509 : }
837 :
838 1048 : fn handle_safekeeper(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> {
839 1048 : let (sub_name, sub_args) = match sub_match.subcommand() {
840 1048 : Some(safekeeper_command_data) => safekeeper_command_data,
841 0 : None => bail!("no safekeeper subcommand provided"),
842 : };
843 :
844 : // All the commands take an optional safekeeper name argument
845 1048 : let sk_id = if let Some(id_str) = sub_args.get_one::<String>("id") {
846 1039 : NodeId(id_str.parse().context("while parsing safekeeper id")?)
847 : } else {
848 9 : DEFAULT_SAFEKEEPER_ID
849 : };
850 1048 : let safekeeper = get_safekeeper(env, sk_id)?;
851 :
852 1048 : match sub_name {
853 1048 : "start" => {
854 509 : let extra_opts = safekeeper_extra_opts(sub_args);
855 :
856 509 : if let Err(e) = safekeeper.start(extra_opts) {
857 0 : eprintln!("safekeeper start failed: {}", e);
858 0 : exit(1);
859 509 : }
860 : }
861 :
862 539 : "stop" => {
863 539 : let immediate =
864 539 : sub_args.get_one::<String>("stop-mode").map(|s| s.as_str()) == Some("immediate");
865 :
866 539 : if let Err(e) = safekeeper.stop(immediate) {
867 0 : eprintln!("safekeeper stop failed: {}", e);
868 0 : exit(1);
869 539 : }
870 : }
871 :
872 0 : "restart" => {
873 0 : let immediate =
874 0 : sub_args.get_one::<String>("stop-mode").map(|s| s.as_str()) == Some("immediate");
875 :
876 0 : if let Err(e) = safekeeper.stop(immediate) {
877 0 : eprintln!("safekeeper stop failed: {}", e);
878 0 : exit(1);
879 0 : }
880 0 :
881 0 : let extra_opts = safekeeper_extra_opts(sub_args);
882 0 : if let Err(e) = safekeeper.start(extra_opts) {
883 0 : eprintln!("safekeeper start failed: {}", e);
884 0 : exit(1);
885 0 : }
886 : }
887 :
888 : _ => {
889 0 : bail!("Unexpected safekeeper subcommand '{}'", sub_name)
890 : }
891 : }
892 1048 : Ok(())
893 1048 : }
894 :
895 4 : fn handle_start_all(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> anyhow::Result<()> {
896 4 : // Endpoints are not started automatically
897 4 :
898 4 : broker::start_broker_process(env)?;
899 :
900 4 : let pageserver = PageServerNode::from_env(env);
901 4 : if let Err(e) = pageserver.start(&pageserver_config_overrides(sub_match)) {
902 0 : eprintln!("pageserver {} start failed: {:#}", env.pageserver.id, e);
903 0 : try_stop_all(env, true);
904 0 : exit(1);
905 4 : }
906 :
907 8 : for node in env.safekeepers.iter() {
908 8 : let safekeeper = SafekeeperNode::from_env(env, node);
909 8 : if let Err(e) = safekeeper.start(vec![]) {
910 0 : eprintln!("safekeeper {} start failed: {:#}", safekeeper.id, e);
911 0 : try_stop_all(env, false);
912 0 : exit(1);
913 8 : }
914 : }
915 4 : Ok(())
916 4 : }
917 :
918 4 : fn handle_stop_all(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> {
919 4 : let immediate =
920 4 : sub_match.get_one::<String>("stop-mode").map(|s| s.as_str()) == Some("immediate");
921 4 :
922 4 : try_stop_all(env, immediate);
923 4 :
924 4 : Ok(())
925 4 : }
926 :
927 4 : fn try_stop_all(env: &local_env::LocalEnv, immediate: bool) {
928 4 : let pageserver = PageServerNode::from_env(env);
929 4 :
930 4 : // Stop all endpoints
931 4 : match ComputeControlPlane::load(env.clone()) {
932 4 : Ok(cplane) => {
933 8 : for (_k, node) in cplane.endpoints {
934 4 : if let Err(e) = node.stop(false) {
935 2 : eprintln!("postgres stop failed: {e:#}");
936 2 : }
937 : }
938 : }
939 0 : Err(e) => {
940 0 : eprintln!("postgres stop failed, could not restore control plane data from env: {e:#}")
941 : }
942 : }
943 :
944 4 : if let Err(e) = pageserver.stop(immediate) {
945 0 : eprintln!("pageserver {} stop failed: {:#}", env.pageserver.id, e);
946 4 : }
947 :
948 8 : for node in env.safekeepers.iter() {
949 8 : let safekeeper = SafekeeperNode::from_env(env, node);
950 8 : if let Err(e) = safekeeper.stop(immediate) {
951 0 : eprintln!("safekeeper {} stop failed: {:#}", safekeeper.id, e);
952 8 : }
953 : }
954 :
955 4 : if let Err(e) = broker::stop_broker_process(env) {
956 0 : eprintln!("neon broker stop failed: {e:#}");
957 4 : }
958 4 : }
959 :
960 5410 : fn cli() -> Command {
961 5410 : let branch_name_arg = Arg::new("branch-name")
962 5410 : .long("branch-name")
963 5410 : .help("Name of the branch to be created or used as an alias for other services")
964 5410 : .required(false);
965 5410 :
966 5410 : let endpoint_id_arg = Arg::new("endpoint_id")
967 5410 : .help("Postgres endpoint id")
968 5410 : .required(false);
969 5410 :
970 5410 : let safekeeper_id_arg = Arg::new("id").help("safekeeper id").required(false);
971 5410 :
972 5410 : let safekeeper_extra_opt_arg = Arg::new("safekeeper-extra-opt")
973 5410 : .short('e')
974 5410 : .long("safekeeper-extra-opt")
975 5410 : .num_args(1)
976 5410 : .action(ArgAction::Append)
977 5410 : .help("Additional safekeeper invocation options, e.g. -e=--http-auth-public-key-path=foo")
978 5410 : .required(false);
979 5410 :
980 5410 : let tenant_id_arg = Arg::new("tenant-id")
981 5410 : .long("tenant-id")
982 5410 : .help("Tenant id. Represented as a hexadecimal string 32 symbols length")
983 5410 : .required(false);
984 5410 :
985 5410 : let timeline_id_arg = Arg::new("timeline-id")
986 5410 : .long("timeline-id")
987 5410 : .help("Timeline id. Represented as a hexadecimal string 32 symbols length")
988 5410 : .required(false);
989 5410 :
990 5410 : let pg_version_arg = Arg::new("pg-version")
991 5410 : .long("pg-version")
992 5410 : .help("Postgres version to use for the initial tenant")
993 5410 : .required(false)
994 5410 : .value_parser(value_parser!(u32))
995 5410 : .default_value(DEFAULT_PG_VERSION);
996 5410 :
997 5410 : let pg_port_arg = Arg::new("pg-port")
998 5410 : .long("pg-port")
999 5410 : .required(false)
1000 5410 : .value_parser(value_parser!(u16))
1001 5410 : .value_name("pg-port");
1002 5410 :
1003 5410 : let http_port_arg = Arg::new("http-port")
1004 5410 : .long("http-port")
1005 5410 : .required(false)
1006 5410 : .value_parser(value_parser!(u16))
1007 5410 : .value_name("http-port");
1008 5410 :
1009 5410 : let safekeepers_arg = Arg::new("safekeepers")
1010 5410 : .long("safekeepers")
1011 5410 : .required(false)
1012 5410 : .value_name("safekeepers");
1013 5410 :
1014 5410 : let stop_mode_arg = Arg::new("stop-mode")
1015 5410 : .short('m')
1016 5410 : .value_parser(["fast", "immediate"])
1017 5410 : .default_value("fast")
1018 5410 : .help("If 'immediate', don't flush repository data at shutdown")
1019 5410 : .required(false)
1020 5410 : .value_name("stop-mode");
1021 5410 :
1022 5410 : let pageserver_config_args = Arg::new("pageserver-config-override")
1023 5410 : .long("pageserver-config-override")
1024 5410 : .num_args(1)
1025 5410 : .action(ArgAction::Append)
1026 5410 : .help("Additional pageserver's configuration options or overrides, refer to pageserver's 'config-override' CLI parameter docs for more")
1027 5410 : .required(false);
1028 5410 :
1029 5410 : let remote_ext_config_args = Arg::new("remote-ext-config")
1030 5410 : .long("remote-ext-config")
1031 5410 : .num_args(1)
1032 5410 : .help("Configure the S3 bucket that we search for extensions in.")
1033 5410 : .required(false);
1034 5410 :
1035 5410 : let lsn_arg = Arg::new("lsn")
1036 5410 : .long("lsn")
1037 5410 : .help("Specify Lsn on the timeline to start from. By default, end of the timeline would be used.")
1038 5410 : .required(false);
1039 5410 :
1040 5410 : let hot_standby_arg = Arg::new("hot-standby")
1041 5410 : .value_parser(value_parser!(bool))
1042 5410 : .long("hot-standby")
1043 5410 : .help("If set, the node will be a hot replica on the specified timeline")
1044 5410 : .required(false);
1045 5410 :
1046 5410 : let force_arg = Arg::new("force")
1047 5410 : .value_parser(value_parser!(bool))
1048 5410 : .long("force")
1049 5410 : .action(ArgAction::SetTrue)
1050 5410 : .help("Force initialization even if the repository is not empty")
1051 5410 : .required(false);
1052 5410 :
1053 5410 : Command::new("Neon CLI")
1054 5410 : .arg_required_else_help(true)
1055 5410 : .version(GIT_VERSION)
1056 5410 : .subcommand(
1057 5410 : Command::new("init")
1058 5410 : .about("Initialize a new Neon repository, preparing configs for services to start with")
1059 5410 : .arg(pageserver_config_args.clone())
1060 5410 : .arg(
1061 5410 : Arg::new("config")
1062 5410 : .long("config")
1063 5410 : .required(false)
1064 5410 : .value_parser(value_parser!(PathBuf))
1065 5410 : .value_name("config"),
1066 5410 : )
1067 5410 : .arg(pg_version_arg.clone())
1068 5410 : .arg(force_arg)
1069 5410 : )
1070 5410 : .subcommand(
1071 5410 : Command::new("timeline")
1072 5410 : .about("Manage timelines")
1073 5410 : .subcommand(Command::new("list")
1074 5410 : .about("List all timelines, available to this pageserver")
1075 5410 : .arg(tenant_id_arg.clone()))
1076 5410 : .subcommand(Command::new("branch")
1077 5410 : .about("Create a new timeline, using another timeline as a base, copying its data")
1078 5410 : .arg(tenant_id_arg.clone())
1079 5410 : .arg(branch_name_arg.clone())
1080 5410 : .arg(Arg::new("ancestor-branch-name").long("ancestor-branch-name")
1081 5410 : .help("Use last Lsn of another timeline (and its data) as base when creating the new timeline. The timeline gets resolved by its branch name.").required(false))
1082 5410 : .arg(Arg::new("ancestor-start-lsn").long("ancestor-start-lsn")
1083 5410 : .help("When using another timeline as base, use a specific Lsn in it instead of the latest one").required(false)))
1084 5410 : .subcommand(Command::new("create")
1085 5410 : .about("Create a new blank timeline")
1086 5410 : .arg(tenant_id_arg.clone())
1087 5410 : .arg(branch_name_arg.clone())
1088 5410 : .arg(pg_version_arg.clone())
1089 5410 : )
1090 5410 : .subcommand(Command::new("import")
1091 5410 : .about("Import timeline from basebackup directory")
1092 5410 : .arg(tenant_id_arg.clone())
1093 5410 : .arg(timeline_id_arg.clone())
1094 5410 : .arg(Arg::new("node-name").long("node-name")
1095 5410 : .help("Name to assign to the imported timeline"))
1096 5410 : .arg(Arg::new("base-tarfile")
1097 5410 : .long("base-tarfile")
1098 5410 : .value_parser(value_parser!(PathBuf))
1099 5410 : .help("Basebackup tarfile to import")
1100 5410 : )
1101 5410 : .arg(Arg::new("base-lsn").long("base-lsn")
1102 5410 : .help("Lsn the basebackup starts at"))
1103 5410 : .arg(Arg::new("wal-tarfile")
1104 5410 : .long("wal-tarfile")
1105 5410 : .value_parser(value_parser!(PathBuf))
1106 5410 : .help("Wal to add after base")
1107 5410 : )
1108 5410 : .arg(Arg::new("end-lsn").long("end-lsn")
1109 5410 : .help("Lsn the basebackup ends at"))
1110 5410 : .arg(pg_version_arg.clone())
1111 5410 : )
1112 5410 : ).subcommand(
1113 5410 : Command::new("tenant")
1114 5410 : .arg_required_else_help(true)
1115 5410 : .about("Manage tenants")
1116 5410 : .subcommand(Command::new("list"))
1117 5410 : .subcommand(Command::new("create")
1118 5410 : .arg(tenant_id_arg.clone())
1119 5410 : .arg(timeline_id_arg.clone().help("Use a specific timeline id when creating a tenant and its initial timeline"))
1120 5410 : .arg(Arg::new("config").short('c').num_args(1).action(ArgAction::Append).required(false))
1121 5410 : .arg(pg_version_arg.clone())
1122 5410 : .arg(Arg::new("set-default").long("set-default").action(ArgAction::SetTrue).required(false)
1123 5410 : .help("Use this tenant in future CLI commands where tenant_id is needed, but not specified"))
1124 5410 : )
1125 5410 : .subcommand(Command::new("set-default").arg(tenant_id_arg.clone().required(true))
1126 5410 : .about("Set a particular tenant as default in future CLI commands where tenant_id is needed, but not specified"))
1127 5410 : .subcommand(Command::new("config")
1128 5410 : .arg(tenant_id_arg.clone())
1129 5410 : .arg(Arg::new("config").short('c').num_args(1).action(ArgAction::Append).required(false)))
1130 5410 : )
1131 5410 : .subcommand(
1132 5410 : Command::new("pageserver")
1133 5410 : .arg_required_else_help(true)
1134 5410 : .about("Manage pageserver")
1135 5410 : .subcommand(Command::new("status"))
1136 5410 : .subcommand(Command::new("start").about("Start local pageserver").arg(pageserver_config_args.clone()))
1137 5410 : .subcommand(Command::new("stop").about("Stop local pageserver")
1138 5410 : .arg(stop_mode_arg.clone()))
1139 5410 : .subcommand(Command::new("restart").about("Restart local pageserver").arg(pageserver_config_args.clone()))
1140 5410 : )
1141 5410 : .subcommand(
1142 5410 : Command::new("safekeeper")
1143 5410 : .arg_required_else_help(true)
1144 5410 : .about("Manage safekeepers")
1145 5410 : .subcommand(Command::new("start")
1146 5410 : .about("Start local safekeeper")
1147 5410 : .arg(safekeeper_id_arg.clone())
1148 5410 : .arg(safekeeper_extra_opt_arg.clone())
1149 5410 : )
1150 5410 : .subcommand(Command::new("stop")
1151 5410 : .about("Stop local safekeeper")
1152 5410 : .arg(safekeeper_id_arg.clone())
1153 5410 : .arg(stop_mode_arg.clone())
1154 5410 : )
1155 5410 : .subcommand(Command::new("restart")
1156 5410 : .about("Restart local safekeeper")
1157 5410 : .arg(safekeeper_id_arg)
1158 5410 : .arg(stop_mode_arg.clone())
1159 5410 : .arg(safekeeper_extra_opt_arg)
1160 5410 : )
1161 5410 : )
1162 5410 : .subcommand(
1163 5410 : Command::new("endpoint")
1164 5410 : .arg_required_else_help(true)
1165 5410 : .about("Manage postgres instances")
1166 5410 : .subcommand(Command::new("list").arg(tenant_id_arg.clone()))
1167 5410 : .subcommand(Command::new("create")
1168 5410 : .about("Create a compute endpoint")
1169 5410 : .arg(endpoint_id_arg.clone())
1170 5410 : .arg(branch_name_arg.clone())
1171 5410 : .arg(tenant_id_arg.clone())
1172 5410 : .arg(lsn_arg.clone())
1173 5410 : .arg(pg_port_arg.clone())
1174 5410 : .arg(http_port_arg.clone())
1175 5410 : .arg(
1176 5410 : Arg::new("config-only")
1177 5410 : .help("Don't do basebackup, create endpoint directory with only config files")
1178 5410 : .long("config-only")
1179 5410 : .required(false))
1180 5410 : .arg(pg_version_arg.clone())
1181 5410 : .arg(hot_standby_arg.clone())
1182 5410 : )
1183 5410 : .subcommand(Command::new("start")
1184 5410 : .about("Start postgres.\n If the endpoint doesn't exist yet, it is created.")
1185 5410 : .arg(endpoint_id_arg.clone())
1186 5410 : .arg(tenant_id_arg.clone())
1187 5410 : .arg(branch_name_arg)
1188 5410 : .arg(timeline_id_arg)
1189 5410 : .arg(lsn_arg)
1190 5410 : .arg(pg_port_arg)
1191 5410 : .arg(http_port_arg)
1192 5410 : .arg(pg_version_arg)
1193 5410 : .arg(hot_standby_arg)
1194 5410 : .arg(safekeepers_arg)
1195 5410 : .arg(remote_ext_config_args)
1196 5410 : )
1197 5410 : .subcommand(
1198 5410 : Command::new("stop")
1199 5410 : .arg(endpoint_id_arg)
1200 5410 : .arg(tenant_id_arg)
1201 5410 : .arg(
1202 5410 : Arg::new("destroy")
1203 5410 : .help("Also delete data directory (now optional, should be default in future)")
1204 5410 : .long("destroy")
1205 5410 : .action(ArgAction::SetTrue)
1206 5410 : .required(false)
1207 5410 : )
1208 5410 : )
1209 5410 :
1210 5410 : )
1211 5410 : // Obsolete old name for 'endpoint'. We now just print an error if it's used.
1212 5410 : .subcommand(
1213 5410 : Command::new("pg")
1214 5410 : .hide(true)
1215 5410 : .arg(Arg::new("ignore-rest").allow_hyphen_values(true).num_args(0..).required(false))
1216 5410 : .trailing_var_arg(true)
1217 5410 : )
1218 5410 : .subcommand(
1219 5410 : Command::new("start")
1220 5410 : .about("Start page server and safekeepers")
1221 5410 : .arg(pageserver_config_args)
1222 5410 : )
1223 5410 : .subcommand(
1224 5410 : Command::new("stop")
1225 5410 : .about("Stop page server and safekeepers")
1226 5410 : .arg(stop_mode_arg)
1227 5410 : )
1228 5410 : }
1229 :
1230 1 : #[test]
1231 1 : fn verify_cli() {
1232 1 : cli().debug_assert();
1233 1 : }
|