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: fix ProjectName validation, custom Path extractor for parsing it #1354

Merged
merged 17 commits into from Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Expand Up @@ -84,7 +84,7 @@ rmp-serde = "1.1.1"
semver = { version = "1.0.17", features = ["serde"] }
serde = { version = "1.0.148", default-features = false }
serde_json = "1.0.89"
sqlx = "0.7.1"
sqlx = { version = "0.7.1", features = ["runtime-tokio", "tls-rustls"] }
strfmt = "0.2.2"
strum = { version = "0.24.1", features = ["derive"] }
tempfile = "3.4.0"
Expand Down
9 changes: 2 additions & 7 deletions auth/Cargo.toml
Expand Up @@ -7,6 +7,7 @@ repository.workspace = true

[dependencies]
anyhow = { workspace = true }
async-stripe = { version = "0.25.1", default-features = false, features = ["checkout", "runtime-tokio-hyper-rustls"] }
async-trait = { workspace = true }
axum = { workspace = true, features = ["headers"] }
axum-sessions = { workspace = true }
Expand All @@ -17,13 +18,7 @@ opentelemetry = { workspace = true }
rand = { workspace = true }
ring = { workspace = true }
serde = { workspace = true, features = ["derive"] }
sqlx = { workspace = true, features = [
"sqlite",
"json",
"runtime-tokio-rustls",
"migrate",
] }
async-stripe = { version = "0.25.1", default-features = false, features = ["checkout", "runtime-tokio-hyper-rustls"] }
sqlx = { workspace = true, features = ["sqlite", "json", "migrate"] }
strum = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["full"] }
Expand Down
20 changes: 6 additions & 14 deletions auth/src/error.rs
@@ -1,8 +1,7 @@
use std::error::Error as StdError;

use axum::http::{header, HeaderValue, StatusCode};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;

use serde::{ser::SerializeMap, Serialize};
use shuttle_common::models::error::ApiError;
Expand Down Expand Up @@ -57,17 +56,10 @@ impl IntoResponse for Error {
_ => StatusCode::INTERNAL_SERVER_ERROR,
};

(
code,
[(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
)],
Json(ApiError {
message: self.to_string(),
status_code: code.as_u16(),
}),
)
.into_response()
ApiError {
message: self.to_string(),
status_code: code.as_u16(),
}
.into_response()
}
}
11 changes: 5 additions & 6 deletions cargo-shuttle/src/args.rs
Expand Up @@ -12,7 +12,7 @@ use clap::{
Parser, ValueEnum,
};
use clap_complete::Shell;
use shuttle_common::{models::project::DEFAULT_IDLE_MINUTES, project::ProjectName, resource};
use shuttle_common::{models::project::DEFAULT_IDLE_MINUTES, resource};
use uuid::Uuid;

#[derive(Parser)]
Expand Down Expand Up @@ -44,7 +44,7 @@ pub struct ProjectArgs {
pub working_directory: PathBuf,
/// Specify the name of the project (overrides crate name)
#[arg(global = true, long)]
pub name: Option<ProjectName>,
pub name: Option<String>,
}

impl ProjectArgs {
Expand All @@ -60,7 +60,7 @@ impl ProjectArgs {
Ok(path)
}

pub fn project_name(&self) -> anyhow::Result<ProjectName> {
pub fn project_name(&self) -> anyhow::Result<String> {
let workspace_path = self.workspace_path()?;

// NOTE: If crates cache is missing this blocks for several seconds during download
Expand All @@ -69,15 +69,14 @@ impl ProjectArgs {
.exec()
.context("failed to get cargo metadata")?;
let package_name = if let Some(root_package) = meta.root_package() {
root_package.name.clone().parse()?
root_package.name.clone()
} else {
workspace_path
.file_name()
.context("failed to get project name from workspace path")?
.to_os_string()
.into_string()
.expect("workspace file name should be valid unicode")
.parse()?
.expect("workspace directory name should be valid unicode")
};

Ok(package_name)
Expand Down
111 changes: 34 additions & 77 deletions cargo-shuttle/src/client.rs
Expand Up @@ -8,7 +8,6 @@ use reqwest_retry::RetryTransientMiddleware;
use serde::{Deserialize, Serialize};
use shuttle_common::models::deployment::DeploymentRequest;
use shuttle_common::models::{deployment, project, secret, service, ToJson};
use shuttle_common::project::ProjectName;
use shuttle_common::secrets::Secret;
use shuttle_common::{resource, ApiKey, ApiUrl, LogItem, VersionInfo};
use tokio::net::TcpStream;
Expand Down Expand Up @@ -57,28 +56,25 @@ impl Client {
.context("parsing API version info")
}

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

self.client
.get(url)
.send()
.await?
.json()
.await
.context("failed to check project name availability")?
.to_json()
.await
.context("parsing name check response")
}

pub async fn deploy(
&self,
project: &ProjectName,
project: &str,
deployment_req: DeploymentRequest,
) -> Result<deployment::Response> {
let path = format!(
"/projects/{}/services/{}",
project.as_str(),
project.as_str()
);
let path = format!("/projects/{project}/services/{project}");
let deployment_req = rmp_serde::to_vec(&deployment_req)
.context("serialize DeploymentRequest as a MessagePack byte vector")?;

Expand All @@ -96,48 +92,31 @@ impl Client {
.await
}

pub async fn stop_service(&self, project: &ProjectName) -> Result<service::Summary> {
let path = format!(
"/projects/{}/services/{}",
project.as_str(),
project.as_str()
);
pub async fn stop_service(&self, project: &str) -> Result<service::Summary> {
let path = format!("/projects/{project}/services/{project}");

self.delete(path).await
}

pub async fn get_service(&self, project: &ProjectName) -> Result<service::Summary> {
let path = format!(
"/projects/{}/services/{}",
project.as_str(),
project.as_str()
);
pub async fn get_service(&self, project: &str) -> Result<service::Summary> {
let path = format!("/projects/{project}/services/{project}");

self.get(path).await
}

pub async fn get_service_resources(
&self,
project: &ProjectName,
) -> Result<Vec<resource::Response>> {
let path = format!(
"/projects/{}/services/{}/resources",
project.as_str(),
project.as_str(),
);
pub async fn get_service_resources(&self, project: &str) -> Result<Vec<resource::Response>> {
let path = format!("/projects/{project}/services/{project}/resources");

self.get(path).await
}

pub async fn delete_service_resource(
&self,
project: &ProjectName,
project: &str,
resource_type: &resource::Type,
) -> Result<()> {
let path = format!(
"/projects/{}/services/{}/resources/{}",
project.as_str(),
project.as_str(),
"/projects/{project}/services/{project}/resources/{}",
utf8_percent_encode(
&resource_type.to_string(),
percent_encoding::NON_ALPHANUMERIC
Expand All @@ -149,10 +128,10 @@ impl Client {

pub async fn create_project(
&self,
project: &ProjectName,
project: &str,
config: &project::Config,
) -> Result<project::Response> {
let path = format!("/projects/{}", project.as_str());
let path = format!("/projects/{project}");

self.post(path, Some(config))
.await
Expand All @@ -161,8 +140,8 @@ impl Client {
.await
}

pub async fn clean_project(&self, project: &ProjectName) -> Result<Vec<String>> {
let path = format!("/projects/{}/clean", project.as_str(),);
pub async fn clean_project(&self, project: &str) -> Result<Vec<String>> {
let path = format!("/projects/{project}/clean");

self.post(path, Option::<String>::None)
.await
Expand All @@ -171,8 +150,8 @@ impl Client {
.await
}

pub async fn get_project(&self, project: &ProjectName) -> Result<project::Response> {
let path = format!("/projects/{}", project.as_str());
pub async fn get_project(&self, project: &str) -> Result<project::Response> {
let path = format!("/projects/{project}");

self.get(path).await
}
Expand All @@ -183,42 +162,29 @@ impl Client {
self.get(path).await
}

pub async fn stop_project(&self, project: &ProjectName) -> Result<project::Response> {
let path = format!("/projects/{}", project.as_str());
pub async fn stop_project(&self, project: &str) -> Result<project::Response> {
let path = format!("/projects/{project}");

self.delete(path).await
}

pub async fn delete_project(&self, project: &ProjectName, dry_run: bool) -> Result<String> {
pub async fn delete_project(&self, project: &str, dry_run: bool) -> Result<String> {
let path = format!(
"/projects/{}/delete{}",
project.as_str(),
"/projects/{project}/delete{}",
if dry_run { "?dry_run=true" } else { "" }
);

self.delete(path).await
}

pub async fn get_secrets(&self, project: &ProjectName) -> Result<Vec<secret::Response>> {
let path = format!(
"/projects/{}/secrets/{}",
project.as_str(),
project.as_str()
);
pub async fn get_secrets(&self, project: &str) -> Result<Vec<secret::Response>> {
let path = format!("/projects/{project}/secrets/{project}");

self.get(path).await
}

pub async fn get_logs(
&self,
project: &ProjectName,
deployment_id: &Uuid,
) -> Result<Vec<LogItem>> {
let path = format!(
"/projects/{}/deployments/{}/logs",
project.as_str(),
deployment_id
);
pub async fn get_logs(&self, project: &str, deployment_id: &Uuid) -> Result<Vec<LogItem>> {
let path = format!("/projects/{project}/deployments/{deployment_id}/logs");

self.get(path)
.await
Expand All @@ -227,27 +193,22 @@ impl Client {

pub async fn get_logs_ws(
&self,
project: &ProjectName,
project: &str,
deployment_id: &Uuid,
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
let path = format!(
"/projects/{}/ws/deployments/{}/logs",
project.as_str(),
deployment_id
);
let path = format!("/projects/{project}/ws/deployments/{deployment_id}/logs");

self.ws_get(path).await
}

pub async fn get_deployments(
&self,
project: &ProjectName,
project: &str,
page: u32,
limit: u32,
) -> Result<Vec<deployment::Response>> {
let path = format!(
"/projects/{}/deployments?page={}&limit={}",
project.as_str(),
"/projects/{project}/deployments?page={}&limit={}",
page.saturating_sub(1),
limit,
);
Expand All @@ -257,14 +218,10 @@ impl Client {

pub async fn get_deployment_details(
&self,
project: &ProjectName,
project: &str,
deployment_id: &Uuid,
) -> Result<deployment::Response> {
let path = format!(
"/projects/{}/deployments/{}",
project.as_str(),
deployment_id
);
let path = format!("/projects/{project}/deployments/{deployment_id}");

self.get(path).await
}
Expand Down
13 changes: 6 additions & 7 deletions cargo-shuttle/src/config.rs
Expand Up @@ -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, project::ProjectName, ApiKey, ApiUrl};
use shuttle_common::{constants::API_URL_DEFAULT, ApiKey, ApiUrl};
use tracing::trace;

use crate::args::ProjectArgs;
Expand Down Expand Up @@ -146,7 +146,7 @@ impl GlobalConfig {
/// Project-local config for things like customizing project name
#[derive(Deserialize, Serialize, Default)]
pub struct ProjectConfig {
pub name: Option<ProjectName>,
pub name: Option<String>,
pub assets: Option<Vec<String>>,
}

Expand Down Expand Up @@ -374,7 +374,7 @@ impl RequestContext {
///
/// # Panics
/// Panics if the project configuration has not been loaded.
pub fn project_name(&self) -> &ProjectName {
pub fn project_name(&self) -> &str {
self.project
.as_ref()
.unwrap()
Expand All @@ -383,6 +383,7 @@ impl RequestContext {
.name
.as_ref()
.unwrap()
.as_str()
}

/// # Panics
Expand All @@ -400,9 +401,7 @@ impl RequestContext {

#[cfg(test)]
mod tests {
use std::{path::PathBuf, str::FromStr};

use shuttle_common::project::ProjectName;
use std::path::PathBuf;

use crate::{args::ProjectArgs, config::RequestContext};

Expand Down Expand Up @@ -446,7 +445,7 @@ mod tests {
fn setting_name_overrides_name_in_config() {
let project_args = ProjectArgs {
working_directory: path_from_workspace_root("examples/axum/hello-world/"),
name: Some(ProjectName::from_str("my-fancy-project-name").unwrap()),
name: Some("my-fancy-project-name".to_owned()),
};

let local_config = RequestContext::get_local_config(&project_args).unwrap();
Expand Down