From c2a9a36db419b010c5596eab577fcfff06210b6e Mon Sep 17 00:00:00 2001 From: jonaro00 <54029719+jonaro00@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:30:46 +0200 Subject: [PATCH 1/7] refa: move ToJson to api-client --- Cargo.lock | 1 + api-client/Cargo.toml | 1 + api-client/src/lib.rs | 5 ++++- api-client/src/util.rs | 45 ++++++++++++++++++++++++++++++++++++++++ common/src/models/mod.rs | 45 ---------------------------------------- 5 files changed, 51 insertions(+), 46 deletions(-) create mode 100644 api-client/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index d195545d3..c49e79866 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5398,6 +5398,7 @@ dependencies = [ "anyhow", "async-trait", "headers", + "http 0.2.12", "percent-encoding", "reqwest 0.11.27", "reqwest-middleware", diff --git a/api-client/Cargo.toml b/api-client/Cargo.toml index 9dda0b991..699b6912e 100644 --- a/api-client/Cargo.toml +++ b/api-client/Cargo.toml @@ -13,6 +13,7 @@ shuttle-common = { workspace = true, features = ["models"] } anyhow = { workspace = true } async-trait = { workspace = true } headers = { workspace = true } +http = { workspace = true } percent-encoding = { workspace = true } reqwest = { workspace = true, features = ["json"] } reqwest-middleware = "0.2.5" diff --git a/api-client/src/lib.rs b/api-client/src/lib.rs index 354bb4d4e..cd642bdc0 100644 --- a/api-client/src/lib.rs +++ b/api-client/src/lib.rs @@ -12,7 +12,7 @@ use shuttle_common::log::{LogsRange, LogsResponseBeta}; use shuttle_common::models::deployment::{ DeploymentRequest, DeploymentRequestBeta, UploadArchiveResponseBeta, }; -use shuttle_common::models::{deployment, project, service, team, user, ToJson}; +use shuttle_common::models::{deployment, project, service, team, user}; use shuttle_common::resource::{ProvisionResourceRequest, ShuttleResourceOutput}; use shuttle_common::{resource, LogItem, VersionInfo}; use tokio::net::TcpStream; @@ -27,6 +27,9 @@ use crate::middleware::LoggingMiddleware; #[cfg(feature = "tracing")] use tracing::{debug, error}; +mod util; +use util::ToJson; + #[derive(Clone)] pub struct ShuttleApiClient { pub client: ClientWithMiddleware, diff --git a/api-client/src/util.rs b/api-client/src/util.rs new file mode 100644 index 000000000..978a54cb8 --- /dev/null +++ b/api-client/src/util.rs @@ -0,0 +1,45 @@ +use anyhow::{Context, Result}; +use async_trait::async_trait; +use http::StatusCode; +use serde::de::DeserializeOwned; +use shuttle_common::models::error::ApiError; + +/// A to_json wrapper for handling our error states +#[async_trait] +pub trait ToJson { + async fn to_json(self) -> Result; +} + +#[async_trait] +impl ToJson for reqwest::Response { + async fn to_json(self) -> Result { + let status_code = self.status(); + let full = self.bytes().await?; + + #[cfg(feature = "tracing")] + tracing::trace!( + response = %String::from_utf8(full.to_vec()).unwrap_or_else(|_| format!("[{} bytes]", full.len())), + "parsing response to json" + ); + + if matches!( + status_code, + StatusCode::OK | StatusCode::SWITCHING_PROTOCOLS + ) { + serde_json::from_slice(&full).context("failed to parse a successful response") + } else { + #[cfg(feature = "tracing")] + tracing::trace!("parsing response to common error"); + + let res: ApiError = match serde_json::from_slice(&full) { + Ok(res) => res, + _ => ApiError { + message: "Failed to parse response from the server.".to_string(), + status_code: status_code.as_u16(), + }, + }; + + Err(res.into()) + } + } +} diff --git a/common/src/models/mod.rs b/common/src/models/mod.rs index 1d9820003..03e6aba1a 100644 --- a/common/src/models/mod.rs +++ b/common/src/models/mod.rs @@ -7,48 +7,3 @@ pub mod service; pub mod stats; pub mod team; pub mod user; - -use anyhow::{Context, Result}; -use async_trait::async_trait; -use http::StatusCode; -use serde::de::DeserializeOwned; -use tracing::trace; - -/// A to_json wrapper for handling our error states -#[async_trait] -pub trait ToJson { - async fn to_json(self) -> Result; -} - -#[async_trait] -impl ToJson for reqwest::Response { - async fn to_json(self) -> Result { - let status_code = self.status(); - let full = self.bytes().await?; - - let str_repr = - String::from_utf8(full.to_vec()).unwrap_or_else(|_| format!("[{} bytes]", full.len())); - trace!( - response = %str_repr, - "parsing response to json" - ); - - if matches!( - status_code, - StatusCode::OK | StatusCode::SWITCHING_PROTOCOLS - ) { - serde_json::from_slice(&full).context("failed to parse a successful response") - } else { - trace!("parsing response to common error"); - let res: error::ApiError = match serde_json::from_slice(&full) { - Ok(res) => res, - _ => error::ApiError { - message: "Failed to parse response from the server.".to_string(), - status_code: status_code.as_u16(), - }, - }; - - Err(res.into()) - } - } -} From 7dc9ffad8321ee5d1935e63787fec9d8a71c757e Mon Sep 17 00:00:00 2001 From: jonaro00 <54029719+jonaro00@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:36:46 +0200 Subject: [PATCH 2/7] wip: runtime args, c-s beta local run, c-s provisioner api mimic --- Cargo.lock | 1 + cargo-shuttle/Cargo.toml | 1 + cargo-shuttle/src/lib.rs | 293 +++++++++++++++++++++--- cargo-shuttle/src/provisioner_server.rs | 2 +- deployer/src/runtime_manager.rs | 7 +- runtime/src/beta.rs | 68 +++--- runtime/src/start.rs | 4 +- runtime/tests/integration/helpers.rs | 9 +- service/src/builder.rs | 2 +- service/src/runner.rs | 12 +- 10 files changed, 318 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c49e79866..e3dd0d44f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1002,6 +1002,7 @@ dependencies = [ "globset", "headers", "home", + "hyper 0.14.30", "ignore", "indicatif", "indoc", diff --git a/cargo-shuttle/Cargo.toml b/cargo-shuttle/Cargo.toml index 9a75e970a..e6d2a9537 100644 --- a/cargo-shuttle/Cargo.toml +++ b/cargo-shuttle/Cargo.toml @@ -35,6 +35,7 @@ gix = { version = "0.63.0", default-features = false, features = [ globset = "0.4.13" headers = { workspace = true } home = { workspace = true } +hyper = { workspace = true } ignore = "0.4.20" indicatif = "0.17.3" indoc = "2.0.1" diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index cb85811be..60f55c87b 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -5,6 +5,7 @@ mod provisioner_server; mod suggestions; use std::collections::{BTreeMap, HashMap}; +use std::convert::Infallible; use std::ffi::OsString; use std::fmt::Write as FmtWrite; use std::fs::{read_to_string, File}; @@ -16,6 +17,7 @@ use std::str::FromStr; use anyhow::{anyhow, bail, Context, Result}; use args::{ConfirmationArgs, GenerateCommand, TableArgs}; +use chrono::Utc; use clap::{parser::ValueSource, CommandFactory, FromArgMatches}; use clap_complete::{generate, Shell}; use clap_mangen::Man; @@ -27,12 +29,15 @@ use flate2::Compression; use futures::{StreamExt, TryFutureExt}; use git2::{Repository, StatusOptions}; use globset::{Glob, GlobSetBuilder}; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{body, Body, Method, Request as HyperRequest, Response, Server}; use ignore::overrides::OverrideBuilder; use ignore::WalkBuilder; use indicatif::ProgressBar; use indoc::{formatdoc, printdoc}; use reqwest::header::HeaderMap; use shuttle_api_client::ShuttleApiClient; +use shuttle_common::resource::ProvisionResourceRequest; use shuttle_common::{ constants::{ headers::X_CARGO_SHUTTLE_VERSION, API_URL_DEFAULT, DEFAULT_IDLE_MINUTES, EXAMPLES_REPO, @@ -56,7 +61,7 @@ use shuttle_common::{ resource::{self, ResourceInput, ShuttleResourceOutput}, semvers_are_compatible, templates::TemplatesSchema, - ApiKey, DatabaseResource, DbInput, LogItem, VersionInfo, + ApiKey, DatabaseResource, DbInput, LogItem, LogItemBeta, VersionInfo, }; use shuttle_proto::{ provisioner::{provisioner_server::Provisioner, DatabaseRequest}, @@ -72,7 +77,7 @@ use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Child; use tokio::time::{sleep, Duration}; use tonic::{Request, Status}; -use tracing::{debug, error, trace, warn}; +use tracing::{debug, error, info, trace, warn}; use uuid::Uuid; use zip::write::FileOptions; @@ -233,7 +238,13 @@ impl Shuttle { Command::Login(login_args) => self.login(login_args, args.offline).await, Command::Logout(logout_args) => self.logout(logout_args).await, Command::Feedback => self.feedback(), - Command::Run(run_args) => self.local_run(run_args).await, + Command::Run(run_args) => { + if self.beta { + self.local_run_beta(run_args).await + } else { + self.local_run(run_args).await + } + } Command::Deploy(deploy_args) => self.deploy(deploy_args).await, Command::Status => self.status().await, Command::Logs(logs_args) => { @@ -1267,12 +1278,7 @@ impl Shuttle { Ok(CommandOutcome::Ok) } - async fn spin_local_runtime( - beta: bool, - run_args: &RunArgs, - service: &BuiltService, - idx: u16, - ) -> Result> { + fn get_secrets(run_args: &RunArgs, service: &BuiltService) -> Result> { let secrets_file = run_args.secret_args.secrets.clone().or_else(|| { let crate_dir = service.crate_directory(); // Prioritise crate-local prod secrets over workspace dev secrets (in the rare case that both exist) @@ -1300,8 +1306,11 @@ impl Shuttle { Default::default() }; - trace!(path = ?service.executable_path, "using alpha runtime"); - if let Err(err) = check_version(&service.executable_path).await { + Ok(secrets) + } + + async fn check_and_warn_runtime_version(path: &Path) -> Result<()> { + if let Err(err) = check_version(path).await { warn!("{}", err); if let Some(mismatch) = err.downcast_ref::() { println!("Warning: {}.", mismatch); @@ -1310,13 +1319,13 @@ impl Shuttle { // should help the user to update cargo-shuttle. printdoc! {" Hint: A newer version of cargo-shuttle is available. - Check out the installation docs for how to update: {SHUTTLE_INSTALL_DOCS_URL}", + Check out the installation docs for how to update: {SHUTTLE_INSTALL_DOCS_URL}", }; } else { printdoc! {" Hint: A newer version of shuttle-runtime is available. - Change its version to {} in Cargo.toml to update it, or - run this command: cargo add shuttle-runtime@{}", + Change its version to {} in Cargo.toml to update it, + or run this command: cargo add shuttle-runtime@{}", mismatch.cargo_shuttle, mismatch.cargo_shuttle, }; @@ -1325,23 +1334,32 @@ impl Shuttle { return Err(err.context( format!( "Failed to verify the version of shuttle-runtime in {}. Is cargo targeting the correct executable?", - service.executable_path.display() + path.display() ) )); } } - let runtime_executable = service.executable_path.clone(); + Ok(()) + } + + async fn spin_local_runtime( + run_args: &RunArgs, + service: &BuiltService, + idx: u16, + ) -> Result> { + let secrets = Shuttle::get_secrets(run_args, service)?; + + trace!(path = ?service.executable_path, "runtime executable"); + + Shuttle::check_and_warn_runtime_version(&service.executable_path).await?; + + let runtime_executable = service.executable_path.clone(); let port = portpicker::pick_unused_port().expect("unable to find available port for gRPC server"); // Child process and gRPC client for sending requests to it - let (mut runtime, mut runtime_client) = runner::start( - beta, - port, - runtime_executable, - service.workspace_path.as_path(), - ) - .await?; + let (mut runtime, mut runtime_client) = + runner::start(port, runtime_executable, service.workspace_path.as_path()).await?; let service_name = service.service_name()?; let deployment_id: Uuid = Default::default(); @@ -1416,9 +1434,7 @@ impl Shuttle { service_name.as_str(), false, false, - // Set beta to false to avoid breaking local run with beta changes. - // TODO: make local run compatible with --beta. - false + false, ) ); @@ -1625,7 +1641,7 @@ impl Shuttle { } async fn pre_local_run(&self, run_args: &RunArgs) -> Result> { - trace!("starting a local run for a service: {run_args:?}"); + trace!("starting a local run with args: {run_args:?}"); let (tx, mut rx) = tokio::sync::mpsc::channel::(256); tokio::task::spawn(async move { @@ -1671,10 +1687,26 @@ impl Shuttle { exit(0); } } + fn find_available_port_beta(run_args: &mut RunArgs) { + let original_port = run_args.port; + for port in (run_args.port..=std::u16::MAX).step_by(10) { + if !portpicker::is_free_tcp(port) { + continue; + } + run_args.port = port; + break; + } + + if run_args.port != original_port { + eprintln!( + "Port {} is already in use. Using port {}.", + original_port, run_args.port, + ) + }; + } #[cfg(target_family = "unix")] async fn local_run(&self, mut run_args: RunArgs) -> Result { - debug!("starting local run"); let services = self.pre_local_run(&run_args).await?; let mut sigterm_notif = @@ -1694,7 +1726,7 @@ impl Shuttle { // We must cover the case of starting multiple workspace services and receiving a signal in parallel. // This must stop all the existing runtimes and creating new ones. signal_received = tokio::select! { - res = Shuttle::spin_local_runtime(self.beta, &run_args, service, i as u16) => { + res = Shuttle::spin_local_runtime(&run_args, service, i as u16) => { match res { Ok(runtime) => { Shuttle::add_runtime_info(runtime, &mut runtimes).await?; @@ -1787,6 +1819,209 @@ impl Shuttle { Ok(CommandOutcome::Ok) } + async fn local_run_beta(&self, mut run_args: RunArgs) -> Result { + let services = self.pre_local_run(&run_args).await?; + let service = services + .first() + .expect("at least one shuttle service") + .to_owned(); + + trace!(path = ?service.executable_path, "runtime executable"); + + let secrets = Shuttle::get_secrets(&run_args, &service)?; + Shuttle::find_available_port_beta(&mut run_args); + Shuttle::check_and_warn_runtime_version(&service.executable_path).await?; + + let runtime_executable = service.executable_path.clone(); + let api_port = portpicker::pick_unused_port() + .expect("failed to find available port for local provisioner server"); + let api_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), api_port); + let ip = if run_args.external { + Ipv4Addr::UNSPECIFIED + } else { + Ipv4Addr::LOCALHOST + }; + + let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(handler)) }); + + async fn handler( + req: HyperRequest, + ) -> std::result::Result, hyper::Error> { + let method = req.method().clone(); + let uri = req.uri().clone(); + debug!("Received {method} {uri}"); + + let body = body::to_bytes(req.into_body()).await?.to_vec(); + let res = match (method, uri.to_string().as_str()) { + (Method::GET, "/projects/proj_LOCAL/resources/secrets") => resource::Response { + config: serde_json::Value::Null, + r#type: resource::Type::Secrets, + data: serde_json::to_value(HashMap::::new()).unwrap(), + }, + (Method::POST, "/projects/proj_LOCAL/resources") => { + let prov = LocalProvisioner::new().unwrap(); + let shuttle_resource: ProvisionResourceRequest = + serde_json::from_slice(&body).unwrap(); + // TODO: Reject req if version field mismatch + + match shuttle_resource.r#type { + resource::Type::Database(db_type) => { + let config: DbInput = serde_json::from_value(shuttle_resource.config) + .context("deserializing resource config")?; + let res = match config.local_uri { + Some(local_uri) => DatabaseResource::ConnectionString(local_uri), + None => DatabaseResource::Info( + prov.provision_database(Request::new(DatabaseRequest { + project_name: project_name.to_string(), + db_type: Some(db_type.into()), + db_name: config.db_name, + })) + .await + .context("Failed to start database container. Make sure that a Docker engine is running.")? + .into_inner() + .into(), + ), + }; + resource::Response { + r#type: shuttle_resource.r#type, + config: serde_json::Value::Null, + data: serde_json::to_value(&res).unwrap(), + } + } + resource::Type::Container => { + let config = serde_json::from_value(shuttle_resource.config) + .context("deserializing resource config")?; + let res = prov.start_container(config).await.context("Failed to start Docker container. Make sure that a Docker engine is running.")?; + } + resource::Type::Secrets => { + panic!("bruh?"); + } + _ => { + unimplemented!("resource not supported"); + } + } + } + _ => todo!(), + }; + + let res = Response::new(Body::from(serde_json::to_vec(&res).unwrap())); + + Ok(res) + } + let server = Server::bind(&api_addr).serve(make_svc); + tokio::spawn(async move { + if let Err(e) = server.await { + eprintln!("Server error: {}", e); + exit(1); + } + }); + + println!( + "\n {} {} on http://{}:{}\n", + "Starting".bold().green(), + service.package_name, + ip, + run_args.port, + ); + + info!( + path = %runtime_executable.display(), + "Spawning runtime process", + ); + let mut runtime = tokio::process::Command::new( + dunce::canonicalize(runtime_executable).context("canonicalize path of executable")?, + ) + .current_dir(&service.workspace_path) + .args(&["--run"]) + .envs([ + ("SHUTTLE_BETA", "true"), + ("SHUTTLE_PROJECT_ID", "proj_LOCAL"), + ("SHUTTLE_PROJECT_NAME", "TODO"), + ("SHUTTLE_ENV", Environment::Local.to_string().as_str()), + ("SHUTTLE_RUNTIME_IP", ip.to_string().as_str()), + ("SHUTTLE_RUNTIME_PORT", run_args.port.to_string().as_str()), + ( + "SHUTTLE_API", + format!("http://127.0.0.1:{}", api_port).as_str(), + ), + ]) + .stdout(std::process::Stdio::piped()) + .kill_on_drop(true) + .spawn() + .context("spawning runtime process")?; + + let child_stdout = runtime + .stdout + .take() + .context("child process did not have a handle to stdout")?; + let mut reader = BufReader::new(child_stdout).lines(); + let raw = run_args.raw; + tokio::spawn(async move { + while let Some(line) = reader.next_line().await.unwrap() { + if raw { + println!("{}", line); + } else { + let log_item = LogItemBeta::new(Utc::now(), "app".to_owned(), line); + println!("{log_item}"); + } + } + }); + + #[cfg(target_family = "unix")] + { + let mut sigterm_notif = + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("Can not get the SIGTERM signal receptor"); + let mut sigint_notif = + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) + .expect("Can not get the SIGINT signal receptor"); + tokio::select! { + _ = sigterm_notif.recv() => { + eprintln!("cargo-shuttle received SIGTERM. Killing all the runtimes..."); + }, + _ = sigint_notif.recv() => { + eprintln!("cargo-shuttle received SIGINT. Killing all the runtimes..."); + } + }; + } + #[cfg(target_family = "windows")] + { + let mut ctrl_break_notif = tokio::signal::windows::ctrl_break() + .expect("Can not get the CtrlBreak signal receptor"); + let mut ctrl_c_notif = + tokio::signal::windows::ctrl_c().expect("Can not get the CtrlC signal receptor"); + let mut ctrl_close_notif = tokio::signal::windows::ctrl_close() + .expect("Can not get the CtrlClose signal receptor"); + let mut ctrl_logoff_notif = tokio::signal::windows::ctrl_logoff() + .expect("Can not get the CtrlLogoff signal receptor"); + let mut ctrl_shutdown_notif = tokio::signal::windows::ctrl_shutdown() + .expect("Can not get the CtrlShutdown signal receptor"); + + tokio::select! { + _ = ctrl_break_notif.recv() => { + eprintln!("cargo-shuttle received ctrl-break."); + }, + _ = ctrl_c_notif.recv() => { + eprintln!("cargo-shuttle received ctrl-c."); + }, + _ = ctrl_close_notif.recv() => { + eprintln!("cargo-shuttle received ctrl-close."); + }, + _ = ctrl_logoff_notif.recv() => { + eprintln!("cargo-shuttle received ctrl-logoff."); + }, + _ = ctrl_shutdown_notif.recv() => { + eprintln!("cargo-shuttle received ctrl-shutdown."); + } + } + } + runtime.kill().await?; + + // println!("Run `cargo shuttle deploy` to deploy your Shuttle service."); + + Ok(CommandOutcome::Ok) + } + #[cfg(target_family = "windows")] async fn handle_signals() -> bool { let mut ctrl_break_notif = tokio::signal::windows::ctrl_break() diff --git a/cargo-shuttle/src/provisioner_server.rs b/cargo-shuttle/src/provisioner_server.rs index 256b3a16d..f62de5b4e 100644 --- a/cargo-shuttle/src/provisioner_server.rs +++ b/cargo-shuttle/src/provisioner_server.rs @@ -39,7 +39,7 @@ pub struct LocalProvisioner { impl LocalProvisioner { pub fn new() -> Result { // This only constructs the client and does not try to connect. - // A "no such file" error will happen on the first request to Docker. + // If the socket is not found, a "no such file" error will happen on the first request to Docker. Ok(Self { docker: Docker::connect_with_local_defaults()?, }) diff --git a/deployer/src/runtime_manager.rs b/deployer/src/runtime_manager.rs index f46f9e487..a0c261adc 100644 --- a/deployer/src/runtime_manager.rs +++ b/deployer/src/runtime_manager.rs @@ -57,10 +57,9 @@ impl RuntimeManager { .unwrap_or_default() ); - let (mut process, runtime_client) = - runner::start(false, port, runtime_executable, project_path) - .await - .context("failed to start shuttle runtime")?; + let (mut process, runtime_client) = runner::start(port, runtime_executable, project_path) + .await + .context("failed to start shuttle runtime")?; let stdout = process .stdout diff --git a/runtime/src/beta.rs b/runtime/src/beta.rs index 1d376c1a5..04028f005 100644 --- a/runtime/src/beta.rs +++ b/runtime/src/beta.rs @@ -20,8 +20,6 @@ use shuttle_service::{Environment, ResourceFactory, Service}; use crate::__internals::{Loader, Runner}; -const HEALTH_CHECK_PORT: u16 = 8001; - struct BetaEnvArgs { /// Are we running in a Shuttle deployment? shuttle: bool, @@ -32,6 +30,8 @@ struct BetaEnvArgs { ip: IpAddr, /// Port to open service on port: u16, + /// Port to open health check on + healthz_port: Option, /// Where to reach the required Shuttle API endpoints (mainly for provisioning) api_url: String, /// Key for the API calls (if relevant) @@ -59,6 +59,9 @@ impl BetaEnvArgs { .expect("runtime port env var") .parse() .expect("invalid port"), + healthz_port: std::env::var("SHUTTLE_HEALTHZ_PORT") + .map(|s| s.parse().expect("invalid healthz port")) + .ok(), api_url: std::env::var("SHUTTLE_API").expect("api url env var"), api_key: std::env::var("SHUTTLE_API_KEY").ok(), } @@ -73,6 +76,7 @@ pub async fn start(loader: impl Loader + Send + 'static, runner: impl Runner + S env, ip, port, + healthz_port, api_url, api_key, } = BetaEnvArgs::parse(); @@ -80,44 +84,44 @@ pub async fn start(loader: impl Loader + Send + 'static, runner: impl Runner + S let service_addr = SocketAddr::new(ip, port); let client = ShuttleApiClient::new(api_url, api_key, None); - let secrets: BTreeMap = match client - .get_secrets_beta(&project_id) - .await - .and_then(|r| serde_json::from_value(r.data).context("failed to deserialize secrets")) - { - Ok(s) => s, - Err(e) => { - eprintln!("Runtime Secret Loading phase failed: {e}"); - exit(101); - } - }; - - // if running on Shuttle, start a health check server - if shuttle { + // start a health check server if requested + if let Some(healthz_port) = healthz_port { tokio::task::spawn(async move { let make_service = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(|_req| async move { Result::, hyper::Error>::Ok(Response::new(Body::empty())) })) }); - let server = Server::bind(&SocketAddr::new( - Ipv4Addr::LOCALHOST.into(), - HEALTH_CHECK_PORT, - )) - .serve(make_service); + let server = Server::bind(&SocketAddr::new(Ipv4Addr::LOCALHOST.into(), healthz_port)) + .serve(make_service); if let Err(e) = server.await { - eprintln!("Internal health check error: {}", e); + eprintln!("Health check error: {}", e); exit(200); } }); } - // Sorts secrets by key + // + // LOADING / PROVISIONING PHASE + // + + let secrets: BTreeMap = match client + .get_secrets_beta(&project_id) + .await + .and_then(|r| serde_json::from_value(r.data).context("failed to deserialize secrets")) + { + Ok(s) => s, + Err(e) => { + eprintln!("Runtime Secret Loading phase failed: {e}"); + exit(101); + } + }; + + // Sort secrets by key let secrets = BTreeMap::from_iter(secrets.into_iter().map(|(k, v)| (k, Secret::new(v)))); let factory = ResourceFactory::new(project_name, secrets.clone(), env); - let mut resources = match loader.load(factory).await { Ok(r) => r, Err(e) => { @@ -188,9 +192,15 @@ pub async fn start(loader: impl Loader + Send + 'static, runner: impl Runner + S // TODO?: call API to say running state is being entered - // Tell sidecar to shut down. - // Ignore error, since the endpoint does not send a response. - let _ = client.client.get("/__shuttle/shutdown").send().await; + if shuttle { + // Tell sidecar to shut down. + // Ignore error, since the endpoint does not send a response. + let _ = client.client.get("/__shuttle/shutdown").send().await; + } + + // + // RESOURCE INIT PHASE + // let service = match runner.run(resources).await { Ok(s) => s, @@ -200,6 +210,10 @@ pub async fn start(loader: impl Loader + Send + 'static, runner: impl Runner + S } }; + // + // RUNNING PHASE + // + if let Err(e) = service.bind(service_addr).await { eprintln!("Service encountered an error in `bind`: {e}"); exit(1); diff --git a/runtime/src/start.rs b/runtime/src/start.rs index 78cbc6d81..3f5b2e5ac 100644 --- a/runtime/src/start.rs +++ b/runtime/src/start.rs @@ -7,7 +7,7 @@ use crate::{ #[derive(Default)] struct Args { - /// Enable compatibility with beta platform + /// Enable compatibility with beta platform [env: SHUTTLE_BETA] beta: bool, /// Alpha (required): Port to open gRPC server on port: Option, @@ -44,7 +44,7 @@ impl Args { if args.beta { if !args.run { - return Err(anyhow::anyhow!("--run is required with --beta")); + return Err(anyhow::anyhow!("--run is required with SHUTTLE_BETA")); } } else if args.port.is_none() { return Err(anyhow::anyhow!("--port is required")); diff --git a/runtime/tests/integration/helpers.rs b/runtime/tests/integration/helpers.rs index e8c184efc..9fe093634 100644 --- a/runtime/tests/integration/helpers.rs +++ b/runtime/tests/integration/helpers.rs @@ -45,13 +45,8 @@ pub async fn spawn_runtime(project_path: &str) -> Result { let runtime_executable = service.executable_path.clone(); - let (runtime, runtime_client) = runner::start( - false, - runtime_port, - runtime_executable, - Path::new(project_path), - ) - .await?; + let (runtime, runtime_client) = + runner::start(runtime_port, runtime_executable, Path::new(project_path)).await?; Ok(TestRuntime { runtime_client, diff --git a/service/src/builder.rs b/service/src/builder.rs index a40525c97..7c29e3e0c 100644 --- a/service/src/builder.rs +++ b/service/src/builder.rs @@ -117,7 +117,7 @@ pub async fn build_workspace( tx.clone(), ) .await?; - trace!("alpha packages compiled"); + trace!("packages compiled"); Ok(services) } diff --git a/service/src/runner.rs b/service/src/runner.rs index 5cf5d4b53..89d7555d3 100644 --- a/service/src/runner.rs +++ b/service/src/runner.rs @@ -9,20 +9,12 @@ use tokio::process; use tracing::info; pub async fn start( - beta: bool, port: u16, runtime_executable: PathBuf, project_path: &Path, ) -> anyhow::Result<(process::Child, runtime::Client)> { - let mut args = vec![]; - if beta { - args.push("--beta".to_owned()); - args.push("--run".to_owned()); - } else { - let port_str = port.to_string(); - args.push("--port".to_owned()); - args.push(port_str); - } + let port_str = port.to_string(); + let args = vec!["--port", &port_str]; info!( args = %format!("{} {}", runtime_executable.display(), args.join(" ")), From 184725a8c3d4a75c8e951dffbabb9c54ae28ddad Mon Sep 17 00:00:00 2001 From: jonaro00 <54029719+jonaro00@users.noreply.github.com> Date: Fri, 2 Aug 2024 03:02:04 +0200 Subject: [PATCH 3/7] wip --- cargo-shuttle/src/lib.rs | 87 ++++------------- cargo-shuttle/src/provisioner_server.rs | 121 ++++++++++++++++++++++++ runtime/src/beta.rs | 4 +- 3 files changed, 142 insertions(+), 70 deletions(-) diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index 60f55c87b..b74b10dca 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -14,6 +14,7 @@ use std::net::{Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::process::exit; use std::str::FromStr; +use std::sync::Arc; use anyhow::{anyhow, bail, Context, Result}; use args::{ConfirmationArgs, GenerateCommand, TableArgs}; @@ -30,14 +31,14 @@ use futures::{StreamExt, TryFutureExt}; use git2::{Repository, StatusOptions}; use globset::{Glob, GlobSetBuilder}; use hyper::service::{make_service_fn, service_fn}; -use hyper::{body, Body, Method, Request as HyperRequest, Response, Server}; +use hyper::Server; use ignore::overrides::OverrideBuilder; use ignore::WalkBuilder; use indicatif::ProgressBar; use indoc::{formatdoc, printdoc}; +use provisioner_server::beta::{handler, ProvApiState}; use reqwest::header::HeaderMap; use shuttle_api_client::ShuttleApiClient; -use shuttle_common::resource::ProvisionResourceRequest; use shuttle_common::{ constants::{ headers::X_CARGO_SHUTTLE_VERSION, API_URL_DEFAULT, DEFAULT_IDLE_MINUTES, EXAMPLES_REPO, @@ -1820,6 +1821,7 @@ impl Shuttle { } async fn local_run_beta(&self, mut run_args: RunArgs) -> Result { + let project_name = self.ctx.project_name().to_owned(); let services = self.pre_local_run(&run_args).await?; let service = services .first() @@ -1842,76 +1844,23 @@ impl Shuttle { Ipv4Addr::LOCALHOST }; - let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(handler)) }); - - async fn handler( - req: HyperRequest, - ) -> std::result::Result, hyper::Error> { - let method = req.method().clone(); - let uri = req.uri().clone(); - debug!("Received {method} {uri}"); - - let body = body::to_bytes(req.into_body()).await?.to_vec(); - let res = match (method, uri.to_string().as_str()) { - (Method::GET, "/projects/proj_LOCAL/resources/secrets") => resource::Response { - config: serde_json::Value::Null, - r#type: resource::Type::Secrets, - data: serde_json::to_value(HashMap::::new()).unwrap(), - }, - (Method::POST, "/projects/proj_LOCAL/resources") => { - let prov = LocalProvisioner::new().unwrap(); - let shuttle_resource: ProvisionResourceRequest = - serde_json::from_slice(&body).unwrap(); - // TODO: Reject req if version field mismatch - - match shuttle_resource.r#type { - resource::Type::Database(db_type) => { - let config: DbInput = serde_json::from_value(shuttle_resource.config) - .context("deserializing resource config")?; - let res = match config.local_uri { - Some(local_uri) => DatabaseResource::ConnectionString(local_uri), - None => DatabaseResource::Info( - prov.provision_database(Request::new(DatabaseRequest { - project_name: project_name.to_string(), - db_type: Some(db_type.into()), - db_name: config.db_name, - })) - .await - .context("Failed to start database container. Make sure that a Docker engine is running.")? - .into_inner() - .into(), - ), - }; - resource::Response { - r#type: shuttle_resource.r#type, - config: serde_json::Value::Null, - data: serde_json::to_value(&res).unwrap(), - } - } - resource::Type::Container => { - let config = serde_json::from_value(shuttle_resource.config) - .context("deserializing resource config")?; - let res = prov.start_container(config).await.context("Failed to start Docker container. Make sure that a Docker engine is running.")?; - } - resource::Type::Secrets => { - panic!("bruh?"); - } - _ => { - unimplemented!("resource not supported"); - } - } - } - _ => todo!(), - }; - - let res = Response::new(Body::from(serde_json::to_vec(&res).unwrap())); - - Ok(res) - } + let state = Arc::new(ProvApiState { + project_name, + secrets, + }); + let make_svc = make_service_fn(move |_conn| { + let state = state.clone(); + async { + Ok::<_, Infallible>(service_fn(move |req| { + let state = state.clone(); + handler(state, req) + })) + } + }); let server = Server::bind(&api_addr).serve(make_svc); tokio::spawn(async move { if let Err(e) = server.await { - eprintln!("Server error: {}", e); + eprintln!("Provisioner server error: {}", e); exit(1); } }); diff --git a/cargo-shuttle/src/provisioner_server.rs b/cargo-shuttle/src/provisioner_server.rs index f62de5b4e..3f31883d2 100644 --- a/cargo-shuttle/src/provisioner_server.rs +++ b/cargo-shuttle/src/provisioner_server.rs @@ -494,3 +494,124 @@ fn db_type_to_config(db_type: Type, database_name: &str) -> EngineConfig { }, } } + +pub mod beta { + use std::{collections::HashMap, sync::Arc}; + + use anyhow::{bail, Context, Result}; + use hyper::{body, Body, Method, Request as HyperRequest, Response}; + use shuttle_common::{ + resource::{self, ProvisionResourceRequest}, + DatabaseResource, DbInput, + }; + use shuttle_proto::provisioner::{provisioner_server::Provisioner, DatabaseRequest}; + use shuttle_service::ShuttleResourceOutput; + use tonic::Request; + use tracing::debug; + + use super::LocalProvisioner; + + #[derive(Clone)] + pub struct ProvApiState { + pub project_name: String, + pub secrets: HashMap, + } + + pub async fn handler( + state: Arc, + req: HyperRequest, + ) -> std::result::Result, hyper::Error> { + let method = req.method().clone(); + let uri = req.uri().clone(); + debug!("Received {method} {uri}"); + + let body = body::to_bytes(req.into_body()).await?.to_vec(); + let res = match provision(state, method, uri.to_string().as_str(), body).await { + Ok(bytes) => Response::new(Body::from(bytes)), + Err(e) => { + eprintln!("Encountered error when provisioning: {e}"); + Response::builder().status(500).body(Body::empty()).unwrap() + } + }; + + Ok(res) + } + + pub async fn provision( + state: Arc, + method: Method, + uri: &str, + body: Vec, + ) -> Result> { + Ok(match (method, uri) { + (Method::GET, "/projects/proj_LOCAL/resources/secrets") => { + serde_json::to_vec(&resource::Response { + config: serde_json::Value::Null, + r#type: resource::Type::Secrets, + data: serde_json::to_value(&state.secrets).unwrap(), + }) + .unwrap() + } + (Method::POST, "/projects/proj_LOCAL/resources") => { + let prov = LocalProvisioner::new().unwrap(); + let shuttle_resource: ProvisionResourceRequest = + serde_json::from_slice(&body).context("deserializing resource request")?; + // TODO: Reject req if version field mismatch + + let response = match shuttle_resource.r#type { + resource::Type::Database(db_type) => { + let config: DbInput = serde_json::from_value(shuttle_resource.config) + .context("deserializing resource config")?; + let res = match config.local_uri { + Some(local_uri) => DatabaseResource::ConnectionString(local_uri), + None => DatabaseResource::Info( + prov.provision_database(Request::new(DatabaseRequest { + project_name: state.project_name.clone(), + db_type: Some(db_type.into()), + db_name: config.db_name, + })) + .await + .context("Failed to start database container. Make sure that a Docker engine is running.")? + .into_inner() + .into(), + ), + }; + resource::Response { + r#type: shuttle_resource.r#type, + config: serde_json::Value::Null, + data: serde_json::to_value(&res).unwrap(), + } + } + resource::Type::Container => { + let config = serde_json::from_value(shuttle_resource.config) + .context("deserializing resource config")?; + let res = prov.start_container(config) + .await + .context("Failed to start Docker container. Make sure that a Docker engine is running.")?; + resource::Response { + r#type: shuttle_resource.r#type, + config: serde_json::Value::Null, + data: serde_json::to_value(&res).unwrap(), + } + } + resource::Type::Secrets => resource::Response { + r#type: shuttle_resource.r#type, + config: serde_json::Value::Null, + data: serde_json::to_value(&state.secrets).unwrap(), + }, + _ => { + bail!("Resource not supported"); + } + }; + + serde_json::to_vec(&ShuttleResourceOutput { + output: response, + custom: serde_json::Value::Null, + state: Some(resource::ResourceState::Ready), + }) + .unwrap() + } + _ => bail!("Received unsupported resource request"), + }) + } +} diff --git a/runtime/src/beta.rs b/runtime/src/beta.rs index 04028f005..e4e1765a7 100644 --- a/runtime/src/beta.rs +++ b/runtime/src/beta.rs @@ -105,6 +105,7 @@ pub async fn start(loader: impl Loader + Send + 'static, runner: impl Runner + S // // LOADING / PROVISIONING PHASE // + println!("Loading resources..."); let secrets: BTreeMap = match client .get_secrets_beta(&project_id) @@ -159,7 +160,7 @@ pub async fn start(loader: impl Loader + Send + 'static, runner: impl Runner + S *bytes = serde_json::to_vec(&secrets).expect("to serialize struct"); continue; } - // TODO?: Add prints/tracing to show which resource is being provisioned + println!("Provisioning {}", shuttle_resource.r#type); loop { match client .provision_resource_beta(&project_id, shuttle_resource.clone()) @@ -213,6 +214,7 @@ pub async fn start(loader: impl Loader + Send + 'static, runner: impl Runner + S // // RUNNING PHASE // + println!("Starting service!"); if let Err(e) = service.bind(service_addr).await { eprintln!("Service encountered an error in `bind`: {e}"); From c3dc0f91c3bed3b25922d5234a2e010fd809f957 Mon Sep 17 00:00:00 2001 From: jonaro00 <54029719+jonaro00@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:59:22 +0200 Subject: [PATCH 4/7] fix: local run resource passing & exits --- cargo-shuttle/src/lib.rs | 43 +++++++++++++++++++------ cargo-shuttle/src/provisioner_server.rs | 33 ++++++++----------- runtime/src/beta.rs | 13 +++++--- 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index b74b10dca..1c973e3f5 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -1917,7 +1917,7 @@ impl Shuttle { }); #[cfg(target_family = "unix")] - { + let exit_result = { let mut sigterm_notif = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) .expect("Can not get the SIGTERM signal receptor"); @@ -1925,16 +1925,21 @@ impl Shuttle { tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) .expect("Can not get the SIGINT signal receptor"); tokio::select! { + exit_result = runtime.wait() => { + Some(exit_result) + } _ = sigterm_notif.recv() => { - eprintln!("cargo-shuttle received SIGTERM. Killing all the runtimes..."); + eprintln!("cargo-shuttle received SIGTERM. Killing the runtime..."); + None }, _ = sigint_notif.recv() => { - eprintln!("cargo-shuttle received SIGINT. Killing all the runtimes..."); + eprintln!("cargo-shuttle received SIGINT. Killing the runtime..."); + None } - }; - } + } + }; #[cfg(target_family = "windows")] - { + let exit_result = { let mut ctrl_break_notif = tokio::signal::windows::ctrl_break() .expect("Can not get the CtrlBreak signal receptor"); let mut ctrl_c_notif = @@ -1945,28 +1950,48 @@ impl Shuttle { .expect("Can not get the CtrlLogoff signal receptor"); let mut ctrl_shutdown_notif = tokio::signal::windows::ctrl_shutdown() .expect("Can not get the CtrlShutdown signal receptor"); - tokio::select! { + exit_result = runtime.wait() => { + Some(exit_result) + } _ = ctrl_break_notif.recv() => { eprintln!("cargo-shuttle received ctrl-break."); + None }, _ = ctrl_c_notif.recv() => { eprintln!("cargo-shuttle received ctrl-c."); + None }, _ = ctrl_close_notif.recv() => { eprintln!("cargo-shuttle received ctrl-close."); + None }, _ = ctrl_logoff_notif.recv() => { eprintln!("cargo-shuttle received ctrl-logoff."); + None }, _ = ctrl_shutdown_notif.recv() => { eprintln!("cargo-shuttle received ctrl-shutdown."); + None } } + }; + match exit_result { + Some(Ok(exit_status)) => { + bail!( + "Runtime process exited with code {}", + exit_status.code().unwrap_or_default() + ); + } + Some(Err(e)) => { + bail!("Failed to wait for runtime process to exit: {e}"); + } + None => { + runtime.kill().await?; + } } - runtime.kill().await?; - // println!("Run `cargo shuttle deploy` to deploy your Shuttle service."); + println!("Run `cargo shuttle deploy` to deploy your Shuttle service."); Ok(CommandOutcome::Ok) } diff --git a/cargo-shuttle/src/provisioner_server.rs b/cargo-shuttle/src/provisioner_server.rs index 3f31883d2..fd0f8193a 100644 --- a/cargo-shuttle/src/provisioner_server.rs +++ b/cargo-shuttle/src/provisioner_server.rs @@ -537,7 +537,7 @@ pub mod beta { Ok(res) } - pub async fn provision( + async fn provision( state: Arc, method: Method, uri: &str, @@ -576,10 +576,10 @@ pub mod beta { .into(), ), }; - resource::Response { - r#type: shuttle_resource.r#type, - config: serde_json::Value::Null, - data: serde_json::to_value(&res).unwrap(), + ShuttleResourceOutput { + output: serde_json::to_value(&res).unwrap(), + custom: serde_json::Value::Null, + state: Some(resource::ResourceState::Ready), } } resource::Type::Container => { @@ -588,28 +588,23 @@ pub mod beta { let res = prov.start_container(config) .await .context("Failed to start Docker container. Make sure that a Docker engine is running.")?; - resource::Response { - r#type: shuttle_resource.r#type, - config: serde_json::Value::Null, - data: serde_json::to_value(&res).unwrap(), + ShuttleResourceOutput { + output: serde_json::to_value(&res).unwrap(), + custom: serde_json::Value::Null, + state: Some(resource::ResourceState::Ready), } } - resource::Type::Secrets => resource::Response { - r#type: shuttle_resource.r#type, - config: serde_json::Value::Null, - data: serde_json::to_value(&state.secrets).unwrap(), + resource::Type::Secrets => ShuttleResourceOutput { + output: serde_json::to_value(&state.secrets).unwrap(), + custom: serde_json::Value::Null, + state: Some(resource::ResourceState::Ready), }, _ => { bail!("Resource not supported"); } }; - serde_json::to_vec(&ShuttleResourceOutput { - output: response, - custom: serde_json::Value::Null, - state: Some(resource::ResourceState::Ready), - }) - .unwrap() + serde_json::to_vec(&response).unwrap() } _ => bail!("Received unsupported resource request"), }) diff --git a/runtime/src/beta.rs b/runtime/src/beta.rs index e4e1765a7..53b7aa6d6 100644 --- a/runtime/src/beta.rs +++ b/runtime/src/beta.rs @@ -16,7 +16,7 @@ use shuttle_common::{ resource::{ResourceInput, ResourceState, Type}, secrets::Secret, }; -use shuttle_service::{Environment, ResourceFactory, Service}; +use shuttle_service::{Environment, ResourceFactory, Service, ShuttleResourceOutput}; use crate::__internals::{Loader, Runner}; @@ -157,7 +157,12 @@ pub async fn start(loader: impl Loader + Send + 'static, runner: impl Runner + S { // Secrets don't need to be requested here since we already got them above. if shuttle_resource.r#type == Type::Secrets { - *bytes = serde_json::to_vec(&secrets).expect("to serialize struct"); + *bytes = serde_json::to_vec(&ShuttleResourceOutput { + output: serde_json::to_value(&secrets).unwrap(), + custom: serde_json::Value::Null, + state: Some(ResourceState::Ready), + }) + .expect("to serialize struct"); continue; } println!("Provisioning {}", shuttle_resource.r#type); @@ -166,12 +171,12 @@ pub async fn start(loader: impl Loader + Send + 'static, runner: impl Runner + S .provision_resource_beta(&project_id, shuttle_resource.clone()) .await { - Ok(o) => match o.state.expect("resource to have a state") { + Ok(output) => match output.state.clone().expect("resource to have a state") { ResourceState::Provisioning | ResourceState::Authorizing => { tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await; } ResourceState::Ready => { - *bytes = serde_json::to_vec(&o.output).expect("to serialize struct"); + *bytes = serde_json::to_vec(&output).expect("to serialize struct"); break; } bad_state => { From bcc852dacc73db47889e033d904def29980a1034 Mon Sep 17 00:00:00 2001 From: jonaro00 <54029719+jonaro00@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:04:04 +0200 Subject: [PATCH 5/7] clippy --- cargo-shuttle/src/lib.rs | 4 ++-- cargo-shuttle/src/provisioner_server.rs | 2 +- gateway/src/project.rs | 4 ++-- resource-recorder/tests/integration.rs | 4 +++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index 1c973e3f5..ef68f4f1b 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -641,7 +641,7 @@ impl Shuttle { if should_create_environment { // Set the project working directory path to the init path, // so `load_project` is ran with the correct project path - project_args.working_directory = path.clone(); + project_args.working_directory.clone_from(&path); self.load_project(&project_args)?; self.project_start(DEFAULT_IDLE_MINUTES).await?; @@ -1881,7 +1881,7 @@ impl Shuttle { dunce::canonicalize(runtime_executable).context("canonicalize path of executable")?, ) .current_dir(&service.workspace_path) - .args(&["--run"]) + .args(["--run"]) .envs([ ("SHUTTLE_BETA", "true"), ("SHUTTLE_PROJECT_ID", "proj_LOCAL"), diff --git a/cargo-shuttle/src/provisioner_server.rs b/cargo-shuttle/src/provisioner_server.rs index fd0f8193a..407ac1746 100644 --- a/cargo-shuttle/src/provisioner_server.rs +++ b/cargo-shuttle/src/provisioner_server.rs @@ -589,7 +589,7 @@ pub mod beta { .await .context("Failed to start Docker container. Make sure that a Docker engine is running.")?; ShuttleResourceOutput { - output: serde_json::to_value(&res).unwrap(), + output: serde_json::to_value(res).unwrap(), custom: serde_json::Value::Null, state: Some(resource::ResourceState::Ready), } diff --git a/gateway/src/project.rs b/gateway/src/project.rs index b553e5310..85d61f4ca 100644 --- a/gateway/src/project.rs +++ b/gateway/src/project.rs @@ -732,7 +732,7 @@ pub async fn refresh_with_retry( let mut proj = Box::new(project); loop { - let refreshed = proj.refresh(ctx).await; + let refreshed = proj.clone().refresh(ctx).await; match refreshed.as_ref() { Ok(Project::Errored(err)) => match &err.ctx { Some(err_ctx) => { @@ -740,7 +740,7 @@ pub async fn refresh_with_retry( return refreshed; } else { num_attempt += 1; - proj = err_ctx.clone(); + proj.clone_from(err_ctx); tokio::time::sleep(Duration::from_millis(100_u64 * 2_u64.pow(num_attempt))) .await } diff --git a/resource-recorder/tests/integration.rs b/resource-recorder/tests/integration.rs index dc201a32e..94cea2515 100644 --- a/resource-recorder/tests/integration.rs +++ b/resource-recorder/tests/integration.rs @@ -258,7 +258,9 @@ async fn manage_resources() { "should update last_updated" ); - service_db.last_updated = response.resources[1].last_updated.clone(); + service_db + .last_updated + .clone_from(&response.resources[1].last_updated); let expected = ResourcesResponse { success: true, From 563982e8bfaccd8b1344f3ce2aaeecc8e1427924 Mon Sep 17 00:00:00 2001 From: jonaro00 <54029719+jonaro00@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:56:26 +0200 Subject: [PATCH 6/7] nit: abstract prov server --- cargo-shuttle/src/lib.rs | 19 ++------------ cargo-shuttle/src/provisioner_server.rs | 33 +++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index ef68f4f1b..1bed9d706 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -36,7 +36,7 @@ use ignore::overrides::OverrideBuilder; use ignore::WalkBuilder; use indicatif::ProgressBar; use indoc::{formatdoc, printdoc}; -use provisioner_server::beta::{handler, ProvApiState}; +use provisioner_server::beta::{handler, ProvApiState, ProvisionerServerBeta}; use reqwest::header::HeaderMap; use shuttle_api_client::ShuttleApiClient; use shuttle_common::{ @@ -1848,22 +1848,7 @@ impl Shuttle { project_name, secrets, }); - let make_svc = make_service_fn(move |_conn| { - let state = state.clone(); - async { - Ok::<_, Infallible>(service_fn(move |req| { - let state = state.clone(); - handler(state, req) - })) - } - }); - let server = Server::bind(&api_addr).serve(make_svc); - tokio::spawn(async move { - if let Err(e) = server.await { - eprintln!("Provisioner server error: {}", e); - exit(1); - } - }); + ProvisionerServerBeta::start(state, &api_addr); println!( "\n {} {} on http://{}:{}\n", diff --git a/cargo-shuttle/src/provisioner_server.rs b/cargo-shuttle/src/provisioner_server.rs index 407ac1746..220a279c0 100644 --- a/cargo-shuttle/src/provisioner_server.rs +++ b/cargo-shuttle/src/provisioner_server.rs @@ -496,10 +496,16 @@ fn db_type_to_config(db_type: Type, database_name: &str) -> EngineConfig { } pub mod beta { - use std::{collections::HashMap, sync::Arc}; + use std::{ + collections::HashMap, convert::Infallible, net::SocketAddr, process::exit, sync::Arc, + }; use anyhow::{bail, Context, Result}; - use hyper::{body, Body, Method, Request as HyperRequest, Response}; + use hyper::{ + body, + service::{make_service_fn, service_fn}, + Body, Method, Request as HyperRequest, Response, Server, + }; use shuttle_common::{ resource::{self, ProvisionResourceRequest}, DatabaseResource, DbInput, @@ -517,6 +523,29 @@ pub mod beta { pub secrets: HashMap, } + pub struct ProvisionerServerBeta; + + impl ProvisionerServerBeta { + pub fn start(state: Arc, api_addr: &SocketAddr) { + let make_svc = make_service_fn(move |_conn| { + let state = state.clone(); + async { + Ok::<_, Infallible>(service_fn(move |req| { + let state = state.clone(); + handler(state, req) + })) + } + }); + let server = Server::bind(api_addr).serve(make_svc); + tokio::spawn(async move { + if let Err(e) = server.await { + eprintln!("Provisioner server error: {}", e); + exit(1); + } + }); + } + } + pub async fn handler( state: Arc, req: HyperRequest, From 4cf9dcbd62d65602df2fddd1e8c2fb3517a89da5 Mon Sep 17 00:00:00 2001 From: jonaro00 <54029719+jonaro00@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:59:43 +0200 Subject: [PATCH 7/7] add project name env var, nits --- cargo-shuttle/src/lib.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index 1bed9d706..8f7491d9e 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -5,7 +5,6 @@ mod provisioner_server; mod suggestions; use std::collections::{BTreeMap, HashMap}; -use std::convert::Infallible; use std::ffi::OsString; use std::fmt::Write as FmtWrite; use std::fs::{read_to_string, File}; @@ -17,12 +16,10 @@ use std::str::FromStr; use std::sync::Arc; use anyhow::{anyhow, bail, Context, Result}; -use args::{ConfirmationArgs, GenerateCommand, TableArgs}; use chrono::Utc; use clap::{parser::ValueSource, CommandFactory, FromArgMatches}; use clap_complete::{generate, Shell}; use clap_mangen::Man; -use config::RequestContext; use crossterm::style::Stylize; use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select}; use flate2::write::GzEncoder; @@ -30,13 +27,10 @@ use flate2::Compression; use futures::{StreamExt, TryFutureExt}; use git2::{Repository, StatusOptions}; use globset::{Glob, GlobSetBuilder}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::Server; use ignore::overrides::OverrideBuilder; use ignore::WalkBuilder; use indicatif::ProgressBar; use indoc::{formatdoc, printdoc}; -use provisioner_server::beta::{handler, ProvApiState, ProvisionerServerBeta}; use reqwest::header::HeaderMap; use shuttle_api_client::ShuttleApiClient; use shuttle_common::{ @@ -84,9 +78,12 @@ use zip::write::FileOptions; pub use crate::args::{Command, ProjectArgs, RunArgs, ShuttleArgs}; use crate::args::{ - DeployArgs, DeploymentCommand, InitArgs, LoginArgs, LogoutArgs, LogsArgs, ProjectCommand, - ProjectStartArgs, ResourceCommand, TemplateLocation, + ConfirmationArgs, DeployArgs, DeploymentCommand, GenerateCommand, InitArgs, LoginArgs, + LogoutArgs, LogsArgs, ProjectCommand, ProjectStartArgs, ResourceCommand, TableArgs, + TemplateLocation, }; +use crate::config::RequestContext; +use crate::provisioner_server::beta::{ProvApiState, ProvisionerServerBeta}; use crate::provisioner_server::LocalProvisioner; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -599,7 +596,7 @@ impl Shuttle { }; // 5. Initialize locally - init::generate_project( + crate::init::generate_project( path.clone(), project_args .name @@ -1845,7 +1842,7 @@ impl Shuttle { }; let state = Arc::new(ProvApiState { - project_name, + project_name: project_name.clone(), secrets, }); ProvisionerServerBeta::start(state, &api_addr); @@ -1870,7 +1867,7 @@ impl Shuttle { .envs([ ("SHUTTLE_BETA", "true"), ("SHUTTLE_PROJECT_ID", "proj_LOCAL"), - ("SHUTTLE_PROJECT_NAME", "TODO"), + ("SHUTTLE_PROJECT_NAME", project_name.as_str()), ("SHUTTLE_ENV", Environment::Local.to_string().as_str()), ("SHUTTLE_RUNTIME_IP", ip.to_string().as_str()), ("SHUTTLE_RUNTIME_PORT", run_args.port.to_string().as_str()),