1use std::path::Path;
5
6use serde::Serialize;
7
8use crate::config::{self, AuthRef, Config, Profile};
9use crate::error::CoreError;
10
11#[derive(Debug, Serialize)]
13pub struct ProfileAddResult {
14 pub name: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub label: Option<String>,
19 pub url: String,
21 pub auth_kind: &'static str,
23 pub active: bool,
25}
26
27#[derive(Debug, Serialize)]
30pub struct ProfileSummary {
31 pub name: String,
33 #[serde(skip_serializing_if = "Option::is_none")]
36 pub label: Option<String>,
37 pub url: String,
39 pub auth_kind: &'static str,
41}
42
43#[derive(Debug, Serialize)]
45pub struct ProfileListResult {
46 pub active: Option<String>,
48 pub profiles: Vec<ProfileSummary>,
50}
51
52#[derive(Debug, Serialize)]
54pub struct ProfileUseResult {
55 pub active: String,
57}
58
59pub fn add(
64 config_path: &Path,
65 name: &str,
66 url_str: &str,
67 label: Option<&str>,
68 auth: AuthRef,
69 set_active: bool,
70) -> Result<ProfileAddResult, CoreError> {
71 let url = url::Url::parse(url_str).map_err(|err| CoreError::ConfigInvalid {
72 reason: format!("invalid URL for profile {name:?}: {url_str} ({err})"),
73 })?;
74 let mut config = config::load(config_path)?;
75 if config
76 .profiles
77 .insert(
78 name.to_string(),
79 Profile {
80 url: url.clone(),
81 label: label.map(str::to_string),
82 ssl_verify: true,
83 auth: auth.clone(),
84 webdev_secret: None,
85 poll_interval_secs: None,
86 },
87 )
88 .is_some()
89 {
90 tracing::warn!(profile = name, "overwriting existing profile");
91 }
92 if set_active {
93 config.active = Some(name.to_string());
94 }
95 config::save(config_path, &config)?;
96
97 Ok(ProfileAddResult {
98 name: name.to_string(),
99 label: label.map(str::to_string),
100 url: url.to_string(),
101 auth_kind: auth.kind(),
102 active: config.active.as_deref() == Some(name),
103 })
104}
105
106pub fn list(config: &Config) -> ProfileListResult {
108 ProfileListResult {
109 active: config.active.clone(),
110 profiles: config
111 .profiles
112 .iter()
113 .map(|(name, profile)| ProfileSummary {
114 name: name.clone(),
115 label: profile.label.clone(),
116 url: profile.url.to_string(),
117 auth_kind: profile.auth.kind(),
118 })
119 .collect(),
120 }
121}
122
123pub fn use_profile(config_path: &Path, name: &str) -> Result<ProfileUseResult, CoreError> {
127 let mut config = config::load(config_path)?;
128 if !config.profiles.contains_key(name) {
129 return Err(CoreError::ProfileNotFound {
130 name: name.to_string(),
131 known: config.profiles.keys().cloned().collect(),
132 });
133 }
134 config.active = Some(name.to_string());
135 config::save(config_path, &config)?;
136 Ok(ProfileUseResult {
137 active: name.to_string(),
138 })
139}
140
141#[cfg(test)]
142mod tests {
143 use super::{add, list, use_profile};
144 use crate::config::{AuthRef, load};
145 use crate::error::CoreError;
146
147 fn temp_config_path() -> (tempfile::TempDir, std::path::PathBuf) {
148 let dir = tempfile::tempdir().expect("tempdir");
149 let path = dir.path().join("config.toml");
150 (dir, path)
151 }
152
153 #[test]
156 fn add_list_use_round_trip() {
157 let (_dir, path) = temp_config_path();
158
159 let added = add(
160 &path,
161 "dev",
162 "http://localhost:9088",
163 Some("Dev rig"),
164 AuthRef::TokenEnv {
165 token_env: "IGNITION_TOKEN".into(),
166 },
167 true,
168 )
169 .expect("add dev");
170 assert!(added.active, "--active sets it");
171 assert_eq!(added.url, "http://localhost:9088/");
172
173 add(
174 &path,
175 "prod",
176 "https://gw.example.com:8443",
177 None,
178 AuthRef::Keyring {
179 keyring: "profile:prod".into(),
180 },
181 false,
182 )
183 .expect("add prod");
184
185 let listed = list(&load(&path).expect("load"));
186 assert_eq!(listed.active.as_deref(), Some("dev"));
187 assert_eq!(listed.profiles.len(), 2);
188 assert_eq!(listed.profiles[0].name, "dev", "BTreeMap order");
189 assert_eq!(listed.profiles[0].label.as_deref(), Some("Dev rig"));
190 assert_eq!(listed.profiles[0].auth_kind, "token_env");
191 assert_eq!(listed.profiles[1].label, None);
192 assert_eq!(listed.profiles[1].auth_kind, "keyring");
193
194 let used = use_profile(&path, "prod").expect("use prod");
195 assert_eq!(used.active, "prod");
196 assert_eq!(load(&path).expect("load").active.as_deref(), Some("prod"));
197 }
198
199 #[test]
202 fn add_rejects_invalid_url_and_use_rejects_unknown() {
203 let (_dir, path) = temp_config_path();
204
205 let err = add(&path, "dev", "not a url", None, AuthRef::default(), false)
206 .expect_err("invalid URL rejected");
207 assert!(matches!(err, CoreError::ConfigInvalid { .. }));
208 assert_eq!(err.exit_code(), 3);
209
210 add(
211 &path,
212 "dev",
213 "http://localhost:9088",
214 None,
215 AuthRef::default(),
216 false,
217 )
218 .expect("add dev");
219 let err = use_profile(&path, "nope").expect_err("unknown profile");
220 match err {
221 CoreError::ProfileNotFound {
222 ref name,
223 ref known,
224 } => {
225 assert_eq!(name, "nope");
226 assert_eq!(known, &vec!["dev".to_string()]);
227 }
228 other => panic!("wrong error: {other}"),
229 }
230 }
231}