Skip to content
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

Closed
wants to merge 19 commits into from
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0"
resolver = "2"
members = [
"./crates/application",
"./crates/cli",
"./crates/identity",
"./crates/network",
"./crates/node",
Expand Down
33 changes: 33 additions & 0 deletions crates/cli/Cargo.toml
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" }
Comment on lines +27 to +33
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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?

21 changes: 14 additions & 7 deletions crates/node/src/cli.rs → crates/cli/src/cli.rs
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let _c = RootCommand::parse();

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),
}
}
}
83 changes: 83 additions & 0 deletions crates/cli/src/cli/init.rs
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this also need to be part of the RootArgs to prevent duplication?


/// Force initialization even if the directory already exists
#[clap(short, long)]
pub force: bool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would the user want to init node again?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.
Can remove if needed

}

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(())
}
}
91 changes: 91 additions & 0 deletions crates/cli/src/cli/link.rs
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 {
Copy link
Member

Choose a reason for hiding this comment

The 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 context subcommand:

$ calimero context <context-id> install <resource>

where

resource := (application-spec[@version]) {OR} (file-path)
application-spec := (scoped-spec) {OR} (unique-spec)
scoped-spec := (<user-name>/<package-name>)
unique-spec := (bs58-encoded-unique-32-byte-application-id)
file-path := (relative-path) {OR} (absolute-path)

for example:

  • Download the latest version of the application from the registry

     $ calimero context 33ovekjBBk2GxfZUueMPKMUe5ofqfp2AoEcXpAM71aAQ install petar/only-peers
  • Download version 0.2.0 of the application from the registry

     $ calimero context 33ovekjBBk2GxfZUueMPKMUe5ofqfp2AoEcXpAM71aAQ install petar/[email protected]
  • Download the latest version of the application from the registry

     $ calimero context 33ovekjBBk2GxfZUueMPKMUe5ofqfp2AoEcXpAM71aAQ install 4TdrU3ruw6VquHforZ1ojjQ46dmnRwSP3faKB8evjviB
  • Download version 0.3.4 of the application from the registry

     $ calimero context 33ovekjBBk2GxfZUueMPKMUe5ofqfp2AoEcXpAM71aAQ install [email protected]
  • Installs the specified wasm file (copied to the apps folder)

     $ calimero context 33ovekjBBk2GxfZUueMPKMUe5ofqfp2AoEcXpAM71aAQ install ./path/to/binary.wasm
  • Installs the specified wasm file (linked into the apps folder)

     $ calimero context 33ovekjBBk2GxfZUueMPKMUe5ofqfp2AoEcXpAM71aAQ install --link ./path/to/binary.wasm

/// 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> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semver::Version didn't work?

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
match symlink(self.path, app_path.join("binary.wasm")) {
Ok(_) => {}
Err(err) => eyre::bail!("Symlinking failed: {}", err),
}
if let Err(err) = symlink(self.path, app_path.join("binary.wasm")) {
eyre::bail!("Symlinking failed: {}", err);
}

info!(
"Application {} linked to node {}\nPath to linked file at {}",
self.app_name,
self.node_name,
app_path.join("binary.wasm")
);
return Ok(());
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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

}
}
19 changes: 12 additions & 7 deletions crates/node/src/cli/run.rs → crates/cli/src/cli/run.rs
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,
Expand All @@ -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(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLI should start a node binary, not invoke the start method.

Copy link
Member

Choose a reason for hiding this comment

The 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(),
Expand Down
Loading
Loading