Skip to content
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

refactor(cli): restructure commands and main #128

Merged
merged 2 commits into from
Dec 20, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ use clap_complete::{generate, Generator, Shell};

use crate::{Cli, CliError};

pub fn completion(shell: &Option<Shell>) -> Result<(), CliError> {
if let Some(shell) = shell {
#[derive(clap::Args)]
pub struct Args {
#[arg(long, value_enum)]
shell: Option<Shell>,
}

pub fn completion(args: &Args) -> Result<(), CliError> {
if let Some(shell) = &args.shell {
let mut cmd = Cli::command();
print_completions(shell, &mut cmd);
}

Ok(())
}
fn print_completions<G: Generator + Clone>(gen: &G, cmd: &mut Command) {
Expand Down
35 changes: 21 additions & 14 deletions linkup-cli/src/health.rs → linkup-cli/src/commands/health.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@ use serde::Serialize;

use crate::{linkup_dir_path, local_config::LocalState, services, CliError};

#[derive(clap::Args)]
pub struct Args {
// Output status in JSON format
#[arg(long)]
json: bool,
}

pub fn health(args: &Args) -> Result<(), CliError> {
let health = Health::load()?;

let health = if args.json {
serde_json::to_string_pretty(&health).unwrap()
} else {
format!("{}", health)
};

println!("{}", health);

Ok(())
}

#[derive(Debug, Serialize)]
struct System {
os_name: String,
Expand Down Expand Up @@ -262,17 +283,3 @@ impl Display for Health {
Ok(())
}
}

pub fn health(json: bool) -> Result<(), CliError> {
let health = Health::load()?;

let health = if json {
serde_json::to_string_pretty(&health).unwrap()
} else {
format!("{}", health)
};

println!("{}", health);

Ok(())
}
56 changes: 56 additions & 0 deletions linkup-cli/src/commands/local.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use crate::{
local_config::{upload_state, LocalState, ServiceTarget},
CliError,
};

#[derive(clap::Args)]
pub struct Args {
service_names: Vec<String>,

#[arg(
short,
long,
help = "Route all the services to local. Cannot be used with SERVICE_NAMES.",
conflicts_with = "service_names"
)]
all: bool,
}

pub async fn local(args: &Args) -> Result<(), CliError> {
if args.service_names.is_empty() && !args.all {
return Err(CliError::NoSuchService(
"No service names provided".to_string(),
));
}

let mut state = LocalState::load()?;

if args.all {
for service in state.services.iter_mut() {
service.current = ServiceTarget::Local;
}
} else {
for service_name in &args.service_names {
let service = state
.services
.iter_mut()
.find(|s| s.name.as_str() == service_name)
.ok_or_else(|| CliError::NoSuchService(service_name.to_string()))?;
service.current = ServiceTarget::Local;
}
}

state.save()?;
upload_state(&state).await?;

if args.all {
println!("Linkup is routing all traffic to the local servers");
} else {
println!(
"Linkup is routing {} traffic to the local server",
args.service_names.join(", ")
);
}

Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,32 @@ use std::{
process::{Command, Stdio},
};

use clap::Subcommand;

use crate::{
local_config::{config_path, get_config},
services, CliError, Result, LINKUP_CF_TLS_API_ENV_VAR,
};

#[derive(clap::Args)]
pub struct Args {
#[clap(subcommand)]
pub subcommand: LocalDNSSubcommand,
}

#[derive(Subcommand)]
pub enum LocalDNSSubcommand {
Install,
Uninstall,
}

pub fn local_dns(args: &Args, config: &Option<String>) -> Result<()> {
match args.subcommand {
LocalDNSSubcommand::Install => install(config),
LocalDNSSubcommand::Uninstall => uninstall(config),
}
}

pub fn install(config_arg: &Option<String>) -> Result<()> {
if std::env::var(LINKUP_CF_TLS_API_ENV_VAR).is_err() {
println!("local-dns uses Cloudflare to enable https through local certificates.");
Expand Down
23 changes: 23 additions & 0 deletions linkup-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
pub mod completion;
pub mod health;
pub mod local;
pub mod local_dns;
pub mod preview;
pub mod remote;
pub mod reset;
pub mod server;
pub mod start;
pub mod status;
pub mod stop;

pub use {completion::completion, completion::Args as CompletionArgs};
pub use {health::health, health::Args as HealthArgs};
pub use {local::local, local::Args as LocalArgs};
pub use {local_dns::local_dns, local_dns::Args as LocalDnsArgs};
pub use {preview::preview, preview::Args as PreviewArgs};
pub use {remote::remote, remote::Args as RemoteArgs};
pub use {reset::reset, reset::Args as ResetArgs};
pub use {server::server, server::Args as ServerArgs};
pub use {start::start, start::Args as StartArgs};
pub use {status::status, status::Args as StatusArgs};
pub use {stop::stop, stop::Args as StopArgs};
25 changes: 18 additions & 7 deletions linkup-cli/src/preview.rs → linkup-cli/src/commands/preview.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
use crate::commands::status::{format_state_domains, SessionStatus};
use crate::local_config::{config_path, get_config};
use crate::status::{format_state_domains, SessionStatus};
use crate::worker_client::WorkerClient;
use crate::CliError;
use clap::builder::ValueParser;
use linkup::CreatePreviewRequest;

pub async fn preview(
config: &Option<String>,
services: &[(String, String)],
#[derive(clap::Args)]
pub struct Args {
#[arg(
help = "<service>=<url> pairs to preview.",
value_parser = ValueParser::new(parse_services_tuple),
required = true,
num_args = 1..,
)]
services: Vec<(String, String)>,

#[arg(long, help = "Print the request body instead of sending it.")]
print_request: bool,
) -> Result<(), CliError> {
}

pub async fn preview(args: &Args, config: &Option<String>) -> Result<(), CliError> {
let config_path = config_path(config)?;
let input_config = get_config(&config_path)?;
let create_preview_request: CreatePreviewRequest =
input_config.create_preview_request(services);
input_config.create_preview_request(&args.services);
let url = input_config.linkup.remote.clone();
let create_req_json = serde_json::to_string(&create_preview_request)
.map_err(|e| CliError::LoadConfig(url.to_string(), e.to_string()))?;

if print_request {
if args.print_request {
println!("{}", create_req_json);
return Ok(());
}
Expand Down
56 changes: 56 additions & 0 deletions linkup-cli/src/commands/remote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use crate::{
local_config::{upload_state, LocalState, ServiceTarget},
CliError,
};

#[derive(clap::Args)]
pub struct Args {
service_names: Vec<String>,

#[arg(
short,
long,
help = "Route all the services to remote. Cannot be used with SERVICE_NAMES.",
conflicts_with = "service_names"
)]
all: bool,
}

pub async fn remote(args: &Args) -> Result<(), CliError> {
if args.service_names.is_empty() && !args.all {
return Err(CliError::NoSuchService(
"No service names provided".to_string(),
));
}

let mut state = LocalState::load()?;

if args.all {
for service in state.services.iter_mut() {
service.current = ServiceTarget::Remote;
}
} else {
for service_name in &args.service_names {
let service = state
.services
.iter_mut()
.find(|s| s.name.as_str() == service_name)
.ok_or_else(|| CliError::NoSuchService(service_name.to_string()))?;
service.current = ServiceTarget::Remote;
}
}

state.save()?;
upload_state(&state).await?;

if args.all {
println!("Linkup is routing all traffic to the remote servers");
} else {
println!(
"Linkup is routing {} traffic to the remote server",
args.service_names.join(", ")
);
}

Ok(())
}
13 changes: 13 additions & 0 deletions linkup-cli/src/commands/reset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use crate::{commands, local_config::LocalState, CliError};

#[derive(clap::Args)]
pub struct Args {}

pub async fn reset(_args: &Args) -> Result<(), CliError> {
let _ = LocalState::load()?;

commands::stop(&commands::StopArgs {}, false)?;
commands::start(&commands::StartArgs { no_tunnel: false }, false, &None).await?;

Ok(())
}
22 changes: 22 additions & 0 deletions linkup-cli/src/commands/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use std::fs;

use crate::CliError;

#[derive(clap::Args)]
pub struct Args {
#[arg(long)]
pidfile: String,
}

pub async fn server(args: &Args) -> Result<(), CliError> {
let pid = std::process::id();
fs::write(&args.pidfile, pid.to_string())?;

let res = linkup_local_server::start_server().await;

if let Err(pid_file_err) = fs::remove_file(&args.pidfile) {
eprintln!("Failed to remove pidfile: {}", pid_file_err);
}

res.map_err(|e| e.into())
}
37 changes: 15 additions & 22 deletions linkup-cli/src/start.rs → linkup-cli/src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,35 @@ use colored::Colorize;
use crossterm::{cursor, ExecutableCommand};

use crate::{
commands::status::{format_state_domains, SessionStatus},
env_files::write_to_env_file,
local_config::{config_path, config_to_state, get_config},
services::{self, BackgroundService},
status::{format_state_domains, SessionStatus},
};
use crate::{local_config::LocalState, CliError};

const LOADING_CHARS: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];

pub struct StartArgs<'a> {
/// Path to the Linkup config to be used as base in case `fresh_state` argument is `true`.
pub config_arg: &'a Option<String>,

/// If there should not be a Cloudflare tunnel.
#[derive(clap::Args)]
pub struct Args {
#[clap(
short,
long,
help = "Start linkup in partial mode without a tunnel. Not all requests will succeed."
)]
pub no_tunnel: bool,

/// Boolean representing if should refresh the state to what is defined on `config_arg`.
pub fresh_state: bool,
}

impl<'a> Default for StartArgs<'a> {
fn default() -> Self {
Self {
config_arg: &None,
no_tunnel: false,
fresh_state: false,
}
}
}

pub async fn start<'a>(args: StartArgs<'_>) -> Result<(), CliError> {
pub async fn start<'a>(
args: &Args,
fresh_state: bool,
config_arg: &Option<String>,
) -> Result<(), CliError> {
env_logger::init();

let mut state = if args.fresh_state {
let mut state = if fresh_state {
let is_paid = services::CloudflareTunnel::use_paid_tunnels();
let state = load_and_save_state(args.config_arg, args.no_tunnel, is_paid)?;
let state = load_and_save_state(config_arg, args.no_tunnel, is_paid)?;
set_linkup_env(state.clone())?;

state
Expand Down
Loading
Loading