Skip to content

Commit 443b4c9

Browse files
committed
feat: auto update + update notification
1 parent 6743194 commit 443b4c9

File tree

23 files changed

+750
-133
lines changed

23 files changed

+750
-133
lines changed

Cargo.lock

Lines changed: 243 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
[package]
22
name = "hop"
3-
version = "0.1.7"
3+
version = "0.1.8"
44
edition = "2021"
55

66
[dependencies]
77
log = "0.4"
88
dirs = "4.0"
99
regex = "1.5"
10+
runas = "0.2"
1011
ignore = "0.4"
1112
console = "0.15"
1213
tabwriter = "1.2"
@@ -31,3 +32,6 @@ reqwest = { version = "0.11", features = [
3132

3233
], default-features = false }
3334
tokio-tungstenite = { version = "0.17", features = ["rustls-tls-webpki-roots"] }
35+
36+
[target.'cfg(windows)'.dependencies]
37+
async_zip = "0.0.8"

src/commands/auth/login.rs

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ use tokio::sync::mpsc::{channel, Sender};
77
use tokio::task;
88

99
use crate::commands::ignite::util::parse_key_val;
10-
use crate::config::{PAT_FALLBACK_URL, WEB_AUTH_URL};
1110
use crate::state::State;
1211

12+
const WEB_AUTH_URL: &str = "https://console.hop.io/cli-auth";
13+
const PAT_FALLBACK_URL: &str = "https://console.hop.io/settings/pats";
14+
1315
#[derive(Debug, Parser)]
1416
#[clap(about = "Login to Hop")]
1517
pub struct LoginOptions {
16-
#[clap(long = "browserless", help = "Do not use a browser to login")]
17-
pub browserless: bool,
18+
#[clap(name = "pat", help = "Personal Access Token")]
19+
pub pat: Option<String>,
1820
}
1921

2022
async fn request_handler(
@@ -89,6 +91,33 @@ async fn web_auth(port: u16) -> Result<String, std::io::Error> {
8991
}
9092

9193
pub async fn handle_login(options: LoginOptions, mut state: State) -> Result<(), std::io::Error> {
94+
let token = match options.pat {
95+
Some(pat) => pat,
96+
None => browser_login().await,
97+
};
98+
99+
// update the token assuming it's a valid PAT
100+
state.update_http_token(token.clone());
101+
102+
// for sanity fetch the user info
103+
state.login().await;
104+
105+
let me = state.ctx.me.clone().unwrap();
106+
107+
// save the state
108+
state.auth.authorized.insert(me.user.id.clone(), token);
109+
state.auth.save().await?;
110+
111+
state.ctx.default_user = Some(me.user.id);
112+
state.ctx.save().await?;
113+
114+
// output the login info
115+
log::info!("Logged in as: `{}` ({})", me.user.username, me.user.email);
116+
117+
Ok(())
118+
}
119+
120+
async fn browser_login() -> String {
92121
let port = portpicker::pick_unused_port().unwrap();
93122

94123
let callback_url = format!("http://localhost:{}/", port);
@@ -99,43 +128,21 @@ pub async fn handle_login(options: LoginOptions, mut state: State) -> Result<(),
99128
);
100129

101130
// lunch a web server to handle the auth request
102-
let token = if !options.browserless && webbrowser::open(&auth_url).is_ok() {
131+
if webbrowser::open(&auth_url).is_ok() {
103132
log::info!("Opening browser to: {}", auth_url);
104133

105134
web_auth(port)
106135
.await
107136
.expect("Error while starting web auth server")
108137
} else {
109-
if !options.browserless {
110-
log::info!("Could not open web a browser.");
111-
log::info!("Please provide a personal access token manually.");
112-
log::info!("You can create one at {}", PAT_FALLBACK_URL);
113-
}
138+
log::info!("Could not open web a browser.");
139+
log::info!("Please provide a personal access token manually.");
140+
log::info!("You can create one at {}", PAT_FALLBACK_URL);
114141

115142
// falback to simpe input
116143
dialoguer::Password::new()
117144
.with_prompt("Enter your token")
118145
.interact()
119146
.unwrap()
120-
};
121-
122-
// update the token assuming it's a valid PAT
123-
state.update_http_token(token.clone());
124-
125-
// for sanity fetch the user info
126-
state.login().await;
127-
128-
let me = state.ctx.me.clone().unwrap();
129-
130-
// save the state
131-
state.auth.authorized.insert(me.user.id.clone(), token);
132-
state.auth.save().await?;
133-
134-
state.ctx.default_user = Some(me.user.id);
135-
state.ctx.save().await?;
136-
137-
// output the login info
138-
log::info!("Logged in as: `{}` ({})", me.user.username, me.user.email);
139-
140-
Ok(())
147+
}
141148
}

src/commands/auth/logout.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use clap::Parser;
22

3+
use crate::config::EXEC_NAME;
34
use crate::state::State;
45
use crate::store::context::Context;
56

@@ -14,7 +15,10 @@ pub async fn hanndle_logout(
1415
let user_id = state.ctx.default_user;
1516

1617
if user_id.is_none() {
17-
panic!("You are not logged in. Please run `hop auth login` first.");
18+
panic!(
19+
"You are not logged in. Please run `{} auth login` first.",
20+
EXEC_NAME
21+
);
1822
}
1923

2024
// clear all state

src/commands/deploy/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ use self::types::{Event, Message};
1414
use self::util::{compress, env_file_to_map};
1515
use crate::commands::containers::types::ContainerOptions;
1616
use crate::commands::containers::utils::{create_containers, rollout};
17-
use crate::commands::ignite::create::{CreateOptions, DeploymentConfig};
17+
use crate::commands::ignite::create::{CreateOptions, DeploymentConfig, WEB_DEPLOYMENTS_URL};
1818
use crate::commands::ignite::types::SingleDeployment;
1919
use crate::commands::ignite::util::{create_deployment, create_deployment_config};
20-
use crate::config::{HOP_BUILD_BASE_URL, HOP_REGISTRY_URL, WEB_DEPLOYMENTS_URL};
2120
use crate::state::State;
2221
use crate::store::hopfile::HopFile;
2322

23+
const HOP_BUILD_BASE_URL: &str = "https://builder.hop.io/v1";
24+
const HOP_REGISTRY_URL: &str = "registry.hop.io";
25+
2426
#[derive(Debug, Parser)]
2527
#[structopt(about = "Deploy a new container")]
2628
pub struct DeployOptions {

src/commands/ignite/create.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ use super::types::{Env, RamSizes, ScalingStrategy};
44
use crate::commands::containers::types::ContainerType;
55
use crate::commands::containers::utils::create_containers;
66
use crate::commands::ignite::util::{create_deployment, create_deployment_config};
7-
use crate::config::WEB_DEPLOYMENTS_URL;
87
use crate::state::State;
98

9+
pub const WEB_DEPLOYMENTS_URL: &str = "https://console.hop.io/ignite/deployment/";
10+
1011
#[derive(Debug, Parser, Default, PartialEq, Clone)]
1112
pub struct DeploymentConfig {
1213
#[clap(short = 'n', long = "name", help = "Name of the deployment")]
@@ -67,7 +68,7 @@ pub struct DeploymentConfig {
6768
pub env: Option<Vec<Env>>,
6869
}
6970

70-
#[derive(Debug, Parser)]
71+
#[derive(Debug, Parser, Default, PartialEq, Clone)]
7172
pub struct CreateOptions {
7273
#[clap(flatten)]
7374
pub config: DeploymentConfig,
@@ -86,7 +87,7 @@ pub async fn handle_create(options: CreateOptions, state: State) -> Result<(), s
8687
project.id
8788
);
8889

89-
let is_not_guided = options.config != DeploymentConfig::default();
90+
let is_not_guided = options != CreateOptions::default();
9091

9192
let (deployment_config, container_options) =
9293
create_deployment_config(options, is_not_guided, None).await;

src/commands/mod.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ pub mod auth;
22
pub mod containers;
33
pub mod deploy;
44
pub mod ignite;
5-
pub mod link;
5+
mod link;
66
pub mod projects;
7-
pub mod secrets;
8-
pub mod whoami;
7+
mod secrets;
8+
pub mod update;
9+
mod whoami;
910

1011
use clap::Subcommand;
1112

@@ -15,6 +16,7 @@ use self::ignite::{handle_deployments, IgniteOptions};
1516
use self::link::{handle_link, LinkOptions};
1617
use self::projects::{handle_projects, ProjectsOptions};
1718
use self::secrets::{handle_secrets, SecretsOptions};
19+
use self::update::{handle_update, UpdateOptions};
1820
use self::whoami::{handle_whoami, WhoamiOptions};
1921
use crate::state::State;
2022

@@ -28,18 +30,21 @@ pub enum Commands {
2830
Whoami(WhoamiOptions),
2931
Ignite(IgniteOptions),
3032
Link(LinkOptions),
33+
Update(UpdateOptions),
3134
}
3235

3336
pub async fn handle_command(command: Commands, mut state: State) -> Result<(), std::io::Error> {
3437
match command {
3538
Commands::Auth(options) => handle_auth(options, state).await,
39+
Commands::Update(options) => handle_update(options, state).await,
3640

3741
authorized_command => {
3842
// login so these commands can run
3943
state.login().await;
4044

4145
match authorized_command {
4246
Commands::Auth(_) => unreachable!(),
47+
Commands::Update(_) => unreachable!(),
4348
Commands::Projects(options) => handle_projects(options, state).await,
4449
Commands::Secrets(options) => handle_secrets(options, state).await,
4550
Commands::Deploy(options) => handle_deploy(options, state).await,

src/commands/projects/delete.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,7 @@ pub struct DeleteOptions {
1515
}
1616

1717
pub async fn handle_delete(options: DeleteOptions, mut state: State) -> Result<(), std::io::Error> {
18-
let projects = state
19-
.ctx
20-
.me
21-
.clone()
22-
.expect("You are not logged in. Please run `hop auth login` first.")
23-
.projects;
18+
let projects = state.ctx.me.clone().unwrap().projects;
2419

2520
if projects.is_empty() {
2621
panic!("No projects found");

src/commands/projects/switch.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,7 @@ pub struct SwitchOptions {
1111
}
1212

1313
pub async fn handle_switch(options: SwitchOptions, mut state: State) -> Result<(), std::io::Error> {
14-
let projects = state
15-
.ctx
16-
.me
17-
.clone()
18-
.expect("You are not logged in. Please run `hop auth login` first.")
19-
.projects;
14+
let projects = state.ctx.me.clone().unwrap().projects;
2015

2116
if projects.is_empty() {
2217
panic!("No projects found");

src/commands/update/mod.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
pub mod types;
2+
pub mod util;
3+
4+
use clap::Parser;
5+
6+
use self::util::{check_version, download, swap_executables, unpack};
7+
use crate::config::VERSION;
8+
use crate::state::http::HttpClient;
9+
use crate::state::State;
10+
11+
#[derive(Debug, Parser)]
12+
#[clap(about = "Update Hop to the latest version")]
13+
pub struct UpdateOptions {
14+
#[clap(short = 'f', long = "force", help = "Force update")]
15+
pub force: bool,
16+
17+
#[clap(short = 'b', long = "beta", help = "Update to beta version")]
18+
pub beta: bool,
19+
}
20+
21+
pub async fn handle_update(options: UpdateOptions, _state: State) -> Result<(), std::io::Error> {
22+
let http = HttpClient::new(None, None);
23+
24+
let (update, version) = check_version(options.beta).await;
25+
26+
if !update && !options.force {
27+
log::info!("CLI is up to date");
28+
return Ok(());
29+
} else {
30+
log::info!("Found new version {} (current: {})", version, VERSION);
31+
}
32+
33+
// download the new release
34+
let packed_temp = download(http, version.clone())
35+
.await
36+
.expect("Failed to download");
37+
38+
// unpack the new release
39+
let unpacked = unpack(packed_temp).await?;
40+
41+
// swap the executables
42+
swap_executables(std::env::current_exe()?, unpacked).await?;
43+
44+
log::info!("Updated to {}", version);
45+
46+
Ok(())
47+
}

0 commit comments

Comments
 (0)