diff --git a/PROGRESS.md b/PROGRESS.md index 5c5701f..84521ce 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -15,9 +15,9 @@ - [x] Create Genesis - [x] Generate faucet and bootstrap accounts - [x] Build genesis -- [ ] Docker Build +- [x] Docker Build - [x] Build Bootstrap Image - - [ ] Push Image to registry + - [x] Push Image to registry - [ ] Create & Deploy Secrets - [ ] Bootstrap - [ ] Validator (regular) diff --git a/fetch-spl.sh b/fetch-spl.sh deleted file mode 100755 index bb8e84e..0000000 --- a/fetch-spl.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -# -# Fetches the latest SPL programs and produces the solana-genesis command-line -# arguments needed to install them -# - -set -e - -upgradeableLoader=BPFLoaderUpgradeab1e11111111111111111111111 - -fetch_program() { - declare name=$1 - declare version=$2 - declare address=$3 - declare loader=$4 - - declare so=spl_$name-$version.so - - if [[ $loader == "$upgradeableLoader" ]]; then - genesis_args+=(--upgradeable-program "$address" "$loader" "$so" none) - else - genesis_args+=(--bpf-program "$address" "$loader" "$so") - fi - - if [[ -r $so ]]; then - return - fi - - if [[ -r ~/.cache/solana-spl/$so ]]; then - cp ~/.cache/solana-spl/"$so" "$so" - else - echo "Downloading $name $version" - so_name="spl_${name//-/_}.so" - ( - set -x - curl -L --retry 5 --retry-delay 2 --retry-connrefused \ - -o "$so" \ - "https://github.com/solana-labs/solana-program-library/releases/download/$name-v$version/$so_name" - ) - - mkdir -p ~/.cache/solana-spl - cp "$so" ~/.cache/solana-spl/"$so" - fi - -} - -fetch_program token 3.5.0 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA BPFLoader2111111111111111111111111111111111 -fetch_program token-2022 0.9.0 TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb BPFLoaderUpgradeab1e11111111111111111111111 -fetch_program memo 1.0.0 Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo BPFLoader1111111111111111111111111111111111 -fetch_program memo 3.0.0 MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr BPFLoader2111111111111111111111111111111111 -fetch_program associated-token-account 1.1.2 ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL BPFLoader2111111111111111111111111111111111 -fetch_program feature-proposal 1.0.0 Feat1YXHhH6t1juaWF74WLcfv4XoNocjXA6sPWHNgAse BPFLoader2111111111111111111111111111111111 - -echo "${genesis_args[@]}" > spl-genesis-args.sh - -echo -echo "Available SPL programs:" -ls -l spl_*.so - -echo -echo "solana-genesis command-line arguments (spl-genesis-args.sh):" -cat spl-genesis-args.sh diff --git a/src/docker.rs b/src/docker.rs index 1abccde..30fc5dd 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -1,36 +1,64 @@ use { - crate::{new_spinner_progress_bar, release::DeployMethod, ValidatorType, BUILD}, + crate::{new_spinner_progress_bar, release::DeployMethod, ValidatorType, BUILD, ROCKET}, log::*, std::{ env, error::Error, + fmt::{self, Display, Formatter}, fs, path::{Path, PathBuf}, - process::{Command, Output, Stdio}, + process::{Command, Stdio}, }, }; -pub struct DockerConfig { - pub base_image: String, - pub image_name: String, - pub tag: String, - pub registry: String, - deploy_method: DeployMethod, +pub struct DockerImage { + registry: String, + validator_type: ValidatorType, + image_name: String, + tag: String, } -impl DockerConfig { +impl DockerImage { + // Constructor to create a new instance of DockerImage pub fn new( - base_image: String, + registry: String, + validator_type: ValidatorType, image_name: String, tag: String, - registry: String, - deploy_method: DeployMethod, ) -> Self { - DockerConfig { - base_image, + DockerImage { + registry, + validator_type, image_name, tag, - registry, + } + } + + pub fn validator_type(&self) -> ValidatorType { + self.validator_type + } +} + +// Put DockerImage in format for building, pushing, and pulling +impl Display for DockerImage { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!( + f, + "{}/{}-{}:{}", + self.registry, self.validator_type, self.image_name, self.tag + ) + } +} + +pub struct DockerConfig { + pub base_image: String, + deploy_method: DeployMethod, +} + +impl DockerConfig { + pub fn new(base_image: String, deploy_method: DeployMethod) -> Self { + DockerConfig { + base_image, deploy_method, } } @@ -38,8 +66,9 @@ impl DockerConfig { pub fn build_image( &self, solana_root_path: &Path, - validator_type: &ValidatorType, + docker_image: &DockerImage, ) -> Result<(), Box> { + let validator_type = docker_image.validator_type(); match validator_type { ValidatorType::Bootstrap => (), ValidatorType::Standard | ValidatorType::RPC | ValidatorType::Client => { @@ -49,34 +78,27 @@ impl DockerConfig { .into()); } } - let image_name = format!("{validator_type}-{}", self.image_name); + let docker_path = solana_root_path.join(format!("docker-build/{validator_type}")); - match self.create_base_image(solana_root_path, image_name, &docker_path, validator_type) { - Ok(res) => { - if res.status.success() { - info!("Successfully created base Image"); - Ok(()) - } else { - error!("Failed to build base image"); - Err(String::from_utf8_lossy(&res.stderr).into()) - } - } - Err(err) => Err(err), - } + self.create_base_image( + solana_root_path, + docker_image, + &docker_path, + &validator_type, + )?; + + Ok(()) } fn create_base_image( &self, solana_root_path: &Path, - image_name: String, + docker_image: &DockerImage, docker_path: &PathBuf, validator_type: &ValidatorType, - ) -> Result> { + ) -> Result<(), Box> { self.create_dockerfile(validator_type, docker_path, None)?; - trace!("Tmp: {}", docker_path.as_path().display()); - trace!("Exists: {}", docker_path.as_path().exists()); - // We use std::process::Command here because Docker-rs is very slow building dockerfiles // when they are in large repos. Docker-rs doesn't seem to support the `--file` flag natively. // so we result to using std::process::Command @@ -86,10 +108,7 @@ impl DockerConfig { let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(format!("{BUILD}Building {validator_type} docker image...",)); - let command = format!( - "docker build -t {}/{image_name}:{} -f {dockerfile:?} {context_path}", - self.registry, self.tag, - ); + let command = format!("docker build -t {docker_image} -f {dockerfile:?} {context_path}"); let output = match Command::new("sh") .arg("-c") @@ -102,11 +121,15 @@ impl DockerConfig { { Ok(res) => Ok(res), Err(err) => Err(Box::new(err) as Box), - }; + }?; + + if !output.status.success() { + return Err(output.status.to_string().into()); + } progress_bar.finish_and_clear(); info!("{validator_type} image build complete"); - output + Ok(()) } fn copy_file_to_docker( @@ -152,30 +175,19 @@ impl DockerConfig { let dockerfile = format!( r#" FROM {} -RUN apt-get update -RUN apt-get install -y iputils-ping curl vim bzip2 +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 -RUN useradd -ms /bin/bash solana -RUN adduser solana sudo USER solana - -RUN mkdir -p /home/solana/k8s-cluster-scripts -# TODO: this needs to be changed for non bootstrap, this should be ./src/scripts/-startup-scripts.sh -COPY {startup_script_directory}/bootstrap-startup-script.sh /home/solana/k8s-cluster-scripts - -RUN mkdir -p /home/solana/ledger +COPY --chown=solana:solana {startup_script_directory} /home/solana/k8s-cluster-scripts COPY --chown=solana:solana ./config-k8s/bootstrap-validator /home/solana/ledger - -RUN mkdir -p /home/solana/.cargo/bin - -COPY ./{solana_build_directory}/bin/ /home/solana/.cargo/bin/ -COPY ./{solana_build_directory}/version.yml /home/solana/ - -RUN mkdir -p /home/solana/config +COPY --chown=solana:solana ./{solana_build_directory}/bin/ /home/solana/.cargo/bin/ +COPY --chown=solana:solana ./{solana_build_directory}/version.yml /home/solana/ ENV PATH="/home/solana/.cargo/bin:${{PATH}}" WORKDIR /home/solana - "#, self.base_image ); @@ -187,4 +199,31 @@ WORKDIR /home/solana )?; Ok(()) } + + pub fn push_image(docker_image: &DockerImage) -> Result<(), Box> { + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message(format!( + "{ROCKET}Pushing {} image to registry...", + docker_image.validator_type() + )); + let command = format!("docker push '{}'", docker_image); + let output = match Command::new("sh") + .arg("-c") + .arg(&command) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to execute command") + .wait_with_output() + { + Ok(res) => Ok(res), + Err(err) => Err(Box::new(err) as Box), + }?; + + if !output.status.success() { + return Err(output.status.to_string().into()); + } + progress_bar.finish_and_clear(); + Ok(()) + } } diff --git a/src/genesis.rs b/src/genesis.rs index eaa902c..e4b805c 100644 --- a/src/genesis.rs +++ b/src/genesis.rs @@ -1,5 +1,5 @@ use { - crate::{new_spinner_progress_bar, ValidatorType, SUN}, + crate::{fetch_spl, new_spinner_progress_bar, ValidatorType, SUN}, log::*, rand::Rng, solana_core::gen_keys::GenKeys, @@ -24,48 +24,30 @@ pub const DEFAULT_INTERNAL_NODE_SOL: f64 = 100.0; pub const DEFAULT_BOOTSTRAP_NODE_STAKE_SOL: f64 = 10.0; pub const DEFAULT_BOOTSTRAP_NODE_SOL: f64 = 100.0; -fn fetch_spl(fetch_spl_file: &PathBuf) -> Result<(), Box> { - let output = Command::new("bash") - .arg(fetch_spl_file) - .output() // Capture the output of the script - .expect("Failed to run fetch-spl.sh script"); - - // Check if the script execution was successful - if output.status.success() { - Ok(()) - } else { - Err(format!( - "Failed to fun fetch-spl.sh script: {}", - String::from_utf8_lossy(&output.stderr) - ) - .into()) - } -} - -fn parse_spl_genesis_file(spl_file: &PathBuf) -> Result, Box> { +fn parse_spl_genesis_file( + spl_file: &PathBuf, + solana_root_path: &Path, +) -> Result, Box> { // Read entire file into a String let mut file = File::open(spl_file)?; let mut content = String::new(); file.read_to_string(&mut content)?; - // Split by whitespace - let mut args = Vec::new(); - let mut tokens_iter = content.split_whitespace(); - - while let Some(token) = tokens_iter.next() { - args.push(token.to_string()); - // Find flag delimiters - if token.starts_with("--") { - for next_token in tokens_iter.by_ref() { - if next_token.starts_with("--") { - args.push(next_token.to_string()); - } else { - args.push(next_token.to_string()); - break; - } + let args = content + .split_whitespace() + .map(String::from) + .map(|arg| { + if arg.ends_with(".so") { + solana_root_path + .join(&arg) + .into_os_string() + .into_string() + .unwrap() + } else { + arg } - } - } + }) + .collect::>(); Ok(args) } @@ -198,7 +180,7 @@ impl Genesis { Ok(()) } - fn setup_genesis_flags(&self) -> Vec { + fn setup_genesis_flags(&self) -> Result, Box> { let mut args = vec![ "--bootstrap-validator-lamports".to_string(), sol_to_lamports( @@ -229,15 +211,27 @@ impl Genesis { "--faucet-pubkey".to_string(), self.config_dir .join("faucet.json") - .to_string_lossy() - .to_string(), + .into_os_string() + .into_string() + .map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Invalid Unicode data in path: {:?}", err), + ) + })?, "--cluster-type".to_string(), self.flags.cluster_type.to_string(), "--ledger".to_string(), self.config_dir .join("bootstrap-validator") - .to_string_lossy() - .to_string(), + .into_os_string() + .into_string() + .map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Invalid Unicode data in path: {:?}", err), + ) + })?, ]; if self.flags.enable_warmup_epochs { @@ -245,16 +239,15 @@ impl Genesis { } args.push("--bootstrap-validator".to_string()); - ["identity", "vote-account", "stake-account"] - .iter() - .for_each(|account_type| { - args.push( - self.config_dir - .join(format!("bootstrap-validator/{account_type}.json")) - .to_string_lossy() - .to_string(), - ); - }); + for account_type in ["identity", "vote-account", "stake-account"].iter() { + let path = self + .config_dir + .join(format!("bootstrap-validator/{account_type}.json")) + .into_os_string() + .into_string() + .map_err(|_| "Failed to convert path to string")?; + args.push(path); + } if let Some(slots_per_epoch) = self.flags.slots_per_epoch { args.push("--slots-per-epoch".to_string()); @@ -266,25 +259,26 @@ impl Genesis { args.push(lamports_per_signature.to_string()); } - args + Ok(args) } - pub fn setup_spl_args(&self, solana_root_path: &Path) -> Result, Box> { - let fetch_spl_file = solana_root_path.join("fetch-spl.sh"); - fetch_spl(&fetch_spl_file)?; + pub async fn setup_spl_args( + &self, + solana_root_path: &Path, + ) -> Result, Box> { + fetch_spl(solana_root_path).await?; - // add in spl let spl_file = solana_root_path.join("spl-genesis-args.sh"); - parse_spl_genesis_file(&spl_file) + parse_spl_genesis_file(&spl_file, solana_root_path) } - pub fn generate( + pub async fn generate( &mut self, solana_root_path: &Path, build_path: &Path, ) -> Result<(), Box> { - let mut args = self.setup_genesis_flags(); - let mut spl_args = self.setup_spl_args(solana_root_path)?; + let mut args = self.setup_genesis_flags()?; + let mut spl_args = self.setup_spl_args(solana_root_path).await?; args.append(&mut spl_args); let progress_bar = new_spinner_progress_bar(); @@ -300,8 +294,8 @@ impl Genesis { if !output.status.success() { return Err(format!( - "Failed to create genesis. err: {}", - String::from_utf8_lossy(&output.stderr) + "Failed to create genesis. err: {:?}", + String::from_utf8(output.stderr) ) .into()); } diff --git a/src/lib.rs b/src/lib.rs index c1ad9cb..77e7331 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ use { std::{ env, fs::File, - io::{BufReader, Cursor, Read}, + io::{BufReader, Cursor, Read, Write}, path::{Path, PathBuf}, time::Duration, }, @@ -16,6 +16,8 @@ use { url::Url, }; +const UPGRADEABLE_LOADER: &str = "BPFLoaderUpgradeab1e11111111111111111111111"; + pub fn get_solana_root() -> PathBuf { PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR")).to_path_buf() } @@ -61,6 +63,7 @@ pub mod release; static BUILD: Emoji = Emoji("👷 ", ""); static PACKAGE: Emoji = Emoji("📦 ", ""); +static ROCKET: Emoji = Emoji("🚀 ", ""); static SUN: Emoji = Emoji("🌞 ", ""); static TRUCK: Emoji = Emoji("🚚 ", ""); @@ -87,8 +90,7 @@ pub fn cat_file(path: &PathBuf) -> std::io::Result<()> { pub async fn download_to_temp( url: &str, - file_name: &str, - solana_root_path: PathBuf, + file_path: &Path, // full path to file including filename ) -> Result<(), Box> { let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(format!("{TRUCK}Downloading...")); @@ -110,8 +112,7 @@ pub async fn download_to_temp( .into()); } - let file_name: PathBuf = solana_root_path.join(file_name); - let mut out = File::create(file_name).expect("failed to create file"); + let mut out = File::create(file_path).expect("failed to create file"); let mut content = Cursor::new(response.bytes().await?); std::io::copy(&mut content, &mut out)?; @@ -137,3 +138,80 @@ pub fn extract_release_archive( Ok(()) } + +async fn fetch_program( + name: &str, + version: &str, + solana_root_path: &Path, +) -> Result<(), Box> { + let so_filename = format!("spl_{}-{}.so", name.replace('-', "_"), version); + let so_path = solana_root_path.join(&so_filename); + + if !so_path.exists() { + info!("Downloading {} {}", name, version); + let url = format!( + "https://github.com/solana-labs/solana-program-library/releases/download/{}-v{}/{}", + name, version, so_filename + ); + + download_to_temp(&url, &so_path) + .await + .map_err(|err| format!("Unable to download {url}. Error: {err}"))?; + } + + Ok(()) +} + +pub async fn fetch_spl(solana_root_path: &Path) -> Result<(), Box> { + let mut genesis_args = vec![]; + + let programs = vec![ + ( + "token", + "3.5.0", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "BPFLoader2111111111111111111111111111111111", + ), + ( + "token-2022", + "0.9.0", + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", + UPGRADEABLE_LOADER, + ), + ( + "associated-token-account", + "1.1.2", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "BPFLoader2111111111111111111111111111111111", + ), + ]; + + for (name, version, address, loader) in programs { + fetch_program(name, version, solana_root_path).await?; + + let arg = if loader == UPGRADEABLE_LOADER { + format!( + "--upgradeable-program {} {} spl_{}-{}.so none", + address, + loader, + name.replace('-', "_"), + version + ) + } else { + format!( + "--bpf-program {} {} spl_{}-{}.so", + address, + loader, + name.replace('-', "_"), + version + ) + }; + genesis_args.push(arg); + } + + // Write genesis args to file + let mut file = std::fs::File::create(solana_root_path.join("spl-genesis-args.sh"))?; + writeln!(file, "{}", genesis_args.join(" "))?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 1f9bcbe..c2bc567 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,11 @@ use { std::fs, strum::VariantNames, validator_lab::{ - docker::DockerConfig, - genesis::{Genesis, GenesisFlags}, + docker::{DockerConfig, DockerImage}, + genesis::{ + Genesis, GenesisFlags, DEFAULT_BOOTSTRAP_NODE_SOL, DEFAULT_BOOTSTRAP_NODE_STAKE_SOL, + DEFAULT_FAUCET_LAMPORTS, DEFAULT_MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, + }, kubernetes::Kubernetes, release::{BuildConfig, BuildType, DeployMethod}, SolanaRoot, ValidatorType, @@ -59,32 +62,32 @@ fn parse_matches() -> clap::ArgMatches { Arg::with_name("slots_per_epoch") .long("slots-per-epoch") .takes_value(true) - .help("override the number of slots in an epoch"), + .help("override the number of slots in an epoch. Default for cluster_type: development -> 8192. + Default for cluster_type: devnet, testnet, mainnet-beta -> 432000 (1 epoch every ~= 2 days)"), ) .arg( Arg::with_name("target_lamports_per_signature") .long("target-lamports-per-signature") .takes_value(true) - .help("Genesis config. target lamports per signature"), + .help("Genesis config. target lamports per signature. Default: 10000"), ) .arg( Arg::with_name("faucet_lamports") .long("faucet-lamports") .takes_value(true) + .default_value(&DEFAULT_FAUCET_LAMPORTS.to_string()) .help("Override the default 500000000000000000 lamports minted in genesis"), ) .arg( Arg::with_name("enable_warmup_epochs") .long("enable-warmup-epochs") - .takes_value(true) - .possible_values(["true", "false"]) - .default_value("true") .help("Genesis config. enable warmup epoch. defaults to true"), ) .arg( Arg::with_name("max_genesis_archive_unpacked_size") .long("max-genesis-archive-unpacked-size") .takes_value(true) + .default_value(&DEFAULT_MAX_GENESIS_ARCHIVE_UNPACKED_SIZE.to_string()) .help("Genesis config. max_genesis_archive_unpacked_size"), ) .arg( @@ -101,12 +104,14 @@ fn parse_matches() -> clap::ArgMatches { Arg::with_name("bootstrap_validator_sol") .long("bootstrap-validator-sol") .takes_value(true) + .default_value(&DEFAULT_BOOTSTRAP_NODE_SOL.to_string()) .help("Genesis config. bootstrap validator sol"), ) .arg( Arg::with_name("bootstrap_validator_stake_sol") .long("bootstrap-validator-stake-sol") .takes_value(true) + .default_value(&DEFAULT_BOOTSTRAP_NODE_STAKE_SOL.to_string()) .help("Genesis config. bootstrap validator stake sol"), ) //Docker config @@ -244,7 +249,7 @@ async fn main() { .parse() .expect("Invalid value for faucet_lamports") }), - enable_warmup_epochs: matches.value_of("enable_warmup_epochs").unwrap() == "true", + enable_warmup_epochs: matches.is_present("enable_warmup_epochs"), max_genesis_archive_unpacked_size: matches .value_of("max_genesis_archive_unpacked_size") .map(|value_str| { @@ -298,7 +303,10 @@ async fn main() { } // creates genesis and writes to binary file - match genesis.generate(solana_root.get_root_path(), &build_path) { + match genesis + .generate(solana_root.get_root_path(), &build_path) + .await + { Ok(_) => info!("Created genesis successfully"), Err(err) => { error!("generate genesis error! {err}"); @@ -312,19 +320,34 @@ async fn main() { .value_of("base_image") .unwrap_or_default() .to_string(), + deploy_method, + ); + + let validator_type = ValidatorType::Bootstrap; + let docker_image = DockerImage::new( + matches.value_of("registry_name").unwrap().to_string(), + validator_type, matches.value_of("image_name").unwrap().to_string(), matches .value_of("image_tag") .unwrap_or_default() .to_string(), - matches.value_of("registry_name").unwrap().to_string(), - deploy_method, ); if build_config.docker_build() { - let image_type = ValidatorType::Bootstrap; - match docker.build_image(solana_root.get_root_path(), &image_type) { - Ok(_) => info!("{image_type} image built successfully"), + match docker.build_image(solana_root.get_root_path(), &docker_image) { + Ok(_) => info!("{} image built successfully", docker_image.validator_type()), + Err(err) => { + error!("Exiting........ {err}"); + return; + } + } + + match DockerConfig::push_image(&docker_image) { + Ok(_) => info!( + "{} image pushed successfully", + docker_image.validator_type() + ), Err(err) => { error!("Error. Failed to build imge: {err}"); return; diff --git a/src/release.rs b/src/release.rs index 49fb08f..ba76c65 100644 --- a/src/release.rs +++ b/src/release.rs @@ -180,13 +180,9 @@ impl BuildConfig { ); info!("download_url: {download_url}"); - download_to_temp( - download_url.as_str(), - tar_filename, - self.solana_root_path.clone(), - ) - .await - .map_err(|err| format!("Unable to download {download_url}. Error: {err}"))?; + download_to_temp(download_url.as_str(), &file_path) + .await + .map_err(|err| format!("Unable to download {download_url}. Error: {err}"))?; Ok(()) }