Skip to content

Upgraded installer #493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
988 changes: 820 additions & 168 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ clap = "2.33.3"
cloudtruth-config = { path = "crates/cloudtruth-config", version = "1.2.3" }
cloudtruth-installer = { path = "crates/cloudtruth-installer", version = "1.2.3" }
cloudtruth-restapi = { path = "crates/cloudtruth-restapi" }
color-eyre = "0.5"
color-eyre = "0.6"
csv = "1.1.6"
directories = "3.0"
edit = "0.1.2"
Expand Down
2 changes: 1 addition & 1 deletion crates/cloudtruth-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ license = "Apache-2.0"
[dependencies]
chrono = "0.4.24"
cloudtruth-restapi = { path = "../cloudtruth-restapi" }
color-eyre = "0.6.2"
color-eyre = "0.6"
directories = "5.0.0"
indoc = "2.0.1"
once_cell = "1.17.1"
Expand Down
24 changes: 18 additions & 6 deletions crates/cloudtruth-installer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
[package]
name = "cloudtruth-installer"
description = "Local installation manager for CloudTruth"
version = "1.2.3"
edition = "2021"
license = "Apache-2.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
powershell_script = "1.0.4"
reqwest = { version = "~0.9", default-features = false, features = ["default-tls-vendored"] }
serde_json = "1.0.94"
tempfile = "3.4.0"
bytes = "1.4"
ci_info = "0.14"
clap = { version = "4.0.0", features = ["derive", "env"] }
color-eyre = "0.6"
derive_more = "0.99"
flate2 = "1.0"
futures-core = "0.3"
futures-util = "0.3"
is-terminal = "0.4"
octocrab = { version = ">=0.22.0" }
once_cell = "1.17"
reqwest = { version = "~0.11", default-features = false, features = ["stream", "rustls-tls-native-roots"] }
serde_json = "1.0"
tar = "0.4"
tempfile = "3.4"
tokio = { version = "1.28", features = ["full"] }
which = "4.4"
7 changes: 7 additions & 0 deletions crates/cloudtruth-installer/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fn main() {
// Make TARGET environment variable available to code at build-time
println!(
"cargo:rustc-env=TARGET={}",
std::env::var("TARGET").unwrap()
);
}
115 changes: 115 additions & 0 deletions crates/cloudtruth-installer/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use clap::Parser;
use is_terminal::IsTerminal;
use once_cell::sync::OnceCell;

static GLOBALS: OnceCell<Globals> = OnceCell::new();

#[derive(Debug)]
struct Globals {
verbose: bool,
interactive: bool,
}

/// Global verbose flag.
/// Initialized after CLI parsing, and set to false otherwise.
pub fn verbose() -> bool {
match GLOBALS.get() {
Some(globals) => globals.verbose,
_ => false,
}
}

/// Global non-interactive flag. Indicates that we should not prompt the user for input.
/// Initialized after CLI parsing, and set to false otherwise.
pub fn interactive() -> bool {
match GLOBALS.get() {
Some(globals) => globals.interactive,
_ => false,
}
}

pub fn parse() -> Cli {
let cli = Cli::parse();
init_globals(&cli);
cli
}

/// initialize global statics (verbosity, non-interactive, etc)
/// this funciton will panic if called twice
fn init_globals(cli: &Cli) {
GLOBALS
.set(Globals {
verbose: cli.verbose,
interactive: cli.is_interactive(),
})
.expect("CLI globals were initialized twice")
}

#[derive(Debug, clap::Parser)]
/// CloudTruth installer CLI
/// #[command(author, version, about, long_about)]
pub struct Cli {
/// Subcommands
#[command(subcommand)]
pub command: Subcommand,
/// Show verbose information
#[arg(global = true, short, long, default_value_t = false)]
verbose: bool,
/// Force interactive mode, always prompt and ask for confirmations
#[arg(global = true, short = 'i', long, overrides_with = "non_interactive")]
interactive: bool,
/// Force non-interactive mode, do not prompt or ask for confirmations
#[arg(global = true, short = 'n', long, overrides_with = "interactive")]
non_interactive: bool,
}

impl Cli {
pub fn is_interactive(&self) -> bool {
!self.non_interactive
&& (self.interactive
|| !is_ci() && std::io::stdin().is_terminal() && std::io::stdout().is_terminal())
}
}

#[derive(Debug, clap::Subcommand)]
pub enum Subcommand {
#[command(about = "Install a Cloudtruth CLI ")]
Install(InstallCommand),
}

#[derive(Debug, clap::Args)]
pub struct InstallCommand {
/// Version of the program to install (defaults to latest)
pub version: Option<String>,
#[command(flatten)]
github_opts: GitHubOptions,
}

/// Options for GitHub API (for internal release workflows)
#[derive(Debug, clap::Args)]
#[group(multiple = true, required = false)]
pub struct GitHubOptions {
#[arg(
help_heading = "GitHub API Options (for internal CloudTruth release pipeline)",
long,
env = "CLOUDTRUTH_INSTALLER_GITHUB_AUTH_TOKEN",
requires = "release_id"
)]
/// GitHub API Auth Token
auth_token: Option<String>,
#[arg(
help_heading = "GitHub API Options (for internal CloudTruth release pipeline)",
long,
env = "CLOUDTRUTH_INSTALLER_GITHUB_RELEASE_ID",
requires = "auth_token"
)]
/// GitHub API Release ID
release_id: Option<String>,
}

/// Helper to detect common CI environment variables
fn is_ci() -> bool {
/// List is from watson/ci-info
static IS_CI: OnceCell<bool> = OnceCell::new();
*IS_CI.get_or_init(ci_info::is_ci)
}
54 changes: 54 additions & 0 deletions crates/cloudtruth-installer/src/github.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use bytes::Bytes;
use color_eyre::{eyre::eyre, Result};
use futures_core::Stream;
use futures_util::StreamExt;
use std::path::Path;
use tokio::{fs::File, io::AsyncWriteExt};

/// Get tag name of latest GitHub release
pub async fn get_latest_version() -> Result<String> {
Ok(octocrab::instance()
.repos("cloudtruth", "cloudtruth-cli")
.releases()
.get_latest()
.await?
.tag_name)
}

/// Download asset from GitHub
async fn get_release_asset(
version: &str,
asset_name: &str,
) -> Result<impl Stream<Item = reqwest::Result<Bytes>>> {
let github = octocrab::instance();
let download_url = github
.repos("cloudtruth", "cloudtruth-cli")
.releases()
.get_by_tag(version)
.await?
.assets
.into_iter()
.find(|asset| asset.name == asset_name)
.map(|asset| asset.browser_download_url)
.ok_or_else(|| eyre!("Could not find release asset {asset_name} in release {version}"))?;
Ok(reqwest::get(download_url).await?.bytes_stream())
}

pub async fn download_release_asset(
version: &str,
asset_name: &str,
download_path: &Path,
) -> Result<()> {
let mut f = File::create(download_path).await?;
let mut stream = get_release_asset(version, asset_name).await?;
while let Some(chunk) = stream.next().await {
f.write_all(&chunk?).await?;
}
Ok(())
}

// Get package name for the current build target, version, and file extension
pub fn asset_name(version: &str, ext: &str) -> String {
const TARGET: &str = env!("TARGET");
format!("cloudtruth-{version}-{TARGET}.{ext}")
}
100 changes: 39 additions & 61 deletions crates/cloudtruth-installer/src/install.rs
Original file line number Diff line number Diff line change
@@ -1,65 +1,43 @@
use crate::InstallError;
use std::io;
use std::io::Write;
#[cfg(not(target_os = "windows"))]
#[rustfmt::skip]
use {
crate::version::binary_version,
std::fs,
std::process::Command,
std::str,
tempfile::tempdir,
};
use crate::{cli::InstallCommand, github, package_manager::choose_package_manager};

#[cfg(target_os = "windows")]
pub fn install_latest_version(quiet: bool) -> Result<(), InstallError> {
let text = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../install.ps1"));
let result = powershell_script::run(text);
match result {
Ok(output) => {
if !quiet {
if let Some(stdout_str) = output.stdout() {
io::stdout().write_all(stdout_str.as_bytes())?;
}
}
Ok(())
}
Err(err) => Err(InstallError::InstallFailed(err.to_string())),
}
}

#[cfg(not(target_os = "windows"))]
pub fn install_latest_version(quiet: bool) -> Result<(), InstallError> {
let filename = format!("cloudtruth-cli-install-{}.sh", binary_version());
let tempdir = tempdir()?;
let fullpath = tempdir.path().join(filename);
let fullname = fullpath.to_str().unwrap();
let text = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../install.sh"));
use color_eyre::Result;
use flate2::read::GzDecoder;
use std::{fs::File, path::Path};
use tar::Archive;
use tempfile::tempdir;

// write the install script to a file to a temporary directory
fs::write(fullname, text)?;

// attempt the chmod, and hope for success -- ignore failure
let _ = Command::new("chmod").arg("a+x").arg(fullname).output();
pub fn unpack_tar_gz(src_file_path: &Path, dest_file_path: &Path) -> Result<()> {
let tar_gz = File::open(src_file_path)?;
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
archive.unpack(dest_file_path)?;
Ok(())
}

// now, actually run the installation script
let result = Command::new(fullname).output();
match result {
Ok(output) => match output.status.success() {
true => {
if !quiet {
io::stdout().write_all(&output.stdout)?;
}
Ok(())
}
false => {
if !quiet {
io::stdout().write_all(&output.stdout)?;
}
let stderr = str::from_utf8(&output.stderr)?;
Err(InstallError::InstallFailed(stderr.to_string()))
}
},
Err(err) => Err(InstallError::FailedToRunInstall(err.to_string())),
}
pub async fn install(cmd: InstallCommand) -> Result<()> {
let pkg_manager = choose_package_manager();
let version = match cmd.version {
Some(version) => version,
None => github::get_latest_version().await?,
};
let tmp_dir = tempdir()?;
let ext = if let Some(pkg_manager) = &pkg_manager {
pkg_manager.package_ext()
} else if cfg!(target_os = "windows") {
"zip"
} else {
"tar.gz"
};
let asset_name = github::asset_name(&version, ext);
let download_path = tmp_dir.path().join(&asset_name);
github::download_release_asset(&version, &asset_name, &download_path).await?;
if let Some(pkg_manager) = pkg_manager {
pkg_manager.install(&download_path)?
} else if cfg!(target_os = "windows") {
todo!()
} else {
unpack_tar_gz(&download_path, tmp_dir.path())?;
println!("{:?}", tmp_dir.path());
};
Ok(())
}
41 changes: 0 additions & 41 deletions crates/cloudtruth-installer/src/install_errors.rs

This file was deleted.

Loading