mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-10 09:40:23 -04:00
Multiple server configurations for CLI (#214)
Alters the CLI's configuration format to support storing multiple server configurations, and having each server configuration store an (optional) default identity. Many CLI commands take an additional -s argument, the server on which to operate, which defaults to the configured default_server. Using -s consistently requires renaming the publish skip-clippy short flag to -S. Per discussion with @jdetter , this PR does not directly associate identities with servers. Instead, each server configuration stores the server's "fingerprint" (i.e. PEM-formatted ECDSA public key), and the CLI uses that public key to decode tokens to determine if they apply to a given server. This architecture allows the CLI to behave reasonably when multiple server configurations use the same set of tokens, e.g. if multiple distinct URLs resolve to the same SpacetimeDB instance. For example, one could imagine a configuration with server configurations for both http://127.0.0.1:3000 and http://localhost:3000, which should use the same set of identities.
This commit is contained in:
Generated
+2
@@ -4153,11 +4153,13 @@ dependencies = [
|
||||
"insta",
|
||||
"is-terminal",
|
||||
"itertools",
|
||||
"jsonwebtoken",
|
||||
"reqwest",
|
||||
"rustyline",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slab",
|
||||
"spacetimedb-core",
|
||||
"spacetimedb-lib",
|
||||
"spacetimedb-standalone",
|
||||
"syntect",
|
||||
|
||||
@@ -17,6 +17,7 @@ bench = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
spacetimedb-core = { path = "../core", version = "0.6.0" }
|
||||
spacetimedb-lib = { path = "../lib", version = "0.6.1", features = ["cli"] }
|
||||
spacetimedb-standalone = { path = "../standalone", version = "0.6.1", optional = true }
|
||||
|
||||
@@ -32,6 +33,7 @@ email_address.workspace = true
|
||||
futures.workspace = true
|
||||
is-terminal.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
reqwest.workspace = true
|
||||
rustyline.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
+869
-84
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,12 @@ pub fn cli() -> clap::Command {
|
||||
.help("arguments as a JSON array")
|
||||
.default_value("[]"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server hosting the database"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("as_identity")
|
||||
.long("as-identity")
|
||||
@@ -52,19 +58,20 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), Error> {
|
||||
let database = args.get_one::<String>("database").unwrap();
|
||||
let reducer_name = args.get_one::<String>("reducer_name").unwrap();
|
||||
let arg_json = args.get_one::<String>("arguments").unwrap();
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
|
||||
let as_identity = args.get_one::<String>("as_identity");
|
||||
let anon_identity = args.get_flag("anon_identity");
|
||||
|
||||
let address = database_address(&config, database).await?;
|
||||
let address = database_address(&config, database, server).await?;
|
||||
|
||||
let builder = reqwest::Client::new().post(format!(
|
||||
"{}/database/call/{}/{}",
|
||||
config.get_host_url(),
|
||||
config.get_host_url(server)?,
|
||||
address,
|
||||
reducer_name
|
||||
));
|
||||
let auth_header = get_auth_header_only(&mut config, anon_identity, as_identity).await;
|
||||
let auth_header = get_auth_header_only(&mut config, anon_identity, as_identity, server).await;
|
||||
let builder = add_auth_header_opt(builder, &auth_header);
|
||||
|
||||
let res = builder.body(arg_json.to_owned()).send().await?;
|
||||
@@ -78,9 +85,18 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), Error> {
|
||||
let error = Err(e).context(format!("Response text: {}", response_text));
|
||||
|
||||
let error_msg = if response_text.starts_with("no such reducer") {
|
||||
no_such_reducer(config, &address, database, &auth_header, reducer_name).await
|
||||
no_such_reducer(config, &address, database, &auth_header, reducer_name, server).await
|
||||
} else if response_text.starts_with("invalid arguments") {
|
||||
invalid_arguments(config, &address, database, &auth_header, reducer_name, &response_text).await
|
||||
invalid_arguments(
|
||||
config,
|
||||
&address,
|
||||
database,
|
||||
&auth_header,
|
||||
reducer_name,
|
||||
&response_text,
|
||||
server,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
return error;
|
||||
};
|
||||
@@ -99,6 +115,7 @@ async fn invalid_arguments(
|
||||
auth_header: &Option<String>,
|
||||
reducer: &str,
|
||||
text: &str,
|
||||
server: Option<&str>,
|
||||
) -> String {
|
||||
let mut error = format!(
|
||||
"Invalid arguments provided for reducer `{}` for database `{}` resolving to address `{}`.",
|
||||
@@ -114,7 +131,7 @@ async fn invalid_arguments(
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if let Some(sig) = schema_json(config, addr, auth_header, true)
|
||||
if let Some(sig) = schema_json(config, addr, auth_header, true, server)
|
||||
.await
|
||||
.and_then(|schema| reducer_signature(schema, reducer))
|
||||
{
|
||||
@@ -174,13 +191,20 @@ fn reducer_signature(schema_json: Value, reducer_name: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
/// Returns an error message for when `reducer` does not exist in `db`.
|
||||
async fn no_such_reducer(config: Config, addr: &str, db: &str, auth_header: &Option<String>, reducer: &str) -> String {
|
||||
async fn no_such_reducer(
|
||||
config: Config,
|
||||
addr: &str,
|
||||
db: &str,
|
||||
auth_header: &Option<String>,
|
||||
reducer: &str,
|
||||
server: Option<&str>,
|
||||
) -> String {
|
||||
let mut error = format!(
|
||||
"No such reducer `{}` for database `{}` resolving to address `{}`.",
|
||||
reducer, db, addr
|
||||
);
|
||||
|
||||
if let Some(schema) = schema_json(config, addr, auth_header, false).await {
|
||||
if let Some(schema) = schema_json(config, addr, auth_header, false, server).await {
|
||||
add_reducer_ctx_to_err(&mut error, schema, reducer);
|
||||
}
|
||||
|
||||
@@ -229,8 +253,18 @@ fn add_reducer_ctx_to_err(error: &mut String, schema_json: Value, reducer_name:
|
||||
/// Fetch the schema as JSON for the database at `address`.
|
||||
///
|
||||
/// The value of `expand` determines how detailed information to fetch.
|
||||
async fn schema_json(config: Config, address: &str, auth_header: &Option<String>, expand: bool) -> Option<Value> {
|
||||
let builder = reqwest::Client::new().get(format!("{}/database/schema/{}", config.get_host_url(), address));
|
||||
async fn schema_json(
|
||||
config: Config,
|
||||
address: &str,
|
||||
auth_header: &Option<String>,
|
||||
expand: bool,
|
||||
server: Option<&str>,
|
||||
) -> Option<Value> {
|
||||
let builder = reqwest::Client::new().get(format!(
|
||||
"{}/database/schema/{}",
|
||||
config.get_host_url(server).ok()?,
|
||||
address
|
||||
));
|
||||
let builder = add_auth_header_opt(builder, auth_header);
|
||||
|
||||
builder
|
||||
|
||||
@@ -17,17 +17,24 @@ pub fn cli() -> clap::Command {
|
||||
.help("The identity to use for deleting this database")
|
||||
.long_help("The identity to use for deleting this database. If no identity is provided, the default one will be used."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server hosting the database")
|
||||
)
|
||||
.after_help("Run `spacetime help delete` for more detailed information.\n")
|
||||
}
|
||||
|
||||
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let database = args.get_one::<String>("database").unwrap();
|
||||
let identity_or_name = args.get_one::<String>("identity");
|
||||
|
||||
let address = database_address(&config, database).await?;
|
||||
let address = database_address(&config, database, server).await?;
|
||||
|
||||
let builder = reqwest::Client::new().post(format!("{}/database/delete/{}", config.get_host_url(), address));
|
||||
let auth_header = get_auth_header_only(&mut config, false, identity_or_name).await;
|
||||
let builder = reqwest::Client::new().post(format!("{}/database/delete/{}", config.get_host_url(server)?, address));
|
||||
let auth_header = get_auth_header_only(&mut config, false, identity_or_name, server).await;
|
||||
let builder = add_auth_header_opt(builder, &auth_header);
|
||||
builder.send().await?.error_for_status()?;
|
||||
|
||||
|
||||
@@ -38,6 +38,12 @@ pub fn cli() -> clap::Command {
|
||||
.action(SetTrue)
|
||||
.help("If this flag is present, no identity will be provided when describing the database"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server hosting the database"),
|
||||
)
|
||||
.after_help("Run `spacetime help describe` for more detailed information.\n")
|
||||
}
|
||||
|
||||
@@ -46,23 +52,24 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
let expand = !args.get_flag("brief");
|
||||
let entity_name = args.get_one::<String>("entity_name");
|
||||
let entity_type = args.get_one::<String>("entity_type");
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
|
||||
let as_identity = args.get_one::<String>("as_identity");
|
||||
let anon_identity = args.get_flag("anon_identity");
|
||||
|
||||
let address = database_address(&config, database).await?;
|
||||
let address = database_address(&config, database, server).await?;
|
||||
|
||||
let builder = reqwest::Client::new().get(match entity_name {
|
||||
None => format!("{}/database/schema/{}", config.get_host_url(), address),
|
||||
None => format!("{}/database/schema/{}", config.get_host_url(server)?, address),
|
||||
Some(entity_name) => format!(
|
||||
"{}/database/schema/{}/{}/{}",
|
||||
config.get_host_url(),
|
||||
config.get_host_url(server)?,
|
||||
address,
|
||||
format_args!("{}s", entity_type.unwrap()),
|
||||
entity_name
|
||||
),
|
||||
});
|
||||
let auth_header = get_auth_header_only(&mut config, anon_identity, as_identity).await;
|
||||
let auth_header = get_auth_header_only(&mut config, anon_identity, as_identity, server).await;
|
||||
let builder = add_auth_header_opt(builder, &auth_header);
|
||||
|
||||
let descr = builder
|
||||
|
||||
@@ -33,14 +33,32 @@ fn get_subcommands() -> Vec<Command> {
|
||||
.arg(Arg::new("identity").long("identity").short('i').help(
|
||||
"The identity that should own this tld. If no identity is specified, then the default identity is used",
|
||||
))
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server on which to register the domain"),
|
||||
)
|
||||
.after_help("Run `spacetime dns register-tld --help` for more detailed information.\n"),
|
||||
Command::new("lookup")
|
||||
.about("Resolves a domain to a database address")
|
||||
.arg(Arg::new("domain").required(true).help("The name of the domain to lookup"))
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server on which to look up the domain name"),
|
||||
)
|
||||
.after_help("Run `spacetime dns lookup --help` for more detailed information"),
|
||||
Command::new("reverse-lookup")
|
||||
.about("Returns the domains for the provided database address")
|
||||
.arg(Arg::new("address").required(true).help("The address you would like to find all of the known domains for"))
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server on which to look up the address"),
|
||||
)
|
||||
.after_help("Run `spacetime dns reverse-lookup --help` for more detailed information.\n"),
|
||||
Command::new("set-name")
|
||||
.about("Sets the domain of the database")
|
||||
@@ -49,6 +67,12 @@ fn get_subcommands() -> Vec<Command> {
|
||||
.arg(Arg::new("identity").long("identity").short('i').long_help(
|
||||
"The identity that owns the tld for this domain. If no identity is specified, the default identity is used.",
|
||||
).help("The identity that owns the tld for this domain"))
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server on which to set the name"),
|
||||
)
|
||||
.after_help("Run `spacetime dns set-name --help` for more detailed information.\n"),
|
||||
]
|
||||
}
|
||||
@@ -66,8 +90,9 @@ async fn exec_subcommand(config: Config, cmd: &str, args: &ArgMatches) -> Result
|
||||
async fn exec_register_tld(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let tld = args.get_one::<String>("tld").unwrap().clone();
|
||||
let identity = args.get_one::<String>("identity");
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
|
||||
match spacetime_register_tld(&mut config, &tld, identity).await? {
|
||||
match spacetime_register_tld(&mut config, &tld, identity, server).await? {
|
||||
RegisterTldResult::Success { domain } => {
|
||||
println!("Registered domain: {}", domain);
|
||||
}
|
||||
@@ -85,8 +110,9 @@ async fn exec_register_tld(mut config: Config, args: &ArgMatches) -> Result<(),
|
||||
|
||||
pub async fn exec_dns_lookup(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let domain = args.get_one::<String>("domain").unwrap();
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
|
||||
let response = spacetime_dns(&config, domain).await?;
|
||||
let response = spacetime_dns(&config, domain, server).await?;
|
||||
match response {
|
||||
DnsLookupResponse::Success { domain: _, address } => {
|
||||
println!("{}", address);
|
||||
@@ -100,7 +126,8 @@ pub async fn exec_dns_lookup(config: Config, args: &ArgMatches) -> Result<(), an
|
||||
|
||||
pub async fn exec_reverse_dns(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let addr = args.get_one::<String>("address").unwrap();
|
||||
let response = spacetime_reverse_dns(&config, addr).await?;
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let response = spacetime_reverse_dns(&config, addr, server).await?;
|
||||
if response.names.is_empty() {
|
||||
Err(anyhow::anyhow!("Could not find a name for the address: {}", addr))
|
||||
} else {
|
||||
@@ -115,10 +142,13 @@ pub async fn exec_set_name(mut config: Config, args: &ArgMatches) -> Result<(),
|
||||
let domain = args.get_one::<String>("domain").unwrap();
|
||||
let address = args.get_one::<String>("address").unwrap();
|
||||
let identity = args.get_one::<String>("identity");
|
||||
let auth_header = get_auth_header_only(&mut config, false, identity).await.unwrap();
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let auth_header = get_auth_header_only(&mut config, false, identity, server)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let builder = reqwest::Client::new().get(Url::parse_with_params(
|
||||
format!("{}/database/set_name", config.get_host_url()).as_str(),
|
||||
format!("{}/database/set_name", config.get_host_url(server)?).as_str(),
|
||||
[
|
||||
("domain", domain.clone()),
|
||||
("address", address.clone()),
|
||||
|
||||
@@ -21,6 +21,12 @@ fn get_energy_subcommands() -> Vec<clap::Command> {
|
||||
.long_help(
|
||||
"The identity to check the balance for. If no identity is provided, the default one will be used.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server from which to request balance information"),
|
||||
),
|
||||
clap::Command::new("set-balance")
|
||||
.about("Update the current budget balance for a database")
|
||||
@@ -37,6 +43,12 @@ fn get_energy_subcommands() -> Vec<clap::Command> {
|
||||
"The identity to set a balance for. If no identity is provided, the default one will be used.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server on which to update the identity's balance"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("quiet")
|
||||
.long("quiet")
|
||||
@@ -65,9 +77,10 @@ async fn exec_update_balance(config: Config, args: &ArgMatches) -> Result<(), an
|
||||
let hex_id = args.get_one::<String>("identity");
|
||||
let balance = *args.get_one::<i128>("balance").unwrap();
|
||||
let quiet = args.get_flag("quiet");
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
|
||||
let hex_id = hex_id_or_default(hex_id, &config);
|
||||
let res = set_balance(&reqwest::Client::new(), &config, hex_id, balance).await?;
|
||||
let hex_id = hex_id_or_default(hex_id, &config, server);
|
||||
let res = set_balance(&reqwest::Client::new(), &config, hex_id, balance, server).await?;
|
||||
|
||||
if !quiet {
|
||||
println!("{}", res.text().await?);
|
||||
@@ -79,10 +92,11 @@ async fn exec_update_balance(config: Config, args: &ArgMatches) -> Result<(), an
|
||||
async fn exec_status(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
// let project_name = args.value_of("project name").unwrap();
|
||||
let hex_id = args.get_one::<String>("identity");
|
||||
let hex_id = hex_id_or_default(hex_id, &config);
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let hex_id = hex_id_or_default(hex_id, &config, server);
|
||||
|
||||
let status = reqwest::Client::new()
|
||||
.get(format!("{}/energy/{}", config.get_host_url(), hex_id))
|
||||
.get(format!("{}/energy/{}", config.get_host_url(server)?, hex_id,))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
@@ -94,8 +108,8 @@ async fn exec_status(config: Config, args: &ArgMatches) -> Result<(), anyhow::Er
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hex_id_or_default<'a>(hex_id: Option<&'a String>, config: &'a Config) -> &'a String {
|
||||
hex_id.unwrap_or_else(|| &config.get_default_identity_config().unwrap().identity)
|
||||
fn hex_id_or_default<'a>(hex_id: Option<&'a String>, config: &'a Config, server: Option<&str>) -> &'a String {
|
||||
hex_id.unwrap_or_else(|| &config.get_default_identity_config(server).unwrap().identity)
|
||||
}
|
||||
|
||||
pub(super) async fn set_balance(
|
||||
@@ -103,12 +117,13 @@ pub(super) async fn set_balance(
|
||||
config: &Config,
|
||||
hex_identity: &str,
|
||||
balance: i128,
|
||||
server: Option<&str>,
|
||||
) -> anyhow::Result<reqwest::Response> {
|
||||
// TODO: this really should be form data in POST body, not query string parameter, but gotham
|
||||
// does not support that on the server side without an extension.
|
||||
// see https://github.com/gotham-rs/gotham/issues/11
|
||||
client
|
||||
.post(format!("{}/energy/{}", config.get_host_url(), hex_identity))
|
||||
.post(format!("{}/energy/{}", config.get_host_url(server)?, hex_identity,))
|
||||
.query(&[("balance", balance)])
|
||||
.send()
|
||||
.await?
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use crate::{
|
||||
config::{Config, IdentityConfig},
|
||||
util::{init_default, IdentityTokenJson, InitDefaultResultType},
|
||||
util::{init_default, y_or_n, IdentityTokenJson, InitDefaultResultType},
|
||||
};
|
||||
use std::io::Write;
|
||||
|
||||
use crate::util::{is_hex_identity, print_identity_config};
|
||||
use anyhow::Context;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use email_address::EmailAddress;
|
||||
use reqwest::{StatusCode, Url};
|
||||
use serde::Deserialize;
|
||||
use spacetimedb::auth::identity::decode_token;
|
||||
use spacetimedb_lib::recovery::RecoveryCodeResponse;
|
||||
use tabled::{object::Columns, Alignment, Modify, Style, Table, Tabled};
|
||||
|
||||
@@ -22,26 +24,71 @@ pub fn cli() -> Command {
|
||||
|
||||
fn get_subcommands() -> Vec<Command> {
|
||||
vec![
|
||||
Command::new("list").about("List saved identities"),
|
||||
Command::new("set-default").about("Set the default identity").arg(
|
||||
Arg::new("identity")
|
||||
.help("The identity string or name that should become the new default identity")
|
||||
.required(true),
|
||||
),
|
||||
Command::new("set-email")
|
||||
.about("Associates an email address with an identity")
|
||||
Command::new("list").about("List saved identities which apply to a server")
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.help("The nickname, host name or URL of the server to list identities for")
|
||||
.conflicts_with("all")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("all")
|
||||
.short('a')
|
||||
.long("all")
|
||||
.help("List all stored identities, regardless of server")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("server")
|
||||
)
|
||||
// TODO: project flag?
|
||||
,
|
||||
Command::new("set-default").about("Set the default identity for a server")
|
||||
.arg(
|
||||
Arg::new("identity")
|
||||
.help("The identity string or name that should become the new default identity")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The server nickname, host name or URL of the server which should use this identity as a default")
|
||||
)
|
||||
// TODO: project flag?
|
||||
,
|
||||
Command::new("set-email")
|
||||
.about("Associates an email address with an identity")
|
||||
.arg(
|
||||
Arg::new("identity")
|
||||
.help("The identity string or name that should be associated with the email")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("email")
|
||||
.help("The email that should be assigned to the provided identity")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The server that should be informed of the email change")
|
||||
.conflicts_with("all-servers")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("all-servers")
|
||||
.long("all-servers")
|
||||
.short('a')
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Inform all known servers of the email change")
|
||||
.conflicts_with("server")
|
||||
),
|
||||
Command::new("init-default")
|
||||
.about("Initialize a new default identity if it is missing from the global config")
|
||||
.about("Initialize a new default identity if it is missing from a server's config")
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server for which to set the default identity"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("name")
|
||||
.long("name")
|
||||
@@ -57,6 +104,12 @@ fn get_subcommands() -> Vec<Command> {
|
||||
),
|
||||
Command::new("new")
|
||||
.about("Creates a new identity")
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server from which to request the identity"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-save")
|
||||
.help("Don't save save to local config, just create a new identity")
|
||||
@@ -83,12 +136,26 @@ fn get_subcommands() -> Vec<Command> {
|
||||
.help("Creates an identity without a recovery email")
|
||||
.conflicts_with("email")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("default")
|
||||
.help("Make the new identity the default for the server")
|
||||
.long("default")
|
||||
.short('d')
|
||||
.conflicts_with("no-save")
|
||||
.action(ArgAction::SetTrue),
|
||||
),
|
||||
Command::new("remove")
|
||||
.about("Removes a saved identity from your spacetime config")
|
||||
.arg(Arg::new("identity")
|
||||
.help("The identity string or name to delete")
|
||||
.required_unless_present("all")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("all-server")
|
||||
.long("all-server")
|
||||
.short('s')
|
||||
.help("Remove all identities associated with a particular server")
|
||||
.conflicts_with_all(["identity", "all"])
|
||||
)
|
||||
.arg(
|
||||
Arg::new("all")
|
||||
@@ -96,16 +163,16 @@ fn get_subcommands() -> Vec<Command> {
|
||||
.short('a')
|
||||
.help("Remove all identities from your spacetime config")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("identity")
|
||||
.required_unless_present("identity"),
|
||||
.conflicts_with_all(["identity", "all-server"])
|
||||
).arg(
|
||||
Arg::new("force")
|
||||
.long("force")
|
||||
.help("Removes all identities without prompting (for CI usage)")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("identity")
|
||||
.required_unless_present("identity"),
|
||||
),
|
||||
)
|
||||
// TODO: project flag?
|
||||
,
|
||||
Command::new("token").about("Print the token for an identity").arg(
|
||||
Arg::new("identity")
|
||||
.help("The identity string or name that we should print the token for")
|
||||
@@ -136,12 +203,21 @@ fn get_subcommands() -> Vec<Command> {
|
||||
.long("name")
|
||||
.short('n')
|
||||
.help("A name for the newly imported identity"),
|
||||
)
|
||||
// TODO: project flag?
|
||||
,
|
||||
Command::new("find").about("Find an identity for an email")
|
||||
.arg(
|
||||
Arg::new("email")
|
||||
.required(true)
|
||||
.help("The email associated with the identity that you would like to find"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The server to search for identities matching the email"),
|
||||
),
|
||||
Command::new("find").about("Find an identity for an email").arg(
|
||||
Arg::new("email")
|
||||
.required(true)
|
||||
.help("The email associated with the identity that you would like to find"),
|
||||
),
|
||||
Command::new("recover")
|
||||
.about("Recover an existing identity and import it into your local config")
|
||||
.arg(
|
||||
@@ -151,7 +227,15 @@ fn get_subcommands() -> Vec<Command> {
|
||||
)
|
||||
.arg(Arg::new("identity").required(true).help(
|
||||
"The identity you would like to recover. This identity must be associated with the email provided.",
|
||||
)),
|
||||
))
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The server from which to request recovery codes"),
|
||||
)
|
||||
// TODO: project flag?
|
||||
,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -182,7 +266,7 @@ async fn exec_set_default(mut config: Config, args: &ArgMatches) -> Result<(), a
|
||||
let identity = config
|
||||
.resolve_name_to_identity(args.get_one::<String>("identity").map(|s| s.as_ref()))?
|
||||
.unwrap();
|
||||
config.set_default_identity(identity);
|
||||
config.set_default_identity(identity, args.get_one::<String>("server").map(|s| s.as_ref()))?;
|
||||
config.save();
|
||||
Ok(())
|
||||
}
|
||||
@@ -195,7 +279,12 @@ async fn exec_init_default(mut config: Config, args: &ArgMatches) -> Result<(),
|
||||
let nickname = args.get_one::<String>("name").map(|s| s.to_owned());
|
||||
let quiet = args.get_flag("quiet");
|
||||
|
||||
let init_default_result = init_default(&mut config, nickname).await?;
|
||||
let init_default_result = init_default(
|
||||
&mut config,
|
||||
nickname,
|
||||
args.get_one::<String>("server").map(|s| s.as_ref()),
|
||||
)
|
||||
.await?;
|
||||
let identity_config = init_default_result.identity_config;
|
||||
let result_type = init_default_result.result_type;
|
||||
|
||||
@@ -221,13 +310,20 @@ async fn exec_remove(mut config: Config, args: &ArgMatches) -> Result<(), anyhow
|
||||
let identity_or_name = args.get_one::<String>("identity");
|
||||
let force = args.get_flag("force");
|
||||
let all = args.get_flag("all");
|
||||
let all_server = args.get_one::<String>("all-server").map(|s| s.as_str());
|
||||
|
||||
if !all && identity_or_name.is_none() {
|
||||
if !all && identity_or_name.is_none() && all_server.is_none() {
|
||||
return Err(anyhow::anyhow!("Must provide an identity or name to remove"));
|
||||
}
|
||||
|
||||
if force && !all {
|
||||
return Err(anyhow::anyhow!("The --force flag can only be used with --all"));
|
||||
if force && !(all || all_server.is_some()) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The --force flag can only be used with --all or --all-server"
|
||||
));
|
||||
}
|
||||
|
||||
fn should_continue(force: bool, prompt: &str) -> anyhow::Result<bool> {
|
||||
Ok(force || y_or_n(&format!("Are you sure you want to remove all identities{}?", prompt))?)
|
||||
}
|
||||
|
||||
if let Some(identity_or_name) = identity_or_name {
|
||||
@@ -237,37 +333,44 @@ async fn exec_remove(mut config: Config, args: &ArgMatches) -> Result<(), anyhow
|
||||
config.delete_identity_config_by_name(identity_or_name.as_str())
|
||||
}
|
||||
.ok_or(anyhow::anyhow!("No such identity or name: {}", identity_or_name))?;
|
||||
config.update_default_identity();
|
||||
config.save();
|
||||
config.update_all_default_identities();
|
||||
println!(" Removed identity");
|
||||
print_identity_config(&ic);
|
||||
} else if let Some(server) = all_server {
|
||||
if !should_continue(force, &format!(" which apply to server {}", server))? {
|
||||
println!(" Aborted");
|
||||
return Ok(());
|
||||
}
|
||||
let removed = config.remove_identities_for_server(Some(server))?;
|
||||
let count = removed.len();
|
||||
println!(
|
||||
" {} {} removed:",
|
||||
count,
|
||||
if count == 1 { "identity" } else { "identities" }
|
||||
);
|
||||
for identity_config in removed {
|
||||
println!("{}", identity_config.identity);
|
||||
}
|
||||
} else {
|
||||
if config.identity_configs().is_empty() {
|
||||
println!(" No identities to remove");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut input = String::new();
|
||||
if !force {
|
||||
print!("Are you sure you want to remove all identities? (y/n) ");
|
||||
std::io::stdout().flush()?;
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
|
||||
if input.trim() != "y" {
|
||||
println!(" Aborted");
|
||||
return Ok(());
|
||||
}
|
||||
if !should_continue(force, "")? {
|
||||
println!(" Aborted");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let identity_count = config.identity_configs().len();
|
||||
config.delete_all_identity_configs();
|
||||
config.save();
|
||||
println!(
|
||||
" {} {} removed.",
|
||||
identity_count,
|
||||
if identity_count > 1 { "identities" } else { "identity" }
|
||||
if identity_count == 1 { "identity" } else { "identities" }
|
||||
);
|
||||
}
|
||||
config.save();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -275,6 +378,9 @@ async fn exec_remove(mut config: Config, args: &ArgMatches) -> Result<(), anyhow
|
||||
async fn exec_new(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let save = !args.get_flag("no-save");
|
||||
let alias = args.get_one::<String>("name");
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let default = *args.get_one::<bool>("default").unwrap();
|
||||
|
||||
if let Some(alias) = alias {
|
||||
if config.name_exists(alias) {
|
||||
return Err(anyhow::anyhow!("An identity with that name already exists."));
|
||||
@@ -302,11 +408,11 @@ async fn exec_new(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
}
|
||||
|
||||
let mut builder = reqwest::Client::new().post(Url::parse_with_params(
|
||||
format!("{}/identity", config.get_host_url()).as_str(),
|
||||
format!("{}/identity", config.get_host_url(server)?).as_str(),
|
||||
query_params,
|
||||
)?);
|
||||
|
||||
if let Some(identity_token) = config.get_default_identity_config() {
|
||||
if let Ok(identity_token) = config.get_default_identity_config(server) {
|
||||
builder = builder.basic_auth("token", Some(identity_token.token.clone()));
|
||||
}
|
||||
|
||||
@@ -319,8 +425,8 @@ async fn exec_new(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
token: identity_token.token,
|
||||
nickname: alias.map(|s| s.to_string()),
|
||||
});
|
||||
if config.default_identity().is_none() {
|
||||
config.set_default_identity(identity.clone());
|
||||
if default || config.default_identity(server).is_err() {
|
||||
config.set_default_identity(identity.clone(), server)?;
|
||||
}
|
||||
|
||||
config.save();
|
||||
@@ -367,24 +473,57 @@ struct LsRow {
|
||||
}
|
||||
|
||||
/// Executes the `identity list` command which lists all identities in the config.
|
||||
async fn exec_list(config: Config, _args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
async fn exec_list(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let mut rows: Vec<LsRow> = Vec::new();
|
||||
for identity_token in config.identity_configs() {
|
||||
let default_str = if config.default_identity().is_some()
|
||||
&& config.default_identity().as_ref().unwrap() == &identity_token.identity
|
||||
{
|
||||
"***"
|
||||
} else {
|
||||
""
|
||||
|
||||
if *args.get_one::<bool>("all").unwrap() {
|
||||
for identity_token in config.identity_configs() {
|
||||
rows.push(LsRow {
|
||||
default: "".to_string(),
|
||||
identity: identity_token.identity.clone(),
|
||||
name: identity_token.nickname.clone().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
|
||||
let server_name = config.server_nick_or_host(server)?;
|
||||
let decoding_key = config.server_decoding_key(server).with_context(|| {
|
||||
format!(
|
||||
"Cannot list identities for server without a saved fingerprint: {server_name}
|
||||
Fetch the server's fingerprint with:
|
||||
\tspacetime server fingerprint {server_name}"
|
||||
)
|
||||
})?;
|
||||
let default_identity = config.default_identity(server);
|
||||
|
||||
let is_default = |id| {
|
||||
if let Ok(default_identity) = default_identity {
|
||||
id == default_identity
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
rows.push(LsRow {
|
||||
default: default_str.to_string(),
|
||||
identity: identity_token.clone().identity,
|
||||
name: identity_token.nickname.clone().unwrap_or_default(),
|
||||
// TODO(jdetter): We'll have to look this up via a query
|
||||
// email: identity_token.email.unwrap_or_default(),
|
||||
});
|
||||
|
||||
for identity_token in config.identity_configs() {
|
||||
if decode_token(&decoding_key, &identity_token.token).is_ok() {
|
||||
rows.push(LsRow {
|
||||
default: if is_default(&identity_token.identity) {
|
||||
"***"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
.to_string(),
|
||||
identity: identity_token.identity.clone(),
|
||||
name: identity_token.nickname.clone().unwrap_or_default(),
|
||||
// TODO(jdetter): We'll have to look this up via a query
|
||||
// email: identity_token.email.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
println!("Identities for {}:", config.server_nick_or_host(server)?);
|
||||
}
|
||||
|
||||
let table = Table::new(&rows)
|
||||
.with(Style::empty())
|
||||
.with(Modify::new(Columns::first()).with(Alignment::right()));
|
||||
@@ -406,8 +545,9 @@ struct GetIdentityResponseEntry {
|
||||
/// Executes the `identity find` command which finds an identity by email.
|
||||
async fn exec_find(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let email = args.get_one::<String>("email").unwrap().clone();
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let client = reqwest::Client::new();
|
||||
let builder = client.get(format!("{}/identity?email={}", config.get_host_url(), email));
|
||||
let builder = client.get(format!("{}/identity?email={}", config.get_host_url(server)?, email));
|
||||
|
||||
let res = builder.send().await?;
|
||||
|
||||
@@ -469,6 +609,7 @@ async fn exec_set_name(mut config: Config, args: &ArgMatches) -> Result<(), anyh
|
||||
/// Executes the `identity set-email` command which sets the email for an identity.
|
||||
async fn exec_set_email(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let email = args.get_one::<String>("email").unwrap().clone();
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let identity = config
|
||||
.resolve_name_to_identity(args.get_one::<String>("identity").map(|s| s.as_ref()))?
|
||||
.unwrap();
|
||||
@@ -476,9 +617,11 @@ async fn exec_set_email(config: Config, args: &ArgMatches) -> Result<(), anyhow:
|
||||
.get_identity_config_by_identity(identity.as_str())
|
||||
.unwrap_or_else(|| panic!("Could not find identity: {}", identity));
|
||||
|
||||
// TODO: check that the identity is valid for the server
|
||||
|
||||
let mut builder = reqwest::Client::new().post(format!(
|
||||
"{}/identity/{}/set-email?email={}",
|
||||
config.get_host_url(),
|
||||
config.get_host_url(server)?,
|
||||
identity_config.identity,
|
||||
email
|
||||
));
|
||||
@@ -502,6 +645,7 @@ async fn exec_set_email(config: Config, args: &ArgMatches) -> Result<(), anyhow:
|
||||
/// Executes the `identity recover` command which recovers an identity from an email.
|
||||
async fn exec_recover(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let email = args.get_one::<String>("email").unwrap();
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let identity = config
|
||||
.resolve_name_to_identity(args.get_one::<String>("identity").map(|s| s.as_str()))?
|
||||
.unwrap();
|
||||
@@ -522,7 +666,7 @@ async fn exec_recover(mut config: Config, args: &ArgMatches) -> Result<(), anyho
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let builder = client.get(Url::parse_with_params(
|
||||
format!("{}/database/request_recovery_code", config.get_host_url()).as_str(),
|
||||
format!("{}/database/request_recovery_code", config.get_host_url(server)?,).as_str(),
|
||||
query_params,
|
||||
)?);
|
||||
let res = builder.send().await?;
|
||||
@@ -547,7 +691,7 @@ async fn exec_recover(mut config: Config, args: &ArgMatches) -> Result<(), anyho
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let builder = client.get(Url::parse_with_params(
|
||||
format!("{}/database/confirm_recovery_code", config.get_host_url()).as_str(),
|
||||
format!("{}/database/confirm_recovery_code", config.get_host_url(server)?,).as_str(),
|
||||
vec![
|
||||
("code", code.to_string().as_str()),
|
||||
("email", email.as_str()),
|
||||
@@ -566,7 +710,7 @@ async fn exec_recover(mut config: Config, args: &ArgMatches) -> Result<(), anyho
|
||||
token: response.token,
|
||||
};
|
||||
config.identity_configs_mut().push(identity_config.clone());
|
||||
config.update_default_identity();
|
||||
config.set_default_identity_if_unset(server, &identity_config.identity)?;
|
||||
config.save();
|
||||
println!("Success. Identity imported.");
|
||||
print_identity_config(&identity_config);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::Config;
|
||||
use anyhow::Context;
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
@@ -13,6 +14,12 @@ pub fn cli() -> Command {
|
||||
.required(true)
|
||||
.help("The identity to list databases for"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server from which to list databases"),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -26,18 +33,21 @@ struct AddressRow {
|
||||
}
|
||||
|
||||
pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let identity = match args.get_one::<String>("identity") {
|
||||
Some(value) => value.to_string(),
|
||||
None => match config.default_identity() {
|
||||
Some(default_ident) => default_ident.to_string(),
|
||||
None => {
|
||||
return Err(anyhow::anyhow!("No default identity, and no identity provided!"));
|
||||
}
|
||||
},
|
||||
None => config
|
||||
.default_identity(server)
|
||||
.map(str::to_string)
|
||||
.with_context(|| "No default identity, and no identity provided!")?,
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let mut builder = client.get(format!("{}/identity/{}/databases", config.get_host_url(), identity));
|
||||
let mut builder = client.get(format!(
|
||||
"{}/identity/{}/databases",
|
||||
config.get_host_url(server)?,
|
||||
identity
|
||||
));
|
||||
|
||||
if let Some(identity_token) = config.get_identity_config_by_identity(&identity) {
|
||||
builder = builder.basic_auth("token", Some(identity_token.token.clone()));
|
||||
|
||||
@@ -16,6 +16,12 @@ pub fn cli() -> clap::Command {
|
||||
.required(true)
|
||||
.help("The domain or address of the database to print logs from"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server hosting the database"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("identity")
|
||||
.long("identity")
|
||||
@@ -79,22 +85,23 @@ struct LogsParams {
|
||||
}
|
||||
|
||||
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let num_lines = args.get_one::<u32>("num_lines").copied();
|
||||
let database = args.get_one::<String>("database").unwrap();
|
||||
let follow = args.get_flag("follow");
|
||||
|
||||
let cloned_config = config.clone();
|
||||
let identity = cloned_config.resolve_name_to_identity(args.get_one::<String>("identity").map(|x| x.as_str()))?;
|
||||
let auth_header = get_auth_header(&mut config, false, identity.as_deref())
|
||||
let auth_header = get_auth_header(&mut config, false, identity.as_deref(), server)
|
||||
.await
|
||||
.map(|x| x.0);
|
||||
|
||||
let address = database_address(&config, database).await?;
|
||||
let address = database_address(&config, database, server).await?;
|
||||
|
||||
// TODO: num_lines should default to like 10 if follow is specified?
|
||||
let query_parms = LogsParams { num_lines, follow };
|
||||
|
||||
let builder = reqwest::Client::new().get(format!("{}/database/logs/{}", config.get_host_url(), address));
|
||||
let builder = reqwest::Client::new().get(format!("{}/database/logs/{}", config.get_host_url(server)?, address));
|
||||
let builder = add_auth_header_opt(builder, &auth_header);
|
||||
let res = builder.query(&query_parms).send().await?;
|
||||
let status = res.status();
|
||||
|
||||
@@ -2,15 +2,15 @@ use anyhow::bail;
|
||||
use clap::Arg;
|
||||
use clap::ArgAction::SetTrue;
|
||||
use clap::ArgMatches;
|
||||
use reqwest::Url;
|
||||
use reqwest::{StatusCode, Url};
|
||||
use spacetimedb_lib::name::PublishOp;
|
||||
use spacetimedb_lib::name::{is_address, parse_domain_name, PublishResult};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::util::init_default;
|
||||
use crate::util::{add_auth_header_opt, get_auth_header};
|
||||
use crate::util::{init_default, unauth_error_context};
|
||||
|
||||
pub fn cli() -> clap::Command {
|
||||
clap::Command::new("publish")
|
||||
@@ -70,7 +70,7 @@ pub fn cli() -> clap::Command {
|
||||
.arg(
|
||||
Arg::new("skip_clippy")
|
||||
.long("skip_clippy")
|
||||
.short('s')
|
||||
.short('S')
|
||||
.action(SetTrue)
|
||||
.env("SPACETIME_SKIP_CLIPPY")
|
||||
.value_parser(clap::builder::FalseyValueParser::new())
|
||||
@@ -87,11 +87,18 @@ pub fn cli() -> clap::Command {
|
||||
Arg::new("name|address")
|
||||
.help("A valid domain or address for this database"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, domain name or URL of the server to host the database."),
|
||||
)
|
||||
.after_help("Run `spacetime help publish` for more detailed information.")
|
||||
}
|
||||
|
||||
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let cloned_config = config.clone();
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_str());
|
||||
let identity = cloned_config.resolve_name_to_identity(args.get_one::<String>("identity").map(|s| s.as_str()))?;
|
||||
let name_or_address = args.get_one::<String>("name|address");
|
||||
let path_to_project = args.get_one::<PathBuf>("path_to_project").unwrap();
|
||||
@@ -134,7 +141,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
let program_bytes = fs::read(path_to_wasm)?;
|
||||
|
||||
let mut builder = reqwest::Client::new().post(Url::parse_with_params(
|
||||
format!("{}/database/publish", config.get_host_url()).as_str(),
|
||||
format!("{}/database/publish", config.get_host_url(server)?).as_str(),
|
||||
query_params,
|
||||
)?);
|
||||
|
||||
@@ -143,11 +150,11 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
// TODO(jdetter): We should maybe have some sort of user prompt here for them to be able to
|
||||
// easily create a new identity with an email
|
||||
let identity = if !anon_identity {
|
||||
if identity.is_none() && config.default_identity().is_none() {
|
||||
init_default(&mut config, None).await?;
|
||||
if identity.is_none() && config.default_identity(server).is_err() {
|
||||
init_default(&mut config, None, server).await?;
|
||||
}
|
||||
|
||||
let (auth_header, chosen_identity) = get_auth_header(&mut config, anon_identity, identity.as_deref())
|
||||
let (auth_header, chosen_identity) = get_auth_header(&mut config, anon_identity, identity.as_deref(), server)
|
||||
.await
|
||||
.unzip();
|
||||
|
||||
@@ -158,6 +165,16 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
};
|
||||
|
||||
let res = builder.body(program_bytes).send().await?;
|
||||
if res.status() == StatusCode::UNAUTHORIZED && !anon_identity {
|
||||
if let Some(identity) = &identity {
|
||||
let err = res.text().await?;
|
||||
return unauth_error_context(
|
||||
Err(anyhow::anyhow!(err)),
|
||||
&identity.to_hex(),
|
||||
config.server_nick_or_host(server)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
if res.status().is_client_error() || res.status().is_server_error() {
|
||||
let err = res.text().await?;
|
||||
bail!(err)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use crate::{util::VALID_PROTOCOLS, Config};
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use crate::{
|
||||
util::{host_or_url_to_host_and_protocol, spacetime_server_fingerprint, y_or_n, VALID_PROTOCOLS},
|
||||
Config,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use tabled::{object::Columns, Alignment, Modify, Style, Table, Tabled};
|
||||
|
||||
pub fn cli() -> Command {
|
||||
Command::new("server")
|
||||
@@ -11,25 +16,114 @@ pub fn cli() -> Command {
|
||||
|
||||
fn get_subcommands() -> Vec<Command> {
|
||||
vec![
|
||||
Command::new("set")
|
||||
.about("Changes the host and protocol values for future interactions with SpacetimeDB")
|
||||
Command::new("list").about("List stored server configurations"),
|
||||
Command::new("set-default")
|
||||
.about("Set the default server for future operations")
|
||||
.arg(
|
||||
Arg::new("url")
|
||||
.help(
|
||||
"The URL of the SpacetimeDB server to connect to. Example: https://spacetimedb.com/spacetimedb",
|
||||
)
|
||||
Arg::new("server")
|
||||
.help("The nickname, host name or URL of the new default server")
|
||||
.required(true),
|
||||
),
|
||||
Command::new("show").about("Shows the server that is currently configured"),
|
||||
Command::new("add")
|
||||
.about("Add a new server configuration")
|
||||
.arg(Arg::new("url").help("The URL of the server to add").required(true))
|
||||
.arg(Arg::new("name").help("Nickname for this server").required(true))
|
||||
.arg(
|
||||
Arg::new("default")
|
||||
.help("Make the new server the default server for future operations")
|
||||
.long("default")
|
||||
.short('d')
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-fingerprint")
|
||||
.help("Skip fingerprinting the server")
|
||||
.long("no-fingerprint")
|
||||
.action(ArgAction::SetTrue),
|
||||
),
|
||||
Command::new("remove")
|
||||
.about("Remove a saved server configuration")
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.help("The nickname, host name or URL of the server to remove")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("delete-identities")
|
||||
.help("Also delete all identities which apply to the server")
|
||||
.long("delete-identities")
|
||||
.short('I')
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("force")
|
||||
.help("Do not prompt before deleting identities")
|
||||
.long("force")
|
||||
.short('f')
|
||||
.action(ArgAction::SetTrue),
|
||||
),
|
||||
Command::new("fingerprint")
|
||||
.about("Show or update a saved server's fingerprint")
|
||||
.arg(Arg::new("server").help("The nickname, host name or URL of the server"))
|
||||
.arg(
|
||||
Arg::new("force")
|
||||
.help("Save changes to the server's configuration without confirming")
|
||||
.short('f')
|
||||
.long("force")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("delete-obsolete-identities")
|
||||
.help("Delete obsoleted identities if the server's fingerprint has changed")
|
||||
.long("delete-obsolete-identities")
|
||||
.short('I')
|
||||
.action(ArgAction::SetTrue),
|
||||
),
|
||||
Command::new("ping")
|
||||
.about("Checks to see if a SpacetimeDB host is online")
|
||||
.arg(Arg::new("server").help("The nickname, host name or URL of the server to ping")),
|
||||
Command::new("edit")
|
||||
.about("Update a saved server's nickname, host name or protocol")
|
||||
.arg(Arg::new("server").help("The nickname, host name or URL of the server"))
|
||||
.arg(
|
||||
Arg::new("url")
|
||||
.help(
|
||||
"The URL of the SpacetimeDB server to connect to. Example: https://spacetimedb.com/spacetimedb. If no URL is provided, then the configured server is used."
|
||||
)
|
||||
.required(false),
|
||||
Arg::new("nickname")
|
||||
.help("A new nickname to assign the server configuration")
|
||||
.short('n')
|
||||
.long("nickname"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("host")
|
||||
.help("A new hostname to assign the server configuration")
|
||||
.short('H')
|
||||
.long("host"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("protocol")
|
||||
.help("A new protocol to assign the server configuration; http or https")
|
||||
.short('p')
|
||||
.long("protocol"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-fingerprint")
|
||||
.help("Skip fingerprinting the server")
|
||||
.long("no-fingerprint")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("delete-obsolete-identities")
|
||||
.help("Delete obsoleted identities if the server's fingerprint has changed")
|
||||
.long("delete-obsolete-identities")
|
||||
.short('I')
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("force")
|
||||
.help("Do not prompt before saving the edited configuration")
|
||||
.long("force")
|
||||
.short('f')
|
||||
.action(ArgAction::SetTrue),
|
||||
),
|
||||
// TODO: set-name, set-protocol, set-host, set-url
|
||||
]
|
||||
}
|
||||
|
||||
@@ -40,32 +134,96 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
|
||||
|
||||
async fn exec_subcommand(config: Config, cmd: &str, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
match cmd {
|
||||
"set" => exec_set(config, args).await,
|
||||
"show" => exec_show(config, args).await,
|
||||
"list" => exec_list(config, args).await,
|
||||
"set-default" => exec_set_default(config, args).await,
|
||||
"add" => exec_add(config, args).await,
|
||||
"remove" => exec_remove(config, args).await,
|
||||
"fingerprint" => exec_fingerprint(config, args).await,
|
||||
"ping" => exec_ping(config, args).await,
|
||||
"edit" => exec_edit(config, args).await,
|
||||
unknown => Err(anyhow::anyhow!("Invalid subcommand: {}", unknown)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn exec_set(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let url = args.get_one::<String>("url").unwrap();
|
||||
#[derive(Tabled)]
|
||||
#[tabled(rename_all = "UPPERCASE")]
|
||||
struct LsRow {
|
||||
default: String,
|
||||
hostname: String,
|
||||
protocol: String,
|
||||
nickname: String,
|
||||
}
|
||||
|
||||
let protocol: &str;
|
||||
let host: &str;
|
||||
|
||||
if url.contains("://") {
|
||||
protocol = url.split("://").next().unwrap();
|
||||
host = url.split("://").last().unwrap();
|
||||
|
||||
if !VALID_PROTOCOLS.contains(&protocol) {
|
||||
return Err(anyhow::anyhow!("Invalid protocol: {}", protocol));
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Invalid url: {}", url));
|
||||
pub async fn exec_list(config: Config, _args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let mut rows: Vec<LsRow> = Vec::new();
|
||||
for server_config in config.server_configs() {
|
||||
let default = if let Some(default_name) = config.default_server_name() {
|
||||
server_config.nick_or_host_or_url_is(default_name)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
rows.push(LsRow {
|
||||
default: if default { "***" } else { "" }.to_string(),
|
||||
hostname: server_config.host.to_string(),
|
||||
protocol: server_config.protocol.to_string(),
|
||||
nickname: server_config.nickname.as_deref().unwrap_or("").to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
config.set_host(host);
|
||||
config.set_protocol(protocol);
|
||||
let table = Table::new(&rows)
|
||||
.with(Style::empty())
|
||||
.with(Modify::new(Columns::first()).with(Alignment::right()));
|
||||
println!("{}", table);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn exec_set_default(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let server = args.get_one::<String>("server").unwrap();
|
||||
config.set_default_server(server)?;
|
||||
config.save();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn valid_protocol_or_error(protocol: &str) -> anyhow::Result<()> {
|
||||
if !VALID_PROTOCOLS.contains(&protocol) {
|
||||
Err(anyhow::anyhow!("Invalid protocol: {}", protocol))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn exec_add(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let url = args.get_one::<String>("url").unwrap();
|
||||
let nickname = args.get_one::<String>("name");
|
||||
let default = *args.get_one::<bool>("default").unwrap();
|
||||
let no_fingerprint = *args.get_one::<bool>("no-fingerprint").unwrap();
|
||||
|
||||
let (host, protocol) = host_or_url_to_host_and_protocol(url);
|
||||
let protocol = protocol.ok_or_else(|| anyhow::anyhow!("Invalid url: {}", url))?;
|
||||
|
||||
valid_protocol_or_error(protocol)?;
|
||||
|
||||
let fingerprint = if no_fingerprint {
|
||||
None
|
||||
} else {
|
||||
let fingerprint = spacetime_server_fingerprint(url).await.with_context(|| {
|
||||
format!(
|
||||
"Unable to retrieve fingerprint for server: {url}
|
||||
Is the server running?
|
||||
Add a server without retrieving its fingerprint with:
|
||||
\tspacetime server add {url} --no-fingerprint",
|
||||
)
|
||||
})?;
|
||||
println!("For server {}, got fingerprint:\n{}", url, fingerprint);
|
||||
Some(fingerprint)
|
||||
};
|
||||
|
||||
config.add_server(host.to_string(), protocol.to_string(), fingerprint, nickname.cloned())?;
|
||||
|
||||
if default {
|
||||
config.set_default_server(host)?;
|
||||
}
|
||||
|
||||
println!("Host: {}", host);
|
||||
println!("Protocol: {}", protocol);
|
||||
@@ -75,25 +233,178 @@ pub async fn exec_set(mut config: Config, args: &ArgMatches) -> Result<(), anyho
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn exec_show(config: Config, _args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
println!("{}", config.get_host_url());
|
||||
pub async fn exec_remove(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let server = args.get_one::<String>("server").unwrap();
|
||||
let delete_identities = args.get_flag("delete-identities");
|
||||
let force = args.get_flag("force");
|
||||
|
||||
let deleted_ids = config.remove_server(server, delete_identities)?;
|
||||
|
||||
if !deleted_ids.is_empty() {
|
||||
println!(
|
||||
"Deleting {} {}:",
|
||||
deleted_ids.len(),
|
||||
if deleted_ids.len() == 1 {
|
||||
" identity"
|
||||
} else {
|
||||
"identities"
|
||||
}
|
||||
);
|
||||
for id in deleted_ids {
|
||||
println!("{}", id.identity);
|
||||
}
|
||||
if !(force || y_or_n("Continue?")?) {
|
||||
anyhow::bail!("Aborted");
|
||||
}
|
||||
|
||||
config.update_all_default_identities();
|
||||
}
|
||||
|
||||
config.save();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_server_fingerprint(
|
||||
config: &mut Config,
|
||||
server: Option<&str>,
|
||||
delete_identities: bool,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
let url = config.get_host_url(server)?;
|
||||
let nick_or_host = config.server_nick_or_host(server)?;
|
||||
let new_fing = spacetime_server_fingerprint(&url)
|
||||
.await
|
||||
.context("Error fetching server fingerprint")?;
|
||||
if let Some(saved_fing) = config.server_fingerprint(server)? {
|
||||
if saved_fing == new_fing {
|
||||
println!("Fingerprint is unchanged for server {}:\n{}", nick_or_host, saved_fing);
|
||||
|
||||
Ok(false)
|
||||
} else {
|
||||
println!(
|
||||
"Fingerprint has changed for server {}.\nWas:\n{}\nNew:\n{}",
|
||||
nick_or_host, saved_fing, new_fing
|
||||
);
|
||||
|
||||
if delete_identities {
|
||||
// Unfortunate clone because we need to mutate `config`
|
||||
// while holding `saved_fing`.
|
||||
let saved_fing = saved_fing.to_string();
|
||||
|
||||
let deleted_ids = config.remove_identities_for_fingerprint(&saved_fing)?;
|
||||
if !deleted_ids.is_empty() {
|
||||
println!(
|
||||
"Deleting {} obsolete {}:",
|
||||
deleted_ids.len(),
|
||||
if deleted_ids.len() == 1 {
|
||||
"identity"
|
||||
} else {
|
||||
"identities"
|
||||
}
|
||||
);
|
||||
for id in deleted_ids {
|
||||
println!("{}", id.identity);
|
||||
}
|
||||
}
|
||||
|
||||
config.update_all_default_identities();
|
||||
}
|
||||
|
||||
config.set_server_fingerprint(server, new_fing)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"No saved fingerprint for server {}. New fingerprint:\n{}",
|
||||
nick_or_host, new_fing
|
||||
);
|
||||
|
||||
config.set_server_fingerprint(server, new_fing)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn exec_fingerprint(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_str());
|
||||
let delete_identities = args.get_flag("delete-obsolete-identities");
|
||||
let force = args.get_flag("force");
|
||||
|
||||
if update_server_fingerprint(&mut config, server, delete_identities).await? {
|
||||
if !(force || y_or_n("Continue?")?) {
|
||||
anyhow::bail!("Aborted");
|
||||
}
|
||||
|
||||
config.save();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn exec_ping(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let server = match args.get_one::<String>("url") {
|
||||
Some(s) => s.clone(),
|
||||
None => config.get_host_url(),
|
||||
};
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let url = config.get_host_url(server)?;
|
||||
|
||||
let builder = reqwest::Client::new().get(format!("{}/database/ping", server).as_str());
|
||||
let builder = reqwest::Client::new().get(format!("{}/database/ping", url).as_str());
|
||||
match builder.send().await {
|
||||
Ok(_) => {
|
||||
println!("Server is online: {}", server);
|
||||
println!("Server is online: {}", url);
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Server could not be reached: {}", server);
|
||||
println!("Server could not be reached: {}", url);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn exec_edit(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let server = args
|
||||
.get_one::<String>("server")
|
||||
.map(|s| s.as_str())
|
||||
.expect("Supply a server to spacetime server edit");
|
||||
|
||||
let old_url = config.get_host_url(Some(server))?;
|
||||
|
||||
let new_nick = args.get_one::<String>("nickname").map(|s| s.as_str());
|
||||
let new_host = args.get_one::<String>("host").map(|s| s.as_str());
|
||||
let new_proto = args.get_one::<String>("protocol").map(|s| s.as_str());
|
||||
|
||||
let no_fingerprint = args.get_flag("no-fingerprint");
|
||||
let delete_identities = args.get_flag("delete-obsolete-identities");
|
||||
let force = args.get_flag("force");
|
||||
|
||||
if let Some(new_proto) = new_proto {
|
||||
valid_protocol_or_error(new_proto)?;
|
||||
}
|
||||
|
||||
let (old_nick, old_host, old_proto) = config.edit_server(server, new_nick, new_host, new_proto)?;
|
||||
|
||||
if let (Some(new_nick), Some(old_nick)) = (new_nick, old_nick) {
|
||||
println!("Changing nickname from {} to {}", old_nick, new_nick);
|
||||
}
|
||||
if let (Some(new_host), Some(old_host)) = (new_host, old_host) {
|
||||
println!("Changing host from {} to {}", old_host, new_host);
|
||||
}
|
||||
if let (Some(new_proto), Some(old_proto)) = (new_proto, old_proto) {
|
||||
println!("Changing protocol from {} to {}", old_proto, new_proto);
|
||||
}
|
||||
|
||||
let new_url = config.get_host_url(Some(server))?;
|
||||
|
||||
if old_url != new_url {
|
||||
if no_fingerprint {
|
||||
config.delete_server_fingerprint(Some(&new_url))?;
|
||||
} else {
|
||||
update_server_fingerprint(&mut config, Some(&new_url), delete_identities).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if !(force || y_or_n("Continue?")?) {
|
||||
anyhow::bail!("Aborted");
|
||||
}
|
||||
|
||||
config.save();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -51,17 +51,24 @@ pub fn cli() -> clap::Command {
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("If this flag is present, no identity will be provided when querying the database")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server hosting the database"),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn parse_req(mut config: Config, args: &ArgMatches) -> Result<Connection, anyhow::Error> {
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let database = args.get_one::<String>("database").unwrap();
|
||||
let as_identity = args.get_one::<String>("as_identity");
|
||||
let anon_identity = args.get_flag("anon_identity");
|
||||
|
||||
Ok(Connection {
|
||||
host: config.get_host_url(),
|
||||
auth_header: get_auth_header_only(&mut config, anon_identity, as_identity).await,
|
||||
address: database_address(&config, database).await?,
|
||||
host: config.get_host_url(server)?,
|
||||
auth_header: get_auth_header_only(&mut config, anon_identity, as_identity, server).await,
|
||||
address: database_address(&config, database, server).await?,
|
||||
database: database.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,13 +19,31 @@ fn get_energy_subcommands() -> Vec<clap::Command> {
|
||||
clap::Command::new("get")
|
||||
.about("Retrieve a copy of the trace log for a database, if tracing is turned on")
|
||||
.arg(Arg::new("database").required(true))
|
||||
.arg(Arg::new("outputfile").required(true).help("path to write tracelog to")),
|
||||
.arg(Arg::new("outputfile").required(true).help("path to write tracelog to"))
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server running tracing"),
|
||||
),
|
||||
clap::Command::new("stop")
|
||||
.about("Stop tracing on a given database")
|
||||
.arg(Arg::new("database").required(true)),
|
||||
.arg(Arg::new("database").required(true))
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server running tracing"),
|
||||
),
|
||||
clap::Command::new("replay")
|
||||
.about("Replay a tracelog on a temporary fresh DB instance on the server")
|
||||
.arg(Arg::new("tracefile").required(true).help("path to read tracelog from")),
|
||||
.arg(Arg::new("tracefile").required(true).help("path to read tracelog from"))
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.long("server")
|
||||
.short('s')
|
||||
.help("The nickname, host name or URL of the server running tracing"),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -45,11 +63,12 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
|
||||
|
||||
pub async fn exec_replay(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let tracefile = args.get_one::<String>("tracefile").unwrap();
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
match std::fs::read(tracefile) {
|
||||
Ok(o) => {
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.post(format!("{}/tracelog/replay", config.get_host_url()))
|
||||
.post(format!("{}/tracelog/replay", config.get_host_url(server)?))
|
||||
.body(o)
|
||||
.send()
|
||||
.await?;
|
||||
@@ -70,11 +89,16 @@ pub async fn exec_replay(config: Config, args: &ArgMatches) -> Result<(), anyhow
|
||||
|
||||
pub async fn exec_stop(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let database = args.get_one::<String>("database").unwrap();
|
||||
let address = database_address(&config, database).await?;
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let address = database_address(&config, database, server).await?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.post(format!("{}/tracelog/database/{}/stop", config.get_host_url(), address))
|
||||
.post(format!(
|
||||
"{}/tracelog/database/{}/stop",
|
||||
config.get_host_url(server)?,
|
||||
address
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
@@ -93,11 +117,16 @@ pub async fn exec_stop(config: Config, args: &ArgMatches) -> Result<(), anyhow::
|
||||
|
||||
pub async fn exec_get(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
|
||||
let database = args.get_one::<String>("database").unwrap();
|
||||
let address = database_address(&config, database).await?;
|
||||
let server = args.get_one::<String>("server").map(|s| s.as_ref());
|
||||
let address = database_address(&config, database, server).await?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.get(format!("{}/tracelog/database/{}", config.get_host_url(), address))
|
||||
.get(format!(
|
||||
"{}/tracelog/database/{}",
|
||||
config.get_host_url(server)?,
|
||||
address
|
||||
))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
|
||||
+98
-18
@@ -1,28 +1,34 @@
|
||||
use anyhow::Context;
|
||||
use base64::{engine::general_purpose::STANDARD as BASE_64_STD, Engine as _};
|
||||
use reqwest::RequestBuilder;
|
||||
use serde::Deserialize;
|
||||
use spacetimedb_lib::name::{is_address, DnsLookupResponse, RegisterTldResult, ReverseDNSResponse};
|
||||
use spacetimedb_lib::Identity;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
|
||||
use crate::config::{Config, IdentityConfig};
|
||||
|
||||
/// Determine the address of the `database`.
|
||||
pub async fn database_address(config: &Config, database: &str) -> Result<String, anyhow::Error> {
|
||||
pub async fn database_address(config: &Config, database: &str, server: Option<&str>) -> Result<String, anyhow::Error> {
|
||||
if is_address(database) {
|
||||
return Ok(database.to_string());
|
||||
}
|
||||
match spacetime_dns(config, database).await? {
|
||||
match spacetime_dns(config, database, server).await? {
|
||||
DnsLookupResponse::Success { domain: _, address } => Ok(address),
|
||||
DnsLookupResponse::Failure { domain } => Err(anyhow::anyhow!("The dns resolution of `{}` failed.", domain)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a name to a database address.
|
||||
pub async fn spacetime_dns(config: &Config, domain: &str) -> Result<DnsLookupResponse, anyhow::Error> {
|
||||
pub async fn spacetime_dns(
|
||||
config: &Config,
|
||||
domain: &str,
|
||||
server: Option<&str>,
|
||||
) -> Result<DnsLookupResponse, anyhow::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{}/database/dns/{}", config.get_host_url(), domain);
|
||||
let url = format!("{}/database/dns/{}", config.get_host_url(server)?, domain);
|
||||
let res = client.get(url).send().await?.error_for_status()?;
|
||||
let bytes = res.bytes().await.unwrap();
|
||||
Ok(serde_json::from_slice(&bytes[..]).unwrap())
|
||||
@@ -35,12 +41,13 @@ pub async fn spacetime_register_tld(
|
||||
config: &mut Config,
|
||||
tld: &str,
|
||||
identity: Option<&String>,
|
||||
server: Option<&str>,
|
||||
) -> Result<RegisterTldResult, anyhow::Error> {
|
||||
let auth_header = get_auth_header_only(config, false, identity).await.unwrap();
|
||||
let auth_header = get_auth_header_only(config, false, identity, server).await.unwrap();
|
||||
|
||||
// TODO(jdetter): Fix URL encoding on specifying this domain
|
||||
let builder =
|
||||
reqwest::Client::new().get(format!("{}/database/register_tld?tld={}", config.get_host_url(), tld).as_str());
|
||||
let builder = reqwest::Client::new()
|
||||
.get(format!("{}/database/register_tld?tld={}", config.get_host_url(server)?, tld).as_str());
|
||||
let builder = add_auth_header_opt(builder, &Some(auth_header));
|
||||
|
||||
let res = builder.send().await?.error_for_status()?;
|
||||
@@ -48,10 +55,21 @@ pub async fn spacetime_register_tld(
|
||||
Ok(serde_json::from_slice(&bytes[..]).unwrap())
|
||||
}
|
||||
|
||||
pub async fn spacetime_server_fingerprint(url: &str) -> anyhow::Result<String> {
|
||||
let builder = reqwest::Client::new().get(format!("{}/identity/public-key", url).as_str());
|
||||
let res = builder.send().await?.error_for_status()?;
|
||||
let fingerprint = res.text().await?;
|
||||
Ok(fingerprint)
|
||||
}
|
||||
|
||||
/// Returns all known names for the given address.
|
||||
pub async fn spacetime_reverse_dns(config: &Config, address: &str) -> Result<ReverseDNSResponse, anyhow::Error> {
|
||||
pub async fn spacetime_reverse_dns(
|
||||
config: &Config,
|
||||
address: &str,
|
||||
server: Option<&str>,
|
||||
) -> Result<ReverseDNSResponse, anyhow::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{}/database/reverse_dns/{}", config.get_host_url(), address);
|
||||
let url = format!("{}/database/reverse_dns/{}", config.get_host_url(server)?, address);
|
||||
let res = client.get(url).send().await?.error_for_status()?;
|
||||
let bytes = res.bytes().await.unwrap();
|
||||
Ok(serde_json::from_slice(&bytes[..]).unwrap())
|
||||
@@ -73,15 +91,19 @@ pub struct InitDefaultResult {
|
||||
pub result_type: InitDefaultResultType,
|
||||
}
|
||||
|
||||
pub async fn init_default(config: &mut Config, nickname: Option<String>) -> Result<InitDefaultResult, anyhow::Error> {
|
||||
pub async fn init_default(
|
||||
config: &mut Config,
|
||||
nickname: Option<String>,
|
||||
server: Option<&str>,
|
||||
) -> Result<InitDefaultResult, anyhow::Error> {
|
||||
if config.name_exists(nickname.as_ref().unwrap_or(&"".to_string())) {
|
||||
return Err(anyhow::anyhow!("A default identity already exists."));
|
||||
}
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let builder = client.post(format!("{}/identity", config.get_host_url()));
|
||||
let builder = client.post(format!("{}/identity", config.get_host_url(server)?));
|
||||
|
||||
if let Some(identity_config) = config.get_default_identity_config() {
|
||||
if let Ok(identity_config) = config.get_default_identity_config(server) {
|
||||
return Ok(InitDefaultResult {
|
||||
identity_config: identity_config.clone(),
|
||||
result_type: InitDefaultResultType::Existing,
|
||||
@@ -104,8 +126,8 @@ pub async fn init_default(config: &mut Config, nickname: Option<String>) -> Resu
|
||||
nickname: nickname.clone(),
|
||||
};
|
||||
config.identity_configs_mut().push(identity_config.clone());
|
||||
if config.default_identity().is_none() {
|
||||
config.set_default_identity(identity);
|
||||
if config.default_identity(server).is_err() {
|
||||
config.set_default_identity(identity, server)?;
|
||||
}
|
||||
config.save();
|
||||
Ok(InitDefaultResult {
|
||||
@@ -119,9 +141,12 @@ pub async fn init_default(config: &mut Config, nickname: Option<String>) -> Resu
|
||||
/// identity, or return an error if it cannot be found. If you do not specify
|
||||
/// an identity this function will either get the default identity if one exists
|
||||
/// or create and save a new default identity.
|
||||
|
||||
// TODO: validate identity by server's public key
|
||||
pub async fn select_identity_config(
|
||||
config: &mut Config,
|
||||
identity_or_name: Option<&str>,
|
||||
server: Option<&str>,
|
||||
) -> Result<IdentityConfig, anyhow::Error> {
|
||||
let resolve_identity_to_identity_config = |ident: &str| -> Result<IdentityConfig, anyhow::Error> {
|
||||
config
|
||||
@@ -141,7 +166,7 @@ pub async fn select_identity_config(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(init_default(config, None).await?.identity_config)
|
||||
Ok(init_default(config, None, server).await?.identity_config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,8 +183,9 @@ pub async fn get_auth_header_only(
|
||||
config: &mut Config,
|
||||
anon_identity: bool,
|
||||
identity_or_name: Option<&String>,
|
||||
server: Option<&str>,
|
||||
) -> Option<String> {
|
||||
get_auth_header(config, anon_identity, identity_or_name.map(|x| x.as_str()))
|
||||
get_auth_header(config, anon_identity, identity_or_name.map(|x| x.as_str()), server)
|
||||
.await
|
||||
.map(|(ah, _)| ah)
|
||||
}
|
||||
@@ -180,9 +206,10 @@ pub async fn get_auth_header(
|
||||
config: &mut Config,
|
||||
anon_identity: bool,
|
||||
identity_or_name: Option<&str>,
|
||||
server: Option<&str>,
|
||||
) -> Option<(String, Identity)> {
|
||||
if !anon_identity {
|
||||
let identity_config = match select_identity_config(config, identity_or_name).await {
|
||||
let identity_config = match select_identity_config(config, identity_or_name, server).await {
|
||||
Ok(ic) => ic,
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
@@ -249,7 +276,7 @@ impl clap::ValueEnum for ModuleLanguage {
|
||||
}
|
||||
|
||||
pub fn detect_module_language(path_to_project: &Path) -> ModuleLanguage {
|
||||
// TODO: Possible add a config file during spacetime init with the language
|
||||
// TODO: Possible add a config file durlng spacetime init with the language
|
||||
// check for Cargo.toml
|
||||
if path_to_project.join("Cargo.toml").exists() {
|
||||
ModuleLanguage::Rust
|
||||
@@ -257,3 +284,56 @@ pub fn detect_module_language(path_to_project: &Path) -> ModuleLanguage {
|
||||
ModuleLanguage::Csharp
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url_to_host_and_protocol(url: &str) -> anyhow::Result<(&str, &str)> {
|
||||
if contains_protocol(url) {
|
||||
let protocol = url.split("://").next().unwrap();
|
||||
let host = url.split("://").last().unwrap();
|
||||
|
||||
if !VALID_PROTOCOLS.contains(&protocol) {
|
||||
Err(anyhow::anyhow!("Invalid protocol: {}", protocol))
|
||||
} else {
|
||||
Ok((host, protocol))
|
||||
}
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Invalid url: {}", url))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_protocol(name_or_url: &str) -> bool {
|
||||
name_or_url.contains("://")
|
||||
}
|
||||
|
||||
pub fn host_or_url_to_host_and_protocol(host_or_url: &str) -> (&str, Option<&str>) {
|
||||
if contains_protocol(host_or_url) {
|
||||
let (host, protocol) = url_to_host_and_protocol(host_or_url).unwrap();
|
||||
(host, Some(protocol))
|
||||
} else {
|
||||
(host_or_url, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompt the user for `y` or `n` from stdin.
|
||||
///
|
||||
/// Return `false` unless the input is `y`.
|
||||
pub fn y_or_n(prompt: &str) -> anyhow::Result<bool> {
|
||||
let mut input = String::new();
|
||||
print!("{} (y/n)", prompt);
|
||||
std::io::stdout().flush()?;
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
|
||||
Ok(input.trim() == "y")
|
||||
}
|
||||
|
||||
pub fn unauth_error_context<T>(res: anyhow::Result<T>, identity: &str, server: &str) -> anyhow::Result<T> {
|
||||
res.with_context(|| {
|
||||
format!(
|
||||
"Identity {identity} is not valid for server {server}.
|
||||
Has the server rotated its keys?
|
||||
Remove the outdated identity with:
|
||||
\tspacetime identity remove {identity}
|
||||
Generate a new identity with:
|
||||
\tspacetime identity new --no-email --server {server}"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ impl StandaloneEnv {
|
||||
public_key,
|
||||
private_key,
|
||||
public_key_bytes,
|
||||
|
||||
config,
|
||||
});
|
||||
energy_monitor.set_standalone_env(this.clone());
|
||||
|
||||
+5
-1
@@ -1,3 +1,7 @@
|
||||
default_server = '127.0.0.1:3000'
|
||||
identity_configs = []
|
||||
|
||||
[[server_configs]]
|
||||
host = '127.0.0.1:3000'
|
||||
protocol = 'http'
|
||||
identity_configs = []
|
||||
nickname = 'localhost'
|
||||
|
||||
@@ -35,6 +35,16 @@ reset_project() {
|
||||
export PROJECT_PATH
|
||||
}
|
||||
|
||||
# This cleans up the temporary project directory after a test
|
||||
clear_project() {
|
||||
rm -rf "$PROJECT_PATH"
|
||||
}
|
||||
|
||||
# This cleans up the temporary config file after a test
|
||||
clear_config() {
|
||||
rm -f "$SPACETIME_CONFIG_FILE"
|
||||
}
|
||||
|
||||
random_string() {
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo $RANDOM | md5 -q | head -c 20
|
||||
|
||||
@@ -40,7 +40,7 @@ EOF
|
||||
|
||||
fsed "s/REPLACE_VALUE/$1/g" "${PROJECT_PATH}/src/lib.rs"
|
||||
|
||||
run_test cargo run publish --project-path "$PROJECT_PATH" --clear-database -d -s
|
||||
run_test cargo run publish --project-path "$PROJECT_PATH" --clear-database -d -S
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
@@ -53,6 +53,8 @@ EOF
|
||||
[[ "$(grep 'Julie' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ 2:Julie! ]]
|
||||
[[ "$(grep 'Robert' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ 1:Robert! ]]
|
||||
[[ "$(grep 'World' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ World! ]]
|
||||
|
||||
clear_project
|
||||
}
|
||||
|
||||
do_test u8
|
||||
|
||||
@@ -68,6 +68,8 @@ EOF
|
||||
[[ "$(grep 'Robert' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ 2:Robert! ]]
|
||||
[[ "$(grep 'Success' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ 1:Success! ]]
|
||||
[[ "$(grep 'World' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ World! ]]
|
||||
|
||||
clear_project
|
||||
}
|
||||
|
||||
do_test u8
|
||||
|
||||
@@ -12,7 +12,7 @@ source "./test/lib.include"
|
||||
[ -d ../BitCraftMini ]
|
||||
|
||||
# 2. Compile the Spacetime Module
|
||||
run_test cargo run publish -s -d --project-path "../BitCraftMini/Server" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "../BitCraftMini/Server" --clear-database
|
||||
ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
sleep 2
|
||||
mkdir -p ../BitCraftMini/Client/Assets/_Project/autogen
|
||||
|
||||
@@ -31,7 +31,7 @@ fn reducer(num: i32) {
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
sleep 2
|
||||
|
||||
@@ -164,7 +164,7 @@ fn find_indexed_people(surname: String) {
|
||||
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ TOKEN=$(cat "$TEST_OUT")
|
||||
|
||||
reset_config
|
||||
|
||||
# Fetch the server's fingerprint.
|
||||
# The fingerprint is required for `identity list`.
|
||||
run_test cargo run server fingerprint localhost -f
|
||||
|
||||
run_test cargo run identity import "$IDENT" "$TOKEN"
|
||||
run_test cargo run identity list
|
||||
exit 0
|
||||
|
||||
@@ -10,6 +10,10 @@ set -x
|
||||
|
||||
source "./test/lib.include"
|
||||
|
||||
# Fetch the server's fingerprint.
|
||||
# The fingerprint is required for `identity list`.
|
||||
run_test cargo run server fingerprint localhost -f
|
||||
|
||||
run_test cargo run identity new --no-email
|
||||
run_test cargo run identity new --no-email
|
||||
IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')
|
||||
|
||||
@@ -10,6 +10,10 @@ set -x
|
||||
|
||||
source "./test/lib.include"
|
||||
|
||||
# Fetch the server's fingerprint.
|
||||
# The fingerprint is required for `identity list`.
|
||||
run_test cargo run server fingerprint localhost -f
|
||||
|
||||
run_test cargo run identity new --no-email
|
||||
run_test cargo run identity new --no-email
|
||||
IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')
|
||||
|
||||
@@ -10,6 +10,10 @@ set -x
|
||||
|
||||
source "./test/lib.include"
|
||||
|
||||
# Fetch the server's fingerprint.
|
||||
# The fingerprint is required for `identity list`.
|
||||
run_test cargo run server fingerprint localhost -f
|
||||
|
||||
run_test cargo run identity new --no-email
|
||||
run_test cargo run identity new --no-email
|
||||
IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')
|
||||
|
||||
@@ -52,7 +52,7 @@ pub fn say_friends() {
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn say_hello() {
|
||||
EOF
|
||||
|
||||
## Publish your module
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
# We have to give the database some time to setup our instance
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ fn second() {
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ source "./test/lib.include"
|
||||
run_test cargo run identity new --no-email
|
||||
IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')
|
||||
TOKEN="$(cargo run identity token "$IDENT")"
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
sleep 2
|
||||
DATABASE="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ source "./test/lib.include"
|
||||
run_test cargo run identity new --no-email
|
||||
IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')
|
||||
run_test cargo run identity set-default "$IDENT"
|
||||
run_test cargo run publish -s -d --project-path="$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path="$PROJECT_PATH" --clear-database
|
||||
ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
reset_config
|
||||
|
||||
@@ -10,7 +10,7 @@ set -euox pipefail
|
||||
source "./test/lib.include"
|
||||
|
||||
run_test cargo run identity new --no-email
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
sleep 2
|
||||
DATABASE="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ set -euox pipefail
|
||||
source "./test/lib.include"
|
||||
|
||||
run_test cargo run identity new --no-email
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
sleep 2
|
||||
DATABASE="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ source "./test/lib.include"
|
||||
run_test cargo run identity new --no-email
|
||||
IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')
|
||||
run_test cargo run identity set-default "$IDENT"
|
||||
run_test cargo run publish -s -d --project-path="$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path="$PROJECT_PATH" --clear-database
|
||||
ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
reset_config
|
||||
if cargo run publish -s -d "$ADDRESS" --project-path="$PROJECT_PATH" --clear-database ; then exit 1; fi
|
||||
if cargo run publish -S -d "$ADDRESS" --project-path="$PROJECT_PATH" --clear-database ; then exit 1; fi
|
||||
|
||||
@@ -14,11 +14,12 @@ RAND_DOMAIN=$(random_string)
|
||||
run_test cargo run identity new --no-email
|
||||
IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')
|
||||
run_test cargo run dns register-tld "$RAND_DOMAIN"
|
||||
clear_project
|
||||
reset_project
|
||||
run_test cargo run publish -s -d "$RAND_DOMAIN" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -s -d "$RAND_DOMAIN/test" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -s -d "$RAND_DOMAIN/test/test2" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d "$RAND_DOMAIN" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d "$RAND_DOMAIN/test" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d "$RAND_DOMAIN/test/test2" --project-path "$PROJECT_PATH" --clear-database
|
||||
|
||||
run_fail_test cargo run publish -s -d "$RAND_DOMAIN//test" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_fail_test cargo run publish -s -d "$RAND_DOMAIN/test/" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_fail_test cargo run publish -s -d "$RAND_DOMAIN/test//test2" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_fail_test cargo run publish -S -d "$RAND_DOMAIN//test" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_fail_test cargo run publish -S -d "$RAND_DOMAIN/test/" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_fail_test cargo run publish -S -d "$RAND_DOMAIN/test//test2" --project-path "$PROJECT_PATH" --clear-database
|
||||
|
||||
@@ -11,7 +11,7 @@ source "./test/lib.include"
|
||||
|
||||
RAND=$(random_string)
|
||||
run_test cargo run dns register-tld "$RAND"
|
||||
run_test cargo run publish -s -d "$RAND" --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d "$RAND" --project-path "$PROJECT_PATH" --clear-database
|
||||
ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
if [ "$ADDRESS" == "" ] ; then
|
||||
exit 1
|
||||
|
||||
@@ -10,7 +10,7 @@ set -euox pipefail
|
||||
source "./test/lib.include"
|
||||
|
||||
run_test cargo run identity init-default
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
RAND_NAME="$(random_string)"
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$DESCRIBE_TEST" = 1 ] ; then
|
||||
echo "NO DESCRIPTION FOR THIS TEST!"
|
||||
exit
|
||||
echo "Verify that we can add and list server configurations"
|
||||
exit
|
||||
fi
|
||||
|
||||
set -euox pipefail
|
||||
|
||||
source "./test/lib.include"
|
||||
|
||||
run_test cargo run server set "https://spacetimedb.com/spacetimedb"
|
||||
[ "$(grep Host "$TEST_OUT")" == "Host: spacetimedb.com/spacetimedb" ]
|
||||
run_test cargo run server add "https://testnet.spacetimedb.com" testnet --no-fingerprint
|
||||
[ "$(grep Host "$TEST_OUT")" == "Host: testnet.spacetimedb.com" ]
|
||||
[ "$(grep Protocol "$TEST_OUT")" == "Protocol: https" ]
|
||||
[ "$(grep host "$SPACETIME_CONFIG_FILE")" == "host = 'spacetimedb.com/spacetimedb'" ]
|
||||
[ "$(grep protocol "$SPACETIME_CONFIG_FILE")" == "protocol = 'https'" ]
|
||||
|
||||
run_test cargo run server set "http://127.0.0.1:3000/spacetimedb"
|
||||
[ "$(grep Host "$TEST_OUT")" == "Host: 127.0.0.1:3000/spacetimedb" ]
|
||||
[ "$(grep Protocol "$TEST_OUT")" == "Protocol: http" ]
|
||||
[ "$(grep host "$SPACETIME_CONFIG_FILE")" == "host = '127.0.0.1:3000/spacetimedb'" ]
|
||||
[ "$(grep protocol "$SPACETIME_CONFIG_FILE")" == "protocol = 'http'" ]
|
||||
run_test cargo run server list
|
||||
[[ "$(grep testnet.spacetimedb.com "$TEST_OUT")" =~ [[:space:]]*testnet\.spacetimedb\.com[[:space:]]+https[[:space:]]+testnet[[:space:]]* ]]
|
||||
[[ "$(grep 127.0.0.1:3000 "$TEST_OUT")" =~ [[:space:]]*\*\*\*[[:space:]]+127\.0\.0\.1:3000[[:space:]]+http[[:space:]]* ]]
|
||||
|
||||
run_test cargo run server set "http://127.0.0.1"
|
||||
[ "$(grep Host "$TEST_OUT")" == "Host: 127.0.0.1" ]
|
||||
[ "$(grep Protocol "$TEST_OUT")" == "Protocol: http" ]
|
||||
[ "$(grep host "$SPACETIME_CONFIG_FILE")" == "host = '127.0.0.1'" ]
|
||||
[ "$(grep protocol "$SPACETIME_CONFIG_FILE")" == "protocol = 'http'" ]
|
||||
run_test cargo run server fingerprint 127.0.0.1:3000 -f
|
||||
grep "No saved fingerprint for server 127.0.0.1:3000." "$TEST_OUT"
|
||||
|
||||
run_test cargo run server fingerprint 127.0.0.1:3000
|
||||
grep "Fingerprint is unchanged for server 127.0.0.1:3000" "$TEST_OUT"
|
||||
|
||||
run_test cargo run server fingerprint localhost
|
||||
grep "Fingerprint is unchanged for server localhost" "$TEST_OUT"
|
||||
|
||||
@@ -90,7 +90,7 @@ pub fn test() {
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
# We have to give the database some time to setup our instance
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn say_hello() {
|
||||
EOF
|
||||
|
||||
IDENT=$(basename "$PROJECT_PATH")
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" "$IDENT"
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" "$IDENT"
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
|
||||
run_test cargo run call "$IDENT" add '["Robert"]'
|
||||
@@ -49,7 +49,7 @@ run_test cargo run logs "$IDENT" 100
|
||||
[ ' Hello, World!' == "$(grep 'World' "$TEST_OUT" | tail -n 4 | cut -d: -f4-)" ]
|
||||
|
||||
# Unchanged module is ok
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" "$IDENT"
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" "$IDENT"
|
||||
[ "1" == "$(grep -c "Updated database" "$TEST_OUT")" ]
|
||||
|
||||
# Changing an existing table isn't
|
||||
@@ -66,7 +66,7 @@ pub struct Person {
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" "$IDENT" || true
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" "$IDENT" || true
|
||||
[ "1" == "$(grep -c "Error: Database update rejected" "$TEST_OUT")" ]
|
||||
|
||||
# Adding a table is ok, and invokes update
|
||||
@@ -92,7 +92,7 @@ pub fn on_module_update() {
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" "$IDENT"
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" "$IDENT"
|
||||
[ "1" == "$(grep -c "Updated database" "$TEST_OUT")" ]
|
||||
run_test cargo run logs "$IDENT" 1
|
||||
[ ' MODULE UPDATED' == "$(grep 'MODULE UPDATED' "$TEST_OUT" | tail -n 1 | cut -d: -f4-)" ]
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn say_hello() {
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn my_repeating_reducer(prev: Timestamp) {
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
sleep 2
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn say_hello() {
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
run_test cargo run call "$IDENT" add '["Robert"]'
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn my_repeating_reducer(prev: Timestamp) {
|
||||
pub fn dummy() {}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn say_hello() {
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish -s -d --project-path "$PROJECT_PATH" --clear-database
|
||||
run_test cargo run publish -S -d --project-path "$PROJECT_PATH" --clear-database
|
||||
[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ]
|
||||
IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user