`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",
"semver 0.9.0",
"serde",
"serde_derive",
"serde_json",
"serde_yaml",
"solana-clap-utils",
"solana-client",

View File

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

View File

@ -1,30 +1,32 @@
use crate::{
config::{Config, ExplicitRelease},
stop_process::stop_process,
update_manifest::{SignedUpdateManifest, UpdateManifest},
use {
crate::{
config::{Config, ExplicitRelease},
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)]
pub struct ReleaseVersion {
@ -37,6 +39,7 @@ static TRUCK: Emoji = Emoji("🚚 ", "");
static LOOKING_GLASS: Emoji = Emoji("🔍 ", "");
static BULLET: Emoji = Emoji("", "* ");
static SPARKLE: Emoji = Emoji("", "");
static WRAPPED_PRESENT: Emoji = Emoji("🎁 ", "");
static PACKAGE: Emoji = Emoji("📦 ", "");
static INFORMATION: Emoji = Emoji(" ", "");
static RECYCLING: Emoji = Emoji("♻️ ", "");
@ -544,7 +547,7 @@ pub fn init(
config
};
update(config_file)?;
init_or_update(config_file, true, false)?;
let path_modified = if !no_modify_path {
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(
config_file: &str,
local_info_only: bool,
eval: bool,
) -> Result<Option<UpdateManifest>, 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(
&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)?;
if eval {
@ -604,7 +612,7 @@ pub fn info(
println!("SOLANA_INSTALL_ACTIVE_CHANNEL={}", channel,);
Option::<String>::None
});
return Ok(None);
return Ok(());
}
println_name_value("Configuration:", &config_file);
@ -613,6 +621,17 @@ pub fn info(
&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 {
match explicit_release {
ExplicitRelease::Semver(release_semver) => {
@ -630,51 +649,30 @@ pub fn info(
);
}
}
return Ok(None);
}
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());
print_release_version(&config);
} else {
println_name_value("JSON RPC URL:", &config.json_rpc_url);
println_name_value(
&format!("{}download URL:", BULLET),
&update_manifest.download_url,
"Update manifest pubkey:",
&config.update_manifest_pubkey.to_string(),
);
}
match config.current_update_manifest {
Some(ref update_manifest) => {
println_name_value("Installed version:", "");
print_update_manifest(&update_manifest);
}
None => {
println_name_value("Installed version:", "None");
match config.current_update_manifest {
Some(ref update_manifest) => {
println_name_value("Installed version:", "");
print_release_version(&config);
print_update_manifest(&update_manifest);
}
None => {
println_name_value("Installed version:", "None");
}
}
}
if local_info_only {
Ok(None)
Ok(())
} else {
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 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))
}
update(config_file, true).map(|_| ())
}
}
@ -845,24 +843,137 @@ pub fn gc(config_file: &str) -> Result<(), String> {
Ok(())
}
pub fn update(config_file: &str) -> Result<bool, String> {
let mut config = Config::load(config_file)?;
let update_manifest = info(config_file, false, false)?;
#[derive(Debug, Deserialize, Serialize)]
pub struct GithubRelease {
pub tag_name: String,
pub prerelease: bool,
}
let release_dir = if let Some(explicit_release) = &config.explicit_release {
let (download_url, release_dir) = match explicit_release {
ExplicitRelease::Semver(release_semver) => {
let download_url = github_release_download_url(release_semver);
let release_dir = config.release_dir(&release_semver);
let download_url = if release_dir.exists() {
// If this release_semver has already been successfully downloaded, no update
// needed
println!("{} found in cache", release_semver);
None
} else {
Some(download_url)
};
(download_url, release_dir)
#[derive(Debug, Deserialize, Serialize)]
pub struct GithubReleases(Vec<GithubRelease>);
fn semver_of(string: &str) -> Result<semver::Version, String> {
if string.starts_with('v') {
semver::Version::parse(string.split_at(1).1)
} else {
semver::Version::parse(string)
}
.map_err(|err| err.to_string())
}
fn check_for_newer_github_release(
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) => {
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 =
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() {
println_name_value(
&format!("{}Release commit:", BULLET),
&update_release_version.commit[0..7],
);
(download_url, release_dir)
(
format!(
"{} commit {}",
release_channel,
&update_release_version.commit[0..7]
),
Some((download_url, None)),
release_dir,
)
} else {
let current_release_version =
load_release_version(&current_release_version_yml)?;
if update_release_version.commit == current_release_version.commit {
// Same commit, no update required
println!(
"Latest {} build ({}) found in cache",
release_channel,
&current_release_version.commit[0..7],
);
(None, release_dir)
} else {
println_name_value(
&format!("{}Release commit:", BULLET),
&format!(
"{} => {}:",
&current_release_version.commit[0..7],
&update_release_version.commit[0..7],
),
);
if let Ok(active_release_version) =
load_release_version(&config.active_release_dir().join("version.yml"))
{
if current_release_version.commit == active_release_version.commit {
// Same version, no update required
println!(
"Install is up to date. {} is the latest commit for {}",
&active_release_version.commit[0..7],
release_channel
);
return Ok(false);
}
}
(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 {
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);
}
let update_manifest = update_manifest.unwrap();
println!("\n{}", style("An update is available:").bold());
print_update_manifest(&update_manifest);
if timestamp_secs()
< 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());
}
}
config.current_update_manifest = Some(update_manifest.clone());
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,
Some(&update_manifest.download_sha256),
let download_url = update_manifest.download_url;
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 {}: {}",
update_manifest.download_url, err
)
})?;
};
if check_only {
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| {
format!(
"Unable to extract {:?} to {:?}: {}",
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| {
format!(
@ -1000,7 +1137,19 @@ pub fn update(config_file: &str) -> Result<bool, String> {
config.save(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)
}
@ -1063,7 +1212,7 @@ pub fn run(
};
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) => {
// Update successful, kill current process so it will be restart
if let Some(ref mut child) = child_option {

View File

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

View File

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

View File

@ -1,14 +1,16 @@
use serde_derive::{Deserialize, Serialize};
use solana_config_program::ConfigState;
use solana_sdk::{
hash::Hash,
pubkey::Pubkey,
signature::{Signable, Signature},
use {
serde::{Deserialize, Serialize},
solana_config_program::ConfigState,
solana_sdk::{
hash::Hash,
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
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
pub struct UpdateManifest {
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