`solana-install update` now updates a named release to the latest patch version

This commit is contained in:
Michael Vines 2021-03-11 17:52:16 -08:00
parent 66b4124a68
commit 79280b304b
6 changed files with 319 additions and 166 deletions

2
Cargo.lock generated
View File

@ -4647,7 +4647,7 @@ dependencies = [
"reqwest 0.10.8", "reqwest 0.10.8",
"semver 0.9.0", "semver 0.9.0",
"serde", "serde",
"serde_derive", "serde_json",
"serde_yaml", "serde_yaml",
"solana-clap-utils", "solana-clap-utils",
"solana-client", "solana-client",

View File

@ -22,8 +22,8 @@ indicatif = "0.15.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
nix = "0.19.0" nix = "0.19.0"
reqwest = { version = "0.10.8", default-features = false, features = ["blocking", "rustls-tls", "json"] } reqwest = { version = "0.10.8", default-features = false, features = ["blocking", "rustls-tls", "json"] }
serde = "1.0.122" serde = { version = "1.0.122", features = ["derive"] }
serde_derive = "1.0.103" serde_json = "1.0.62"
serde_yaml = "0.8.13" serde_yaml = "0.8.13"
solana-clap-utils = { path = "../clap-utils", version = "1.6.0" } solana-clap-utils = { path = "../clap-utils", version = "1.6.0" }
solana-client = { path = "../client", version = "1.6.0" } solana-client = { path = "../client", version = "1.6.0" }

View File

@ -1,30 +1,32 @@
use crate::{ use {
config::{Config, ExplicitRelease}, crate::{
stop_process::stop_process, config::{Config, ExplicitRelease},
update_manifest::{SignedUpdateManifest, UpdateManifest}, stop_process::stop_process,
update_manifest::{SignedUpdateManifest, UpdateManifest},
},
chrono::{Local, TimeZone},
console::{style, Emoji},
indicatif::{ProgressBar, ProgressStyle},
serde::{Deserialize, Serialize},
solana_client::rpc_client::RpcClient,
solana_config_program::{config_instruction, get_config_data, ConfigState},
solana_sdk::{
hash::{Hash, Hasher},
message::Message,
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signable, Signer},
transaction::Transaction,
},
std::{
fs::{self, File},
io::{self, BufReader, Read},
path::{Path, PathBuf},
sync::mpsc,
time::{Duration, Instant, SystemTime},
},
tempfile::TempDir,
url::Url,
}; };
use chrono::{Local, TimeZone};
use console::{style, Emoji};
use indicatif::{ProgressBar, ProgressStyle};
use serde_derive::Deserialize;
use solana_client::rpc_client::RpcClient;
use solana_config_program::{config_instruction, get_config_data, ConfigState};
use solana_sdk::{
hash::{Hash, Hasher},
message::Message,
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signable, Signer},
transaction::Transaction,
};
use std::{
fs::{self, File},
io::{self, BufReader, Read},
path::{Path, PathBuf},
sync::mpsc,
time::{Duration, Instant, SystemTime},
};
use tempfile::TempDir;
use url::Url;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ReleaseVersion { pub struct ReleaseVersion {
@ -37,6 +39,7 @@ static TRUCK: Emoji = Emoji("🚚 ", "");
static LOOKING_GLASS: Emoji = Emoji("🔍 ", ""); static LOOKING_GLASS: Emoji = Emoji("🔍 ", "");
static BULLET: Emoji = Emoji("", "* "); static BULLET: Emoji = Emoji("", "* ");
static SPARKLE: Emoji = Emoji("", ""); static SPARKLE: Emoji = Emoji("", "");
static WRAPPED_PRESENT: Emoji = Emoji("🎁 ", "");
static PACKAGE: Emoji = Emoji("📦 ", ""); static PACKAGE: Emoji = Emoji("📦 ", "");
static INFORMATION: Emoji = Emoji(" ", ""); static INFORMATION: Emoji = Emoji(" ", "");
static RECYCLING: Emoji = Emoji("♻️ ", ""); static RECYCLING: Emoji = Emoji("♻️ ", "");
@ -544,7 +547,7 @@ pub fn init(
config config
}; };
update(config_file)?; init_or_update(config_file, true, false)?;
let path_modified = if !no_modify_path { let path_modified = if !no_modify_path {
add_to_path(&config.active_release_bin_dir().to_str().unwrap()) add_to_path(&config.active_release_bin_dir().to_str().unwrap())
@ -582,11 +585,16 @@ fn release_channel_version_url(release_channel: &str) -> String {
) )
} }
pub fn info( fn print_update_manifest(update_manifest: &UpdateManifest) {
config_file: &str, let when = Local.timestamp(update_manifest.timestamp_secs as i64, 0);
local_info_only: bool, println_name_value(&format!("{}release date:", BULLET), &when.to_string());
eval: bool, println_name_value(
) -> Result<Option<UpdateManifest>, String> { &format!("{}download URL:", BULLET),
&update_manifest.download_url,
);
}
pub fn info(config_file: &str, local_info_only: bool, eval: bool) -> Result<(), String> {
let config = Config::load(config_file)?; let config = Config::load(config_file)?;
if eval { if eval {
@ -604,7 +612,7 @@ pub fn info(
println!("SOLANA_INSTALL_ACTIVE_CHANNEL={}", channel,); println!("SOLANA_INSTALL_ACTIVE_CHANNEL={}", channel,);
Option::<String>::None Option::<String>::None
}); });
return Ok(None); return Ok(());
} }
println_name_value("Configuration:", &config_file); println_name_value("Configuration:", &config_file);
@ -613,6 +621,17 @@ pub fn info(
&config.active_release_dir().to_str().unwrap_or("?"), &config.active_release_dir().to_str().unwrap_or("?"),
); );
fn print_release_version(config: &Config) {
if let Ok(release_version) =
load_release_version(&config.active_release_dir().join("version.yml"))
{
println_name_value(
&format!("{}Release commit:", BULLET),
&release_version.commit[0..7],
);
}
}
if let Some(explicit_release) = &config.explicit_release { if let Some(explicit_release) = &config.explicit_release {
match explicit_release { match explicit_release {
ExplicitRelease::Semver(release_semver) => { ExplicitRelease::Semver(release_semver) => {
@ -630,51 +649,30 @@ pub fn info(
); );
} }
} }
return Ok(None); print_release_version(&config);
} } else {
println_name_value("JSON RPC URL:", &config.json_rpc_url);
println_name_value("JSON RPC URL:", &config.json_rpc_url);
println_name_value(
"Update manifest pubkey:",
&config.update_manifest_pubkey.to_string(),
);
fn print_update_manifest(update_manifest: &UpdateManifest) {
let when = Local.timestamp(update_manifest.timestamp_secs as i64, 0);
println_name_value(&format!("{}release date:", BULLET), &when.to_string());
println_name_value( println_name_value(
&format!("{}download URL:", BULLET), "Update manifest pubkey:",
&update_manifest.download_url, &config.update_manifest_pubkey.to_string(),
); );
}
match config.current_update_manifest { match config.current_update_manifest {
Some(ref update_manifest) => { Some(ref update_manifest) => {
println_name_value("Installed version:", ""); println_name_value("Installed version:", "");
print_update_manifest(&update_manifest); print_release_version(&config);
} print_update_manifest(&update_manifest);
None => { }
println_name_value("Installed version:", "None"); None => {
println_name_value("Installed version:", "None");
}
} }
} }
if local_info_only { if local_info_only {
Ok(None) Ok(())
} else { } else {
let progress_bar = new_spinner_progress_bar(); update(config_file, true).map(|_| ())
progress_bar.set_message(&format!("{}Checking for updates...", LOOKING_GLASS));
let rpc_client = RpcClient::new(config.json_rpc_url.clone());
let manifest = get_update_manifest(&rpc_client, &config.update_manifest_pubkey)?;
progress_bar.finish_and_clear();
if Some(&manifest) == config.current_update_manifest.as_ref() {
println!("\n{}", style("Installation is up to date").italic());
Ok(None)
} else {
println!("\n{}", style("An update is available:").bold());
print_update_manifest(&manifest);
Ok(Some(manifest))
}
} }
} }
@ -845,24 +843,137 @@ pub fn gc(config_file: &str) -> Result<(), String> {
Ok(()) Ok(())
} }
pub fn update(config_file: &str) -> Result<bool, String> { #[derive(Debug, Deserialize, Serialize)]
let mut config = Config::load(config_file)?; pub struct GithubRelease {
let update_manifest = info(config_file, false, false)?; pub tag_name: String,
pub prerelease: bool,
}
let release_dir = if let Some(explicit_release) = &config.explicit_release { #[derive(Debug, Deserialize, Serialize)]
let (download_url, release_dir) = match explicit_release { pub struct GithubReleases(Vec<GithubRelease>);
ExplicitRelease::Semver(release_semver) => {
let download_url = github_release_download_url(release_semver); fn semver_of(string: &str) -> Result<semver::Version, String> {
let release_dir = config.release_dir(&release_semver); if string.starts_with('v') {
let download_url = if release_dir.exists() { semver::Version::parse(string.split_at(1).1)
// If this release_semver has already been successfully downloaded, no update } else {
// needed semver::Version::parse(string)
println!("{} found in cache", release_semver); }
None .map_err(|err| err.to_string())
} else { }
Some(download_url)
}; fn check_for_newer_github_release(
(download_url, release_dir) version_filter: Option<semver::VersionReq>,
) -> reqwest::Result<Option<String>> {
let url =
reqwest::Url::parse("https://api.github.com/repos/solana-labs/solana/releases").unwrap();
let client = reqwest::blocking::Client::builder()
.user_agent("solana-install")
.build()?;
let request = client.get(url).build()?;
let response = client.execute(request)?;
let mut releases = response
.json::<GithubReleases>()?
.0
.into_iter()
.filter_map(
|GithubRelease {
tag_name,
prerelease,
}| {
if let Ok(version) = semver_of(&tag_name) {
if !prerelease
&& version_filter
.as_ref()
.map_or(true, |version_filter| version_filter.matches(&version))
{
return Some(version);
}
}
None
},
)
.collect::<Vec<_>>();
releases.sort();
Ok(releases.pop().map(|r| r.to_string()))
}
pub enum SemverUpdateType {
Fixed,
Patch,
_Minor,
}
pub fn update(config_file: &str, check_only: bool) -> Result<bool, String> {
init_or_update(config_file, false, check_only)
}
pub fn init_or_update(config_file: &str, is_init: bool, check_only: bool) -> Result<bool, String> {
let mut config = Config::load(config_file)?;
let semver_update_type = if is_init {
SemverUpdateType::Fixed
} else {
SemverUpdateType::Patch
};
let (updated_version, download_url_and_sha256, release_dir) = if let Some(explicit_release) =
&config.explicit_release
{
match explicit_release {
ExplicitRelease::Semver(current_release_semver) => {
let progress_bar = new_spinner_progress_bar();
progress_bar.set_message(&format!("{}Checking for updates...", LOOKING_GLASS));
let github_release = check_for_newer_github_release(
semver::VersionReq::parse(&format!(
"{}{}",
match semver_update_type {
SemverUpdateType::Fixed => "=",
SemverUpdateType::Patch => "~",
SemverUpdateType::_Minor => "^",
},
current_release_semver
))
.ok(),
)
.map_err(|err| err.to_string())?;
progress_bar.finish_and_clear();
match github_release {
None => {
return Err(format!("Unknown release: {}", current_release_semver));
}
Some(release_semver) => {
if release_semver == *current_release_semver {
if let Ok(active_release_version) = load_release_version(
&config.active_release_dir().join("version.yml"),
) {
if format!("v{}", current_release_semver)
== active_release_version.channel
{
println!(
"Install is up to date. {} is the latest compatible release",
release_semver
);
return Ok(false);
}
}
}
config.explicit_release =
Some(ExplicitRelease::Semver(release_semver.clone()));
let release_dir = config.release_dir(&release_semver);
let download_url_and_sha256 = if release_dir.exists() {
// Release already present in the cache
None
} else {
Some((github_release_download_url(&release_semver), None))
};
(release_semver, download_url_and_sha256, release_dir)
}
}
} }
ExplicitRelease::Channel(release_channel) => { ExplicitRelease::Channel(release_channel) => {
let version_url = release_channel_version_url(release_channel); let version_url = release_channel_version_url(release_channel);
@ -878,59 +989,73 @@ pub fn update(config_file: &str) -> Result<bool, String> {
let current_release_version_yml = let current_release_version_yml =
release_dir.join("solana-release").join("version.yml"); release_dir.join("solana-release").join("version.yml");
let download_url = Some(release_channel_download_url(release_channel)); let download_url = release_channel_download_url(release_channel);
if !current_release_version_yml.exists() { if !current_release_version_yml.exists() {
println_name_value( (
&format!("{}Release commit:", BULLET), format!(
&update_release_version.commit[0..7], "{} commit {}",
); release_channel,
(download_url, release_dir) &update_release_version.commit[0..7]
),
Some((download_url, None)),
release_dir,
)
} else { } else {
let current_release_version = let current_release_version =
load_release_version(&current_release_version_yml)?; load_release_version(&current_release_version_yml)?;
if update_release_version.commit == current_release_version.commit { if update_release_version.commit == current_release_version.commit {
// Same commit, no update required if let Ok(active_release_version) =
println!( load_release_version(&config.active_release_dir().join("version.yml"))
"Latest {} build ({}) found in cache", {
release_channel, if current_release_version.commit == active_release_version.commit {
&current_release_version.commit[0..7], // Same version, no update required
); println!(
(None, release_dir) "Install is up to date. {} is the latest commit for {}",
} else { &active_release_version.commit[0..7],
println_name_value( release_channel
&format!("{}Release commit:", BULLET), );
&format!( return Ok(false);
"{} => {}:", }
&current_release_version.commit[0..7], }
&update_release_version.commit[0..7],
),
);
(download_url, release_dir) // Release already present in the cache
(
format!(
"{} commit {}",
release_channel,
&update_release_version.commit[0..7]
),
None,
release_dir,
)
} else {
(
format!(
"{} (from {})",
&update_release_version.commit[0..7],
&current_release_version.commit[0..7],
),
Some((download_url, None)),
release_dir,
)
} }
} }
} }
};
if let Some(download_url) = download_url {
let (_temp_dir, temp_archive, _temp_archive_sha256) =
download_to_temp(&download_url, None)
.map_err(|err| format!("Unable to download {}: {}", download_url, err))?;
extract_release_archive(&temp_archive, &release_dir).map_err(|err| {
format!(
"Unable to extract {:?} to {:?}: {}",
temp_archive, release_dir, err
)
})?;
} }
release_dir
} else { } else {
if update_manifest.is_none() { let progress_bar = new_spinner_progress_bar();
progress_bar.set_message(&format!("{}Checking for updates...", LOOKING_GLASS));
let rpc_client = RpcClient::new(config.json_rpc_url.clone());
let update_manifest = get_update_manifest(&rpc_client, &config.update_manifest_pubkey)?;
progress_bar.finish_and_clear();
if Some(&update_manifest) == config.current_update_manifest.as_ref() {
println!("Install is up to date");
return Ok(false); return Ok(false);
} }
let update_manifest = update_manifest.unwrap(); println!("\n{}", style("An update is available:").bold());
print_update_manifest(&update_manifest);
if timestamp_secs() if timestamp_secs()
< u64::from_str_radix(crate::build_env::BUILD_SECONDS_SINCE_UNIX_EPOCH, 10).unwrap() < u64::from_str_radix(crate::build_env::BUILD_SECONDS_SINCE_UNIX_EPOCH, 10).unwrap()
@ -943,27 +1068,39 @@ pub fn update(config_file: &str) -> Result<bool, String> {
return Err("Unable to update to an older version".to_string()); return Err("Unable to update to an older version".to_string());
} }
} }
config.current_update_manifest = Some(update_manifest.clone());
let release_dir = config.release_dir(&update_manifest.download_sha256.to_string()); let release_dir = config.release_dir(&update_manifest.download_sha256.to_string());
let (_temp_dir, temp_archive, _temp_archive_sha256) = download_to_temp(
&update_manifest.download_url, let download_url = update_manifest.download_url;
Some(&update_manifest.download_sha256), let archive_sha256 = Some(update_manifest.download_sha256);
(
"latest manifest".to_string(),
Some((download_url, archive_sha256)),
release_dir,
) )
.map_err(|err| { };
format!(
"Unable to download {}: {}", if check_only {
update_manifest.download_url, err println!(
) " {}{}",
})?; WRAPPED_PRESENT,
style(format!("Update available: {}", updated_version)).bold()
);
return Ok(true);
}
if let Some((download_url, archive_sha256)) = download_url_and_sha256 {
let (_temp_dir, temp_archive, _temp_archive_sha256) =
download_to_temp(&download_url, archive_sha256.as_ref())
.map_err(|err| format!("Unable to download {}: {}", download_url, err))?;
extract_release_archive(&temp_archive, &release_dir).map_err(|err| { extract_release_archive(&temp_archive, &release_dir).map_err(|err| {
format!( format!(
"Unable to extract {:?} to {:?}: {}", "Unable to extract {:?} to {:?}: {}",
temp_archive, release_dir, err temp_archive, release_dir, err
) )
})?; })?;
}
config.current_update_manifest = Some(update_manifest);
release_dir
};
let release_target = load_release_target(&release_dir).map_err(|err| { let release_target = load_release_target(&release_dir).map_err(|err| {
format!( format!(
@ -1000,7 +1137,19 @@ pub fn update(config_file: &str) -> Result<bool, String> {
config.save(config_file)?; config.save(config_file)?;
gc(config_file)?; gc(config_file)?;
println!(" {}{}", SPARKLE, style("Update successful").bold()); if is_init {
println!(
" {}{}",
SPARKLE,
style(format!("{} initialized", updated_version)).bold()
);
} else {
println!(
" {}{}",
SPARKLE,
style(format!("Update successful to {}", updated_version)).bold()
);
}
Ok(true) Ok(true)
} }
@ -1063,7 +1212,7 @@ pub fn run(
}; };
if config.explicit_release.is_none() && now.elapsed().as_secs() > config.update_poll_secs { if config.explicit_release.is_none() && now.elapsed().as_secs() > config.update_poll_secs {
match update(config_file) { match update(config_file, false) {
Ok(true) => { Ok(true) => {
// Update successful, kill current process so it will be restart // Update successful, kill current process so it will be restart
if let Some(ref mut child) = child_option { if let Some(ref mut child) = child_option {

View File

@ -1,9 +1,11 @@
use crate::update_manifest::UpdateManifest; use {
use serde_derive::{Deserialize, Serialize}; crate::update_manifest::UpdateManifest,
use solana_sdk::pubkey::Pubkey; serde::{Deserialize, Serialize},
use std::fs::{create_dir_all, File}; solana_sdk::pubkey::Pubkey,
use std::io::{self, Write}; std::fs::{create_dir_all, File},
use std::path::{Path, PathBuf}; std::io::{self, Write},
std::path::{Path, PathBuf},
};
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum ExplicitRelease { pub enum ExplicitRelease {

View File

@ -2,10 +2,12 @@
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
use clap::{crate_description, crate_name, App, AppSettings, Arg, ArgMatches, SubCommand}; use {
use solana_clap_utils::{ clap::{crate_description, crate_name, App, AppSettings, Arg, ArgMatches, SubCommand},
input_parsers::pubkey_of, solana_clap_utils::{
input_validators::{is_pubkey, is_url}, input_parsers::pubkey_of,
input_validators::{is_pubkey, is_url},
},
}; };
mod build_env; mod build_env;
@ -158,9 +160,7 @@ pub fn main() -> Result<(), String> {
Arg::with_name("local_info_only") Arg::with_name("local_info_only")
.short("l") .short("l")
.long("local") .long("local")
.help( .help("only display local information, don't check for updates"),
"only display local information, don't check the cluster for new updates",
),
) )
.arg( .arg(
Arg::with_name("eval") Arg::with_name("eval")
@ -262,7 +262,7 @@ pub fn main() -> Result<(), String> {
) )
} }
("gc", Some(_matches)) => command::gc(config_file), ("gc", Some(_matches)) => command::gc(config_file),
("update", Some(_matches)) => command::update(config_file).map(|_| ()), ("update", Some(_matches)) => command::update(config_file, false).map(|_| ()),
("run", Some(matches)) => { ("run", Some(matches)) => {
let program_name = matches.value_of("program_name").unwrap(); let program_name = matches.value_of("program_name").unwrap();
let program_arguments = matches let program_arguments = matches

View File

@ -1,14 +1,16 @@
use serde_derive::{Deserialize, Serialize}; use {
use solana_config_program::ConfigState; serde::{Deserialize, Serialize},
use solana_sdk::{ solana_config_program::ConfigState,
hash::Hash, solana_sdk::{
pubkey::Pubkey, hash::Hash,
signature::{Signable, Signature}, pubkey::Pubkey,
signature::{Signable, Signature},
},
std::{borrow::Cow, error, io},
}; };
use std::{borrow::Cow, error, io};
/// Information required to download and apply a given update /// Information required to download and apply a given update
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)] #[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
pub struct UpdateManifest { pub struct UpdateManifest {
pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH
pub download_url: String, // Download URL to the release tar.bz2 pub download_url: String, // Download URL to the release tar.bz2