Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 89 additions & 1 deletion src/commands/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use serde::Serialize;

use crate::{
controllers::{
environment::get_matched_environment,
environment::{get_matched_environment, get_or_prompt_environment},
project::{ensure_project_and_environment_exist, get_project, get_service_ids_in_env},
},
errors::RailwayError,
util::prompt::{PromptService, prompt_options},
workspace::{Project, workspaces},
};

use super::*;
Expand All @@ -25,6 +26,10 @@ pub struct Args {

#[derive(Parser)]
enum Commands {
/// List services in the current project
#[clap(alias = "ls")]
List(ListArgs),

/// Link a service to the current project
Link(LinkArgs),

Expand All @@ -44,6 +49,21 @@ enum Commands {
Scale(crate::commands::scale::Args),
}

#[derive(Parser)]
struct ListArgs {
/// Project name or ID to list services for (defaults to linked project)
#[clap(short, long)]
project: Option<String>,

/// Environment to list services from (defaults to linked environment)
#[clap(short, long)]
environment: Option<String>,

/// Output in JSON format
#[clap(long)]
json: bool,
}

#[derive(Parser)]
struct LinkArgs {
/// The service ID/name to link
Expand Down Expand Up @@ -92,6 +112,7 @@ pub async fn command(args: Args) -> Result<()> {
}

match args.command {
Some(Commands::List(list_args)) => list_services(list_args).await,
Some(Commands::Link(link_args)) => link_command(link_args).await,
Some(Commands::Status(status_args)) => status_command(status_args).await,
Some(Commands::Logs(logs_args)) => crate::commands::logs::command(logs_args).await,
Expand All @@ -106,6 +127,73 @@ pub async fn command(args: Args) -> Result<()> {
}
}

async fn list_services(args: ListArgs) -> Result<()> {
let configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
let linked_project = configs.get_linked_project().await?;

let project_explicitly_specified = args.project.is_some();

let project_id = if let Some(project_arg) = args.project {
let all_workspaces = workspaces().await?;
let all_projects: Vec<Project> = all_workspaces
.iter()
.flat_map(|w| {
w.projects()
.into_iter()
.filter(|p| p.deleted_at().is_none())
})
.collect();

if all_projects.is_empty() {
bail!(RailwayError::NoProjects);
}
let project = all_projects
.iter()
.find(|p| {
p.id().to_lowercase() == project_arg.to_lowercase()
|| p.name().to_lowercase() == project_arg.to_lowercase()
})
.ok_or_else(|| RailwayError::ProjectNotFound)?;
project.id().to_string()
} else {
linked_project.project.clone()
};

let project = get_project(&client, &configs, project_id.clone()).await?;
let linked = if project_explicitly_specified {
None
} else {
Some(linked_project)
};

let env_id = get_or_prompt_environment(linked, &project, args.environment, args.json)
.await?
.ok_or(RailwayError::NoEnvironments)?;

let service_ids_in_env = get_service_ids_in_env(&project, &env_id);
let services: Vec<_> = project
.services
.edges
.iter()
.filter(|a| service_ids_in_env.contains(&a.node.id))
.map(|e| &e.node)
.collect();

if args.json {
println!("{}", serde_json::to_string_pretty(&services)?);
} else {
if services.is_empty() {
println!("No services found");
}
for service in services {
println!("{} ({})", service.name.bold(), service.id.dimmed())
}
}

Ok(())
}

async fn link_command(args: LinkArgs) -> Result<()> {
let mut configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
Expand Down
57 changes: 56 additions & 1 deletion src/controllers/environment.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use crate::{
LinkedProject,
commands::queries::{RailwayProject, project::ProjectProjectEnvironmentsEdgesNode},
errors::RailwayError,
queries::project::ProjectProject,
util::prompt::{PromptEnvironment, prompt_select_with_cancel},
};
use anyhow::Result;
use anyhow::{Result, bail};
use is_terminal::IsTerminal;

pub fn get_matched_environment(
project: &RailwayProject,
Expand All @@ -17,3 +21,54 @@ pub fn get_matched_environment(

Ok(environment.node.clone())
}

pub async fn get_or_prompt_environment(
linked_project: Option<LinkedProject>,
project: &ProjectProject,
environment_arg: Option<String>,
json: bool,
) -> Result<Option<String>> {
let environments = project.environments.edges.iter().collect::<Vec<_>>();

let environment_id = if let Some(environment_arg) = environment_arg {
// If the user specified a service, use that
let environment_id = environments.iter().find(|environment| {
environment.node.name == environment_arg || environment.node.id == environment_arg
});
if let Some(environment_id) = environment_id {
Some(environment_id.node.id.to_owned())
} else {
bail!(RailwayError::EnvironmentNotFound(environment_arg));
}
} else if let Some(environment) = linked_project.and_then(|lp| lp.environment) {
Some(environment)
} else {
// If the user didn't specify an environment, and we don't have a linked environment, get the first environment

if environments.is_empty() {
// If there are no environments, backboard will generate one for us
None
} else if environments.len() == 1 {
// If there is just one, use that
Some(environments[0].node.id.clone())
} else {
// If there are multiple environments, prompt the user to select one
if std::io::stdout().is_terminal() && !json {
let prompt_environments: Vec<_> = environments
.iter()
.map(|s| PromptEnvironment(&s.node))
.collect();
match prompt_select_with_cancel("Select an environment", prompt_environments)? {
Some(env) => Some(env.0.id.clone()),
None => bail!("No environment selected. Use --environment to specify one."),
}
} else {
bail!(
"Multiple environments found. Please specify an environment via the `--environment` flag."
)
}
}
};

Ok(environment_id)
}
3 changes: 3 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ pub enum RailwayError {
#[error("Project does not have any services")]
NoServices,

#[error("Project does not have any environments")]
NoEnvironments,

#[error(
"Environment \"{0}\" not found.\nRun `railway environment` to connect to an environment."
)]
Expand Down
14 changes: 13 additions & 1 deletion src/util/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ use std::{
use crate::{
commands::{Configs, queries::project::ProjectProjectServicesEdgesNode},
controllers::variables::Variable,
queries::project::ProjectProjectEnvironmentsEdgesNodeServiceInstancesEdgesNode,
queries::project::{
ProjectProjectEnvironmentsEdgesNode,
ProjectProjectEnvironmentsEdgesNodeServiceInstancesEdgesNode,
},
};
use anyhow::{Context, Result};

Expand Down Expand Up @@ -207,6 +210,15 @@ impl Display for PromptServiceInstance<'_> {
}
}

#[derive(Debug, Clone, PartialEq)]
pub struct PromptEnvironment<'a>(pub &'a ProjectProjectEnvironmentsEdgesNode);

impl Display for PromptEnvironment<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.name)
}
}

/// Bash style completion of paths
#[derive(Clone)]
pub struct PathAutocompleter;
Expand Down
Loading