diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 50574ba80..40a332892 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -108,10 +108,10 @@ This prevents `gateway` from starting up. Now you can start deployer only using: ```bash provisioner_address=$(docker inspect --format '{{(index .NetworkSettings.Networks "shuttle_default").IPAddress}}' shuttle_prod_hello-world-rocket-app_run) -cargo run -p shuttle-deployer -- --provisioner-address $provisioner_address --provisioner-port 8000 --proxy-fqdn local.rs --admin-secret test-key +cargo run -p shuttle-deployer -- --provisioner-address $provisioner_address --provisioner-port 8000 --proxy-fqdn local.rs --admin-secret test-key --project ``` -The `--admin-secret` can safely be changed to your api-key to make testing easier. +The `--admin-secret` can safely be changed to your api-key to make testing easier. While `` needs to match the name of the project that will be deployed to this deployer. This is the `Cargo.toml` or `Shuttle.toml` name for the project. ### Using Podman instead of Docker If you are using Podman over Docker, then expose a rootless socket of Podman using the following command: diff --git a/deployer/src/args.rs b/deployer/src/args.rs index 2950d1f23..0be8f0e58 100644 --- a/deployer/src/args.rs +++ b/deployer/src/args.rs @@ -2,7 +2,7 @@ use std::{net::SocketAddr, path::PathBuf}; use clap::Parser; use fqdn::FQDN; -use shuttle_common::Port; +use shuttle_common::{project::ProjectName, Port}; /// Program to handle the deploys for a single project /// Handling includes, building, testing, and running each service @@ -33,6 +33,10 @@ pub struct Args { #[clap(long, default_value = "0.0.0.0:8000")] pub proxy_address: SocketAddr, + /// Project being served by this deployer + #[clap(long)] + pub project: ProjectName, + /// Secret that will be used to perform admin tasks on this deployer #[clap(long)] pub admin_secret: String, diff --git a/deployer/src/handlers/mod.rs b/deployer/src/handlers/mod.rs index daf56eea1..fb35764a5 100644 --- a/deployer/src/handlers/mod.rs +++ b/deployer/src/handlers/mod.rs @@ -4,6 +4,7 @@ use axum::body::{Body, BoxBody}; use axum::extract::ws::{self, WebSocket}; use axum::extract::{Extension, Path, Query}; use axum::http::{Request, Response}; +use axum::middleware::from_extractor; use axum::routing::{get, Router}; use axum::{extract::BodyStream, Json}; use bytes::BufMut; @@ -13,6 +14,7 @@ use futures::StreamExt; use opentelemetry::global; use opentelemetry_http::HeaderExtractor; use shuttle_common::models::secret; +use shuttle_common::project::ProjectName; use shuttle_common::LogItem; use tower_http::auth::RequireAuthorizationLayer; use tower_http::trace::TraceLayer; @@ -28,11 +30,14 @@ use std::time::Duration; pub use {self::error::Error, self::error::Result}; +mod project; + pub fn make_router( persistence: Persistence, deployment_manager: DeploymentManager, proxy_fqdn: FQDN, admin_secret: String, + project_name: ProjectName, ) -> Router { Router::new() .route("/projects/:project_name/services", get(list_services)) @@ -78,6 +83,8 @@ pub fn make_router( }, ), ) + .route_layer(from_extractor::()) + .layer(Extension(project_name)) } async fn list_services( diff --git a/deployer/src/handlers/project.rs b/deployer/src/handlers/project.rs new file mode 100644 index 000000000..2389cd7e4 --- /dev/null +++ b/deployer/src/handlers/project.rs @@ -0,0 +1,52 @@ +use std::collections::HashMap; + +use async_trait::async_trait; +use axum::extract::{FromRequest, Path, RequestParts}; +use hyper::StatusCode; +use shuttle_common::project::ProjectName; +use tracing::error; + +/// Gaurd to ensure request are for the project served by this deployer +/// Note: this guard needs the `ProjectName` extension to be set +pub struct ProjectNameGuard; + +#[async_trait] +impl FromRequest for ProjectNameGuard +where + B: Send, +{ + type Rejection = StatusCode; + + async fn from_request(req: &mut RequestParts) -> Result { + // We expect some path parameters + let Path(path): Path> = match req.extract().await { + Ok(path) => path, + Err(_) => return Err(StatusCode::NOT_FOUND), + }; + + // All our routes have the `project_name` parameter + let project_name = match path.get("project_name") { + Some(project_name) => project_name, + None => { + error!("ProjectNameGuard found no project name in path"); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; + + // This extractor requires the ProjectName extension to be set + let expected_project_name: &ProjectName = match req.extensions().get() { + Some(expected) => expected, + None => { + error!("ProjectName extension is not set"); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; + + if project_name == expected_project_name.as_str() { + Ok(ProjectNameGuard) + } else { + error!(project_name, "project is not served by this deployer"); + Err(StatusCode::BAD_REQUEST) + } + } +} diff --git a/deployer/src/lib.rs b/deployer/src/lib.rs index 265554402..06befa2dd 100644 --- a/deployer/src/lib.rs +++ b/deployer/src/lib.rs @@ -52,6 +52,7 @@ pub async fn start( deployment_manager, args.proxy_fqdn, args.admin_secret, + args.project, ); let make_service = router.into_make_service(); diff --git a/gateway/src/project.rs b/gateway/src/project.rs index a4a70ad85..cb0a46ff9 100644 --- a/gateway/src/project.rs +++ b/gateway/src/project.rs @@ -389,11 +389,14 @@ impl ProjectCreating { "Image": image, "Hostname": format!("{prefix}{project_name}"), "Labels": { - "shuttle_prefix": prefix + "shuttle_prefix": prefix, + "project.name": project_name, }, "Cmd": [ "--admin-secret", initial_key, + "--project", + project_name, "--api-address", format!("0.0.0.0:{RUNTIME_API_PORT}"), "--provisioner-address", @@ -411,10 +414,7 @@ impl ProjectCreating { ], "Env": [ "RUST_LOG=debug", - ], - "Labels": { - "project.name": project_name, - } + ] }); let mut config = Config::::from(container_config); @@ -617,9 +617,13 @@ pub struct Service { impl Service { pub fn from_container(container: ContainerInspectResponse) -> Result { + // This version can't be enabled while there are active deployers before v0.8.0 since the don't have this label + // TODO: switch to this version when you notice all deployers are greater than v0.8.0 + // let name = safe_unwrap!(container.config.labels.get("project.name")).to_string(); let container_name = safe_unwrap!(container.name.strip_prefix("/")).to_string(); - - let resource_name = safe_unwrap!(container_name.strip_suffix("_run")).to_string(); + let prefix = safe_unwrap!(container.config.labels.get("shuttle_prefix")).to_string(); + let resource_name = + safe_unwrap!(container_name.strip_prefix(&prefix).strip_suffix("_run")).to_string(); let network = safe_unwrap!(container.network_settings.networks) .values()