diff --git a/.circleci/config.yml b/.circleci/config.yml index 38db85504..69c998910 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -634,9 +634,10 @@ workflows: parameters: crate: # - shuttle-admin # no tests + - shuttle-backends - shuttle-codegen - shuttle-common - # - shuttle-common-tests # no tests + # - shuttle-gateway # TODO: move docker-dependant tests to integration tests and activate this - test-workspace-member-with-integration: name: << matrix.crate >> matrix: diff --git a/Cargo.lock b/Cargo.lock index bca412057..5c393aff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5207,6 +5207,7 @@ dependencies = [ "ring 0.17.8", "serde", "serde_json", + "shuttle-backends", "shuttle-common", "shuttle-common-tests", "sqlx", @@ -5220,6 +5221,51 @@ dependencies = [ "wiremock", ] +[[package]] +name = "shuttle-backends" +version = "0.42.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "base64 0.21.7", + "bytes", + "headers", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", + "jsonwebtoken", + "opentelemetry 0.21.0", + "opentelemetry-appender-tracing", + "opentelemetry-http 0.10.0", + "opentelemetry-otlp", + "opentelemetry_sdk", + "pin-project", + "portpicker", + "reqwest", + "ring 0.17.8", + "rustrict", + "serde", + "serde_json", + "serial_test", + "shuttle-common", + "shuttle-proto", + "sqlx", + "strum 0.26.1", + "test-context 0.3.0", + "thiserror", + "tokio", + "tonic 0.10.2", + "tower", + "tower-http 0.4.4", + "tracing", + "tracing-fluent-assertions", + "tracing-opentelemetry", + "tracing-subscriber", + "ttl_cache", + "wiremock", +] + [[package]] name = "shuttle-codegen" version = "0.42.0" @@ -5244,7 +5290,6 @@ dependencies = [ "anyhow", "async-trait", "axum", - "base64 0.21.7", "bytes", "chrono", "comfy-table", @@ -5252,36 +5297,24 @@ dependencies = [ "headers", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", "jsonwebtoken", "opentelemetry 0.21.0", - "opentelemetry-appender-tracing", "opentelemetry-http 0.10.0", - "opentelemetry-otlp", - "opentelemetry_sdk", "pin-project", "proptest", "rand 0.8.5", "reqwest", - "ring 0.17.8", - "rustrict", "semver 1.0.22", "serde", "serde_json", - "serial_test", "sqlx", "strum 0.26.1", - "test-context 0.3.0", "thiserror", - "tokio", "tonic 0.10.2", "tower", - "tower-http 0.4.4", "tracing", - "tracing-fluent-assertions", "tracing-opentelemetry", "tracing-subscriber", - "ttl_cache", "url", "uuid", "wiremock", @@ -5330,6 +5363,7 @@ dependencies = [ "rmp-serde", "serde", "serde_json", + "shuttle-backends", "shuttle-common", "shuttle-common-tests", "shuttle-proto", @@ -5391,6 +5425,7 @@ dependencies = [ "semver 1.0.22", "serde", "serde_json", + "shuttle-backends", "shuttle-common", "shuttle-common-tests", "shuttle-proto", @@ -5427,6 +5462,7 @@ dependencies = [ "pretty_assertions", "prost-types", "serde_json", + "shuttle-backends", "shuttle-common", "shuttle-common-tests", "shuttle-proto", @@ -5453,7 +5489,6 @@ dependencies = [ "prost-types", "serde_json", "shuttle-common", - "test-context 0.3.0", "tokio", "tonic 0.10.2", "tower", @@ -5475,6 +5510,7 @@ dependencies = [ "prost 0.12.3", "rand 0.8.5", "serde_json", + "shuttle-backends", "shuttle-common", "shuttle-common-tests", "shuttle-proto", @@ -5497,6 +5533,7 @@ dependencies = [ "pretty_assertions", "prost-types", "serde_json", + "shuttle-backends", "shuttle-common", "shuttle-common-tests", "shuttle-proto", diff --git a/Cargo.toml b/Cargo.toml index 80a440456..5ea6522f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = [ "admin", - "auth", + "auth", "backends", "cargo-shuttle", "codegen", "common", @@ -30,6 +30,7 @@ repository = "https://github.com/shuttle-hq/shuttle" # https://doc.rust-lang.org/cargo/reference/workspaces.html#the-workspacedependencies-table [workspace.dependencies] +shuttle-backends = { path = "backends", version = "0.42.0" } shuttle-codegen = { path = "codegen", version = "0.42.0" } shuttle-common = { path = "common", version = "0.42.0" } shuttle-common-tests = { path = "common-tests", version = "0.42.0" } diff --git a/admin/Cargo.toml b/admin/Cargo.toml index 94dee93ad..e88728379 100644 --- a/admin/Cargo.toml +++ b/admin/Cargo.toml @@ -2,6 +2,7 @@ name = "shuttle-admin" version = "0.42.0" edition = "2021" +publish = false [dependencies] shuttle-common = { workspace = true, features = ["models"] } diff --git a/auth/Cargo.toml b/auth/Cargo.toml index d647aa375..60914f364 100644 --- a/auth/Cargo.toml +++ b/auth/Cargo.toml @@ -4,13 +4,11 @@ version = "0.42.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] -shuttle-common = { workspace = true, features = [ - "backend", - "models", - "persist", -] } +shuttle-backends = { workspace = true } +shuttle-common = { workspace = true, features = ["models", "persist"] } anyhow = { workspace = true } async-stripe = { version = "0.25.1", default-features = false, features = ["checkout", "runtime-tokio-hyper-rustls"] } @@ -41,7 +39,7 @@ once_cell = { workspace = true } portpicker = { workspace = true } pretty_assertions = { workspace = true } serde_json = { workspace = true } -shuttle-common = { workspace = true, features = ["test-utils"] } +shuttle-backends = { workspace = true, features = ["test-utils"] } shuttle-common-tests = { workspace = true } tower = { workspace = true, features = ["util"] } wiremock = { workspace = true } diff --git a/auth/src/api/builder.rs b/auth/src/api/builder.rs index 09ba0db49..424f869a3 100644 --- a/auth/src/api/builder.rs +++ b/auth/src/api/builder.rs @@ -6,11 +6,9 @@ use axum::{ routing::{delete, get, post, put}, Router, Server, }; -use shuttle_common::{ - backends::{ - client::PermissionsDal, - metrics::{Metrics, TraceLayer}, - }, +use shuttle_backends::{ + client::PermissionsDal, + metrics::{Metrics, TraceLayer}, request_span, }; use sqlx::PgPool; diff --git a/auth/src/api/handlers.rs b/auth/src/api/handlers.rs index ceeb48a69..12a4c4389 100644 --- a/auth/src/api/handlers.rs +++ b/auth/src/api/handlers.rs @@ -90,7 +90,7 @@ pub(crate) async fn convert_key( State(user_manager): State, State(key_manager): State, key: Key, -) -> Result, StatusCode> { +) -> Result, StatusCode> { let user = user_manager .get_user_by_key(key.into()) .await @@ -105,7 +105,7 @@ pub(crate) async fn convert_key( let token = claim.into_token(key_manager.private_key())?; - let response = shuttle_common::backends::auth::ConvertResponse { token }; + let response = shuttle_backends::auth::ConvertResponse { token }; Ok(Json(response)) } diff --git a/auth/src/error.rs b/auth/src/error.rs index 90aa40f11..d1b0f7892 100644 --- a/auth/src/error.rs +++ b/auth/src/error.rs @@ -4,7 +4,7 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde::{ser::SerializeMap, Serialize}; -use shuttle_common::backends::client; +use shuttle_backends::client; use shuttle_common::models::error::ApiError; use stripe::StripeError; diff --git a/auth/src/lib.rs b/auth/src/lib.rs index 2d5bbbe71..906643227 100644 --- a/auth/src/lib.rs +++ b/auth/src/lib.rs @@ -7,7 +7,8 @@ mod user; use std::io; use args::StartArgs; -use shuttle_common::{backends::client::permit, claims::AccountTier, ApiKey}; +use shuttle_backends::client::permit; +use shuttle_common::{claims::AccountTier, ApiKey}; use sqlx::{migrate::Migrator, query, PgPool}; use tracing::info; pub use user::User; diff --git a/auth/src/main.rs b/auth/src/main.rs index d0a34cbe5..e366e47bc 100644 --- a/auth/src/main.rs +++ b/auth/src/main.rs @@ -1,7 +1,8 @@ use std::io; use clap::Parser; -use shuttle_common::{backends::trace::setup_tracing, claims::AccountTier, log::Backend}; +use shuttle_backends::trace::setup_tracing; +use shuttle_common::{claims::AccountTier, log::Backend}; use sqlx::migrate::Migrator; use tracing::trace; diff --git a/auth/src/user.rs b/auth/src/user.rs index 0ce4c68bb..f377d25e6 100644 --- a/auth/src/user.rs +++ b/auth/src/user.rs @@ -8,13 +8,9 @@ use axum::{ TypedHeader, }; use chrono::{DateTime, Utc}; +use shuttle_backends::{client::PermissionsDal, headers::XShuttleAdminSecret}; use shuttle_common::{ - backends::{client::PermissionsDal, headers::XShuttleAdminSecret}, - claims::AccountTier, - limits::Limits, - models, - models::user::UserId, - ApiKey, Secret, + claims::AccountTier, limits::Limits, models, models::user::UserId, ApiKey, Secret, }; use sqlx::{postgres::PgRow, query, FromRow, PgPool, Row}; use stripe::{SubscriptionId, SubscriptionStatus}; diff --git a/auth/tests/api/helpers.rs b/auth/tests/api/helpers.rs index fd318dc6d..1d357ed8e 100644 --- a/auth/tests/api/helpers.rs +++ b/auth/tests/api/helpers.rs @@ -4,11 +4,10 @@ use hyper::http::{header::AUTHORIZATION, Request}; use once_cell::sync::Lazy; use serde_json::{json, Value}; use shuttle_auth::{pgpool_init, ApiBuilder}; +use shuttle_backends::{headers::X_SHUTTLE_ADMIN_SECRET, test_utils::gateway::PermissionsMock}; use shuttle_common::{ - backends::headers::X_SHUTTLE_ADMIN_SECRET, claims::{AccountTier, Claim}, models::user, - test_utils::PermissionsMock, }; use shuttle_common_tests::postgres::DockerInstance; use sqlx::query; diff --git a/backends/Cargo.toml b/backends/Cargo.toml new file mode 100644 index 000000000..d84e44776 --- /dev/null +++ b/backends/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "shuttle-backends" +version = "0.42.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false + +[dependencies] +shuttle-common = { workspace = true, features = ["axum", "claims", "models"] } +shuttle-proto = { workspace = true, features = ["resource-recorder-client"] } + +anyhow = { workspace = true } +async-trait = { workspace = true } +axum = { workspace = true, features = ["matched-path"] } +bytes = { workspace = true } +headers = { workspace = true } +http = { workspace = true } +http-body = { workspace = true } +hyper = { workspace = true } +opentelemetry = { workspace = true } +opentelemetry_sdk = { workspace = true } +opentelemetry-appender-tracing = { workspace = true } +opentelemetry-http = { workspace = true } +opentelemetry-otlp = { workspace = true } +pin-project = { workspace = true } +portpicker = { workspace = true, optional = true } +reqwest = { workspace = true, features = ["json"] } +# keep locked to not accidentally invalidate someone's project name +rustrict = { version = "=0.7.12" } +serde = { workspace = true, features = ["derive", "std"] } +serde_json = { workspace = true } +sqlx = { workspace = true, optional = true } +strum = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tonic = { workspace = true } +tower = { workspace = true } +tower-http = { workspace = true } +tracing = { workspace = true, features = ["std"] } +tracing-opentelemetry = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"]} +ttl_cache = { workspace = true } +wiremock = { workspace = true, optional = true } + +[features] +sqlx = ["dep:sqlx"] +test-utils = ["portpicker", "wiremock"] + +[dev-dependencies] +base64 = { workspace = true } +jsonwebtoken = { workspace = true } +ring = { workspace = true } +serial_test = "3.0.0" +test-context = { workspace = true } +tracing-fluent-assertions = "0.3.0" diff --git a/common/src/backends/auth.rs b/backends/src/auth.rs similarity index 99% rename from common/src/backends/auth.rs rename to backends/src/auth.rs index ada522509..7383a2eba 100644 --- a/common/src/backends/auth.rs +++ b/backends/src/auth.rs @@ -10,12 +10,13 @@ use opentelemetry::global; use opentelemetry_http::HeaderInjector; use pin_project::pin_project; use serde::{Deserialize, Serialize}; +use strum::EnumMessage; use thiserror::Error; use tower::{Layer, Service}; use tracing::{error, trace, Span}; use tracing_opentelemetry::OpenTelemetrySpanExt; -use crate::claims::{Claim, Scope}; +use shuttle_common::claims::{Claim, Scope}; use super::{ cache::{CacheManagement, CacheManager}, @@ -437,13 +438,10 @@ pub trait VerifyClaim { fn get_claim(&self) -> Result; } -#[cfg(feature = "tonic")] impl VerifyClaim for tonic::Request { type Error = tonic::Status; fn verify(&self, required_scope: Scope) -> Result<(), Self::Error> { - use strum::EnumMessage; - let claim = self .extensions() .get::() @@ -485,7 +483,7 @@ mod tests { use serde_json::json; use tower::{ServiceBuilder, ServiceExt}; - use crate::claims::{AccountTier, Claim, Scope}; + use shuttle_common::claims::{AccountTier, Claim, Scope}; use super::{JwtAuthenticationLayer, ScopedLayer}; diff --git a/backends/src/axum.rs b/backends/src/axum.rs new file mode 100644 index 000000000..f2ff21979 --- /dev/null +++ b/backends/src/axum.rs @@ -0,0 +1,127 @@ +use async_trait::async_trait; +use axum::extract::path::ErrorKind; +use axum::{ + extract::{rejection::PathRejection, FromRequestParts}, + http::request::Parts, +}; +use http::StatusCode; +use serde::de::DeserializeOwned; + +use shuttle_common::models::error::ApiError; + +/// Custom `Path` extractor that customizes the error from `axum::extract::Path`. +/// +/// Prints the custom error message if deserialization resulted in a custom de::Error, +/// which is what the [`crate::project_name::ProjectName`] parser uses. +pub struct CustomErrorPath(pub T); + +impl core::ops::Deref for CustomErrorPath { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl core::ops::DerefMut for CustomErrorPath { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[async_trait] +impl FromRequestParts for CustomErrorPath +where + T: DeserializeOwned + Send, + S: Send + Sync, +{ + type Rejection = ApiError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + match axum::extract::Path::::from_request_parts(parts, state).await { + Ok(value) => Ok(Self(value.0)), + Err(rejection) => { + if let PathRejection::FailedToDeserializePathParams(inner) = &rejection { + if let ErrorKind::Message(message) = inner.kind() { + return Err(ApiError { + message: message.clone(), + status_code: StatusCode::BAD_REQUEST.as_u16(), + }); + } + } + + Err(ApiError { + message: rejection.body_text(), + status_code: rejection.status().as_u16(), + }) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use axum::http::StatusCode; + use axum::{body::Body, routing::get, Router}; + use http::Request; + use tower::Service; + + use crate::project_name::ProjectName; + + #[tokio::test] + async fn project_name_paths() { + let mut app = + Router::new() + .route( + "/:project_name", + get( + |CustomErrorPath(project_name): CustomErrorPath| async move { + project_name.to_string() + }, + ), + ) + .route( + "/:project_name/:num", + get( + |CustomErrorPath((project_name, num)): CustomErrorPath<( + ProjectName, + u8, + )>| async move { format!("{project_name} {num}") }, + ), + ); + + let response = app + .call(Request::get("/test123").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert_eq!(&body[..], b"test123"); + + let response = app + .call(Request::get("/__test123").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert!(&body[..].starts_with(br#"{"message":"Invalid project name"#)); + + let response = app + .call(Request::get("/test123/123").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert_eq!(&body[..], b"test123 123"); + + let response = app + .call(Request::get("/test123/asdf").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert!(&body[..].starts_with(br#"{"message":"Invalid URL"#)); + } +} diff --git a/common/src/backends/cache.rs b/backends/src/cache.rs similarity index 100% rename from common/src/backends/cache.rs rename to backends/src/cache.rs diff --git a/common/src/backends/client/gateway.rs b/backends/src/client/gateway.rs similarity index 94% rename from common/src/backends/client/gateway.rs rename to backends/src/client/gateway.rs index ec1e25bfa..3a1d10dcf 100644 --- a/common/src/backends/client/gateway.rs +++ b/backends/src/client/gateway.rs @@ -1,7 +1,7 @@ use http::Method; use tracing::instrument; -use crate::models; +use shuttle_common::models; use super::{header_map_with_bearer, Error, ServicesApiClient}; @@ -78,9 +78,10 @@ impl ProjectsDal for ServicesApiClient { mod tests { use test_context::{test_context, AsyncTestContext}; - use crate::backends::client::ServicesApiClient; - use crate::models::project::{Response, State}; - use crate::test_utils::get_mocked_gateway_server; + use shuttle_common::models::project::{Response, State}; + + use crate::client::ServicesApiClient; + use crate::test_utils::gateway::get_mocked_gateway_server; use super::ProjectsDal; diff --git a/common/src/backends/client/mod.rs b/backends/src/client/mod.rs similarity index 98% rename from common/src/backends/client/mod.rs rename to backends/src/client/mod.rs index c4cf7bca5..9319e8f55 100644 --- a/common/src/backends/client/mod.rs +++ b/backends/src/client/mod.rs @@ -159,10 +159,10 @@ pub fn header_map_with_bearer(token: &str) -> HeaderMap { mod tests { use http::StatusCode; - use crate::models; - use crate::test_utils::get_mocked_gateway_server; + use shuttle_common::models; use super::{Error, ServicesApiClient}; + use crate::test_utils::gateway::get_mocked_gateway_server; // Make sure we handle any unexpected responses correctly #[tokio::test] diff --git a/common/src/backends/client/permit.rs b/backends/src/client/permit.rs similarity index 99% rename from common/src/backends/client/permit.rs rename to backends/src/client/permit.rs index f5750b44b..3f657e5e4 100644 --- a/common/src/backends/client/permit.rs +++ b/backends/src/client/permit.rs @@ -4,8 +4,7 @@ use async_trait::async_trait; use http::{Method, Uri}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{json, Value}; - -use crate::claims::AccountTier; +use shuttle_common::claims::AccountTier; use super::{Error, ServicesApiClient}; @@ -519,7 +518,7 @@ mod tests { use serial_test::serial; use test_context::{test_context, AsyncTestContext}; - use crate::{backends::client::Error, claims::AccountTier}; + use crate::client::Error; use super::*; diff --git a/backends/src/client/resource_recorder.rs b/backends/src/client/resource_recorder.rs new file mode 100644 index 000000000..f8ab28f8b --- /dev/null +++ b/backends/src/client/resource_recorder.rs @@ -0,0 +1,184 @@ +use async_trait::async_trait; +use http::header::AUTHORIZATION; +use shuttle_proto::resource_recorder::Client; +use shuttle_proto::resource_recorder::ProjectResourcesRequest; +use tracing::instrument; + +use shuttle_common::{database, resource}; + +use super::Error; + +/// DAL for access resources data of projects +#[async_trait] +pub trait ResourceDal: Send { + /// Get the resources belonging to a project + async fn get_project_resources( + &mut self, + project_id: &str, + token: &str, + ) -> Result, Error>; + + /// Get only the RDS resources that belong to a project + async fn get_project_rds_resources( + &mut self, + project_id: &str, + token: &str, + ) -> Result, Error> { + let rds_resources = self + .get_project_resources(project_id, token) + .await? + .into_iter() + .filter(|r| { + matches!( + r.r#type, + resource::Type::Database(database::Type::AwsRds(_)) + ) + }) + .collect(); + + Ok(rds_resources) + } +} + +#[async_trait] +impl ResourceDal for &mut T +where + T: ResourceDal, +{ + #[instrument(skip_all, fields(shuttle.project.id = project_id))] + async fn get_project_resources( + &mut self, + project_id: &str, + token: &str, + ) -> Result, Error> { + (**self).get_project_resources(project_id, token).await + } +} + +#[async_trait] +impl ResourceDal for Client { + async fn get_project_resources( + &mut self, + project_id: &str, + token: &str, + ) -> Result, Error> { + let mut req = tonic::Request::new(ProjectResourcesRequest { + project_id: project_id.to_string(), + }); + + req.metadata_mut().insert( + AUTHORIZATION.as_str(), + format!("Bearer {token}") + .parse() + .expect("to construct a bearer token"), + ); + + let resp = (*self) + .get_project_resources(req) + .await? + .into_inner() + .clone(); + + let resources = resp + .resources + .into_iter() + .map(TryInto::try_into) + .collect::, _>>() + .map_err(|error: anyhow::Error| tonic::Status::internal(error.to_string()))?; + + Ok(resources) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use serde_json::json; + use shuttle_common::{database, resource}; + use shuttle_proto::resource_recorder::{get_client, record_request, Client, RecordRequest}; + use test_context::{test_context, AsyncTestContext}; + use tonic::Request; + + use crate::test_utils::resource_recorder::get_mocked_resource_recorder; + + struct Wrap(Client); + + impl AsyncTestContext for Wrap { + async fn setup() -> Self { + let port = get_mocked_resource_recorder().await; + + Self(get_client(format!("http://localhost:{port}").parse().unwrap()).await) + } + + async fn teardown(self) {} + } + + #[test_context(Wrap)] + #[tokio::test] + async fn get_project_resources(r_r_client: &mut Wrap) { + // First record some resources + r_r_client + .0 + .record_resources(Request::new(RecordRequest { + project_id: "project_1".to_string(), + service_id: "service_1".to_string(), + resources: vec![ + record_request::Resource { + r#type: "database::shared::postgres".to_string(), + config: serde_json::to_vec(&json!({"public": true})).unwrap(), + data: serde_json::to_vec(&json!({"username": "test"})).unwrap(), + }, + record_request::Resource { + r#type: "database::aws_rds::mariadb".to_string(), + config: serde_json::to_vec(&json!({})).unwrap(), + data: serde_json::to_vec(&json!({"username": "maria"})).unwrap(), + }, + ], + })) + .await + .unwrap(); + + let resources = (&mut r_r_client.0 as &mut dyn ResourceDal) + .get_project_resources("project_1", "user-1") + .await + .unwrap(); + + assert_eq!( + resources, + vec![ + resource::Response { + r#type: resource::Type::Database(database::Type::Shared( + database::SharedEngine::Postgres + )), + config: json!({"public": true}), + data: json!({"username": "test"}), + }, + resource::Response { + r#type: resource::Type::Database(database::Type::AwsRds( + database::AwsRdsEngine::MariaDB + )), + config: json!({}), + data: json!({"username": "maria"}), + } + ] + ); + + // Getting only RDS resources should filter correctly + let resources = (&mut r_r_client.0 as &mut dyn ResourceDal) + .get_project_rds_resources("project_1", "user-1") + .await + .unwrap(); + + assert_eq!( + resources, + vec![resource::Response { + r#type: resource::Type::Database(database::Type::AwsRds( + database::AwsRdsEngine::MariaDB + )), + config: json!({}), + data: json!({"username": "maria"}), + }] + ); + } +} diff --git a/common/src/backends/future.rs b/backends/src/future.rs similarity index 100% rename from common/src/backends/future.rs rename to backends/src/future.rs diff --git a/common/src/backends/headers.rs b/backends/src/headers.rs similarity index 100% rename from common/src/backends/headers.rs rename to backends/src/headers.rs diff --git a/common/src/backends/mod.rs b/backends/src/lib.rs similarity index 93% rename from common/src/backends/mod.rs rename to backends/src/lib.rs index 75a088b19..7e473ff6c 100644 --- a/common/src/backends/mod.rs +++ b/backends/src/lib.rs @@ -1,18 +1,23 @@ use tracing::instrument; -use crate::claims::{AccountTier, Claim, Scope}; +use shuttle_common::claims::{AccountTier, Claim, Scope}; -use self::client::{ProjectsDal, ResourceDal}; +use client::{ProjectsDal, ResourceDal}; pub mod auth; +pub mod axum; pub mod cache; pub mod client; mod future; pub mod headers; pub mod metrics; mod otlp_tracing_bridge; +pub mod project_name; pub mod trace; +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; + #[allow(async_fn_in_trait)] pub trait ClaimExt { /// Verify that the [Claim] has the [Scope::Admin] scope. diff --git a/common/src/backends/metrics.rs b/backends/src/metrics.rs similarity index 99% rename from common/src/backends/metrics.rs rename to backends/src/metrics.rs index 4c8fd2c84..506e665f1 100644 --- a/common/src/backends/metrics.rs +++ b/backends/src/metrics.rs @@ -54,7 +54,7 @@ impl TraceLayer { /// /// # Example /// ``` - /// use shuttle_common::{request_span, backends::metrics::TraceLayer}; + /// use shuttle_backens::{request_span, metrics::TraceLayer}; /// use tracing::field; /// /// TraceLayer::new(|request| { diff --git a/common/src/backends/otlp_tracing_bridge.rs b/backends/src/otlp_tracing_bridge.rs similarity index 100% rename from common/src/backends/otlp_tracing_bridge.rs rename to backends/src/otlp_tracing_bridge.rs diff --git a/backends/src/project_name.rs b/backends/src/project_name.rs new file mode 100644 index 000000000..87d43beeb --- /dev/null +++ b/backends/src/project_name.rs @@ -0,0 +1,157 @@ +use std::collections::HashSet; +use std::fmt::Formatter; +use std::str::FromStr; +use std::sync::OnceLock; + +use rustrict::{Censor, Type}; +use serde::de::Error as DeError; +use serde::{Deserialize, Deserializer, Serialize}; + +use shuttle_common::models::error::InvalidProjectName; + +/// Project names must conform to valid Host segments (or labels) +/// as per [IETF RFC 1123](https://datatracker.ietf.org/doc/html/rfc1123). +/// Initially we'll implement a strict subset of the IETF RFC 1123. +/// Additionaly, while host segments are technically case-insensitive, the filesystem isn't, +/// so we restrict project names to be lower case. We also restrict the use of profanity, +/// as well as a list of reserved words. +#[derive(Clone, Serialize, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[cfg_attr(feature = "sqlx", sqlx(transparent))] +pub struct ProjectName(String); + +impl ProjectName { + pub fn new(name: &str) -> Result { + if Self::is_valid(name) { + Ok(Self(name.to_owned())) + } else { + Err(InvalidProjectName) + } + } + + pub fn is_valid(name: &str) -> bool { + fn is_valid_char(byte: u8) -> bool { + matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'-') + } + + fn is_profanity_free(name: &str) -> bool { + let (_censored, analysis) = Censor::from_str(name).censor_and_analyze(); + !analysis.is(Type::MODERATE_OR_HIGHER) + } + + fn is_reserved(name: &str) -> bool { + static INSTANCE: OnceLock> = OnceLock::new(); + INSTANCE.get_or_init(|| { + HashSet::from(["shuttleapp", "shuttle", "console", "unstable", "staging"]) + }); + + INSTANCE + .get() + .expect("Reserved words not set") + .contains(name) + } + + !name.is_empty() + && name.len() < 64 + && !name.starts_with('-') + && !name.ends_with('-') + && !is_reserved(name) + && name.bytes().all(is_valid_char) + && is_profanity_free(name) + } + + /// Is this a cch project + pub fn is_cch_project(&self) -> bool { + self.starts_with("cch23-") + } +} + +impl std::ops::Deref for ProjectName { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::fmt::Display for ProjectName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl<'de> Deserialize<'de> for ProjectName { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(DeError::custom) + } +} + +impl FromStr for ProjectName { + type Err = InvalidProjectName; + + fn from_str(s: &str) -> Result { + ProjectName::new(s) + } +} + +/// Test examples taken from a [Pop-OS project](https://github.com/pop-os/hostname-validator/blob/master/src/lib.rs) +/// and modified to our use case +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_labels() { + for name in [ + "50-name", + "235235", + "123", + "kebab-case", + "lowercase", + "myassets", + "dachterrasse", + "another-valid-project-name", + "x", + ] { + assert!(ProjectName::is_valid(name)); + } + } + + #[test] + fn invalid_labels() { + for name in [ + "UPPERCASE", + "CamelCase", + "pascalCase", + "InVaLid", + "-invalid-name", + "also-invalid-", + "asdf@fasd", + "@asdfl", + "asd f@", + ".invalid", + "invalid.name", + "invalid.name.", + "__dunder_like__", + "__invalid", + "invalid__", + "test-condom-condom", + "s________e", + "snake_case", + "exactly-16-chars\ + exactly-16-chars\ + exactly-16-chars\ + exactly-16-chars", + "shuttle", + "shuttleapp", + "", + ] { + assert!(!ProjectName::is_valid(name)); + } + } +} diff --git a/common/src/test_utils.rs b/backends/src/test_utils/gateway.rs similarity index 97% rename from common/src/test_utils.rs rename to backends/src/test_utils/gateway.rs index 29c260261..f2d96a5f7 100644 --- a/common/src/test_utils.rs +++ b/backends/src/test_utils/gateway.rs @@ -5,16 +5,14 @@ use std::{ use async_trait::async_trait; use serde::Serialize; +use shuttle_common::claims::AccountTier; use wiremock::{ http, matchers::{method, path, path_regex}, Mock, MockServer, Request, ResponseTemplate, }; -use crate::{ - backends::client::{permit::User, Error, PermissionsDal}, - claims::AccountTier, -}; +use crate::client::{permit::User, Error, PermissionsDal}; pub async fn get_mocked_gateway_server() -> MockServer { let mock_server = MockServer::start().await; diff --git a/proto/src/test_utils/mod.rs b/backends/src/test_utils/mod.rs similarity index 61% rename from proto/src/test_utils/mod.rs rename to backends/src/test_utils/mod.rs index d0a22a208..f915379e8 100644 --- a/proto/src/test_utils/mod.rs +++ b/backends/src/test_utils/mod.rs @@ -1 +1,2 @@ +pub mod gateway; pub mod resource_recorder; diff --git a/proto/src/test_utils/resource_recorder.rs b/backends/src/test_utils/resource_recorder.rs similarity index 99% rename from proto/src/test_utils/resource_recorder.rs rename to backends/src/test_utils/resource_recorder.rs index d51c862e2..447609748 100644 --- a/proto/src/test_utils/resource_recorder.rs +++ b/backends/src/test_utils/resource_recorder.rs @@ -6,7 +6,7 @@ use std::{ use portpicker::pick_unused_port; use tonic::{async_trait, transport::Server, Request, Response, Status}; -use crate::generated::resource_recorder::{ +use shuttle_proto::resource_recorder::{ resource_recorder_server::{ResourceRecorder, ResourceRecorderServer}, ProjectResourcesRequest, RecordRequest, Resource, ResourceIds, ResourceResponse, ResourcesResponse, ResultResponse, ServiceResourcesRequest, diff --git a/common/src/backends/trace.rs b/backends/src/trace.rs similarity index 98% rename from common/src/backends/trace.rs rename to backends/src/trace.rs index a3d0cf220..030e6ce9f 100644 --- a/common/src/backends/trace.rs +++ b/backends/src/trace.rs @@ -6,7 +6,7 @@ use opentelemetry_sdk::{ use tracing::Subscriber; use tracing_subscriber::{fmt, prelude::*, registry::LookupSpan, EnvFilter}; -use crate::log::Backend; +use shuttle_common::log::Backend; use super::otlp_tracing_bridge::{self, ErrorTracingLayer}; diff --git a/common-tests/Cargo.toml b/common-tests/Cargo.toml index 11fa5cff5..8a62776dc 100644 --- a/common-tests/Cargo.toml +++ b/common-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-common-tests" -version.workspace = true +version = "0.42.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/common/Cargo.toml b/common/Cargo.toml index 4d0373921..c7703d2f9 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -11,7 +11,7 @@ rust-version = "1.75" [dependencies] anyhow = { workspace = true } async-trait = { workspace = true, optional = true } -axum = { workspace = true, optional = true } +axum = { workspace = true, features = ["json"], optional = true } bytes = { workspace = true, optional = true } chrono = { workspace = true } comfy-table = { workspace = true, optional = true } @@ -19,59 +19,30 @@ crossterm = { workspace = true, optional = true } headers = { workspace = true, optional = true } http = { workspace = true, optional = true } http-body = { workspace = true, optional = true } -hyper = { workspace = true, optional = true } jsonwebtoken = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true } -opentelemetry_sdk = { workspace = true, optional = true } opentelemetry-http = { workspace = true, optional = true } -opentelemetry-otlp = { workspace = true, optional = true } -opentelemetry-appender-tracing = { workspace = true, optional = true } pin-project = { workspace = true, optional = true } rand = { workspace = true, optional = true } reqwest = { workspace = true, optional = true } -# keep locked to not accidentally invalidate someone's project name -rustrict = { version = "=0.7.12", optional = true } semver = { workspace = true } serde = { workspace = true, features = ["derive", "std"] } serde_json = { workspace = true } sqlx = { workspace = true, optional = true } strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true, optional = true } -tokio = { workspace = true, optional = true } tonic = { workspace = true, optional = true } tower = { workspace = true, optional = true } -tower-http = { workspace = true, optional = true } tracing = { workspace = true, features = ["std"], optional = true } tracing-opentelemetry = { workspace = true, optional = true } tracing-subscriber = { workspace = true, optional = true } -ttl_cache = { workspace = true, optional = true } url = { workspace = true, features = ["serde"] } uuid = { workspace = true, features = ["v4", "serde"], optional = true } zeroize = { workspace = true } wiremock = { workspace = true, optional = true } [features] -backend = [ - "async-trait", - "axum/matched-path", - "axum/json", - "claims", - "hyper", - "opentelemetry_sdk", - "opentelemetry-appender-tracing", - "opentelemetry-otlp", - "models", - "reqwest/json", - "rustrict", # only ProjectName model uses it - "thiserror", - "tokio", - "tonic", - "tower-http", - "tracing-subscriber/env-filter", - "tracing-subscriber/fmt", - "tracing", - "ttl_cache", -] +axum = ["dep:axum"] claims = [ "bytes", "chrono/clock", @@ -100,18 +71,8 @@ persist = ["sqlx", "rand"] sqlx = ["dep:sqlx", "sqlx/sqlite"] service = ["chrono/serde", "display", "tracing", "tracing-subscriber", "uuid"] test-utils = ["wiremock"] +tonic = ["dep:tonic"] tracing = ["dep:tracing"] [dev-dependencies] -axum = { workspace = true } -base64 = { workspace = true } -hyper = { workspace = true } proptest = "1.1.0" -ring = { workspace = true } -serial_test = "3.0.0" -test-context = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -tower = { workspace = true, features = ["util"] } -tracing-fluent-assertions = "0.3.0" -tracing-subscriber = { workspace = true } -wiremock = { workspace = true } diff --git a/common/src/backends/client/resource_recorder.rs b/common/src/backends/client/resource_recorder.rs deleted file mode 100644 index ddf4198ad..000000000 --- a/common/src/backends/client/resource_recorder.rs +++ /dev/null @@ -1,53 +0,0 @@ -use async_trait::async_trait; -use tracing::instrument; - -use crate::{database, resource}; - -use super::Error; - -/// DAL for access resources data of projects -#[async_trait] -pub trait ResourceDal: Send { - /// Get the resources belonging to a project - async fn get_project_resources( - &mut self, - project_id: &str, - token: &str, - ) -> Result, Error>; - - /// Get only the RDS resources that belong to a project - async fn get_project_rds_resources( - &mut self, - project_id: &str, - token: &str, - ) -> Result, Error> { - let rds_resources = self - .get_project_resources(project_id, token) - .await? - .into_iter() - .filter(|r| { - matches!( - r.r#type, - resource::Type::Database(database::Type::AwsRds(_)) - ) - }) - .collect(); - - Ok(rds_resources) - } -} - -#[async_trait] -impl ResourceDal for &mut T -where - T: ResourceDal, -{ - #[instrument(skip_all, fields(shuttle.project.id = project_id))] - async fn get_project_resources( - &mut self, - project_id: &str, - token: &str, - ) -> Result, Error> { - (**self).get_project_resources(project_id, token).await - } -} diff --git a/common/src/lib.rs b/common/src/lib.rs index 0574309fd..c08f3ecc8 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "backend")] -pub mod backends; #[cfg(feature = "claims")] pub mod claims; pub mod constants; @@ -25,9 +23,6 @@ pub mod templates; #[cfg(feature = "tracing")] pub mod tracing; -#[cfg(any(test, feature = "test-utils"))] -pub mod test_utils; - use std::fmt::Debug; use anyhow::bail; diff --git a/common/src/limits.rs b/common/src/limits.rs index b8a60affd..fb26e206b 100644 --- a/common/src/limits.rs +++ b/common/src/limits.rs @@ -8,7 +8,7 @@ use crate::{ #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] pub struct Limits { /// The amount of projects this user can create. - pub(crate) project_limit: u32, + pub project_limit: u32, /// Whether this user has permission to provision RDS instances. #[deprecated( since = "0.38.0", @@ -17,7 +17,7 @@ pub struct Limits { #[serde(skip_deserializing)] rds_access: bool, /// The quantity of RDS instances this user can provision. - pub(crate) rds_quota: u32, + pub rds_quota: u32, } impl Default for Limits { diff --git a/common/src/models/error.rs b/common/src/models/error.rs index 58f312cb5..7cc3eb98e 100644 --- a/common/src/models/error.rs +++ b/common/src/models/error.rs @@ -6,6 +6,13 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::{error, warn}; +#[cfg(feature = "axum")] +impl axum::response::IntoResponse for ApiError { + fn into_response(self) -> axum::response::Response { + (self.status(), axum::Json(self)).into_response() + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct ApiError { pub message: String, @@ -183,139 +190,3 @@ impl From for ApiError { 6. not be a reserved word." )] pub struct InvalidProjectName; - -#[cfg(feature = "backend")] -pub mod axum { - use async_trait::async_trait; - use axum::extract::path::ErrorKind; - use axum::{ - extract::{rejection::PathRejection, FromRequestParts}, - http::request::Parts, - response::{IntoResponse, Json, Response}, - }; - use http::StatusCode; - use serde::de::DeserializeOwned; - - use super::ApiError; - - impl IntoResponse for ApiError { - fn into_response(self) -> Response { - (self.status(), Json(self)).into_response() - } - } - - /// Custom `Path` extractor that customizes the error from `axum::extract::Path`. - /// - /// Prints the custom error message if deserialization resulted in a custom de::Error, - /// which is what the [`shuttle_common::project::ProjectName`] parser uses. - pub struct CustomErrorPath(pub T); - - impl core::ops::Deref for CustomErrorPath { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl core::ops::DerefMut for CustomErrorPath { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - #[async_trait] - impl FromRequestParts for CustomErrorPath - where - T: DeserializeOwned + Send, - S: Send + Sync, - { - type Rejection = ApiError; - - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - match axum::extract::Path::::from_request_parts(parts, state).await { - Ok(value) => Ok(Self(value.0)), - Err(rejection) => { - if let PathRejection::FailedToDeserializePathParams(inner) = &rejection { - if let ErrorKind::Message(message) = inner.kind() { - return Err(ApiError { - message: message.clone(), - status_code: StatusCode::BAD_REQUEST.as_u16(), - }); - } - } - - Err(ApiError { - message: rejection.body_text(), - status_code: rejection.status().as_u16(), - }) - } - } - } - } - - #[cfg(test)] - mod tests { - use crate::models::project::ProjectName; - - use super::*; - use axum::http::StatusCode; - use axum::{body::Body, routing::get, Router}; - use http::Request; - use tower::Service; - - #[tokio::test] - async fn project_name_paths() { - let mut app = Router::new() - .route( - "/:project_name", - get( - |CustomErrorPath(project_name): CustomErrorPath| async move { - project_name.to_string() - }, - ), - ) - .route( - "/:project_name/:num", - get( - |CustomErrorPath((project_name, num)): CustomErrorPath<( - ProjectName, - u8, - )>| async move { format!("{project_name} {num}") }, - ), - ); - - let response = app - .call(Request::get("/test123").body(Body::empty()).unwrap()) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); - assert_eq!(&body[..], b"test123"); - - let response = app - .call(Request::get("/__test123").body(Body::empty()).unwrap()) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); - assert!(&body[..].starts_with(br#"{"message":"Invalid project name"#)); - - let response = app - .call(Request::get("/test123/123").body(Body::empty()).unwrap()) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); - assert_eq!(&body[..], b"test123 123"); - - let response = app - .call(Request::get("/test123/asdf").body(Body::empty()).unwrap()) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); - assert!(&body[..].starts_with(br#"{"message":"Invalid URL"#)); - } - } -} diff --git a/common/src/models/project.rs b/common/src/models/project.rs index f16864b03..b3f423640 100644 --- a/common/src/models/project.rs +++ b/common/src/models/project.rs @@ -233,166 +233,3 @@ pub fn get_projects_table( } } } - -#[cfg(feature = "backend")] -pub use name::ProjectName; -#[cfg(feature = "backend")] -pub mod name { - use std::collections::HashSet; - use std::fmt::Formatter; - use std::str::FromStr; - use std::sync::OnceLock; - - use rustrict::{Censor, Type}; - use serde::de::Error as DeError; - use serde::{Deserialize, Deserializer, Serialize}; - - use crate::models::error::InvalidProjectName; - - /// Project names must conform to valid Host segments (or labels) - /// as per [IETF RFC 1123](https://datatracker.ietf.org/doc/html/rfc1123). - /// Initially we'll implement a strict subset of the IETF RFC 1123. - /// Additionaly, while host segments are technically case-insensitive, the filesystem isn't, - /// so we restrict project names to be lower case. We also restrict the use of profanity, - /// as well as a list of reserved words. - #[derive(Clone, Serialize, Debug, Eq, Hash, PartialEq)] - #[cfg_attr(feature = "persist", derive(sqlx::Type))] - #[cfg_attr(feature = "persist", sqlx(transparent))] - pub struct ProjectName(String); - - impl ProjectName { - pub fn new(name: &str) -> Result { - if Self::is_valid(name) { - Ok(Self(name.to_owned())) - } else { - Err(InvalidProjectName) - } - } - - pub fn is_valid(name: &str) -> bool { - fn is_valid_char(byte: u8) -> bool { - matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'-') - } - - fn is_profanity_free(name: &str) -> bool { - let (_censored, analysis) = Censor::from_str(name).censor_and_analyze(); - !analysis.is(Type::MODERATE_OR_HIGHER) - } - - fn is_reserved(name: &str) -> bool { - static INSTANCE: OnceLock> = OnceLock::new(); - INSTANCE.get_or_init(|| { - HashSet::from(["shuttleapp", "shuttle", "console", "unstable", "staging"]) - }); - - INSTANCE - .get() - .expect("Reserved words not set") - .contains(name) - } - - !name.is_empty() - && name.len() < 64 - && !name.starts_with('-') - && !name.ends_with('-') - && !is_reserved(name) - && name.bytes().all(is_valid_char) - && is_profanity_free(name) - } - - /// Is this a cch project - pub fn is_cch_project(&self) -> bool { - self.starts_with("cch23-") - } - } - - impl std::ops::Deref for ProjectName { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl std::fmt::Display for ProjectName { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } - } - - impl<'de> Deserialize<'de> for ProjectName { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - String::deserialize(deserializer)? - .parse() - .map_err(DeError::custom) - } - } - - impl FromStr for ProjectName { - type Err = InvalidProjectName; - - fn from_str(s: &str) -> Result { - ProjectName::new(s) - } - } - - /// Test examples taken from a [Pop-OS project](https://github.com/pop-os/hostname-validator/blob/master/src/lib.rs) - /// and modified to our use case - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn valid_labels() { - for name in [ - "50-name", - "235235", - "123", - "kebab-case", - "lowercase", - "myassets", - "dachterrasse", - "another-valid-project-name", - "x", - ] { - assert!(ProjectName::is_valid(name)); - } - } - - #[test] - fn invalid_labels() { - for name in [ - "UPPERCASE", - "CamelCase", - "pascalCase", - "InVaLid", - "-invalid-name", - "also-invalid-", - "asdf@fasd", - "@asdfl", - "asd f@", - ".invalid", - "invalid.name", - "invalid.name.", - "__dunder_like__", - "__invalid", - "invalid__", - "test-condom-condom", - "s________e", - "snake_case", - "exactly-16-chars\ - exactly-16-chars\ - exactly-16-chars\ - exactly-16-chars", - "shuttle", - "shuttleapp", - "", - ] { - assert!(!ProjectName::is_valid(name)); - } - } - } -} diff --git a/deployer/Cargo.toml b/deployer/Cargo.toml index b60559d97..b617a0243 100644 --- a/deployer/Cargo.toml +++ b/deployer/Cargo.toml @@ -4,9 +4,11 @@ version = "0.42.0" edition.workspace = true license.workspace = true description = "Service with instances created per project for handling the compilation, loading, and execution of Shuttle services" +publish = false [dependencies] -shuttle-common = { workspace = true, features = ["backend", "models", "sqlx"] } +shuttle-backends = { workspace = true } +shuttle-common = { workspace = true, features = ["models", "sqlx"] } shuttle-proto = { workspace = true, features = ["provisioner-client", "resource-recorder-client"] } shuttle-service = { workspace = true, features = ["builder", "runner"] } diff --git a/deployer/src/args.rs b/deployer/src/args.rs index 756e07159..488ec9d52 100644 --- a/deployer/src/args.rs +++ b/deployer/src/args.rs @@ -2,7 +2,6 @@ use std::{net::SocketAddr, path::PathBuf}; use clap::Parser; use hyper::Uri; -use shuttle_common::models::project::ProjectName; /// Program to handle the deploys for a single project /// Handling includes, building, testing, and running each service @@ -35,7 +34,7 @@ pub struct Args { /// Project being served by this deployer #[clap(long)] - pub project: ProjectName, + pub project: String, /// Project id of the project of this deployer #[clap(long)] diff --git a/deployer/src/deployment/gateway_client.rs b/deployer/src/deployment/gateway_client.rs index f988714ff..e135e50ac 100644 --- a/deployer/src/deployment/gateway_client.rs +++ b/deployer/src/deployment/gateway_client.rs @@ -1,7 +1,5 @@ -use shuttle_common::{ - backends::client::{Error, ServicesApiClient}, - models, -}; +use shuttle_backends::client::{Error, ServicesApiClient}; +use shuttle_common::models; use uuid::Uuid; /// A client that can communicate with the build queue diff --git a/deployer/src/deployment/state_change_layer.rs b/deployer/src/deployment/state_change_layer.rs index 1d2dc18ff..68a734c4b 100644 --- a/deployer/src/deployment/state_change_layer.rs +++ b/deployer/src/deployment/state_change_layer.rs @@ -358,17 +358,11 @@ mod tests { #[async_trait::async_trait] impl BuildQueueClient for StubBuildQueueClient { - async fn get_slot( - &self, - _id: Uuid, - ) -> Result { + async fn get_slot(&self, _id: Uuid) -> Result { Ok(true) } - async fn release_slot( - &self, - _id: Uuid, - ) -> Result<(), shuttle_common::backends::client::Error> { + async fn release_slot(&self, _id: Uuid) -> Result<(), shuttle_backends::client::Error> { Ok(()) } } diff --git a/deployer/src/error.rs b/deployer/src/error.rs index 6e6575482..20d44e0f2 100644 --- a/deployer/src/error.rs +++ b/deployer/src/error.rs @@ -1,4 +1,3 @@ -use shuttle_common::backends; use std::error::Error as StdError; use std::io; use thiserror::Error; @@ -30,7 +29,7 @@ pub enum Error { #[error("Failed to cleanup old deployments: {0}")] OldCleanup(#[source] Box), #[error("Gateway client error: {0}")] - GatewayClient(#[from] backends::client::Error), + GatewayClient(#[from] shuttle_backends::client::Error), #[error("Failed to get runtime: {0}")] Runtime(#[source] anyhow::Error), #[error("Failed to call start on runtime: {0}")] diff --git a/deployer/src/handlers/mod.rs b/deployer/src/handlers/mod.rs index 0eb9dbe0f..375fb1ee2 100644 --- a/deployer/src/handlers/mod.rs +++ b/deployer/src/handlers/mod.rs @@ -20,18 +20,16 @@ use tonic::Code; use tracing::{error, field, info, info_span, instrument, trace, warn}; use uuid::Uuid; +use shuttle_backends::{ + auth::{AdminSecretLayer, AuthPublicKey, JwtAuthenticationLayer, ScopedLayer}, + axum::CustomErrorPath, + metrics::{Metrics, TraceLayer}, + request_span, +}; use shuttle_common::{ - backends::{ - auth::{AdminSecretLayer, AuthPublicKey, JwtAuthenticationLayer, ScopedLayer}, - metrics::{Metrics, TraceLayer}, - }, claims::{Claim, Scope}, - models::{ - deployment::{DeploymentRequest, CREATE_SERVICE_BODY_LIMIT, GIT_STRINGS_MAX_LENGTH}, - error::axum::CustomErrorPath, - project::ProjectName, - }, - request_span, LogItem, + models::deployment::{DeploymentRequest, CREATE_SERVICE_BODY_LIMIT, GIT_STRINGS_MAX_LENGTH}, + LogItem, }; use shuttle_proto::logger::LogsRequest; @@ -56,7 +54,8 @@ pub struct PaginationDetails { #[derive(Clone)] pub struct RouterBuilder { router: Router, - _project_name: ProjectName, + // might be used for tracing instruments? + _project_name: String, auth_uri: Uri, } @@ -64,7 +63,7 @@ impl RouterBuilder { pub fn new( persistence: Persistence, deployment_manager: DeploymentManager, - project_name: ProjectName, + project_name: String, auth_uri: Uri, ) -> Self { let router = Router::new() diff --git a/deployer/src/lib.rs b/deployer/src/lib.rs index da61c8829..de44d5056 100644 --- a/deployer/src/lib.rs +++ b/deployer/src/lib.rs @@ -2,7 +2,8 @@ use std::sync::Arc; pub use persistence::Persistence; pub use runtime_manager::RuntimeManager; -use shuttle_common::{backends::client::ServicesApiClient, log::LogRecorder}; +use shuttle_backends::client::ServicesApiClient; +use shuttle_common::log::LogRecorder; use shuttle_proto::{logger, provisioner}; use tokio::sync::Mutex; use tracing::info; diff --git a/deployer/src/main.rs b/deployer/src/main.rs index db4b3304f..139fa065a 100644 --- a/deployer/src/main.rs +++ b/deployer/src/main.rs @@ -1,8 +1,6 @@ use clap::Parser; -use shuttle_common::{ - backends::trace::setup_tracing, - log::{Backend, DeploymentLogLayer}, -}; +use shuttle_backends::trace::setup_tracing; +use shuttle_common::log::{Backend, DeploymentLogLayer}; use shuttle_deployer::{start, Args, Persistence, RuntimeManager, StateChangeLayer}; use shuttle_proto::logger::{self, Batcher}; use tracing::trace; diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index fd5b454a5..e21623916 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -6,11 +6,8 @@ license.workspace = true publish = false [dependencies] -shuttle-common = { workspace = true, features = [ - "backend", - "models", - "persist", -] } +shuttle-backends = { workspace = true, features = ["sqlx"] } +shuttle-common = { workspace = true, features = ["models", "persist"] } shuttle-proto = { workspace = true, features = ["provisioner-client"] } async-posthog = { git = "https://github.com/shuttle-hq/posthog-rs", branch = "main" } @@ -66,7 +63,7 @@ portpicker = { workspace = true } ring = { workspace = true } rmp-serde = { workspace = true } shuttle-common-tests = { workspace = true } -shuttle-proto = { workspace = true, features = ["test-utils"] } +shuttle-backends = { workspace = true, features = ["test-utils"] } snailquote = "0.3.1" tar = { workspace = true } tempfile = { workspace = true } diff --git a/gateway/src/acme.rs b/gateway/src/acme.rs index 535b230d3..96be153f7 100644 --- a/gateway/src/acme.rs +++ b/gateway/src/acme.rs @@ -8,7 +8,7 @@ use instant_acme::{ Identifier, KeyAuthorization, LetsEncrypt, NewAccount, NewOrder, Order, OrderStatus, }; use rcgen::{Certificate, CertificateParams, DistinguishedName}; -use shuttle_common::models::project::ProjectName; +use shuttle_backends::project_name::ProjectName; use tokio::sync::Mutex; use tokio::time::sleep; use tracing::{error, trace, warn}; diff --git a/gateway/src/api/auth_layer.rs b/gateway/src/api/auth_layer.rs index ffeb07581..36ecb13f6 100644 --- a/gateway/src/api/auth_layer.rs +++ b/gateway/src/api/auth_layer.rs @@ -15,10 +15,10 @@ use hyper_reverse_proxy::ReverseProxy; use once_cell::sync::Lazy; use opentelemetry::global; use opentelemetry_http::HeaderInjector; -use shuttle_common::{ - backends::{auth::ConvertResponse, cache::CacheManagement, headers::XShuttleAdminSecret}, - ApiKey, +use shuttle_backends::{ + auth::ConvertResponse, cache::CacheManagement, headers::XShuttleAdminSecret, }; +use shuttle_common::ApiKey; use tower::{Layer, Service}; use tracing::{error, trace, Span}; use tracing_opentelemetry::OpenTelemetrySpanExt; diff --git a/gateway/src/api/latest.rs b/gateway/src/api/latest.rs index 302ae6fb0..59b6524a7 100644 --- a/gateway/src/api/latest.rs +++ b/gateway/src/api/latest.rs @@ -17,20 +17,18 @@ use futures::Future; use http::{StatusCode, Uri}; use instant_acme::{AccountCredentials, ChallengeType}; use serde::{Deserialize, Serialize}; -use shuttle_common::backends::auth::{AuthPublicKey, JwtAuthenticationLayer, ScopedLayer}; -use shuttle_common::backends::cache::CacheManager; -use shuttle_common::backends::metrics::{Metrics, TraceLayer}; -use shuttle_common::backends::ClaimExt; +use shuttle_backends::auth::{AuthPublicKey, JwtAuthenticationLayer, ScopedLayer}; +use shuttle_backends::axum::CustomErrorPath; +use shuttle_backends::cache::CacheManager; +use shuttle_backends::metrics::{Metrics, TraceLayer}; +use shuttle_backends::project_name::ProjectName; +use shuttle_backends::request_span; +use shuttle_backends::ClaimExt; use shuttle_common::claims::{Scope, EXP_MINUTES}; -use shuttle_common::models::error::axum::CustomErrorPath; use shuttle_common::models::error::ErrorKind; use shuttle_common::models::service; -use shuttle_common::models::{ - admin::ProjectResponse, - project::{self, ProjectName}, - stats, -}; -use shuttle_common::{deployment, request_span, VersionInfo}; +use shuttle_common::models::{admin::ProjectResponse, project, stats}; +use shuttle_common::{deployment, VersionInfo}; use shuttle_proto::provisioner::provisioner_client::ProvisionerClient; use shuttle_proto::provisioner::Ping; use tokio::sync::mpsc::Sender; diff --git a/gateway/src/api/project_caller.rs b/gateway/src/api/project_caller.rs index e8232205c..cddf214ed 100644 --- a/gateway/src/api/project_caller.rs +++ b/gateway/src/api/project_caller.rs @@ -4,8 +4,9 @@ use axum::response::Response; use http::{HeaderMap, Method, Request, StatusCode, Uri}; use hyper::Body; use serde::de::DeserializeOwned; +use shuttle_backends::project_name::ProjectName; use shuttle_common::{ - models::{deployment, error::ErrorKind, project::ProjectName, user::UserId}, + models::{deployment, error::ErrorKind, user::UserId}, resource, }; use uuid::Uuid; diff --git a/gateway/src/auth.rs b/gateway/src/auth.rs index 678308291..ac818a266 100644 --- a/gateway/src/auth.rs +++ b/gateway/src/auth.rs @@ -3,9 +3,9 @@ use std::fmt::Debug; use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; use serde::{Deserialize, Serialize}; +use shuttle_backends::project_name::ProjectName; use shuttle_common::claims::{Claim, Scope}; use shuttle_common::models::error::InvalidProjectName; -use shuttle_common::models::project::ProjectName; use shuttle_common::models::user::UserId; use tracing::{trace, Span}; diff --git a/gateway/src/lib.rs b/gateway/src/lib.rs index 2dc54894d..8f39aad1a 100644 --- a/gateway/src/lib.rs +++ b/gateway/src/lib.rs @@ -17,8 +17,8 @@ use hyper::client::HttpConnector; use hyper::Client; use once_cell::sync::Lazy; use service::ContainerSettings; +use shuttle_backends::project_name::ProjectName; use shuttle_common::models::error::{ApiError, ErrorKind}; -use shuttle_common::models::project::ProjectName; use shuttle_common::models::user::UserId; use strum::Display; use tokio::sync::mpsc::error::SendError; @@ -267,11 +267,11 @@ pub mod tests { use jsonwebtoken::EncodingKey; use rand::distributions::{Alphanumeric, DistString, Distribution, Uniform}; use ring::signature::{self, Ed25519KeyPair, KeyPair}; - use shuttle_common::backends::auth::ConvertResponse; + use shuttle_backends::auth::ConvertResponse; + use shuttle_backends::test_utils::resource_recorder::get_mocked_resource_recorder; use shuttle_common::claims::{AccountTier, Claim}; use shuttle_common::models::deployment::DeploymentRequest; use shuttle_common::models::{project, service}; - use shuttle_proto::test_utils::resource_recorder::get_mocked_resource_recorder; use sqlx::sqlite::SqliteConnectOptions; use sqlx::{query, SqlitePool}; use test_context::AsyncTestContext; diff --git a/gateway/src/main.rs b/gateway/src/main.rs index de704ace2..c7bd5bdf5 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -2,7 +2,7 @@ use async_posthog::ClientOptions; use clap::Parser; use futures::prelude::*; -use shuttle_common::backends::trace::setup_tracing; +use shuttle_backends::trace::setup_tracing; use shuttle_common::log::Backend; use shuttle_gateway::acme::{AcmeClient, CustomDomain}; use shuttle_gateway::api::latest::{ApiBuilder, SVC_DEGRADED_THRESHOLD}; diff --git a/gateway/src/project.rs b/gateway/src/project.rs index 50dbd418a..c64c72664 100644 --- a/gateway/src/project.rs +++ b/gateway/src/project.rs @@ -23,9 +23,9 @@ use once_cell::sync::Lazy; use rand::distributions::{Alphanumeric, DistString}; use serde::de::DeserializeOwned; use serde::{Deserialize, Deserializer, Serialize}; -use shuttle_common::backends::headers::X_SHUTTLE_ADMIN_SECRET; +use shuttle_backends::headers::X_SHUTTLE_ADMIN_SECRET; +use shuttle_backends::project_name::ProjectName; use shuttle_common::constants::{default_idle_minutes, DEFAULT_IDLE_MINUTES}; -use shuttle_common::models::project::ProjectName; use shuttle_common::models::service; use tokio::time::{sleep, timeout}; use tracing::{debug, error, info, instrument, trace, warn}; diff --git a/gateway/src/proxy.rs b/gateway/src/proxy.rs index bac1147fd..6e96728c8 100644 --- a/gateway/src/proxy.rs +++ b/gateway/src/proxy.rs @@ -22,11 +22,11 @@ use hyper_reverse_proxy::ReverseProxy; use once_cell::sync::Lazy; use opentelemetry::global; use opentelemetry_http::HeaderInjector; -use shuttle_common::backends::cache::{CacheManagement, CacheManager}; -use shuttle_common::backends::headers::XShuttleProject; +use shuttle_backends::cache::{CacheManagement, CacheManager}; +use shuttle_backends::headers::XShuttleProject; +use shuttle_backends::project_name::ProjectName; use shuttle_common::constants::DEPLOYER_SERVICE_HTTP_PORT; use shuttle_common::models::error::InvalidProjectName; -use shuttle_common::models::project::ProjectName; use tokio::net::TcpSocket; use tokio::sync::mpsc::Sender; use tower_sanitize_path::SanitizePath; diff --git a/gateway/src/service.rs b/gateway/src/service.rs index 8b70f1078..875a02581 100644 --- a/gateway/src/service.rs +++ b/gateway/src/service.rs @@ -21,10 +21,11 @@ use instant_acme::{AccountCredentials, ChallengeType}; use once_cell::sync::Lazy; use opentelemetry::global; use opentelemetry_http::HeaderInjector; -use shuttle_common::backends::headers::XShuttleAdminSecret; +use shuttle_backends::headers::XShuttleAdminSecret; +use shuttle_backends::project_name::ProjectName; use shuttle_common::claims::AccountTier; use shuttle_common::constants::SHUTTLE_IDLE_DOCS_URL; -use shuttle_common::models::project::{ProjectName, State}; +use shuttle_common::models::project::State; use shuttle_common::models::user::UserId; use sqlx::error::DatabaseError; use sqlx::migrate::Migrator; diff --git a/gateway/src/task.rs b/gateway/src/task.rs index 2420c9d5d..0c60725d6 100644 --- a/gateway/src/task.rs +++ b/gateway/src/task.rs @@ -7,7 +7,7 @@ use std::time::{Duration, Instant}; use futures::Future; use opentelemetry::global; -use shuttle_common::models::project::ProjectName; +use shuttle_backends::project_name::ProjectName; use tokio::sync::mpsc::Sender; use tokio::sync::oneshot; use tokio::time::{sleep, timeout}; diff --git a/gateway/src/worker.rs b/gateway/src/worker.rs index 1873ab377..e20b24986 100644 --- a/gateway/src/worker.rs +++ b/gateway/src/worker.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; -use shuttle_common::models::project::ProjectName; +use shuttle_backends::project_name::ProjectName; use tokio::sync::mpsc::error::SendError; use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::RwLock; diff --git a/logger/Cargo.toml b/logger/Cargo.toml index e033eeb2f..0b44075bc 100644 --- a/logger/Cargo.toml +++ b/logger/Cargo.toml @@ -4,9 +4,11 @@ version = "0.42.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] -shuttle-common = { workspace = true, features = ["backend", "extract_propagation", "tonic"] } +shuttle-backends = { workspace = true } +shuttle-common = { workspace = true, features = ["extract_propagation", "tonic"] } shuttle-proto = { workspace = true, features = ["logger"] } async-trait = { workspace = true } diff --git a/logger/src/lib.rs b/logger/src/lib.rs index 032d31d2f..7889a4b12 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -1,7 +1,8 @@ use async_trait::async_trait; use dal::Log; use dal::{Dal, DalError}; -use shuttle_common::{backends::auth::VerifyClaim, claims::Scope}; +use shuttle_backends::auth::VerifyClaim; +use shuttle_common::claims::Scope; use shuttle_proto::logger::LogLine; use shuttle_proto::logger::{ logger_server::Logger, LogsRequest, LogsResponse, StoreLogsRequest, StoreLogsResponse, diff --git a/logger/src/main.rs b/logger/src/main.rs index 5b7f2be74..2c0bcead7 100644 --- a/logger/src/main.rs +++ b/logger/src/main.rs @@ -1,14 +1,11 @@ use std::time::Duration; use clap::Parser; -use shuttle_common::{ - backends::{ - auth::{AuthPublicKey, JwtAuthenticationLayer}, - trace::setup_tracing, - }, - extract_propagation::ExtractPropagationLayer, - log::Backend, +use shuttle_backends::{ + auth::{AuthPublicKey, JwtAuthenticationLayer}, + trace::setup_tracing, }; +use shuttle_common::{extract_propagation::ExtractPropagationLayer, log::Backend}; use shuttle_logger::{args::Args, Postgres, Service}; use shuttle_proto::logger::logger_server::LoggerServer; use tonic::transport::Server; diff --git a/proto/Cargo.toml b/proto/Cargo.toml index ed3b9382c..84be64a96 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-proto" -version.workspace = true +version = "0.42.0" edition.workspace = true license.workspace = true description = "Library for all the gRPC definitions used by shuttle" @@ -22,13 +22,8 @@ tower = { workspace = true, optional = true } tracing = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } -[dev-dependencies] -test-context = { workspace = true } -portpicker = { workspace = true } - [features] default = [] -test-utils = ["portpicker"] logger = [ "shuttle-common/service", @@ -45,10 +40,4 @@ client = ["shuttle-common/claims", "tower"] logger-client = ["logger", "client", "http"] provisioner-client = ["provisioner", "client", "http"] runtime-client = ["runtime", "client", "anyhow", "tokio", "tracing"] -resource-recorder-client = [ - "resource-recorder", - "client", - "async-trait", - "http", - "shuttle-common/backend", -] +resource-recorder-client = ["resource-recorder", "client", "http"] diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 0a8adce4c..b2676f35a 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -5,9 +5,6 @@ pub use prost; pub use prost_types; pub use tonic; -#[cfg(any(test, feature = "test-utils"))] -pub mod test_utils; - #[cfg(feature = "provisioner")] pub mod provisioner { pub use super::generated::provisioner::*; @@ -241,11 +238,7 @@ pub mod resource_recorder { #[cfg(feature = "resource-recorder-client")] mod _resource_recorder_client { - use super::resource_recorder::*; - - use async_trait::async_trait; - use http::{header::AUTHORIZATION, Uri}; - use shuttle_common::backends::client::{self, ResourceDal}; + use http::Uri; pub type Client = super::resource_recorder::resource_recorder_client::ResourceRecorderClient< shuttle_common::claims::ClaimService< @@ -267,131 +260,6 @@ mod _resource_recorder_client { Client::new(resource_recorder_service) } - - #[async_trait] - impl ResourceDal for Client { - async fn get_project_resources( - &mut self, - project_id: &str, - token: &str, - ) -> Result, client::Error> { - let mut req = tonic::Request::new(ProjectResourcesRequest { - project_id: project_id.to_string(), - }); - - req.metadata_mut().insert( - AUTHORIZATION.as_str(), - format!("Bearer {token}") - .parse() - .expect("to construct a bearer token"), - ); - - let resp = (*self) - .get_project_resources(req) - .await? - .into_inner() - .clone(); - - let resources = resp - .resources - .into_iter() - .map(TryInto::try_into) - .collect::, _>>() - .map_err(|error: anyhow::Error| tonic::Status::internal(error.to_string()))?; - - Ok(resources) - } - } - - #[cfg(test)] - mod tests { - use serde_json::json; - use shuttle_common::{database, resource}; - use test_context::{test_context, AsyncTestContext}; - use tonic::Request; - - use crate::generated::resource_recorder::{record_request, RecordRequest}; - use crate::test_utils::resource_recorder::get_mocked_resource_recorder; - - use super::{get_client, Client, ResourceDal}; - - impl AsyncTestContext for Client { - async fn setup() -> Self { - let port = get_mocked_resource_recorder().await; - - get_client(format!("http://localhost:{port}").parse().unwrap()).await - } - - async fn teardown(self) {} - } - - #[test_context(Client)] - #[tokio::test] - async fn get_project_resources(mut r_r_client: &mut Client) { - // First record some resources - r_r_client - .record_resources(Request::new(RecordRequest { - project_id: "project_1".to_string(), - service_id: "service_1".to_string(), - resources: vec![ - record_request::Resource { - r#type: "database::shared::postgres".to_string(), - config: serde_json::to_vec(&json!({"public": true})).unwrap(), - data: serde_json::to_vec(&json!({"username": "test"})).unwrap(), - }, - record_request::Resource { - r#type: "database::aws_rds::mariadb".to_string(), - config: serde_json::to_vec(&json!({})).unwrap(), - data: serde_json::to_vec(&json!({"username": "maria"})).unwrap(), - }, - ], - })) - .await - .unwrap(); - - let resources = (&mut r_r_client as &mut dyn ResourceDal) - .get_project_resources("project_1", "user-1") - .await - .unwrap(); - - assert_eq!( - resources, - vec![ - resource::Response { - r#type: resource::Type::Database(database::Type::Shared( - database::SharedEngine::Postgres - )), - config: json!({"public": true}), - data: json!({"username": "test"}), - }, - resource::Response { - r#type: resource::Type::Database(database::Type::AwsRds( - database::AwsRdsEngine::MariaDB - )), - config: json!({}), - data: json!({"username": "maria"}), - } - ] - ); - - // Getting only RDS resources should filter correctly - let resources = (&mut r_r_client as &mut dyn ResourceDal) - .get_project_rds_resources("project_1", "user-1") - .await - .unwrap(); - - assert_eq!( - resources, - vec![resource::Response { - r#type: resource::Type::Database(database::Type::AwsRds( - database::AwsRdsEngine::MariaDB - )), - config: json!({}), - data: json!({"username": "maria"}), - }] - ); - } - } } #[cfg(feature = "logger")] diff --git a/provisioner/Cargo.toml b/provisioner/Cargo.toml index d919ce5fb..ef5bc1f21 100644 --- a/provisioner/Cargo.toml +++ b/provisioner/Cargo.toml @@ -7,7 +7,8 @@ description = "Service responsible for provisioning and managing resources for s publish = false [dependencies] -shuttle-common = { workspace = true, features = ["backend", "extract_propagation", "models", "service", "tonic"] } +shuttle-backends = { workspace = true } +shuttle-common = { workspace = true, features = ["extract_propagation", "models", "service", "tonic"] } shuttle-proto = { workspace = true, features = ["provisioner", "resource-recorder-client"] } aws-config = "0.56.1" @@ -29,6 +30,5 @@ ctor = { workspace = true } once_cell = { workspace = true } portpicker = { workspace = true } serde_json = { workspace = true } -shuttle-common = { workspace = true, features = ["test-utils"] } +shuttle-backends = { workspace = true, features = ["test-utils"] } shuttle-common-tests = { workspace = true } -shuttle-proto = { workspace = true, features = ["test-utils"] } diff --git a/provisioner/src/lib.rs b/provisioner/src/lib.rs index 6a6253f92..c54dae22b 100644 --- a/provisioner/src/lib.rs +++ b/provisioner/src/lib.rs @@ -11,11 +11,11 @@ use aws_sdk_rds::{ pub use error::Error; use mongodb::{bson::doc, options::ClientOptions}; use rand::Rng; -use shuttle_common::backends::auth::VerifyClaim; -use shuttle_common::backends::client::ServicesApiClient; -use shuttle_common::backends::ClaimExt; +use shuttle_backends::auth::VerifyClaim; +use shuttle_backends::client::ServicesApiClient; +use shuttle_backends::project_name::ProjectName; +use shuttle_backends::ClaimExt; use shuttle_common::claims::{Claim, Scope}; -use shuttle_common::models::project::ProjectName; pub use shuttle_proto::provisioner::provisioner_server::ProvisionerServer; use shuttle_proto::provisioner::{ aws_rds, database_request::DbType, provisioner_server::Provisioner, shared, AwsRds, diff --git a/provisioner/src/main.rs b/provisioner/src/main.rs index ef2d50e21..ba5dad540 100644 --- a/provisioner/src/main.rs +++ b/provisioner/src/main.rs @@ -1,14 +1,11 @@ use std::{net::SocketAddr, time::Duration}; use clap::Parser; -use shuttle_common::{ - backends::{ - auth::{AuthPublicKey, JwtAuthenticationLayer}, - trace::setup_tracing, - }, - extract_propagation::ExtractPropagationLayer, - log::Backend, +use shuttle_backends::{ + auth::{AuthPublicKey, JwtAuthenticationLayer}, + trace::setup_tracing, }; +use shuttle_common::{extract_propagation::ExtractPropagationLayer, log::Backend}; use shuttle_provisioner::{Args, ProvisionerServer, ShuttleProvisioner}; use tonic::transport::Server; diff --git a/provisioner/tests/provisioner.rs b/provisioner/tests/provisioner.rs index b0866e64d..6d3717e39 100644 --- a/provisioner/tests/provisioner.rs +++ b/provisioner/tests/provisioner.rs @@ -4,10 +4,10 @@ use ctor::dtor; use helpers::{exec_mongosh, exec_psql, DbType, DockerInstance}; use once_cell::sync::Lazy; use serde_json::Value; -use shuttle_common::test_utils::get_mocked_gateway_server; -use shuttle_proto::{ - provisioner::shared, test_utils::resource_recorder::get_mocked_resource_recorder, +use shuttle_backends::test_utils::{ + gateway::get_mocked_gateway_server, resource_recorder::get_mocked_resource_recorder, }; +use shuttle_proto::provisioner::shared; use shuttle_provisioner::ShuttleProvisioner; use tonic::transport::Uri; diff --git a/resource-recorder/Cargo.toml b/resource-recorder/Cargo.toml index 6b0fe4267..b860015f2 100644 --- a/resource-recorder/Cargo.toml +++ b/resource-recorder/Cargo.toml @@ -4,9 +4,11 @@ version = "0.42.0" edition.workspace = true license.workspace = true repository.workspace = true +publish = false [dependencies] -shuttle-common = { workspace = true, features = ["backend", "extract_propagation", "tonic", "sqlx"] } +shuttle-backends = { workspace = true } +shuttle-common = { workspace = true, features = ["extract_propagation", "tonic", "sqlx"] } shuttle-proto = { workspace = true, features = ["resource-recorder"] } async-trait = { workspace = true } @@ -26,5 +28,5 @@ ulid = { workspace = true } portpicker = { workspace = true } pretty_assertions = { workspace = true } serde_json = { workspace = true } -shuttle-common = { workspace = true, features = ["test-utils"] } +shuttle-backends = { workspace = true, features = ["test-utils"] } shuttle-common-tests = { workspace = true } diff --git a/resource-recorder/src/lib.rs b/resource-recorder/src/lib.rs index 35b91dba7..768289e62 100644 --- a/resource-recorder/src/lib.rs +++ b/resource-recorder/src/lib.rs @@ -1,10 +1,8 @@ use async_trait::async_trait; use dal::{Dal, DalError, Resource}; use prost_types::TimestampError; -use shuttle_common::{ - backends::{auth::VerifyClaim, client::ServicesApiClient, ClaimExt}, - claims::{Claim, Scope}, -}; +use shuttle_backends::{auth::VerifyClaim, client::ServicesApiClient, ClaimExt}; +use shuttle_common::claims::{Claim, Scope}; use shuttle_proto::resource_recorder::{ self, resource_recorder_server::ResourceRecorder, ProjectResourcesRequest, RecordRequest, ResourceIds, ResourceResponse, ResourcesResponse, ResultResponse, ServiceResourcesRequest, diff --git a/resource-recorder/src/main.rs b/resource-recorder/src/main.rs index f5a7e1e98..e1767662e 100644 --- a/resource-recorder/src/main.rs +++ b/resource-recorder/src/main.rs @@ -1,15 +1,12 @@ use std::time::Duration; use clap::Parser; -use shuttle_common::{ - backends::{ - auth::{AuthPublicKey, JwtAuthenticationLayer}, - client::ServicesApiClient, - trace::setup_tracing, - }, - extract_propagation::ExtractPropagationLayer, - log::Backend, +use shuttle_backends::{ + auth::{AuthPublicKey, JwtAuthenticationLayer}, + client::ServicesApiClient, + trace::setup_tracing, }; +use shuttle_common::{extract_propagation::ExtractPropagationLayer, log::Backend}; use shuttle_proto::resource_recorder::resource_recorder_server::ResourceRecorderServer; use shuttle_resource_recorder::{args::Args, Service, Sqlite}; use tonic::transport::Server; diff --git a/resource-recorder/tests/integration.rs b/resource-recorder/tests/integration.rs index 7c57a5d5a..dc201a32e 100644 --- a/resource-recorder/tests/integration.rs +++ b/resource-recorder/tests/integration.rs @@ -3,9 +3,9 @@ use std::net::{Ipv4Addr, SocketAddr}; use portpicker::pick_unused_port; use pretty_assertions::{assert_eq, assert_ne}; use serde_json::json; -use shuttle_common::{ - backends::client::ServicesApiClient, claims::Scope, test_utils::get_mocked_gateway_server, -}; +use shuttle_backends::client::ServicesApiClient; +use shuttle_backends::test_utils::gateway::get_mocked_gateway_server; +use shuttle_common::claims::Scope; use shuttle_common_tests::JwtScopesLayer; use shuttle_proto::resource_recorder::{ record_request, resource_recorder_client::ResourceRecorderClient,