-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cli integration #380
Cli integration #380
Changes from all commits
e557202
f7cb785
6993fb6
f476fc2
44c4a15
f697e06
9626f15
4a97340
9225c7a
cc158c4
b18a737
734d5a9
b421153
2fada00
0c9b1b1
c1312c4
08ba17f
f3afedc
8fc5f48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
[package] | ||
name = "calimero-cli" | ||
version = "0.1.0" | ||
authors.workspace = true | ||
edition.workspace = true | ||
repository.workspace = true | ||
license.workspace = true | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
camino = { workspace = true, features = ["serde1"] } | ||
clap = { workspace = true, features = ["env", "derive"] } | ||
tracing-subscriber = { workspace = true, features = ["env-filter"] } | ||
color-eyre.workspace = true | ||
libp2p.workspace = true | ||
dirs.workspace = true | ||
eyre.workspace = true | ||
tokio = { workspace = true, features = ["io-std", "macros"] } | ||
multiaddr.workspace = true | ||
serde = { workspace = true, features = ["derive"] } | ||
sha2.workspace = true | ||
toml.workspace = true | ||
tracing.workspace = true | ||
hex.workspace = true | ||
|
||
calimero-node = { path = "../node" } | ||
calimero-network = { path = "../network" } | ||
calimero-application = { path = "../application" } | ||
calimero-node-primitives = { path = "../node-primitives" } | ||
calimero-primitives = { path = "../primitives" } | ||
calimero-server = { path = "../server", features = ["jsonrpc", "websocket", "admin"] } | ||
calimero-store = { path = "../store" } | ||
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -1,38 +1,45 @@ | ||||
use calimero_node::config; | ||||
use clap::{Parser, Subcommand}; | ||||
|
||||
use crate::config; | ||||
|
||||
mod init; | ||||
mod link; | ||||
mod run; | ||||
|
||||
mod setup; | ||||
#[derive(Debug, Parser)] | ||||
#[clap(author, about, version)] | ||||
#[command(author, about, version)] | ||||
pub struct RootCommand { | ||||
#[clap(flatten)] | ||||
#[command(flatten)] | ||||
pub args: RootArgs, | ||||
|
||||
#[clap(subcommand)] | ||||
#[command(subcommand)] | ||||
pub action: SubCommands, | ||||
} | ||||
|
||||
#[derive(Debug, Subcommand)] | ||||
pub enum SubCommands { | ||||
Init(init::InitCommand), | ||||
Setup(setup::SetupCommand), | ||||
Run(run::RunCommand), | ||||
Link(link::LinkCommand), | ||||
} | ||||
|
||||
#[derive(Debug, Parser)] | ||||
pub struct RootArgs { | ||||
/// Directory for config and data | ||||
#[clap(long, value_name = "PATH", default_value_t = config::default_chat_dir())] | ||||
#[clap(env = "CALIMERO_CHAT_HOME", hide_env_values = true)] | ||||
#[arg(long, value_name = "PATH", default_value_t = config::default_chat_dir())] | ||||
#[arg(env = "CALIMERO_HOME", hide_env_values = true)] | ||||
pub home: camino::Utf8PathBuf, | ||||
} | ||||
|
||||
impl RootCommand { | ||||
pub async fn run(self) -> eyre::Result<()> { | ||||
let _c = RootCommand::parse(); | ||||
match self.action { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
SubCommands::Init(init) => return init.run(self.args), | ||||
SubCommands::Setup(setup) => return setup.run(self.args), | ||||
SubCommands::Run(run) => return run.run(self.args).await, | ||||
SubCommands::Link(link) => link.run(self.args), | ||||
} | ||||
} | ||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
use std::fs; | ||
|
||
use clap::Parser; | ||
use eyre::WrapErr; | ||
use libp2p::identity; | ||
use tracing::{info, warn}; | ||
|
||
use crate::cli; | ||
use crate::config::{ConfigFile, ConfigImpl, InitFile}; | ||
|
||
/// Initialize node and it's identity | ||
#[derive(Debug, Parser)] | ||
pub struct InitCommand { | ||
/// Name of node | ||
#[arg(short, long, value_name = "NAME")] | ||
pub node_name: camino::Utf8PathBuf, | ||
Comment on lines
+14
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't this also need to be part of the |
||
|
||
/// Force initialization even if the directory already exists | ||
#[clap(short, long)] | ||
pub force: bool, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would the user want to init node again? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Someone mentioned that it would be nice to restart the node, so Xabi mentioned that deleting the keys would block users to rejoin the context. So I kept it like this. |
||
} | ||
|
||
impl InitCommand { | ||
pub fn run(self, root_args: cli::RootArgs) -> eyre::Result<()> { | ||
let path = root_args.home.join(&self.node_name); | ||
|
||
fs::create_dir_all(&path) | ||
.wrap_err_with(|| format!("failed to create directory {:?}", &path))?; | ||
|
||
if InitFile::exists(&path) { | ||
match ConfigFile::load(&path) { | ||
Ok(config) => { | ||
if self.force { | ||
warn!( | ||
"Overriding config.toml file for {}, keeping identity", | ||
self.node_name | ||
); | ||
let config_new = InitFile { | ||
identity: config.identity, | ||
}; | ||
config_new.save(&path)?; | ||
return Ok(()); | ||
} else { | ||
eyre::bail!( | ||
"Node {} is already initialized in {:?}", | ||
self.node_name, | ||
path | ||
); | ||
} | ||
} | ||
Err(_err) => match InitFile::load(&path) { | ||
Ok(_config) => { | ||
if self.force { | ||
eyre::bail!( | ||
"Node {} is already initialized in {:?}\nCan not override node identity", | ||
self.node_name, | ||
path | ||
); | ||
} else { | ||
eyre::bail!( | ||
"Node {} is already initialized in {:?}", | ||
self.node_name, | ||
path | ||
); | ||
} | ||
} | ||
Err(err) => eyre::bail!("failed to load existing configuration: {}", err), | ||
}, | ||
} | ||
} | ||
let identity = identity::Keypair::generate_ed25519(); | ||
petarjuki7 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
info!("Generated identity: {:?}", identity.public().to_peer_id()); | ||
|
||
let config = InitFile { | ||
identity: identity.clone(), | ||
}; | ||
|
||
config.save(&path)?; | ||
|
||
println!("{:?}", path); | ||
Ok(()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,91 @@ | ||||||||||||||||
use std::fs; | ||||||||||||||||
#[cfg(unix)] | ||||||||||||||||
use std::os::unix::fs::symlink; | ||||||||||||||||
#[cfg(windows)] | ||||||||||||||||
use std::os::windows::fs::symlink_file as symlink; | ||||||||||||||||
|
||||||||||||||||
use clap::Parser; | ||||||||||||||||
use eyre::WrapErr; | ||||||||||||||||
use sha2::{Digest, Sha256}; | ||||||||||||||||
use tracing::info; | ||||||||||||||||
|
||||||||||||||||
use crate::cli; | ||||||||||||||||
use crate::config::{ConfigFile, ConfigImpl}; | ||||||||||||||||
|
||||||||||||||||
/// Setup symlink to application in the node | ||||||||||||||||
#[derive(Debug, Parser)] | ||||||||||||||||
pub struct LinkCommand { | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be simple and we want to override the application for the context, not the application itself: And I believe it makes sense to scope things out under an initial $ calimero context <context-id> install <resource> where
for example:
|
||||||||||||||||
/// Name of node | ||||||||||||||||
#[arg(short, long, value_name = "NAME")] | ||||||||||||||||
pub node_name: camino::Utf8PathBuf, | ||||||||||||||||
|
||||||||||||||||
/// Path to original file | ||||||||||||||||
#[clap(short, long)] | ||||||||||||||||
pub path: camino::Utf8PathBuf, | ||||||||||||||||
|
||||||||||||||||
/// Name of application | ||||||||||||||||
#[clap(short, long)] | ||||||||||||||||
pub app_name: camino::Utf8PathBuf, | ||||||||||||||||
|
||||||||||||||||
/// Version | ||||||||||||||||
#[clap(short, long, value_parser = validate_version, default_value = "1.0.0")] | ||||||||||||||||
pub version: String, | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
fn validate_version(v: &str) -> Result<String, String> { | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||
let parts: Vec<&str> = v.split('.').collect(); | ||||||||||||||||
if parts.len() != 3 { | ||||||||||||||||
return Err(String::from("Version must have exactly three parts")); | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
for part in parts { | ||||||||||||||||
match part.parse::<u8>() { | ||||||||||||||||
Ok(_) => {} | ||||||||||||||||
Err(e) => return Err(format!("Invalid version number: {}", e)), | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
Ok(v.to_string()) | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
impl LinkCommand { | ||||||||||||||||
pub fn run(self, root_args: cli::RootArgs) -> eyre::Result<()> { | ||||||||||||||||
let path_to_node = root_args.home.join(&self.node_name); | ||||||||||||||||
if ConfigFile::exists(&path_to_node) { | ||||||||||||||||
match ConfigFile::load(&path_to_node) { | ||||||||||||||||
Ok(config) => { | ||||||||||||||||
let id = format!("{}:{}", self.node_name, self.app_name); | ||||||||||||||||
let mut hasher = Sha256::new(); | ||||||||||||||||
hasher.update(id.as_bytes()); | ||||||||||||||||
let hash_string = hex::encode(hasher.finalize()); | ||||||||||||||||
|
||||||||||||||||
let app_path = path_to_node | ||||||||||||||||
.join(config.application.path) | ||||||||||||||||
.join(hash_string) | ||||||||||||||||
.join(self.version); | ||||||||||||||||
|
||||||||||||||||
fs::create_dir_all(&app_path) | ||||||||||||||||
.wrap_err_with(|| format!("failed to create directory {:?}", &app_path))?; | ||||||||||||||||
info!("Linking original file to: {:?}", app_path); | ||||||||||||||||
|
||||||||||||||||
match symlink(self.path, app_path.join("binary.wasm")) { | ||||||||||||||||
Ok(_) => {} | ||||||||||||||||
Err(err) => eyre::bail!("Symlinking failed: {}", err), | ||||||||||||||||
} | ||||||||||||||||
Comment on lines
+71
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
info!( | ||||||||||||||||
"Application {} linked to node {}\nPath to linked file at {}", | ||||||||||||||||
self.app_name, | ||||||||||||||||
self.node_name, | ||||||||||||||||
app_path.join("binary.wasm") | ||||||||||||||||
); | ||||||||||||||||
return Ok(()); | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move this to the end of the function |
||||||||||||||||
} | ||||||||||||||||
Err(err) => { | ||||||||||||||||
eyre::bail!("failed to load existing configuration: {}", err); | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} else { | ||||||||||||||||
eyre::bail!("You have to initialize the node first \nRun command node init -n <NAME>"); | ||||||||||||||||
} | ||||||||||||||||
Comment on lines
+87
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move this to the top if !ConfigFile::exists(..) {
eyre::bail!(..);
}
// at this point the file exists should help flatten out the indentation |
||||||||||||||||
} | ||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,15 @@ | ||
use calimero_node::config::ConfigFile; | ||
use clap::{Parser, ValueEnum}; | ||
|
||
use crate::cli; | ||
use crate::config::{ConfigFile, ConfigImpl}; | ||
|
||
/// Run a node | ||
#[derive(Debug, Parser)] | ||
pub struct RunCommand { | ||
/// Name of node | ||
#[arg(short, long, value_name = "NAME")] | ||
pub node_name: camino::Utf8PathBuf, | ||
|
||
#[clap(long, value_name = "TYPE")] | ||
#[clap(value_enum, default_value_t)] | ||
pub node_type: NodeType, | ||
|
@@ -29,21 +33,22 @@ impl From<NodeType> for calimero_node_primitives::NodeType { | |
|
||
impl RunCommand { | ||
pub async fn run(self, root_args: cli::RootArgs) -> eyre::Result<()> { | ||
if !ConfigFile::exists(&root_args.home) { | ||
eyre::bail!("chat node is not initialized in {:?}", root_args.home); | ||
let path = root_args.home.join(self.node_name); | ||
if !ConfigFile::exists(&path) { | ||
eyre::bail!("chat node is not initialized in {:?}", path); | ||
} | ||
|
||
let config = ConfigFile::load(&root_args.home)?; | ||
let config = ConfigFile::load(&path)?; | ||
|
||
calimero_node::start(calimero_node::NodeConfig { | ||
home: root_args.home.clone(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CLI should start a node binary, not invoke the start method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also CLI should keep track of all started binaries, so the user can stop/interact with any of the running nodes. |
||
home: path.clone(), | ||
node_type: self.node_type.into(), | ||
identity: config.identity.clone(), | ||
store: calimero_store::config::StoreConfig { | ||
path: root_args.home.join(config.store.path), | ||
path: path.join(config.store.path), | ||
}, | ||
application: calimero_application::config::ApplicationConfig { | ||
dir: root_args.home.join(config.application.path), | ||
dir: path.join(config.application.path), | ||
}, | ||
network: calimero_network::config::NetworkConfig { | ||
identity: config.identity.clone(), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't depend on all these crates IMO. CLI should be as small as possible and know as little as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of them are unneeded probably, will clean up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I'm constructing the config struct to run the node (
NodeConfig
), I am using other structs from these crates.Is there a way to get around that?