Skip to content

Commit

Permalink
finish up generic client. update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
gregcusack committed Jul 2, 2024
1 parent e9e44d5 commit 6ba9f16
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 110 deletions.
1 change: 1 addition & 0 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ By here:
- [ ] Other Features
- [x] Heterogeneous Clusters (i.e. multiple validator versions)
- [x] Deploy specific commit
- [x] Generic Clients
- [ ] Deploy with user-defined stake distribution

By here:
Expand Down
119 changes: 118 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ cargo run --bin cluster --
3) Validator, client, and rpc Dockerfiles

After deploying a cluster with a bootstrap, 2 clients, 2 validators, and 3 rpc nodes all running v1.18.13, your `<cluster-data-path>` directory will look something like:

![Cluster Data Path Directory](cluster_data_path_tree.png)

#### Build from Local Repo and Configure Genesis and Bootstrap and Validator Image
Expand Down Expand Up @@ -156,6 +157,88 @@ For steps (2) and (3), when using `--no-bootstrap`, we assume that the directory

Note: We can't deploy heterogeneous clusters across v1.17 and v1.18 due to feature differences. Hope to fix this in the future. Have something where we can specifically define which features to enable.

## Generic Clients
Bring your own client and deploy it in a Validator Lab cluster!
All you need is a containerized version of your client in an accessible docker registry.

Key points/steps:
1) [Containerize your client](#Containerize-your-Client)
2) Any client accounts should be built into the client container image
3) Client arguments are passed in similar to how they are passed into the bench-tps client. For the generic client, use `--generic-client-args`.

For example, let's assume we have a client sending spam. And it takes the following arguments:
```
/home/solana/spammer-executable --target-node <ip:port> --thread-sleep-ms <ms-between-spam-batches> --spam-mode <client-specific-mode>
```
When we go to deploy the generic client, we deploy it in a similar manner to how we deploy the bench-tps client:
```
cargo run --bin cluster -- -n <namespace>
...
generic-client --docker-image <client-docker-image> --executable-path <path-to-executable-in-docker-image> --delay-start <seconds-after-cluster-is-deployed-before-deploying-client> --generic-client-args 'target-node=<ip:port> thread-sleep-ms=<ms-between-spam-batches> spam-mode=<client-specific-mode>'
```

4) Any flag or value the client needs that is cluster specific should be read in from an environment variable. For example, say the client requires the following arguments:
```
/home/solana/spammer-executable --target-node <ip:port> --shred-version <version>
```
Shred-version is cluster specific; it is not known when you deploy a cluster. Modify the shred-version argument in the client code to read in the environment variable `SHRED_VERSION` from the host.
Example:
```
let default_shred_version = env::var("SHRED_VERSION").unwrap_or_else(|_| "0".to_string());
...
.arg(
Arg::with_name("shred_version")
.long("shred-version")
.takes_value(true)
.default_value(&default_shred_version)
.help("Shred version of cluster to spam"),
)
...
```
When you deploy a cluster with your client, leave the `--shred-version` command out since it will be read via environment variable:
```
cargo run --bin cluster -- -n <namespace>
...
generic-client --docker-image <client-docker-image> --executable-path <path-to-executable-in-docker-image> --delay-start <seconds-after-cluster-is-deployed-before-deploying-client> --generic-client-args 'target-node=<ip:port>'
```

The following environment variables are available to each non-bootstrap pod:
```
NAMESPACE # cluster namespace
BOOTSTRAP_RPC_ADDRESS # rpc address of bootstrap node
BOOTSTRAP_GOSSIP_ADDRESS # gossip address of bootstrap node
BOOTSTRAP_FAUCET_ADDRESS # faucet address of bootstrap node
SHRED_VERSION # cluster shred version
```
^ More environment variables to come!

5) Node naming conventions.
Say you want to launch your client and send transactions to a specific validator. Kubernetes makes it easy to identify deployed nodes. Node naming conventions:
```
<node-name>-service.<namespace>.svc.cluster.local:<port>
```
e.g. bootstrap validator RPC port can be reached with:
```
bootstrap-validator-service.<namespace>.svc.cluster.local:8899
```
and a standard validator can be reached with:
```
validator-service-<8-char-commit-or-version>-<validator-index>.<namespace>.svc.cluster.local:<port>
```
examples:
```
# w/ commit
validator-service-bd1a5dfb-7.greg.svc.cluster.local:8001
# or with version
validator-service-v1.18.16-4.greg.svc.cluster.local:8001
```
Say you want to deploy your client with `--target-node <validator-4>` which is running v1.18.16:
```
cargo run --bin cluster -- -n <namespace>
...
generic-client --docker-image <registry>/<image-name>:<tag> --executable-path <path-to-executable-in-docker-image> --delay-start <seconds-after-cluster-is-deployed-before-deploying-client> --generic-client-args 'target-node=validator-service-v1.18.16-4.greg.svc.cluster.local:8001'
```

## Kubernetes Cheatsheet
Create namespace:
```
Expand Down Expand Up @@ -185,4 +268,38 @@ kubectl exec -it -n <namespace> <pod-name> -- /bin/bash
Get information about pod:
```
kubectl describe pod -n <namespace> <pod-name>
```
```

## Containerize your Client
### Dockerfile Template
```
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y iputils-ping curl vim && \
rm -rf /var/lib/apt/lists/* && \
useradd -ms /bin/bash solana && \
adduser solana sudo
USER solana
COPY --chown=solana:solana ./target/release/<client-executable> /home/solana/
COPY --chown=solana:solana ./client-accounts/ /home/solana/client-accounts/
RUN chmod +x /home/solana/<client-executable>
WORKDIR /home/solana
```

### Build client image
```
cd <client-directory>
docker build -t <registry>/<image-name>:<tag> -f <path-to-Dockerfile>/Dockerfile <context-path>
# e.g.
cd client-spam/
docker build -t test-registry/client-spam:latest -f docker/Dockerfile .
```

### Push client image to registry
```
docker push <registry>/<image-name>:<tag>
# e.g.
docker push test-registry/client-spam:latest
```
33 changes: 11 additions & 22 deletions src/client_config.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
use {
log::*,
solana_sdk::pubkey::Pubkey,
std::{
error::Error,
path::PathBuf,
},
std::{error::Error, path::PathBuf},
strum_macros::Display,
};

Expand Down Expand Up @@ -47,41 +43,34 @@ impl ClientTrait for BenchTpsConfig {
}

fn build_command(&self) -> Result<Vec<String>, Box<dyn Error>> {
let mut command = vec!["/home/solana/k8s-cluster-scripts/client-startup-script.sh".to_string()];
let mut command =
vec!["/home/solana/k8s-cluster-scripts/client-startup-script.sh".to_string()];
command.extend(self.generate_client_command_flags());
Ok(command)
}
}

#[derive(Clone, PartialEq, Debug)]
#[derive(Default, Clone, PartialEq, Debug)]
pub struct GenericClientConfig {
pub num_clients: usize,
pub client_duration_seconds: u64,
pub args: Vec<String>,
pub image: String,
pub executable_path: PathBuf,
}

impl Default for GenericClientConfig {
fn default() -> Self {
Self {
num_clients: usize::default(),
client_duration_seconds: u64::default(),
args: Vec::default(),
image: String::default(),
executable_path: PathBuf::default(),
}
}
pub delay_start: u64,
}

impl ClientTrait for GenericClientConfig {
fn generate_client_command_flags(&self) -> Vec<String> {
self.args.clone()
}

/// Build command to run on pod deployment
/// Build command to run on pod deployment
fn build_command(&self) -> Result<Vec<String>, Box<dyn Error>> {
let exec_path_string = self.executable_path.clone().into_os_string()
let exec_path_string = self
.executable_path
.clone()
.into_os_string()
.into_string()
.map_err(|err| {
std::io::Error::new(
Expand Down Expand Up @@ -122,4 +111,4 @@ impl ClientConfig {
pub trait ClientTrait {
fn generate_client_command_flags(&self) -> Vec<String>; // Add this method
fn build_command(&self) -> Result<Vec<String>, Box<dyn Error>>;
}
}
2 changes: 1 addition & 1 deletion src/cluster_images.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use {
crate::{validator::Node, NodeType},
crate::{node::Node, NodeType},
std::{error::Error, result::Result},
};

Expand Down
47 changes: 18 additions & 29 deletions src/docker.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use {
crate::{
new_spinner_progress_bar, startup_scripts::StartupScripts, validator::Node,
NodeType, BUILD, ROCKET, SOLANA_RELEASE, ClientType,
new_spinner_progress_bar, node::Node, startup_scripts::StartupScripts, ClientType,
NodeType, BUILD, ROCKET, SOLANA_RELEASE,
},
log::*,
std::{
Expand All @@ -24,12 +24,7 @@ pub struct DockerImage {

impl DockerImage {
// Constructor to create a new instance of DockerImage
pub fn new(
registry: String,
node_type: NodeType,
image_name: String,
tag: String,
) -> Self {
pub fn new(registry: String, node_type: NodeType, image_name: String, tag: String) -> Self {
DockerImage {
registry,
node_type,
Expand All @@ -41,15 +36,15 @@ impl DockerImage {

/// parse from string <registry>/<name>:<tag>
pub fn new_from_string(image_string: String) -> Result<Self, Box<dyn Error>> {
let parts: Vec<&str> = image_string.split('/').collect();
if parts.len() != 2 {
let split_string: Vec<&str> = image_string.split('/').collect();
if split_string.len() != 2 {
return Err("Invalid format. Expected <registry>/<name>:<tag>".into());
}

let registry = parts[0].to_string();
let registry = split_string[0].to_string();

// Split the second part into name and tag
let name_tag: Vec<&str> = parts[1].split(':').collect();
let name_tag: Vec<&str> = split_string[1].split(':').collect();
if name_tag.len() != 2 {
return Err("Invalid format. Expected <registry>/<name>:<tag>".into());
}
Expand Down Expand Up @@ -81,11 +76,12 @@ impl Display for DockerImage {
write!(f, "{image_path}")
} else {
write!(
f,
"{}/{}-{}-{}:{}",
self.registry, self.node_type, index, self.image_name, self.tag)
f,
"{}/{}-{}-{}:{}",
self.registry, self.node_type, index, self.image_name, self.tag
)
}
},
}
NodeType::Bootstrap | NodeType::Standard | NodeType::RPC => write!(
f,
"{}/{}-{}:{}",
Expand Down Expand Up @@ -119,12 +115,7 @@ impl DockerConfig {
}
};

self.create_base_image(
solana_root_path,
docker_image,
&docker_path,
&node_type,
)?;
self.create_base_image(solana_root_path, docker_image, &docker_path, &node_type)?;

Ok(())
}
Expand All @@ -150,7 +141,7 @@ impl DockerConfig {
"docker build -t {docker_image} -f {} {context_path}",
dockerfile.display()
);
info!("docker command: {command}");
debug!("docker command: {command}");

let output = Command::new("sh")
.arg("-c")
Expand Down Expand Up @@ -267,9 +258,7 @@ COPY --chown=solana:solana ./config-k8s/bench-tps-{index}.yml /home/solana/clien
Err(format!("{bench_tps_path:?} does not exist!").into())
}
}
NodeType::Bootstrap | NodeType::Standard | NodeType::RPC => {
Ok("".to_string())
}
NodeType::Bootstrap | NodeType::Standard | NodeType::RPC => Ok("".to_string()),
}
}

Expand All @@ -285,14 +274,14 @@ COPY --chown=solana:solana ./config-k8s/bench-tps-{index}.yml /home/solana/clien
Ok(child)
}

pub fn push_images<'a, I>(&self, validators: I) -> Result<(), Box<dyn Error>>
pub fn push_images<'a, I>(&self, nodes: I) -> Result<(), Box<dyn Error>>
where
I: IntoIterator<Item = &'a Node>,
{
info!("Pushing images...");
let children: Result<Vec<Child>, _> = validators
let children: Result<Vec<Child>, _> = nodes
.into_iter()
.map(|validator| Self::push_image(validator.image()))
.map(|node| Self::push_image(node.image()))
.collect();

let progress_bar = new_spinner_progress_bar();
Expand Down
16 changes: 8 additions & 8 deletions src/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,13 @@ impl Genesis {

pub fn generate_accounts(
&mut self,
validator_type: NodeType,
node_type: NodeType,
number_of_accounts: usize,
deployment_tag: Option<&str>,
) -> Result<(), Box<dyn Error>> {
info!("generating {number_of_accounts} {validator_type} accounts...");
info!("generating {number_of_accounts} {node_type} accounts...");

let account_types = match validator_type {
let account_types = match node_type {
NodeType::Bootstrap | NodeType::Standard => {
vec!["identity", "stake-account", "vote-account"]
}
Expand Down Expand Up @@ -189,26 +189,26 @@ impl Genesis {
.key_generator
.gen_n_keypairs(total_accounts_to_generate as u64);

self.write_accounts_to_file(&validator_type, &account_types, &keypairs)?;
self.write_accounts_to_file(&node_type, &account_types, &keypairs)?;

Ok(())
}

fn write_accounts_to_file(
&self,
validator_type: &NodeType,
node_type: &NodeType,
account_types: &[String],
keypairs: &[Keypair],
) -> Result<(), Box<dyn Error>> {
for (i, keypair) in keypairs.iter().enumerate() {
let account_index = i / account_types.len();
let account = &account_types[i % account_types.len()];
let filename = match validator_type {
let filename = match node_type {
NodeType::Bootstrap => {
format!("{validator_type}/{account}.json")
format!("{node_type}/{account}.json")
}
NodeType::Standard | NodeType::RPC => {
format!("{validator_type}-{account}-{account_index}.json")
format!("{node_type}-{account}-{account_index}.json")
}
NodeType::Client(_, _) => panic!("Client type not supported"),
};
Expand Down
Loading

0 comments on commit 6ba9f16

Please sign in to comment.