1use std::path::{Path, PathBuf};
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct FileConfig {
13 pub cp_url: Option<String>,
14 pub ca_cert: Option<PathBuf>,
15 pub client_cert: Option<PathBuf>,
16 pub client_key: Option<PathBuf>,
17}
18
19impl FileConfig {
20 pub fn save(&self, path: &Path) -> std::io::Result<()> {
21 if let Some(parent) = path.parent() {
22 std::fs::create_dir_all(parent)?;
23 }
24 let body = toml::to_string_pretty(self).map_err(std::io::Error::other)?;
25 #[cfg(unix)]
27 {
28 use std::os::unix::fs::OpenOptionsExt;
29 let mut f = std::fs::OpenOptions::new()
30 .create(true)
31 .write(true)
32 .truncate(true)
33 .mode(0o600)
34 .open(path)?;
35 std::io::Write::write_all(&mut f, body.as_bytes())?;
36 }
37 #[cfg(not(unix))]
38 {
39 std::fs::write(path, body)?;
40 }
41 Ok(())
42 }
43}
44
45#[derive(Debug, Default, Clone)]
46pub struct Overrides {
47 pub cp_url: Option<String>,
48 pub ca_cert: Option<PathBuf>,
49 pub client_cert: Option<PathBuf>,
50 pub client_key: Option<PathBuf>,
51}
52
53#[derive(Debug)]
54pub enum ConfigError {
55 Read(std::io::Error),
56 Parse(toml::de::Error),
57 Missing { field: &'static str },
58}
59
60impl std::fmt::Display for ConfigError {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 Self::Read(e) => write!(f, "read config: {e}"),
64 Self::Parse(e) => write!(f, "parse config: {e}"),
65 Self::Missing { field } => write!(
66 f,
67 "no {field} in flags, env, or config file. \
68 Run `nixfleet config init` to create one, or pass --{flag} / set NIXFLEET_{env}.",
69 field = field,
70 flag = field.replace('_', "-"),
71 env = field.to_uppercase(),
72 ),
73 }
74 }
75}
76
77impl std::error::Error for ConfigError {}
78
79pub fn load_file(path: &Path) -> Result<FileConfig, ConfigError> {
80 let body = std::fs::read_to_string(path).map_err(ConfigError::Read)?;
81 toml::from_str(&body).map_err(ConfigError::Parse)
82}
83
84pub fn resolve(
85 file_path: Option<&Path>,
86 env: &Overrides,
87 flags: &Overrides,
88) -> Result<crate::ResolvedClientConfig, ConfigError> {
89 let file = match file_path {
90 Some(p) => match load_file(p) {
91 Ok(c) => c,
92 Err(ConfigError::Read(e)) if e.kind() == std::io::ErrorKind::NotFound => {
93 FileConfig::default()
94 }
95 Err(other) => return Err(other),
96 },
97 None => FileConfig::default(),
98 };
99 let cp_url = flags
100 .cp_url
101 .clone()
102 .or_else(|| env.cp_url.clone())
103 .or(file.cp_url)
104 .ok_or(ConfigError::Missing { field: "cp_url" })?;
105 let ca_cert = flags
106 .ca_cert
107 .clone()
108 .or_else(|| env.ca_cert.clone())
109 .or(file.ca_cert)
110 .ok_or(ConfigError::Missing { field: "ca_cert" })?;
111 let client_cert = flags
112 .client_cert
113 .clone()
114 .or_else(|| env.client_cert.clone())
115 .or(file.client_cert)
116 .ok_or(ConfigError::Missing {
117 field: "client_cert",
118 })?;
119 let client_key = flags
120 .client_key
121 .clone()
122 .or_else(|| env.client_key.clone())
123 .or(file.client_key)
124 .ok_or(ConfigError::Missing {
125 field: "client_key",
126 })?;
127 Ok(crate::ResolvedClientConfig {
128 cp_url,
129 ca_cert,
130 client_cert,
131 client_key,
132 })
133}
134
135pub fn default_config_path() -> PathBuf {
136 if let Some(base) = dirs::config_dir() {
137 return base.join("nixfleet").join("config.toml");
138 }
139 PathBuf::from(".nixfleet/config.toml")
140}