Skip to content

Commit

Permalink
feat(cli): introduce rust cli (#140)
Browse files Browse the repository at this point in the history
* 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
QuiiBz authored Sep 22, 2022
1 parent 848e7a5 commit c78ee99
Show file tree
Hide file tree
Showing 16 changed files with 2,002 additions and 59 deletions.
1,116 changes: 1,057 additions & 59 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
members = [
"packages/rust-runtime",
"packages/rust-serverless",
"packages/rust-cli",
]
19 changes: 19 additions & 0 deletions packages/rust-cli/Cargo.toml
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"
36 changes: 36 additions & 0 deletions packages/rust-cli/src/auth.rs
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(())
}
45 changes: 45 additions & 0 deletions packages/rust-cli/src/commands/build.rs
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(())
}
165 changes: 165 additions & 0 deletions packages/rust-cli/src/commands/deploy.rs
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)
}
13 changes: 13 additions & 0 deletions packages/rust-cli/src/commands/dev.rs
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(())
}
69 changes: 69 additions & 0 deletions packages/rust-cli/src/commands/login.rs
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.")),
}
}
29 changes: 29 additions & 0 deletions packages/rust-cli/src/commands/logout.rs
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.")),
}
}
Loading

0 comments on commit c78ee99

Please sign in to comment.