Skip to content

Commit 6b968f3

Browse files
committed
restate up command
A developer-focused version of restate that's embedded into the restate CLI. It starts restate on an ephemeral temporary directory that's auto-deleted after Ctrl+C. 1. Supports --use-random-ports 2. Emits very clean output, it doesn't show the server log. Just a table of addresses. 3. Opens the admin UI automatically on startup in the browser 4. Runs the Counter service on a random port and auto-registers it by default so you can play with the UI immediately with that service. 5. Supports --retain to persist the temporary directory (meant to be used in debugging) and currently it doesn't support choosing your own directory
1 parent 24941cd commit 6b968f3

File tree

6 files changed

+204
-3
lines changed

6 files changed

+204
-3
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ dist = true
1717
formula = "restate"
1818

1919
[features]
20-
default = ["cloud", "no-trace-logging"]
20+
default = ["cloud", "no-trace-logging", "dev-cmd"]
21+
dev-cmd = ["restate-lite", "mock-service-endpoint"]
2122
cloud = []
2223
no-trace-logging = ["tracing/max_level_trace", "tracing/release_max_level_debug"]
2324

@@ -30,6 +31,8 @@ restate-cloud-tunnel-client = { workspace = true }
3031
restate-serde-util = { workspace = true }
3132
restate-time-util = { workspace = true }
3233
restate-types = { workspace = true }
34+
restate-lite = { workspace = true, optional = true }
35+
mock-service-endpoint = { workspace = true, optional = true }
3336

3437
anyhow = { workspace = true }
3538
arc-swap = { workspace = true }

cli/src/app.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ pub struct GlobalOpts {
4949

5050
#[derive(Run, Subcommand, Clone)]
5151
pub enum Command {
52+
#[cfg(feature = "dev-cmd")]
53+
#[clap(name = "dev", visible_alias = "up")]
54+
Dev(dev::Dev),
55+
5256
/// Prints general information about the configured environment
5357
#[clap(name = "whoami")]
5458
WhoAmI(whoami::WhoAmI),

cli/src/build_info.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
// Use of this software is governed by the Business Source License
55
// included in the LICENSE file.
66
//
7-
// As of the Change Date specified in that file, in accordance with
8-
// the Business Source License, use of this software will be governed
7+
// As of the Change Date specified in that file, in accordance with the Business Source License, use of this software will be governed
98
// by the Apache License, Version 2.0.
109

1110
//! Build information
1211
#![allow(dead_code)]
1312

13+
use std::str::FromStr;
1414
use std::sync::OnceLock;
1515

16+
use restate_cli_util::c_println;
17+
use restate_types::SemanticRestateVersion;
18+
1619
/// The version of restate CLI.
1720
pub const RESTATE_CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
1821
pub const RESTATE_CLI_VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
@@ -51,3 +54,40 @@ const RESTATE_CLI_DEBUG: &str = env!("VERGEN_CARGO_DEBUG");
5154
pub fn is_debug() -> bool {
5255
RESTATE_CLI_DEBUG == "true" && RESTATE_CLI_DEBUG_STRIPPED != Some("true")
5356
}
57+
58+
pub async fn check_if_latest_version() {
59+
let octocrab = octocrab::instance();
60+
let Ok(mut latest) = octocrab
61+
.repos("restatedev", "restate")
62+
.releases()
63+
.get_latest()
64+
.await
65+
else {
66+
return;
67+
};
68+
69+
let current_version = SemanticRestateVersion::current().clone();
70+
if latest.tag_name.starts_with("v") {
71+
// ignore if the tag is not a version
72+
let version_str = latest.tag_name.split_off(1);
73+
let Ok(latest_release) = SemanticRestateVersion::from_str(&version_str) else {
74+
return;
75+
};
76+
77+
if matches!(
78+
latest_release.cmp_precedence(&current_version),
79+
std::cmp::Ordering::Greater
80+
) {
81+
c_println!(
82+
"📣⬆️A newer version was released at {}, v{}->v{}. Check it out at {}.",
83+
current_version.to_string(),
84+
latest_release.to_string(),
85+
latest
86+
.published_at
87+
.map(|d| d.to_string())
88+
.unwrap_or("<unknown>".to_owned()),
89+
latest.html_url.to_string()
90+
);
91+
}
92+
}
93+
}

cli/src/commands/dev.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) 2023 - 2025 Restate Software, Inc., Restate GmbH.
2+
// All rights reserved.
3+
//
4+
// Use of this software is governed by the Business Source License
5+
// included in the LICENSE file.
6+
//
7+
// As of the Change Date specified in that file, in accordance with
8+
// the Business Source License, use of this software will be governed
9+
// by the Apache License, Version 2.0.
10+
11+
use anyhow::{Result, anyhow};
12+
use cling::prelude::*;
13+
use comfy_table::{Cell, Table};
14+
use tokio::net::TcpListener;
15+
use tokio::sync::oneshot;
16+
use tokio_util::sync::CancellationToken;
17+
18+
use restate_cli_util::ui::console::StyledTable;
19+
use restate_cli_util::ui::stylesheet;
20+
use restate_cli_util::{CliContext, c_indent_table, c_println};
21+
use restate_lite::{AddressMeta, Options, Restate};
22+
use restate_types::art::render_restate_logo;
23+
use restate_types::net::address::{AdminPort, HttpIngressPort, ListenerPort};
24+
25+
use crate::build_info;
26+
use crate::cli_env::CliEnv;
27+
28+
#[derive(Run, Parser, Collect, Clone)]
29+
#[cling(run = "run")]
30+
pub struct Dev {
31+
/// Start restate on a set of random ports
32+
#[clap(long, short = 'r')]
33+
use_random_ports: bool,
34+
35+
/// Start restate on unix sockets only
36+
#[clap(long, short = 'u')]
37+
use_unix_sockets: bool,
38+
39+
/// Do not delete the temporary data directory after exiting
40+
#[clap(long)]
41+
retain: bool,
42+
}
43+
44+
pub async fn run(State(_env): State<CliEnv>, opts: &Dev) -> Result<()> {
45+
let cancellation = CancellationToken::new();
46+
let temp_dir = tempfile::tempdir()?;
47+
let data_dir = temp_dir.path().to_path_buf();
48+
49+
let options = Options {
50+
enable_tcp: !opts.use_unix_sockets,
51+
use_random_ports: opts.use_random_ports,
52+
data_dir: Some(data_dir.clone()),
53+
..Default::default()
54+
};
55+
56+
let listener = TcpListener::bind("127.0.0.1:0").await?;
57+
let mock_svc_addr = format!("http://{}/", listener.local_addr()?);
58+
59+
let (running_tx, running_rx) = oneshot::channel();
60+
tokio::spawn({
61+
let cancellation = cancellation.clone();
62+
async move {
63+
cancellation
64+
.run_until_cancelled(mock_service_endpoint::listener::run_listener(
65+
listener,
66+
|| {
67+
let _ = running_tx.send(());
68+
},
69+
))
70+
.await
71+
.map(|result| result.map_err(|err| anyhow!("mock service endpoint failed: {err}")))
72+
.unwrap_or(Ok(()))
73+
}
74+
});
75+
76+
let _ = running_rx.await;
77+
78+
if opts.retain {
79+
c_println!(
80+
"{} Data directory will be retained after exit",
81+
stylesheet::HANDSHAKE_ICON
82+
);
83+
let _ = temp_dir.keep();
84+
}
85+
86+
{
87+
let cancellation = cancellation.clone();
88+
let boxed: Box<dyn Fn() + Send> = Box::new(move || cancellation.cancel());
89+
*crate::EXIT_HANDLER.lock().unwrap() = Some(boxed);
90+
}
91+
92+
let restate = Restate::create(options).await?;
93+
// register mock service
94+
if let Err(err) = restate.discover_deployment(&mock_svc_addr).await {
95+
// we'll print this but we'll continue anyway since this is not a catastrophic error
96+
// for the user.
97+
eprintln!("Failed to discover the example `Counter` service deployment: {err:#?}");
98+
}
99+
100+
let addresses = restate.get_advertised_addresses();
101+
102+
let admin_url = addresses
103+
.iter()
104+
.find_map(|address| {
105+
if address.name == AdminPort::NAME {
106+
Some(address.address.clone())
107+
} else {
108+
None
109+
}
110+
})
111+
.expect("Admin port is always set");
112+
c_println!(">> Using data dir: {}", data_dir.display());
113+
render(&addresses);
114+
c_println!();
115+
c_println!("✅ `Counter` service endpoint is running on {mock_svc_addr}");
116+
117+
if let Err(_err) = open::that(&admin_url) {
118+
c_println!("Failed to open browser automatically. Please open {admin_url} manually.")
119+
}
120+
121+
c_println!();
122+
c_println!(
123+
"{} Restate is running - Press Ctrl-C to exit",
124+
stylesheet::TIP_ICON
125+
);
126+
c_println!();
127+
// spawn checking latest release
128+
tokio::spawn(build_info::check_if_latest_version());
129+
cancellation.cancelled().await;
130+
131+
restate.stop().await?;
132+
Ok(())
133+
}
134+
135+
fn render(addresses: &[AddressMeta]) {
136+
let mut table = Table::new_styled();
137+
let logo = render_restate_logo(CliContext::get().colors_enabled());
138+
c_println!("{}", logo);
139+
140+
for address in addresses {
141+
if [HttpIngressPort::NAME, AdminPort::NAME].contains(&address.name.as_str()) {
142+
table.add_row(vec![
143+
Cell::new(address.name.to_string()).add_attribute(comfy_table::Attribute::Bold),
144+
Cell::new(address.address.to_string()),
145+
]);
146+
}
147+
}
148+
149+
c_indent_table!(0, table);
150+
}

cli/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub mod cloud;
1313
pub mod completions;
1414
pub mod config;
1515
pub mod deployments;
16+
#[cfg(feature = "dev-cmd")]
17+
pub mod dev;
1618
pub mod examples;
1719
pub mod invocations;
1820
pub mod services;

0 commit comments

Comments
 (0)