Skip to content

Commit

Permalink
add bootstrap kubernetes labels and validator management structs (#9)
Browse files Browse the repository at this point in the history
* add bootstrap validator selector

* address chido: hard code startup scripts

* add library and validator structs

* upgrade rustls

* address Jon comments from PR #7

* Jon nit from PR #8. rm build_path

* chido. change up secret handling to be more flexible
  • Loading branch information
gregcusack authored Apr 23, 2024
1 parent 1a22c6c commit 87712b9
Show file tree
Hide file tree
Showing 13 changed files with 554 additions and 262 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ k8s-openapi ={ version = "0.20.0", features = ["v1_28"] }
kube = "0.87.2"
log = "0.4.21"
rand = "0.8.5"
rayon = "1.9.0"
reqwest = { version = "0.11.23", features = ["blocking", "brotli", "deflate", "gzip", "rustls-tls", "json"] }
rustls = { version = "0.21.10", default-features = false, features = ["quic"] }
rustls = { version = "0.21.11", default-features = false, features = ["quic"] }
solana-core = "1.18.8"
solana-logger = "1.18.8"
solana-sdk = "1.18.8"
Expand Down
2 changes: 1 addition & 1 deletion PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
- [ ] RPC nodes
- [ ] Client
- [ ] Create & Deploy Selector
- [ ] Bootstrap
- [x] Bootstrap
- [ ] Validator (regular)
- [ ] RPC nodes
- [ ] Client
Expand Down
84 changes: 44 additions & 40 deletions src/docker.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use {
crate::{new_spinner_progress_bar, release::DeployMethod, ValidatorType, BUILD, ROCKET},
crate::{
new_spinner_progress_bar, release::DeployMethod, startup_scripts::StartupScripts,
validator::Validator, DockerPushThreadError, ValidatorType, BUILD, ROCKET,
},
log::*,
rayon::prelude::*,
std::{
error::Error,
fmt::{self, Display, Formatter},
Expand All @@ -10,6 +14,7 @@ use {
},
};

#[derive(Clone)]
pub struct DockerImage {
registry: String,
validator_type: ValidatorType,
Expand Down Expand Up @@ -65,7 +70,6 @@ impl DockerConfig {
pub fn build_image(
&self,
solana_root_path: &Path,
lab_path: &Path,
docker_image: &DockerImage,
) -> Result<(), Box<dyn Error>> {
let validator_type = docker_image.validator_type();
Expand All @@ -82,7 +86,6 @@ impl DockerConfig {
let docker_path = solana_root_path.join(format!("docker-build/{validator_type}"));
self.create_base_image(
solana_root_path,
lab_path,
docker_image,
&docker_path,
&validator_type,
Expand All @@ -94,12 +97,11 @@ impl DockerConfig {
fn create_base_image(
&self,
solana_root_path: &Path,
lab_path: &Path,
docker_image: &DockerImage,
docker_path: &PathBuf,
validator_type: &ValidatorType,
) -> Result<(), Box<dyn Error>> {
self.create_dockerfile(validator_type, docker_path, lab_path, None)?;
self.create_dockerfile(validator_type, docker_path, None)?;

// 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.
Expand All @@ -115,18 +117,15 @@ impl DockerConfig {
dockerfile.display()
);

let output = match Command::new("sh")
let output = 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<dyn Error>),
}?;
.map_err(Box::new)?;

if !output.status.success() {
return Err(output.status.to_string().into());
Expand All @@ -137,44 +136,38 @@ impl DockerConfig {
Ok(())
}

fn copy_file_to_docker(
source_dir: &Path,
docker_dir: &Path,
fn write_startup_script_to_docker_directory(
file_name: &str,
docker_dir: &Path,
) -> std::io::Result<()> {
let source_path = source_dir.join("src/scripts").join(file_name);
let destination_path = docker_dir.join(file_name);
fs::copy(source_path, destination_path)?;
Ok(())
let script_path = docker_dir.join(file_name);
StartupScripts::write_script_to_file(StartupScripts::bootstrap(), &script_path)
}

fn create_dockerfile(
&self,
validator_type: &ValidatorType,
docker_path: &PathBuf,
lab_path: &Path,
content: Option<&str>,
) -> Result<(), Box<dyn Error>> {
if docker_path.exists() {
fs::remove_dir_all(docker_path)?;
}
fs::create_dir_all(docker_path)?;

if let DeployMethod::Local(_) = self.deploy_method {
if validator_type == &ValidatorType::Bootstrap {
let files_to_copy = ["bootstrap-startup-script.sh", "common.sh"];
for file_name in files_to_copy.iter() {
Self::copy_file_to_docker(lab_path, docker_path, file_name)?;
}
if validator_type == &ValidatorType::Bootstrap {
let files_to_copy = ["bootstrap-startup-script.sh", "common.sh"];
for file_name in files_to_copy.iter() {
Self::write_startup_script_to_docker_directory(file_name, docker_path)?;
}
}

let (solana_build_directory, startup_script_directory) =
if let DeployMethod::ReleaseChannel(_) = self.deploy_method {
("solana-release", "./src/scripts".to_string())
} else {
("farf", format!("./docker-build/{validator_type}"))
};
let startup_script_directory = format!("./docker-build/{validator_type}");
let solana_build_directory = if let DeployMethod::ReleaseChannel(_) = self.deploy_method {
"solana-release"
} else {
"farf"
};

let dockerfile = format!(
r#"
Expand All @@ -186,6 +179,7 @@ RUN apt-get update && apt-get install -y iputils-ping curl vim && \
USER solana
COPY --chown=solana:solana {startup_script_directory} /home/solana/k8s-cluster-scripts
RUN chmod +x /home/solana/k8s-cluster-scripts/*
COPY --chown=solana:solana ./config-k8s/bootstrap-validator /home/solana/ledger
COPY --chown=solana:solana ./{solana_build_directory}/bin/ /home/solana/.cargo/bin/
COPY --chown=solana:solana ./{solana_build_directory}/version.yml /home/solana/
Expand All @@ -204,30 +198,40 @@ WORKDIR /home/solana
Ok(())
}

pub fn push_image(docker_image: &DockerImage) -> Result<(), Box<dyn Error>> {
pub fn push_image(docker_image: &DockerImage) -> Result<(), Box<dyn Error + Send>> {
let progress_bar = new_spinner_progress_bar();
progress_bar.set_message(format!(
"{ROCKET}Pushing {} image to registry...",
docker_image.validator_type()
"{ROCKET}Pushing {docker_image} image to registry...",
));
let command = format!("docker push '{}'", docker_image);
let output = match Command::new("sh")
let command = format!("docker push '{docker_image}'");
let output = 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<dyn Error>),
}?;
.expect("Failed to push image");

if !output.status.success() {
return Err(output.status.to_string().into());
return Err(Box::new(DockerPushThreadError::from(
output.status.to_string(),
)));
}
progress_bar.finish_and_clear();
Ok(())
}

pub fn push_images<'a, I>(&self, validators: I) -> Result<(), Box<dyn Error + Send>>
where
I: IntoIterator<Item = &'a Validator>,
{
info!("Pushing images...");
validators
.into_iter()
.collect::<Vec<_>>() // Collect into Vec and thread push
.par_iter()
.try_for_each(|validator| Self::push_image(validator.image()))
}
}
41 changes: 31 additions & 10 deletions src/k8s_helpers.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
use {
k8s_openapi::{api::core::v1::Secret, ByteString},
kube::api::ObjectMeta,
std::{collections::BTreeMap, error::Error, path::PathBuf},
std::{
collections::{BTreeMap, HashMap},
error::Error,
path::PathBuf,
},
};

fn create_secret(name: &str, data: BTreeMap<String, ByteString>) -> Secret {
pub enum SecretType {
Value { v: String },
File { path: PathBuf },
}

fn build_secret(name: &str, data: BTreeMap<String, ByteString>) -> Secret {
Secret {
metadata: ObjectMeta {
name: Some(name.to_string()),
Expand All @@ -15,16 +24,28 @@ fn create_secret(name: &str, data: BTreeMap<String, ByteString>) -> Secret {
}
}

pub fn create_secret_from_files(
pub fn create_secret(
secret_name: &str,
key_files: &[(PathBuf, &str)], //[pathbuf, key type]
secrets: HashMap<String, SecretType>,
) -> Result<Secret, Box<dyn Error>> {
let mut data = BTreeMap::new();
for (file_path, key_type) in key_files {
let file_content = std::fs::read(file_path)
.map_err(|err| format!("Failed to read file '{:?}': {}", file_path, err))?;
data.insert(format!("{}.json", key_type), ByteString(file_content));
let mut data: BTreeMap<String, ByteString> = BTreeMap::new();
for (label, value) in secrets {
match value {
SecretType::Value { v } => {
data.insert(label, ByteString(v.into_bytes()));
}
SecretType::File { path } => {
let file_content = std::fs::read(&path)
.map_err(|err| format!("Failed to read file '{:?}': {}", path, err))?;
data.insert(label, ByteString(file_content));
}
}
}
Ok(build_secret(secret_name, data))
}

Ok(create_secret(secret_name, data))
pub fn create_selector(key: &str, value: &str) -> BTreeMap<String, String> {
let mut btree = BTreeMap::new();
btree.insert(key.to_string(), value.to_string());
btree
}
45 changes: 36 additions & 9 deletions src/kubernetes.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use {
crate::k8s_helpers,
crate::k8s_helpers::{self, SecretType},
k8s_openapi::api::core::v1::{Namespace, Secret},
kube::{
api::{Api, ListParams, PostParams},
Client,
},
std::{error::Error, path::Path},
std::{
collections::{BTreeMap, HashMap},
error::Error,
path::Path,
},
};

pub struct Kubernetes {
Expand Down Expand Up @@ -43,19 +47,42 @@ impl Kubernetes {
let vote_key_path = config_dir.join("bootstrap-validator/vote-account.json");
let stake_key_path = config_dir.join("bootstrap-validator/stake-account.json");

let key_files = vec![
(faucet_key_path, "faucet"),
(identity_key_path, "identity"),
(vote_key_path, "vote"),
(stake_key_path, "stake"),
];
let mut secrets = HashMap::new();
secrets.insert(
"faucet".to_string(),
SecretType::File {
path: faucet_key_path,
},
);
secrets.insert(
"identity".to_string(),
SecretType::File {
path: identity_key_path,
},
);
secrets.insert(
"vote".to_string(),
SecretType::File {
path: vote_key_path,
},
);
secrets.insert(
"stake".to_string(),
SecretType::File {
path: stake_key_path,
},
);

k8s_helpers::create_secret_from_files(secret_name, &key_files)
k8s_helpers::create_secret(secret_name, secrets)
}

pub async fn deploy_secret(&self, secret: &Secret) -> Result<Secret, kube::Error> {
let secrets_api: Api<Secret> =
Api::namespaced(self.k8s_client.clone(), self.namespace.as_str());
secrets_api.create(&PostParams::default(), secret).await
}

pub fn create_selector(&self, key: &str, value: &str) -> BTreeMap<String, String> {
k8s_helpers::create_selector(key, value)
}
}
Loading

0 comments on commit 87712b9

Please sign in to comment.