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

feat(cargo-shuttle): automatic login via console, login --prompt #1913

Merged
merged 12 commits into from
Nov 8, 2024
363 changes: 190 additions & 173 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ strum = { version = "0.26.1", features = ["derive"] }
tar = "0.4.38"
tempfile = "3.4.0"
test-context = "0.3.0"
thiserror = "1.0.37"
thiserror = "2"
tokio = "1.22.0"
tokio-stream = "0.1.11"
tokio-tungstenite = { version = "0.20.1", features = [
Expand Down
4 changes: 2 additions & 2 deletions admin/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ use std::{fs, io, path::PathBuf};

use clap::{Error, Parser, Subcommand};
use shuttle_common::{
constants::API_URL_PRODUCTION,
constants::API_URL_RS,
models::{project::ComputeTier, user::UserId},
};

#[derive(Parser, Debug)]
pub struct Args {
/// run this command against the api at the supplied url
#[arg(long, default_value = API_URL_PRODUCTION, env = "SHUTTLE_API")]
#[arg(long, default_value = API_URL_RS, env = "SHUTTLE_API")]
pub api_url: String,

#[command(subcommand)]
Expand Down
15 changes: 12 additions & 3 deletions api-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ impl ShuttleApiClient {
.context("parsing API version info")
}

pub async fn get_device_auth_ws(&self) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
self.ws_get("/device-auth/ws".to_owned())
.await
.with_context(|| "failed to connect to auth endpoint")
}

pub async fn check_project_name(&self, project_name: &str) -> Result<bool> {
let url = format!("{}/projects/name/{project_name}", self.api_url);

Expand Down Expand Up @@ -483,14 +489,17 @@ impl ShuttleApiClient {
pub async fn ws_get(&self, path: String) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
let ws_url = self.api_url.clone().replace("http", "ws");
let url = format!("{ws_url}{path}");
let mut request = url.into_client_request()?;
let mut req = url.into_client_request()?;

#[cfg(feature = "tracing")]
debug!("WS Request: {} {}", req.method(), req.uri());

if let Some(ref api_key) = self.api_key {
let auth_header = Authorization::bearer(api_key.as_ref())?;
request.headers_mut().typed_insert(auth_header);
req.headers_mut().typed_insert(auth_header);
}

let (stream, _) = connect_async(request).await.with_context(|| {
let (stream, _) = connect_async(req).await.with_context(|| {
#[cfg(feature = "tracing")]
error!("failed to connect to websocket");
"could not connect to websocket"
Expand Down
4 changes: 2 additions & 2 deletions api-client/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ impl ToJson for reqwest::Response {
.unwrap_or_else(|_| format!("[{} bytes]", bytes.len()));

#[cfg(feature = "tracing")]
tracing::trace!(response = %string, "Parsing response to JSON");
tracing::trace!(response = %string, "Parsing response as JSON");

if matches!(
status_code,
Expand All @@ -28,7 +28,7 @@ impl ToJson for reqwest::Response {
serde_json::from_str(&string).context("failed to parse a successful response")
} else {
#[cfg(feature = "tracing")]
tracing::trace!("Parsing response to common error");
tracing::trace!("Parsing response as API error");

let res: ApiError = match serde_json::from_str(&string) {
Ok(res) => res,
Expand Down
9 changes: 7 additions & 2 deletions cargo-shuttle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@ dunce = { workspace = true }
flate2 = { workspace = true }
futures = { workspace = true }
git2 = { version = "0.19.0", default-features = false }
gix = { version = "0.66.0", default-features = false, features = [
gix = { version = "0.67.0", default-features = false, features = [
"blocking-http-transport-reqwest-rust-tls",
"worktree-mutation",
] }
globset = "0.4.13"
headers = { workspace = true }
home = { workspace = true }
# TODO: make local provisioner server use these:
# http = "1"
# http-body-util = "0.1"
# hyper-1 = { package = "hyper", version = "1.0", features = ["full"] }
# hyper-util = { version = "0.1.10", features = ["full"] }
hyper = { workspace = true }
ignore = "0.4.20"
indicatif = "0.17.3"
Expand Down Expand Up @@ -69,6 +74,6 @@ zip = "2.2.0"

[dev-dependencies]
assert_cmd = "2.0.6"
rexpect = "0.5.0"
rexpect = "0.6.0"
# Publication of this crate will fail if this is changed to a workspace dependency
shuttle-common-tests = { path = "../common-tests" }
32 changes: 20 additions & 12 deletions cargo-shuttle/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ use clap::{
Args, Parser, Subcommand, ValueEnum,
};
use clap_complete::Shell;
use shuttle_common::constants::{DEFAULT_IDLE_MINUTES, EXAMPLES_REPO};
use shuttle_common::constants::{DEFAULT_IDLE_MINUTES, EXAMPLES_REPO, SHUTTLE_CONSOLE_URL};
use shuttle_common::resource;

#[derive(Parser)]
#[command(
version,
next_help_heading = "Global options",
// Cargo passes in the subcommand name to the invoked executable. Use a
// hidden, optional positional argument to deal with it.
arg(clap::Arg::new("dummy")
Expand All @@ -26,10 +27,8 @@ use shuttle_common::resource;
.hide(true))
)]
pub struct ShuttleArgs {
#[command(flatten)]
pub project_args: ProjectArgs,
/// URL for the Shuttle API to target (mainly for development)
#[arg(global = true, long, env = "SHUTTLE_API")]
#[arg(global = true, long, env = "SHUTTLE_API", hide = true)]
pub api_url: Option<String>,
/// Disable network requests that are not strictly necessary. Limits some features.
#[arg(global = true, long, env = "SHUTTLE_OFFLINE")]
Expand All @@ -40,6 +39,8 @@ pub struct ShuttleArgs {
/// Target Shuttle's development environment
#[arg(global = true, long, env = "SHUTTLE_BETA", hide = true)]
pub beta: bool,
#[command(flatten)]
pub project_args: ProjectArgs,

#[command(subcommand)]
pub cmd: Command,
Expand All @@ -51,7 +52,7 @@ pub struct ProjectArgs {
/// Specify the working directory
#[arg(global = true, long, visible_alias = "wd", default_value = ".", value_parser = OsStringValueParser::new().try_map(parse_path))]
pub working_directory: PathBuf,
/// Specify the name or id of the project (overrides crate name)
/// Specify the name or id of the project
#[arg(global = true, long = "name", visible_alias = "id")]
// in alpha mode, this is always a name
pub name_or_id: Option<String>,
Expand Down Expand Up @@ -93,10 +94,9 @@ impl ProjectArgs {
}
}

/// A cargo command for the Shuttle platform (https://www.shuttle.rs/)
/// CLI for the Shuttle platform (https://www.shuttle.dev/)
///
/// See the CLI docs (https://docs.shuttle.rs/getting-started/shuttle-commands)
/// for more information.
/// See the CLI docs for more information: https://docs.shuttle.dev/guides/cli
#[derive(Subcommand)]
pub enum Command {
/// Generate a Shuttle project from a template
Expand Down Expand Up @@ -160,6 +160,7 @@ pub enum GenerateCommand {
}

#[derive(Args)]
#[command(next_help_heading = "Table options")]
pub struct TableArgs {
/// Output tables without borders
#[arg(long, default_value_t = false)]
Expand Down Expand Up @@ -197,12 +198,12 @@ pub enum ResourceCommand {
/// List the resources for a project
#[command(visible_alias = "ls")]
List {
#[command(flatten)]
table: TableArgs,

/// Show secrets from resources (e.g. a password in a connection string)
#[arg(long, default_value_t = false)]
show_secrets: bool,

#[command(flatten)]
table: TableArgs,
},
/// Delete a resource
#[command(visible_alias = "rm")]
Expand Down Expand Up @@ -307,10 +308,17 @@ pub struct ProjectStartArgs {
}

#[derive(Args, Clone, Debug, Default)]
#[command(next_help_heading = "Login options")]
pub struct LoginArgs {
/// API key for the Shuttle platform
/// Prompt to paste the API key instead of opening the browser
#[arg(long, conflicts_with = "api_key", alias = "input")]
pub prompt: bool,
/// Log in with this Shuttle API key
#[arg(long)]
pub api_key: Option<String>,
/// URL to the Shuttle Console for automatic login
#[arg(long, env = "SHUTTLE_CONSOLE", default_value = SHUTTLE_CONSOLE_URL, hide_default_value = true)]
pub console_url: String,
}

#[derive(Args, Clone, Debug)]
Expand Down
4 changes: 2 additions & 2 deletions cargo-shuttle/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use shuttle_common::constants::API_URL_BETA;
use shuttle_common::constants::API_URL_DEFAULT;
use shuttle_common::constants::API_URL_DEFAULT_BETA;
use tracing::trace;

use crate::args::ProjectArgs;
Expand Down Expand Up @@ -423,7 +423,7 @@ impl RequestContext {
} else if let Some(api_url) = self.global.as_ref().unwrap().api_url() {
api_url
} else if beta {
API_URL_BETA.to_string()
API_URL_DEFAULT_BETA.to_string()
} else {
API_URL_DEFAULT.to_string()
}
Expand Down
Loading