LCOV - code coverage report
Current view: top level - control_plane/src/bin - neon_local.rs (source / functions) Coverage Total Hit
Test: 8ac049b474321fdc72ddcb56d7165153a1a900e8.info Lines: 82.7 % 964 797
Test Date: 2023-09-06 10:18:01 Functions: 61.8 % 68 42

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

Generated by: LCOV version 2.1-beta