-
-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): introduce rust cli (#140)
* feat(cli): introduce rust cli * feat: add all commands * feat: add login & logout commands * feat: validate inputs * feat: start trpc client * feat: add function bundling * refactor: handle errors * feat: handle all errors * refactor: validate file exists and is not dir * feat: start deploy command * refactor: move to hyper to handle requests w/ multipart * feat: add working deployments * fix: deployments * feat: prettify login & build commands * feat: prettify logout command * feat: prettify deploy command * feat: add undeploy
- Loading branch information
Showing
16 changed files
with
2,002 additions
and
59 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,5 @@ | |
members = [ | ||
"packages/rust-runtime", | ||
"packages/rust-serverless", | ||
"packages/rust-cli", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
[package] | ||
name = "lagon-cli" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
lagon-runtime = { path = "../rust-runtime" } | ||
clap = { version = "3.2", features = ["derive"] } | ||
dialoguer = "0.10.2" | ||
indicatif = "0.17.1" | ||
colored = "2.0.0" | ||
dirs = "4.0.0" | ||
webbrowser = "0.8.0" | ||
tokio = { version = "1", features = ["full"] } | ||
hyper = { version = "0.14", features = ["full"] } | ||
multipart = "0.18.0" | ||
mime = "0.3.16" | ||
serde = { version = "1.0", features = ["derive"] } | ||
serde_json = "1.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
use std::{fs, io}; | ||
|
||
pub fn get_token() -> io::Result<Option<String>> { | ||
let path = dirs::home_dir().unwrap().join(".lagon").join("config"); | ||
|
||
if !path.exists() { | ||
return Ok(None); | ||
} | ||
|
||
match fs::read_to_string(path) { | ||
Ok(content) => Ok(Some(content)), | ||
Err(error) => Err(error), | ||
} | ||
} | ||
|
||
pub fn set_token(token: String) -> io::Result<()> { | ||
let path = dirs::home_dir().unwrap().join(".lagon").join("config"); | ||
|
||
if !path.exists() { | ||
fs::create_dir_all(path.parent().unwrap())?; | ||
} | ||
|
||
fs::write(path, token)?; | ||
|
||
Ok(()) | ||
} | ||
|
||
pub fn rm_token() -> io::Result<()> { | ||
let path = dirs::home_dir().unwrap().join(".lagon").join("config"); | ||
|
||
if path.exists() { | ||
fs::remove_file(path)?; | ||
} | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
use std::{fs, io, path::PathBuf}; | ||
|
||
use crate::utils::{ | ||
bundle_function, debug, print_progress, success, validate_code_file, validate_public_dir, | ||
}; | ||
|
||
pub fn build( | ||
file: PathBuf, | ||
client: Option<PathBuf>, | ||
public_dir: Option<PathBuf>, | ||
) -> io::Result<()> { | ||
validate_code_file(&file)?; | ||
|
||
let client = match client { | ||
Some(client) => { | ||
validate_code_file(&client)?; | ||
Some(client) | ||
} | ||
None => None, | ||
}; | ||
|
||
let public_dir = validate_public_dir(public_dir)?; | ||
let (index, assets) = bundle_function(file, client, public_dir)?; | ||
|
||
let end_progress = print_progress("Writting index.js..."); | ||
fs::create_dir_all(".lagon")?; | ||
fs::write(".lagon/index.js", index)?; | ||
end_progress(); | ||
|
||
for (path, content) in assets { | ||
let message = format!("Writting {}...", path); | ||
let end_progress = print_progress(&message); | ||
fs::write(format!(".lagon/{}", path), content)?; | ||
end_progress(); | ||
} | ||
|
||
println!(); | ||
println!( | ||
"{} {}", | ||
success("Build successful!"), | ||
debug("You can find it in .lagon folder.") | ||
); | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
use std::{ | ||
fmt::{Display, Formatter}, | ||
io::{self, Error, ErrorKind}, | ||
path::PathBuf, | ||
}; | ||
|
||
use dialoguer::{Confirm, Input, Select}; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
use crate::{ | ||
auth::get_token, | ||
utils::{ | ||
create_deployment, debug, get_function_config, info, print_progress, validate_code_file, | ||
validate_public_dir, write_function_config, DeploymentConfig, TrpcClient, | ||
}, | ||
}; | ||
|
||
#[derive(Deserialize, Debug)] | ||
struct Organization { | ||
name: String, | ||
id: String, | ||
} | ||
|
||
impl Display for Organization { | ||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "{}", self.name) | ||
} | ||
} | ||
|
||
type OrganizationsResponse = Vec<Organization>; | ||
|
||
#[derive(Serialize, Debug)] | ||
struct CreateFunctionRequest { | ||
name: String, | ||
domains: Vec<String>, | ||
env: Vec<String>, | ||
cron: Option<String>, | ||
} | ||
|
||
#[derive(Deserialize, Debug)] | ||
struct CreateFunctionResponse { | ||
id: String, | ||
} | ||
|
||
#[derive(Deserialize, Debug)] | ||
struct Function { | ||
id: String, | ||
name: String, | ||
} | ||
|
||
impl Display for Function { | ||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "{}", self.name) | ||
} | ||
} | ||
|
||
type FunctionsResponse = Vec<Function>; | ||
|
||
pub async fn deploy( | ||
file: PathBuf, | ||
client: Option<PathBuf>, | ||
public_dir: Option<PathBuf>, | ||
force: bool, | ||
) -> io::Result<()> { | ||
let token = get_token()?; | ||
|
||
if token.is_none() { | ||
return Err(Error::new( | ||
ErrorKind::Other, | ||
"You are not logged in. Please login with `lagon login`", | ||
)); | ||
} | ||
|
||
let token = token.unwrap(); | ||
|
||
validate_code_file(&file)?; | ||
|
||
let client = match client { | ||
Some(client) => { | ||
validate_code_file(&client)?; | ||
Some(client) | ||
} | ||
None => None, | ||
}; | ||
|
||
let public_dir = validate_public_dir(public_dir)?; | ||
let config = get_function_config()?; | ||
|
||
if config.is_none() { | ||
println!("{}", debug("No deployment config found...")); | ||
println!(); | ||
|
||
let trpc_client = TrpcClient::new(&token); | ||
let response = trpc_client | ||
.query::<(), OrganizationsResponse>("organizations.list", None) | ||
.await | ||
.unwrap(); | ||
let organizations = response.result.data; | ||
|
||
let index = Select::new().items(&organizations).default(0).interact()?; | ||
let organization = &organizations[index]; | ||
|
||
match Confirm::new() | ||
.with_prompt(info("Link to an existing Function?")) | ||
.interact()? | ||
{ | ||
true => { | ||
let response = trpc_client | ||
.query::<(), FunctionsResponse>("functions.list", None) | ||
.await | ||
.unwrap(); | ||
|
||
let index = Select::new() | ||
.items(&response.result.data) | ||
.default(0) | ||
.interact()?; | ||
let function = &response.result.data[index]; | ||
|
||
write_function_config(DeploymentConfig { | ||
function_id: function.id.clone(), | ||
organization_id: organization.id.clone(), | ||
})?; | ||
|
||
create_deployment(function.id.clone(), file, client, public_dir, token)?; | ||
} | ||
false => { | ||
let name = Input::<String>::new() | ||
.with_prompt(info("What is the name of this new Function?")) | ||
.interact_text()?; | ||
|
||
println!(); | ||
let message = format!("Creating Function {}...", name); | ||
let end_progress = print_progress(&message); | ||
|
||
let response = trpc_client | ||
.mutation::<CreateFunctionRequest, CreateFunctionResponse>( | ||
"functions.create", | ||
CreateFunctionRequest { | ||
name, | ||
domains: Vec::new(), | ||
env: Vec::new(), | ||
cron: None, | ||
}, | ||
) | ||
.await | ||
.unwrap(); | ||
|
||
end_progress(); | ||
|
||
write_function_config(DeploymentConfig { | ||
function_id: response.result.data.id.clone(), | ||
organization_id: organization.id.clone(), | ||
})?; | ||
|
||
create_deployment(response.result.data.id, file, client, public_dir, token)?; | ||
} | ||
}; | ||
|
||
return Ok(()); | ||
} | ||
|
||
let config = config.unwrap(); | ||
|
||
create_deployment(config.function_id, file, client, public_dir, token) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
use std::{io, path::PathBuf}; | ||
|
||
pub fn dev( | ||
file: PathBuf, | ||
client: Option<PathBuf>, | ||
public_dir: Option<PathBuf>, | ||
port: Option<u16>, | ||
hostname: Option<String>, | ||
) -> io::Result<()> { | ||
println!("dev"); | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
use std::io::{self, Error, ErrorKind}; | ||
|
||
use dialoguer::{Confirm, Password}; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
use crate::auth::{get_token, set_token}; | ||
use crate::utils::{debug, get_site_url, info, input, print_progress, success, TrpcClient}; | ||
|
||
#[derive(Deserialize, Debug)] | ||
struct CliResponse { | ||
token: String, | ||
} | ||
|
||
#[derive(Serialize, Debug)] | ||
struct CliRequest { | ||
code: String, | ||
} | ||
|
||
pub async fn login() -> io::Result<()> { | ||
if let Some(_) = get_token()? { | ||
if !Confirm::new() | ||
.with_prompt(info( | ||
"You are already logged in. Are you sure you want to log in again?", | ||
)) | ||
.interact()? | ||
{ | ||
return Err(Error::new(ErrorKind::Other, "Login aborted.")); | ||
} | ||
} | ||
|
||
println!(); | ||
|
||
let end_progress = print_progress("Opening browser..."); | ||
let url = get_site_url() + "/cli"; | ||
webbrowser::open(&url).unwrap(); | ||
end_progress(); | ||
|
||
println!(); | ||
println!( | ||
"{}", | ||
info("Please copy and paste the verification from your browser.") | ||
); | ||
|
||
let code = Password::new() | ||
.with_prompt(input("Verification code")) | ||
.interact()?; | ||
|
||
let client = TrpcClient::new(&code); | ||
let request = CliRequest { code: code.clone() }; | ||
|
||
match client | ||
.mutation::<CliRequest, CliResponse>("tokens.authenticate", request) | ||
.await | ||
{ | ||
Ok(response) => { | ||
set_token(response.result.data.token)?; | ||
|
||
println!(); | ||
println!( | ||
"{} {}", | ||
success("You are now logged in."), | ||
debug("You can close your browser tab.") | ||
); | ||
|
||
Ok(()) | ||
} | ||
Err(_) => Err(Error::new(ErrorKind::Other, "Failed to log in.")), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
use std::io::{self, Error, ErrorKind}; | ||
|
||
use dialoguer::Confirm; | ||
|
||
use crate::{ | ||
auth::{get_token, rm_token}, | ||
utils::{info, success}, | ||
}; | ||
|
||
pub fn logout() -> io::Result<()> { | ||
if let None = get_token()? { | ||
return Err(Error::new(ErrorKind::Other, "You are not logged in.")); | ||
} | ||
|
||
match Confirm::new() | ||
.with_prompt(info("Are you sure you want to log out?")) | ||
.interact()? | ||
{ | ||
true => { | ||
rm_token()?; | ||
|
||
println!(); | ||
println!("{}", success("You have been logged out.")); | ||
|
||
Ok(()) | ||
} | ||
false => Err(Error::new(ErrorKind::Other, "Logout aborted.")), | ||
} | ||
} |
Oops, something went wrong.