use std::collections::HashSet;
use anyhow::{Context, Result, bail};
use reqwest::Client;
use crate::{
LinkedProject,
client::{GQLClient, post_graphql},
commands::{
Configs,
queries::{
self,
environment_instances::{
EnvironmentInstancesEnvironmentServiceInstancesEdges,
EnvironmentInstancesEnvironmentServiceInstancesEdgesNode,
EnvironmentInstancesEnvironmentVolumeInstancesEdges,
EnvironmentInstancesEnvironmentVolumeInstancesEdgesNode,
},
project::{ProjectProject, ProjectProjectServicesEdgesNode},
},
},
errors::RailwayError,
};
use super::environment::get_matched_environment;
pub type ProjectServiceInstanceEdge = EnvironmentInstancesEnvironmentServiceInstancesEdges;
pub type ProjectServiceInstanceNode = EnvironmentInstancesEnvironmentServiceInstancesEdgesNode;
pub type ProjectVolumeInstanceEdge = EnvironmentInstancesEnvironmentVolumeInstancesEdges;
pub type ProjectVolumeInstanceNode = EnvironmentInstancesEnvironmentVolumeInstancesEdgesNode;
const ENVIRONMENT_INSTANCE_PAGE_SIZE: i64 = 500;
#[derive(Debug, Clone, Default)]
pub struct ProjectEnvironmentInstances {
pub service_instances: Vec<ProjectServiceInstanceEdge>,
pub volume_instances: Vec<ProjectVolumeInstanceEdge>,
}
pub async fn get_project(
client: &Client,
configs: &Configs,
project_id: String,
) -> Result<queries::RailwayProject, RailwayError> {
let vars = queries::project::Variables { id: project_id };
let project = post_graphql::<queries::Project, _>(client, configs.get_backboard(), vars)
.await
.map_err(|e| {
if let RailwayError::GraphQLError(msg) = &e {
if msg.contains("Project not found") {
return RailwayError::ProjectNotFound;
}
}
e
})?
.project;
Ok(project)
}
pub fn get_service(
project: &ProjectProject,
service_name: String,
) -> Result<ProjectProjectServicesEdgesNode> {
let service = project
.services
.edges
.iter()
.find(|edge| edge.node.name.to_lowercase() == service_name.to_lowercase());
if let Some(service) = service {
return Ok(service.node.clone());
}
bail!(RailwayError::ServiceNotFound(service_name))
}
pub async fn ensure_project_and_environment_exist(
client: &Client,
configs: &Configs,
linked_project: &LinkedProject,
) -> Result<()> {
let project = get_project(client, configs, linked_project.project.clone()).await?;
if project.deleted_at.is_some() {
bail!(RailwayError::ProjectDeleted);
}
if let Some(env_id_or_name) = linked_project
.environment_name
.clone()
.or_else(|| linked_project.environment.clone())
{
let environment = get_matched_environment(&project, env_id_or_name);
match environment {
Ok(environment) => {
if environment.deleted_at.is_some() {
bail!(RailwayError::EnvironmentDeleted);
}
}
Err(error) => match error.downcast_ref::<RailwayError>() {
Some(RailwayError::EnvironmentNotFound(_)) => {
bail!(RailwayError::EnvironmentDeleted);
}
Some(RailwayError::EnvironmentRestricted(_)) => return Err(error),
_ => return Err(error),
},
};
}
Ok(())
}
pub async fn get_environment_instances(
client: &Client,
configs: &Configs,
project_id: &str,
environment_id: &str,
) -> Result<ProjectEnvironmentInstances> {
let mut service_instances = Vec::new();
let mut volume_instances = Vec::new();
let mut service_after = None;
let mut volume_after = None;
let mut service_done = false;
let mut volume_done = false;
loop {
let response = post_graphql::<queries::EnvironmentInstances, _>(
client,
configs.get_backboard(),
queries::environment_instances::Variables {
project_id: project_id.to_string(),
environment_id: environment_id.to_string(),
service_instances_first: Some(if service_done {
0
} else {
ENVIRONMENT_INSTANCE_PAGE_SIZE
}),
service_instances_after: service_after.clone(),
volume_instances_first: Some(if volume_done {
0
} else {
ENVIRONMENT_INSTANCE_PAGE_SIZE
}),
volume_instances_after: volume_after.clone(),
},
)
.await?;
if !service_done {
let connection = response.environment.service_instances;
service_done = !connection.page_info.has_next_page;
service_after = connection.page_info.end_cursor;
service_instances.extend(connection.edges);
}
if !volume_done {
let connection = response.environment.volume_instances;
volume_done = !connection.page_info.has_next_page;
volume_after = connection.page_info.end_cursor;
volume_instances.extend(connection.edges);
}
if service_done && volume_done {
break;
}
}
Ok(ProjectEnvironmentInstances {
service_instances,
volume_instances,
})
}
pub fn get_service_ids_in_env(instances: &ProjectEnvironmentInstances) -> HashSet<String> {
instances
.service_instances
.iter()
.map(|si| si.node.service_id.clone())
.collect()
}
pub fn service_instances_in_env<'a>(
instances: &'a ProjectEnvironmentInstances,
) -> &'a [ProjectServiceInstanceEdge] {
instances.service_instances.as_slice()
}
pub fn volume_instances_in_env<'a>(
instances: &'a ProjectEnvironmentInstances,
) -> &'a [ProjectVolumeInstanceEdge] {
instances.volume_instances.as_slice()
}
pub fn find_service_instance<'a>(
instances: &'a ProjectEnvironmentInstances,
service_id: &str,
) -> Option<&'a ProjectServiceInstanceNode> {
instances
.service_instances
.iter()
.find(|si| si.node.service_id == service_id)
.map(|si| &si.node)
}
pub struct ServiceContext {
pub client: Client,
pub configs: Configs,
pub project: ProjectProject,
pub project_id: String,
pub environment_id: String,
pub environment_name: String,
pub service_id: String,
pub service_name: String,
}
pub async fn resolve_service_context(
project_arg: Option<String>,
service_arg: Option<String>,
environment_arg: Option<String>,
) -> Result<ServiceContext> {
let configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
if project_arg.is_some() && environment_arg.is_none() {
bail!("--environment is required when using --project");
}
let linked_project = if project_arg.is_none() {
Some(configs.get_linked_project().await?)
} else {
None
};
if let Some(ref linked_project) = linked_project {
ensure_project_and_environment_exist(&client, &configs, linked_project).await?;
}
let project_id = project_arg
.or_else(|| linked_project.as_ref().map(|lp| lp.project.clone()))
.ok_or_else(|| {
anyhow::anyhow!("No project specified. Use --project or run `railway link` first")
})?;
let project = get_project(&client, &configs, project_id.clone()).await?;
let env = match environment_arg {
Some(env) => env,
None => linked_project
.as_ref()
.context("No environment linked. Use --environment when using --project")?
.environment_id()?
.to_string(),
};
let environment = get_matched_environment(&project, env)?;
let environment_id = environment.id;
let environment_name = environment.name;
let linked_service = linked_project.and_then(|lp| lp.service);
let services = &project.services.edges;
if services.is_empty() {
bail!(RailwayError::ProjectHasNoServices);
}
let (service_id, service_name) = match (service_arg, linked_service) {
(Some(service_arg), _) => {
let service = services
.iter()
.find(|s| s.node.name == service_arg || s.node.id == service_arg)
.with_context(|| format!("Service '{service_arg}' not found"))?;
(service.node.id.clone(), service.node.name.clone())
}
(_, Some(linked_service)) => {
let name = services
.iter()
.find(|s| s.node.id == linked_service)
.map(|s| s.node.name.clone())
.unwrap_or_else(|| linked_service.clone());
(linked_service, name)
}
_ if services.len() == 1 => {
let service = &services[0].node;
(service.id.clone(), service.name.clone())
}
_ => bail!(RailwayError::NoServiceLinked),
};
Ok(ServiceContext {
client,
configs,
project,
project_id,
environment_id,
environment_name,
service_id,
service_name,
})
}