diff --git a/cargo-shuttle/Cargo.toml b/cargo-shuttle/Cargo.toml index df95b5e24..bd541b49c 100644 --- a/cargo-shuttle/Cargo.toml +++ b/cargo-shuttle/Cargo.toml @@ -4,7 +4,7 @@ version = "0.43.0" edition.workspace = true license.workspace = true repository.workspace = true -description = "A cargo command for the shuttle platform (https://www.shuttle.rs/)" +description = "A cargo command for the Shuttle platform (https://www.shuttle.rs/)" homepage = "https://www.shuttle.rs" [dependencies] @@ -32,10 +32,10 @@ gix = { version = "0.55.2", default-features = false, features = [ "worktree-mutation", ] } globset = "0.4.13" -home = { workspace = true } headers = { workspace = true } -indicatif = "0.17.3" +home = { workspace = true } ignore = "0.4.20" +indicatif = "0.17.3" indoc = "2.0.1" percent-encoding = { workspace = true } portpicker = { workspace = true } diff --git a/cargo-shuttle/src/args.rs b/cargo-shuttle/src/args.rs index 705583507..60b31e89f 100644 --- a/cargo-shuttle/src/args.rs +++ b/cargo-shuttle/src/args.rs @@ -39,6 +39,9 @@ pub struct ShuttleArgs { /// Turn on tracing output for cargo-shuttle and shuttle libraries. #[arg(long, env = "SHUTTLE_DEBUG")] pub debug: bool, + /// Target Shuttle's development environment + #[arg(long, env = "SHUTTLE_BETA")] + pub beta: bool, #[command(subcommand)] pub cmd: Command, @@ -158,7 +161,7 @@ pub enum DeploymentCommand { limit: u32, #[arg(long, default_value_t = false)] - /// Output table in `raw` format + /// Output table without borders raw: bool, }, /// View status of a deployment @@ -173,7 +176,7 @@ pub enum ResourceCommand { /// List all the resources for a project List { #[arg(long, default_value_t = false)] - /// Output table in `raw` format + /// Output table without borders raw: bool, #[arg( @@ -219,7 +222,7 @@ pub enum ProjectCommand { limit: u32, #[arg(long, default_value_t = false)] - /// Output table in `raw` format + /// Output table without borders raw: bool, }, /// Delete a project and all linked data diff --git a/cargo-shuttle/src/client.rs b/cargo-shuttle/src/client.rs index 02fed8a17..91d01a110 100644 --- a/cargo-shuttle/src/client.rs +++ b/cargo-shuttle/src/client.rs @@ -11,8 +11,7 @@ use serde::{Deserialize, Serialize}; use shuttle_common::constants::headers::X_CARGO_SHUTTLE_VERSION; use shuttle_common::models::deployment::DeploymentRequest; use shuttle_common::models::{deployment, project, service, ToJson}; -use shuttle_common::secrets::Secret; -use shuttle_common::{resource, ApiKey, ApiUrl, LogItem, VersionInfo}; +use shuttle_common::{resource, ApiKey, LogItem, VersionInfo}; use tokio::net::TcpStream; use tokio_tungstenite::tungstenite::client::IntoClientRequest; use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream}; @@ -20,17 +19,17 @@ use tracing::error; use uuid::Uuid; #[derive(Clone)] -pub struct Client { - api_url: ApiUrl, - api_key: Option>, +pub struct ShuttleApiClient { client: reqwest::Client, + api_url: String, + api_key: Option, + /// alter behaviour to interact with the new platform + beta: bool, } -impl Client { - pub fn new(api_url: ApiUrl) -> Self { +impl ShuttleApiClient { + pub fn new(api_url: String, api_key: Option, beta: bool) -> Self { Self { - api_url, - api_key: None, client: reqwest::Client::builder() .default_headers( HeaderMap::try_from(&HashMap::from([( @@ -42,11 +41,22 @@ impl Client { .timeout(Duration::from_secs(60)) .build() .unwrap(), + api_url, + api_key, + beta, } } pub fn set_api_key(&mut self, api_key: ApiKey) { - self.api_key = Some(Secret::new(api_key)); + self.api_key = Some(api_key); + } + + fn set_auth_bearer(&self, builder: RequestBuilder) -> RequestBuilder { + if let Some(ref api_key) = self.api_key { + builder.bearer_auth(api_key.as_ref()) + } else { + builder + } } pub async fn get_api_versions(&self) -> Result { @@ -79,13 +89,17 @@ impl Client { project: &str, deployment_req: DeploymentRequest, ) -> Result { - let path = format!("/projects/{project}/services/{project}"); + let path = if self.beta { + format!("/projects/{project}") + } else { + format!("/projects/{project}/services/{project}") + }; let deployment_req = rmp_serde::to_vec(&deployment_req) .context("serialize DeploymentRequest as a MessagePack byte vector")?; let url = format!("{}{}", self.api_url, path); let mut builder = self.client.post(url); - builder = self.set_builder_auth(builder); + builder = self.set_auth_bearer(builder); builder .header("Transfer-Encoding", "chunked") @@ -161,10 +175,8 @@ impl Client { self.get(path).await } - pub async fn get_projects_list(&self, page: u32, limit: u32) -> Result> { - let path = format!("/projects?page={}&limit={}", page.saturating_sub(1), limit); - - self.get(path).await + pub async fn get_projects_list(&self) -> Result> { + self.get("/projects".to_owned()).await } pub async fn stop_project(&self, project: &str) -> Result { @@ -174,7 +186,11 @@ impl Client { } pub async fn delete_project(&self, project: &str) -> Result { - let path = format!("/projects/{project}/delete"); + let path = if self.beta { + format!("/projects/{project}") + } else { + format!("/projects/{project}/delete") + }; self.delete(path).await } @@ -228,12 +244,12 @@ impl Client { } async fn ws_get(&self, path: String) -> Result>> { - let ws_scheme = self.api_url.clone().replace("http", "ws"); - let url = format!("{ws_scheme}{path}"); + let ws_url = self.api_url.clone().replace("http", "ws"); + let url = format!("{ws_url}{path}"); let mut request = url.into_client_request()?; if let Some(ref api_key) = self.api_key { - let auth_header = Authorization::bearer(api_key.expose().as_ref())?; + let auth_header = Authorization::bearer(api_key.as_ref())?; request.headers_mut().typed_insert(auth_header); } @@ -252,8 +268,7 @@ impl Client { let url = format!("{}{}", self.api_url, path); let mut builder = self.client.get(url); - - builder = self.set_builder_auth(builder); + builder = self.set_auth_bearer(builder); builder .send() @@ -267,8 +282,7 @@ impl Client { let url = format!("{}{}", self.api_url, path); let mut builder = self.client.post(url); - - builder = self.set_builder_auth(builder); + builder = self.set_auth_bearer(builder); if let Some(body) = body { let body = serde_json::to_string(&body)?; @@ -283,8 +297,7 @@ impl Client { let url = format!("{}{}", self.api_url, path); let mut builder = self.client.put(url); - - builder = self.set_builder_auth(builder); + builder = self.set_auth_bearer(builder); if let Some(body) = body { let body = serde_json::to_string(&body)?; @@ -302,8 +315,7 @@ impl Client { let url = format!("{}{}", self.api_url, path); let mut builder = self.client.delete(url); - - builder = self.set_builder_auth(builder); + builder = self.set_auth_bearer(builder); builder .send() @@ -312,12 +324,4 @@ impl Client { .to_json() .await } - - fn set_builder_auth(&self, builder: RequestBuilder) -> RequestBuilder { - if let Some(ref api_key) = self.api_key { - builder.bearer_auth(api_key.expose().as_ref()) - } else { - builder - } - } } diff --git a/cargo-shuttle/src/config.rs b/cargo-shuttle/src/config.rs index 73d390d10..7d640081b 100644 --- a/cargo-shuttle/src/config.rs +++ b/cargo-shuttle/src/config.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; -use shuttle_common::{constants::API_URL_DEFAULT, ApiKey, ApiUrl}; +use shuttle_common::{constants::API_URL_DEFAULT, ApiKey}; use tracing::trace; use crate::args::ProjectArgs; @@ -122,7 +122,7 @@ impl ConfigManager for LocalConfigManager { #[derive(Deserialize, Serialize, Default)] pub struct GlobalConfig { api_key: Option, - pub api_url: Option, + pub api_url: Option, } impl GlobalConfig { @@ -138,7 +138,7 @@ impl GlobalConfig { self.api_key = None; } - pub fn api_url(&self) -> Option { + pub fn api_url(&self) -> Option { self.api_url.clone() } } @@ -315,7 +315,7 @@ impl RequestContext { self.api_url = api_url; } - pub fn api_url(&self) -> ApiUrl { + pub fn api_url(&self) -> String { if let Some(api_url) = self.api_url.clone() { api_url } else if let Some(api_url) = self.global.as_ref().unwrap().api_url() { diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index 23b2dbb08..81d3758f1 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -75,7 +75,7 @@ use crate::args::{ DeployArgs, DeploymentCommand, InitArgs, LoginArgs, LogoutArgs, LogsArgs, ProjectCommand, ProjectStartArgs, ResourceCommand, TemplateLocation, }; -use crate::client::Client; +use crate::client::ShuttleApiClient; use crate::provisioner_server::LocalProvisioner; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -96,9 +96,11 @@ pub fn parse_args() -> (ShuttleArgs, bool) { pub struct Shuttle { ctx: RequestContext, - client: Option, + client: Option, version_info: Option, version_warnings: Vec, + /// alter behaviour to interact with the new platform + beta: bool, } impl Shuttle { @@ -109,40 +111,36 @@ impl Shuttle { client: None, version_info: None, version_warnings: vec![], + beta: false, }) } - fn find_available_port(run_args: &mut RunArgs, services_len: usize) { - let default_port = run_args.port; - 'outer: for port in (run_args.port..=std::u16::MAX).step_by(services_len.max(10)) { - for inner_port in port..(port + services_len as u16) { - if !portpicker::is_free_tcp(inner_port) { - continue 'outer; - } - } - run_args.port = port; - break; - } - - if run_args.port != default_port - && !Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt(format!( - "Port {} is already in use. Would you like to continue on port {}?", - default_port, run_args.port - )) - .default(true) - .interact() - .unwrap() - { - exit(0); - } - } - pub async fn run( mut self, args: ShuttleArgs, provided_path_to_init: bool, ) -> Result { + self.beta = args.beta; + if self.beta { + if matches!( + args.cmd, + Command::Project(ProjectCommand::Stop { .. } | ProjectCommand::Restart { .. }) + ) { + unimplemented!("This command is deprecated on the beta platform"); + } + if matches!( + args.cmd, + Command::Deployment(..) + | Command::Resource(..) + | Command::Stop + | Command::Clean + | Command::Status + | Command::Logs { .. } + ) { + unimplemented!("This command is not yet implemented on the beta platform"); + } + eprintln!("INFO: Using beta platform API"); + } if let Some(ref url) = args.api_url { if url != API_URL_DEFAULT { eprintln!("INFO: Targeting non-standard API: {url}"); @@ -190,13 +188,11 @@ impl Shuttle { | Command::Clean | Command::Project(..) ) { - let mut client = Client::new(self.ctx.api_url()); - if !matches!(args.cmd, Command::Init(..)) { - // init command will handle this by itself (log in and set key) if there is no key yet - client.set_api_key(self.ctx.api_key()?); - } + let client = + ShuttleApiClient::new(self.ctx.api_url(), self.ctx.api_key().ok(), self.beta); self.client = Some(client); - if !args.offline { + if !args.offline && !self.beta { + // TODO: re-implement version checking in control to use the c-s version http header self.check_api_versions().await?; } } @@ -244,9 +240,7 @@ impl Shuttle { Command::Project(ProjectCommand::Status { follow }) => { self.project_status(follow).await } - Command::Project(ProjectCommand::List { page, limit, raw }) => { - self.projects_list(page, limit, raw).await - } + Command::Project(ProjectCommand::List { raw, .. }) => self.projects_list(raw).await, Command::Project(ProjectCommand::Stop) => self.project_stop().await, Command::Project(ProjectCommand::Delete(ConfirmationArgs { yes })) => { self.project_delete(yes).await @@ -1392,6 +1386,32 @@ impl Shuttle { build_workspace(working_directory, run_args.release, tx, false).await } + fn find_available_port(run_args: &mut RunArgs, services_len: usize) { + let default_port = run_args.port; + 'outer: for port in (run_args.port..=std::u16::MAX).step_by(services_len.max(10)) { + for inner_port in port..(port + services_len as u16) { + if !portpicker::is_free_tcp(inner_port) { + continue 'outer; + } + } + run_args.port = port; + break; + } + + if run_args.port != default_port + && !Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "Port {} is already in use. Would you like to continue on port {}?", + default_port, run_args.port + )) + .default(true) + .interact() + .unwrap() + { + exit(0); + } + } + #[cfg(target_family = "unix")] async fn local_run(&self, mut run_args: RunArgs) -> Result { debug!("starting local run"); @@ -1920,7 +1940,7 @@ impl Shuttle { ) })?; - if idle_minutes > 0 { + if idle_minutes > 0 && !self.beta { let idle_msg = format!( "Your project will sleep if it is idle for {} minutes.", idle_minutes @@ -1946,15 +1966,10 @@ impl Shuttle { Ok(CommandOutcome::Ok) } - async fn projects_list(&self, page: u32, limit: u32, raw: bool) -> Result { + async fn projects_list(&self, raw: bool) -> Result { let client = self.client.as_ref().unwrap(); - if limit == 0 { - println!(); - return Ok(CommandOutcome::Ok); - } - let limit = limit + 1; - let mut projects = client.get_projects_list(page, limit).await.map_err(|err| { + let projects = client.get_projects_list().await.map_err(|err| { suggestions::project::project_request_failure( err, "Getting projects list failed", @@ -1962,13 +1977,7 @@ impl Shuttle { "getting the projects list fails repeatedly", ) })?; - let page_hint = if projects.len() == limit as usize { - projects.pop(); - true - } else { - false - }; - let projects_table = project::get_projects_table(&projects, page, raw, page_hint); + let projects_table = project::get_projects_table(&projects, raw); println!("{projects_table}"); diff --git a/cargo-shuttle/tests/integration/main.rs b/cargo-shuttle/tests/integration/main.rs index fd16de68a..60516f6a7 100644 --- a/cargo-shuttle/tests/integration/main.rs +++ b/cargo-shuttle/tests/integration/main.rs @@ -22,6 +22,7 @@ async fn cargo_shuttle_command( }, offline: false, debug: false, + beta: false, cmd, }, false, diff --git a/common-tests/src/cargo_shuttle.rs b/common-tests/src/cargo_shuttle.rs index 1cfe17d22..9414f6142 100644 --- a/common-tests/src/cargo_shuttle.rs +++ b/common-tests/src/cargo_shuttle.rs @@ -43,6 +43,7 @@ pub async fn cargo_shuttle_run(working_directory: &str, external: bool) -> Strin }, offline: false, debug: false, + beta: false, cmd: Command::Run(run_args), }, false, diff --git a/common/src/lib.rs b/common/src/lib.rs index c08f3ecc8..1cfd71381 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -29,9 +29,6 @@ use anyhow::bail; use serde::{Deserialize, Serialize}; use zeroize::Zeroize; -pub type ApiUrl = String; -pub type Host = String; - #[derive(Clone, Serialize, Deserialize)] #[cfg_attr(feature = "persist", derive(PartialEq, Eq, Hash, sqlx::Type))] #[cfg_attr(feature = "persist", serde(transparent))] diff --git a/common/src/models/project.rs b/common/src/models/project.rs index b3f423640..5eacf563a 100644 --- a/common/src/models/project.rs +++ b/common/src/models/project.rs @@ -164,19 +164,9 @@ pub struct Config { pub idle_minutes: u64, } -pub fn get_projects_table( - projects: &Vec, - page: u32, - raw: bool, - page_hint: bool, -) -> String { +pub fn get_projects_table(projects: &Vec, raw: bool) -> String { if projects.is_empty() { - // The page starts at 1 in the CLI. - let mut s = if page <= 1 { - "No projects are linked to this account\n".to_string() - } else { - "No more projects are linked to this account\n".to_string() - }; + let mut s = "No projects are linked to this account\n".to_string(); if !raw { s = s.yellow().bold().to_string(); } @@ -222,14 +212,6 @@ pub fn get_projects_table( } } - let formatted_table = format!("\nThese projects are linked to this account\n{table}\n"); - if page_hint { - format!( - "{formatted_table}More projects are available on the next page using `--page {}`\n", - page + 1 - ) - } else { - formatted_table - } + format!("\nThese projects are linked to this account\n{table}\n") } }