use std::path::Path;
use inquire::{Confirm, Select};
use owo_colors::OwoColorize;
use crate::client::{ApiClient, Service};
use crate::config::{EnvVar, PartiriConfig, ServiceConfig, CONFIG_FILE};
use crate::error::Result;
use crate::output::print_success;
pub fn run(client: &ApiClient) -> Result<()> {
if let Ok(existing) = PartiriConfig::load() {
if let Some(id) = &existing.id {
let service = client.read_service(id)?;
let config = map_to_config(
service,
id.clone(),
existing.fk_workspace.clone(),
existing.fk_project.clone(),
)?;
config.save()?;
print_success(&format!("{} updated.", CONFIG_FILE.bold()));
return Ok(());
}
}
if Path::new(CONFIG_FILE).exists() {
let overwrite = Confirm::new(&format!(
"{} already exists. Replace it with the pulled service?",
CONFIG_FILE
))
.with_default(false)
.prompt()
.map_err(|_| "Cancelled.")?;
if !overwrite {
return Ok(());
}
}
println!("\n{}\n", " partiri service pull".bold().cyan());
let workspaces = client.list_workspaces()?;
if workspaces.is_empty() {
return Err("No workspaces found for this API key.".into());
}
let ws_labels: Vec<String> = workspaces
.iter()
.map(|w| format!("{} ({})", w.name, w.id))
.collect();
let ws_choice = Select::new("Select workspace:", ws_labels.clone())
.prompt()
.map_err(|_| "Cancelled.")?;
let (_, ws) = ws_labels
.into_iter()
.zip(workspaces.into_iter())
.find(|(label, _)| label == &ws_choice)
.ok_or("Selected workspace not found in list")?;
let workspace_id = ws.id;
let projects = client.list_projects(&workspace_id)?;
if projects.is_empty() {
return Err("No projects found in the selected workspace.".into());
}
let proj_labels: Vec<String> = projects
.iter()
.map(|p| format!("{} [{}] ({})", p.name, p.environment, p.id))
.collect();
let proj_choice = Select::new("Select project:", proj_labels.clone())
.prompt()
.map_err(|_| "Cancelled.")?;
let (_, proj) = proj_labels
.into_iter()
.zip(projects.into_iter())
.find(|(label, _)| label == &proj_choice)
.ok_or("Selected project not found in list")?;
let project_id = proj.id;
let services = client.list_services(&project_id)?;
if services.is_empty() {
return Err("No services found in the selected project.".into());
}
let svc_labels: Vec<String> = services
.iter()
.map(|s| format!("{} [{}] ({})", s.name, s.deploy_type, s.id))
.collect();
let svc_choice = Select::new("Select service:", svc_labels.clone())
.prompt()
.map_err(|_| "Cancelled.")?;
let (_, svc_entry) = svc_labels
.into_iter()
.zip(services.into_iter())
.find(|(label, _)| label == &svc_choice)
.ok_or("Selected service not found in list")?;
let id = svc_entry.id;
let service = client.read_service(&id)?;
let config = map_to_config(service, id, workspace_id, project_id)?;
config.save()?;
println!();
print_success(&format!("{} saved.", CONFIG_FILE.bold()));
Ok(())
}
pub(crate) fn map_to_config(
svc: Service,
id: String,
fk_workspace: String,
fk_project: String,
) -> Result<PartiriConfig> {
Ok(PartiriConfig {
id: Some(id),
deploy_tag: svc.deploy_tag,
fk_workspace: svc.fk_workspace.unwrap_or(fk_workspace),
fk_project: svc.fk_project.unwrap_or(fk_project),
service: ServiceConfig {
name: svc.name,
deploy_type: svc.deploy_type,
runtime: svc.runtime,
root_path: svc.root_path.unwrap_or_else(|| ".".to_string()),
repository_url: svc.repository_url,
repository_branch: svc.repository_branch,
registry_url: svc.registry_url,
registry_repository_url: svc.registry_repository_url,
fk_service_secret: svc.fk_service_secret,
build_path: svc.build_path,
build_command: svc.build_command,
pre_deploy_command: svc.pre_deploy_command,
run_command: svc.run_command,
fk_region: svc
.fk_region
.filter(|s| !s.is_empty())
.ok_or("Pulled service is missing fk_region")?,
fk_pod: svc
.fk_pod
.filter(|s| !s.is_empty())
.ok_or("Pulled service is missing fk_pod")?,
health_check_path: svc.health_check_path,
maintenance_mode: svc.maintenance_mode.unwrap_or(false),
active: svc.active.unwrap_or(true),
env: svc
.env
.unwrap_or_default()
.into_iter()
.map(|e| EnvVar {
key: e.key,
value: e.value,
})
.collect(),
},
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::{ApiEnvVar, Service};
fn make_service() -> Service {
Service {
id: "svc-123".to_string(),
name: "my-service".to_string(),
deploy_type: "webservice".to_string(),
runtime: "node".to_string(),
external_sd_url: None,
internal_sd_url: None,
repository_url: Some("https://github.com/org/repo".to_string()),
repository_branch: Some("main".to_string()),
registry_url: None,
registry_repository_url: None,
fk_service_secret: None,
root_path: Some(".".to_string()),
build_path: None,
build_command: None,
pre_deploy_command: None,
run_command: Some("npm start".to_string()),
fk_region: Some("region-uuid".to_string()),
fk_pod: Some("pod-uuid".to_string()),
fk_project: Some("proj-1".to_string()),
fk_workspace: Some("ws-1".to_string()),
health_check_path: None,
maintenance_mode: Some(false),
active: Some(true),
env: None,
deploy_tag: None,
created_at: None,
updated_at: None,
}
}
#[test]
fn map_to_config_full_service_produces_correct_config() {
let svc = make_service();
let config = map_to_config(
svc,
"svc-123".to_string(),
"ws-1".to_string(),
"proj-1".to_string(),
)
.unwrap();
assert_eq!(config.id, Some("svc-123".to_string()));
assert_eq!(config.fk_workspace, "ws-1");
assert_eq!(config.fk_project, "proj-1");
assert_eq!(config.service.name, "my-service");
assert_eq!(config.service.deploy_type, "webservice");
assert_eq!(config.service.runtime, "node");
assert_eq!(config.service.fk_region, "region-uuid");
assert_eq!(config.service.fk_pod, "pod-uuid");
assert_eq!(
config.service.repository_url.as_deref(),
Some("https://github.com/org/repo")
);
}
#[test]
fn map_to_config_none_root_path_defaults_to_dot() {
let mut svc = make_service();
svc.root_path = None;
let config = map_to_config(
svc,
"svc-1".to_string(),
"ws-1".to_string(),
"proj-1".to_string(),
)
.unwrap();
assert_eq!(config.service.root_path, ".");
}
#[test]
fn map_to_config_env_vars_are_mapped() {
let mut svc = make_service();
svc.env = Some(vec![
ApiEnvVar {
key: "PORT".to_string(),
value: "3000".to_string(),
},
ApiEnvVar {
key: "NODE_ENV".to_string(),
value: "production".to_string(),
},
]);
let config = map_to_config(
svc,
"svc-1".to_string(),
"ws-1".to_string(),
"proj-1".to_string(),
)
.unwrap();
assert_eq!(config.service.env.len(), 2);
assert_eq!(config.service.env[0].key, "PORT");
assert_eq!(config.service.env[1].key, "NODE_ENV");
}
#[test]
fn map_to_config_none_env_produces_empty_vec() {
let mut svc = make_service();
svc.env = None;
let config = map_to_config(
svc,
"svc-1".to_string(),
"ws-1".to_string(),
"proj-1".to_string(),
)
.unwrap();
assert!(config.service.env.is_empty());
}
#[test]
fn map_to_config_none_fk_region_returns_error() {
let mut svc = make_service();
svc.fk_region = None;
let result = map_to_config(
svc,
"svc-1".to_string(),
"ws-1".to_string(),
"proj-1".to_string(),
);
assert!(result.is_err(), "None fk_region should return an error");
}
#[test]
fn map_to_config_deploy_tag_is_mapped() {
let mut svc = make_service();
svc.deploy_tag = Some("ab12c".to_string());
let config = map_to_config(
svc,
"svc-123".to_string(),
"ws-1".to_string(),
"proj-1".to_string(),
)
.unwrap();
assert_eq!(config.deploy_tag, Some("ab12c".to_string()));
}
#[test]
fn map_to_config_none_deploy_tag_stays_none() {
let svc = make_service();
let config = map_to_config(
svc,
"svc-123".to_string(),
"ws-1".to_string(),
"proj-1".to_string(),
)
.unwrap();
assert!(config.deploy_tag.is_none());
}
#[test]
fn map_to_config_none_fk_pod_returns_error() {
let mut svc = make_service();
svc.fk_pod = None;
let result = map_to_config(
svc,
"svc-1".to_string(),
"ws-1".to_string(),
"proj-1".to_string(),
);
assert!(result.is_err(), "None fk_pod should return an error");
}
#[test]
fn map_to_config_empty_fk_region_returns_error() {
let mut svc = make_service();
svc.fk_region = Some(String::new());
let result = map_to_config(
svc,
"svc-1".to_string(),
"ws-1".to_string(),
"proj-1".to_string(),
);
assert!(result.is_err(), "Empty fk_region should return an error");
}
#[test]
fn map_to_config_empty_fk_pod_returns_error() {
let mut svc = make_service();
svc.fk_pod = Some(String::new());
let result = map_to_config(
svc,
"svc-1".to_string(),
"ws-1".to_string(),
"proj-1".to_string(),
);
assert!(result.is_err(), "Empty fk_pod should return an error");
}
}