diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 566c4596..f06c4d30 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -26,6 +26,7 @@ pub mod logout; pub mod logs; pub mod open; pub mod redeploy; +pub mod restart; pub mod run; pub mod scale; pub mod service; diff --git a/src/commands/restart.rs b/src/commands/restart.rs new file mode 100644 index 00000000..1bf72d94 --- /dev/null +++ b/src/commands/restart.rs @@ -0,0 +1,141 @@ +use colored::*; +use std::time::Duration; + +use crate::{ + consts::TICK_STRING, + controllers::project::{ensure_project_and_environment_exist, get_project}, + errors::RailwayError, + util::prompt::prompt_confirm_with_default, +}; + +use super::*; +use anyhow::{anyhow, bail}; + +/// Restart (no image pull) the latest deployment of a service and wait for healthchecks +#[derive(Parser)] +pub struct Args { + /// The service ID/name to restart + #[clap(long, short)] + service: Option, + + /// Skip confirmation dialog + #[clap(short = 'y', long = "yes")] + bypass: bool, +} + +pub async fn command(args: Args) -> Result<()> { + let configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + ensure_project_and_environment_exist(&client, &configs, &linked_project).await?; + + let project = get_project(&client, &configs, linked_project.project.clone()).await?; + + let service_id = args.service.or_else(|| linked_project.service.clone()).ok_or_else(|| anyhow!("No service found. Please link one via `railway link` or specify one via the `--service` flag."))?; + let service = project + .services + .edges + .iter() + .find(|s| { + s.node.id == service_id || s.node.name.to_lowercase() == service_id.to_lowercase() + }) + .ok_or_else(|| anyhow!(RailwayError::ServiceNotFound(service_id)))?; + + let service_in_env = service + .node + .service_instances + .edges + .iter() + .find(|a| a.node.environment_id == linked_project.environment) + .ok_or_else(|| anyhow!("The service specified doesn't exist in the current environment"))?; + + if let Some(ref latest) = service_in_env.node.latest_deployment { + if latest.can_redeploy { + if !args.bypass { + let env_name = linked_project + .environment_name + .clone() + .unwrap_or("unknown".to_string()); + + let confirmed = prompt_confirm_with_default( + format!( + "Restart the container for service {} in environment {}?", + service.node.name, env_name + ) + .as_str(), + false, + )?; + + if !confirmed { + return Ok(()); + } + } + + let spinner = indicatif::ProgressBar::new_spinner() + .with_style( + indicatif::ProgressStyle::default_spinner() + .tick_chars(TICK_STRING) + .template("{spinner:.green} {msg}")?, + ) + .with_message(format!("Restarting service {}...", service.node.name)); + spinner.enable_steady_tick(Duration::from_millis(100)); + + // Call restart mutation + post_graphql::( + &client, + configs.get_backboard(), + mutations::deployment_restart::Variables { + id: latest.id.clone(), + }, + ) + .await?; + + // Wait for healthchecks via latest deployment status + let max_wait = Duration::from_secs(300); + let poll_interval = Duration::from_secs(2); + let start = std::time::Instant::now(); + loop { + if start.elapsed() > max_wait { + spinner.finish_and_clear(); + bail!("Timed out waiting for health checks after restart"); + } + + let resp = post_graphql::( + &client, + configs.get_backboard(), + queries::latest_deployment::Variables { + service_id: service.node.id.clone(), + environment_id: linked_project.environment.clone(), + }, + ) + .await?; + + let si = resp.service_instance; + if let Some(ld) = si.latest_deployment { + match ld.status { + queries::latest_deployment::DeploymentStatus::SUCCESS => { + spinner.finish_with_message(format!( + "Restart successful for service {}", + service.node.name.green() + )); + return Ok(()); + } + queries::latest_deployment::DeploymentStatus::FAILED + | queries::latest_deployment::DeploymentStatus::CRASHED => { + spinner.finish_and_clear(); + bail!("Restart completed but health checks failed"); + } + _ => {} + } + } + + tokio::time::sleep(poll_interval).await; + } + } + } else { + bail!("No deployment found for service") + } + + Ok(()) +} diff --git a/src/gql/mod.rs b/src/gql/mod.rs index 9f27eeef..411b610f 100644 --- a/src/gql/mod.rs +++ b/src/gql/mod.rs @@ -1,3 +1,5 @@ +#![allow(unused_imports, dead_code)] + pub mod mutations; pub mod queries; pub mod subscriptions; diff --git a/src/gql/mutations/mod.rs b/src/gql/mutations/mod.rs index ddc411a9..2ab83fe6 100644 --- a/src/gql/mutations/mod.rs +++ b/src/gql/mutations/mod.rs @@ -128,6 +128,15 @@ pub struct VolumeAttach; )] pub struct DeploymentRedeploy; +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.json", + query_path = "src/gql/mutations/strings/DeploymentRestart.graphql", + response_derives = "Debug, Serialize, Clone", + skip_serializing_none +)] +pub struct DeploymentRestart; + #[derive(GraphQLQuery)] #[graphql( schema_path = "src/gql/schema.json", diff --git a/src/gql/mutations/strings/DeploymentRestart.graphql b/src/gql/mutations/strings/DeploymentRestart.graphql new file mode 100644 index 00000000..79a1f88b --- /dev/null +++ b/src/gql/mutations/strings/DeploymentRestart.graphql @@ -0,0 +1,3 @@ +mutation DeploymentRestart($id: String!) { + deploymentRestart(id: $id) +} diff --git a/src/main.rs b/src/main.rs index db8b0ac1..b0e1ea40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,6 +53,7 @@ commands!( whoami, volume, redeploy, + restart, scale, check_updates, functions(function, func, fn, funcs, fns) diff --git a/src/util/retry.rs b/src/util/retry.rs index 769f45a2..08024970 100644 --- a/src/util/retry.rs +++ b/src/util/retry.rs @@ -3,12 +3,14 @@ use std::future::Future; use std::time::Duration; use tokio::time::sleep; +type OnRetryFn = Option>; + pub struct RetryConfig { pub max_attempts: u32, pub initial_delay_ms: u64, pub max_delay_ms: u64, pub backoff_multiplier: f64, - pub on_retry: Option>, + pub on_retry: OnRetryFn, } impl Default for RetryConfig {