Author:
Hash:
Timestamp:
+353 -215 +/-18 browse
Kevin Schoon [me@kevinschoon.com]
971a541e67075e5a25727aa39292836fdf1e4a10
Sat, 06 Jul 2024 10:25:08 +0000 (9 months ago)
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index 80e6320..eeeab47 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -502,7 +502,7 @@ dependencies = [ |
6 | "ayllu_rpc", |
7 | "bytes", |
8 | "cc", |
9 | - "clap 4.5.3", |
10 | + "clap 4.5.9", |
11 | "comrak", |
12 | "env_logger 0.10.2", |
13 | "file-mode", |
14 | @@ -555,7 +555,7 @@ dependencies = [ |
15 | "ayllu_api", |
16 | "ayllu_config", |
17 | "ayllu_rpc", |
18 | - "clap 4.5.3", |
19 | + "clap 4.5.9", |
20 | "clap_complete", |
21 | "futures", |
22 | "mailpot", |
23 | @@ -579,9 +579,13 @@ dependencies = [ |
24 | name = "ayllu_config" |
25 | version = "0.2.1" |
26 | dependencies = [ |
27 | + "clap 4.5.9", |
28 | "log", |
29 | "serde", |
30 | + "serde_json", |
31 | + "thiserror", |
32 | "toml 0.7.8", |
33 | + "toml_edit 0.22.14", |
34 | ] |
35 | |
36 | [[package]] |
37 | @@ -878,9 +882,9 @@ dependencies = [ |
38 | |
39 | [[package]] |
40 | name = "clap" |
41 | - version = "4.5.3" |
42 | + version = "4.5.9" |
43 | source = "registry+https://github.com/rust-lang/crates.io-index" |
44 | - checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" |
45 | + checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" |
46 | dependencies = [ |
47 | "clap_builder", |
48 | "clap_derive", |
49 | @@ -888,9 +892,9 @@ dependencies = [ |
50 | |
51 | [[package]] |
52 | name = "clap_builder" |
53 | - version = "4.5.2" |
54 | + version = "4.5.9" |
55 | source = "registry+https://github.com/rust-lang/crates.io-index" |
56 | - checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" |
57 | + checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" |
58 | dependencies = [ |
59 | "anstream", |
60 | "anstyle", |
61 | @@ -905,14 +909,14 @@ version = "4.5.1" |
62 | source = "registry+https://github.com/rust-lang/crates.io-index" |
63 | checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" |
64 | dependencies = [ |
65 | - "clap 4.5.3", |
66 | + "clap 4.5.9", |
67 | ] |
68 | |
69 | [[package]] |
70 | name = "clap_derive" |
71 | - version = "4.5.3" |
72 | + version = "4.5.8" |
73 | source = "registry+https://github.com/rust-lang/crates.io-index" |
74 | - checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" |
75 | + checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" |
76 | dependencies = [ |
77 | "heck 0.5.0", |
78 | "proc-macro2", |
79 | @@ -944,7 +948,7 @@ version = "0.18.0" |
80 | source = "registry+https://github.com/rust-lang/crates.io-index" |
81 | checksum = "482aa5695bca086022be453c700a40c02893f1ba7098a2c88351de55341ae894" |
82 | dependencies = [ |
83 | - "clap 4.5.3", |
84 | + "clap 4.5.9", |
85 | "entities", |
86 | "memchr", |
87 | "once_cell", |
88 | @@ -3902,7 +3906,7 @@ dependencies = [ |
89 | "ayllu_config", |
90 | "ayllu_git", |
91 | "ayllu_rpc", |
92 | - "clap 4.5.3", |
93 | + "clap 4.5.9", |
94 | "clap_complete", |
95 | "reqwest 0.12.3", |
96 | "serde", |
97 | @@ -4446,9 +4450,9 @@ dependencies = [ |
98 | |
99 | [[package]] |
100 | name = "serde_json" |
101 | - version = "1.0.114" |
102 | + version = "1.0.120" |
103 | source = "registry+https://github.com/rust-lang/crates.io-index" |
104 | - checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" |
105 | + checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" |
106 | dependencies = [ |
107 | "itoa", |
108 | "ryu", |
109 | @@ -5120,18 +5124,18 @@ dependencies = [ |
110 | |
111 | [[package]] |
112 | name = "thiserror" |
113 | - version = "1.0.58" |
114 | + version = "1.0.61" |
115 | source = "registry+https://github.com/rust-lang/crates.io-index" |
116 | - checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" |
117 | + checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" |
118 | dependencies = [ |
119 | "thiserror-impl", |
120 | ] |
121 | |
122 | [[package]] |
123 | name = "thiserror-impl" |
124 | - version = "1.0.58" |
125 | + version = "1.0.61" |
126 | source = "registry+https://github.com/rust-lang/crates.io-index" |
127 | - checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" |
128 | + checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" |
129 | dependencies = [ |
130 | "proc-macro2", |
131 | "quote", |
132 | diff --git a/ayllu/src/main.rs b/ayllu/src/main.rs |
133 | index 008fdae..d6c4717 100644 |
134 | --- a/ayllu/src/main.rs |
135 | +++ b/ayllu/src/main.rs |
136 | @@ -46,21 +46,10 @@ struct JobArguments { |
137 | } |
138 | |
139 | #[derive(Subcommand, Debug)] |
140 | - enum ConfigCommand { |
141 | - /// Display the current configuration. |
142 | - Display { |
143 | - #[clap(long, default_value = "false")] |
144 | - as_json: bool, |
145 | - }, |
146 | - /// Generate a sample configuration file. |
147 | - Generate, |
148 | - } |
149 | - |
150 | - #[derive(Subcommand, Debug)] |
151 | enum Command { |
152 | /// Configuration options. |
153 | #[clap(subcommand)] |
154 | - Config(ConfigCommand), |
155 | + Config(ayllu_config::Command), |
156 | /// Perform database migration. |
157 | Migrate { |
158 | #[clap(short, long)] |
159 | @@ -124,25 +113,9 @@ fn do_migration(config: &config::Database, path: Option<String>) -> Result<(), D |
160 | fn main() -> Result<(), Box<dyn Error>> { |
161 | let args: Arguments = Arguments::parse(); |
162 | match args.subcommand { |
163 | - Command::Config(subcommand) => match subcommand { |
164 | - ConfigCommand::Display { as_json: true } => { |
165 | - println!( |
166 | - "{}", |
167 | - init_config(args.config.as_deref(), args.level.as_deref())?.to_json() |
168 | - ); |
169 | - Ok(()) |
170 | - } |
171 | - ConfigCommand::Display { as_json: false } => { |
172 | - println!( |
173 | - "{:#?}", |
174 | - init_config(args.config.as_deref(), args.level.as_deref())? |
175 | - ); |
176 | - Ok(()) |
177 | - } |
178 | - ConfigCommand::Generate => { |
179 | - println!("{}", config::EXAMPLE_CONFIG); |
180 | - Ok(()) |
181 | - } |
182 | + Command::Config(subcommand) => { |
183 | + subcommand.execute::<config::Config>(config::EXAMPLE_CONFIG, args.config)?; |
184 | + Ok(()) |
185 | }, |
186 | Command::Migrate { migrations } => { |
187 | let cfg = init_config(args.config.as_deref(), args.level.as_deref())?; |
188 | diff --git a/containers/ayllu/Containerfile b/containers/ayllu/Containerfile |
189 | index 84d9388..a7a3af0 100644 |
190 | --- a/containers/ayllu/Containerfile |
191 | +++ b/containers/ayllu/Containerfile |
192 | @@ -1,5 +1,5 @@ |
193 | - ARG BASE_BUILD_IMAGE=registry.ayllu-forge.org/ayllu/base-build |
194 | - FROM $BASE_BUILD_IMAGE AS build |
195 | + ARG BUILD_IMAGE=registry.ayllu-forge.org/ayllu/ayllu:base-build |
196 | + FROM $BUILD_IMAGE AS build |
197 | |
198 | ARG TREE_SITTER_DIFF_UPSTREAM_URL="https://github.com/the-mikedavis/tree-sitter-diff/archive" |
199 | ARG TREE_SITTER_DIFF_COMMIT_ID="629676fc3919606964231b2c7b9677d6998a2cb4" |
200 | diff --git a/containers/multiuser-mail/Containerfile b/containers/multiuser-mail/Containerfile |
201 | index 90cd251..d6a8692 100644 |
202 | --- a/containers/multiuser-mail/Containerfile |
203 | +++ b/containers/multiuser-mail/Containerfile |
204 | @@ -1,6 +1,5 @@ |
205 | - # milter and other mail security utilities from |
206 | - ARG BASE_BUILD_IMAGE |
207 | - FROM $BASE_BUILD_IMAGE AS build |
208 | + ARG BUILD_IMAGE |
209 | + FROM $BUILD_IMAGE AS build |
210 | |
211 | ARG DKIMDO_VERSION="0.1.1" |
212 | ARG DKIM_MILTER="0.1.0" |
213 | diff --git a/containers/multiuser/init/config.sh b/containers/multiuser/init/config.sh |
214 | index 31b993d..57f51e5 100755 |
215 | --- a/containers/multiuser/init/config.sh |
216 | +++ b/containers/multiuser/init/config.sh |
217 | @@ -4,4 +4,4 @@ |
218 | # at runtime e.g. ayllu config set http.address = .... |
219 | [ -n "${AYLLU_LISTEN_ADDRESS}" ] && { |
220 | sed -i "s/127.0.0.1:8080/$AYLLU_LISTEN_ADDRESS/" /etc/ayllu/config.toml |
221 | - } || true |
222 | + } |
223 | diff --git a/containers/multiuser/init/users.sh b/containers/multiuser/init/users.sh |
224 | index 8d1a791..ad75133 100755 |
225 | --- a/containers/multiuser/init/users.sh |
226 | +++ b/containers/multiuser/init/users.sh |
227 | @@ -4,7 +4,6 @@ set -e |
228 | |
229 | # first setup the ayllu user |
230 | AYLLU_HOME="/home/ayllu" |
231 | - AYLLU_CONFIG="/etc/ayllu/config.toml" |
232 | AYLLU_SSH_AUTHORIZED_KEYS_FILE="$AYLLU_HOME/.ssh/authorized_keys" |
233 | |
234 | mkdir -p /var/lib/ayllu |
235 | @@ -33,7 +32,7 @@ do |
236 | adduser -h "/home/$username" -D -g "Ayllu Managed User" "$username" |
237 | # create a directory called "repos" which we give full access to both the |
238 | # user and Ayllu. |
239 | - mkdir "/home/$username/repos" |
240 | + mkdir -p "/home/$username/repos" |
241 | chmod g+s "/home/$username/repos" |
242 | setfacl -d -m g::rwx "/home/$username/repos" |
243 | |
244 | diff --git a/containers/multiuser/service/dropbear/run b/containers/multiuser/service/dropbear/run |
245 | index 3a10acd..1d82859 100755 |
246 | --- a/containers/multiuser/service/dropbear/run |
247 | +++ b/containers/multiuser/service/dropbear/run |
248 | @@ -45,4 +45,4 @@ if [ -n "$DROPBEAR_SSH_PORT" ] ; then |
249 | DROPBEAR_FLAGS="$DROPBEAR_FLAGS -p $DROPBEAR_SSH_PORT" |
250 | fi |
251 | |
252 | - exec dropbear $DROPBEAR_FLAGS |
253 | + exec dropbear "$DROPBEAR_FLAGS" |
254 | diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml |
255 | index ab1f5af..c042d5b 100644 |
256 | --- a/crates/config/Cargo.toml |
257 | +++ b/crates/config/Cargo.toml |
258 | @@ -9,3 +9,7 @@ edition = "2021" |
259 | toml = "0.7.4" |
260 | serde = { version = "1.0", features = ["derive"] } |
261 | log = "0.4.19" |
262 | + toml_edit = { version = "0.22.14", features = ["serde"] } |
263 | + thiserror = "1.0.61" |
264 | + clap = "4.5.9" |
265 | + serde_json = "1.0.120" |
266 | diff --git a/crates/config/src/edit.rs b/crates/config/src/edit.rs |
267 | new file mode 100644 |
268 | index 0000000..4a104df |
269 | --- /dev/null |
270 | +++ b/crates/config/src/edit.rs |
271 | @@ -0,0 +1,123 @@ |
272 | + use std::path::PathBuf; |
273 | + |
274 | + use toml_edit::visit_mut::VisitMut; |
275 | + use toml_edit::{DocumentMut, Formatted, Item, Value}; |
276 | + |
277 | + use crate::error::Error; |
278 | + use crate::reader::try_read; |
279 | + |
280 | + /// convert parsed serde_toml values into toml_edit "annotated" values |
281 | + pub fn v2v(value: toml::Value) -> Value { |
282 | + match value { |
283 | + toml::Value::String(v) => Value::String(Formatted::new(v.to_string())), |
284 | + toml::Value::Integer(v) => Value::Integer(Formatted::new(v)), |
285 | + toml::Value::Float(v) => Value::Float(Formatted::new(v)), |
286 | + toml::Value::Boolean(v) => Value::Boolean(Formatted::new(v)), |
287 | + toml::Value::Datetime(v) => Value::Datetime(Formatted::new(v)), |
288 | + toml::Value::Array(v) => v.into_iter().map(v2v).collect(), |
289 | + toml::Value::Table(v) => v.into_iter().map(|(k, v)| (k, v2v(v))).collect(), |
290 | + } |
291 | + } |
292 | + |
293 | + #[derive(Default)] |
294 | + struct Visitor { |
295 | + pub target: String, |
296 | + pub state: Vec<String>, |
297 | + pub replace: Option<toml::Value>, |
298 | + pub found: Option<Value>, |
299 | + } |
300 | + |
301 | + impl VisitMut for Visitor { |
302 | + fn visit_table_like_kv_mut(&mut self, key: toml_edit::KeyMut<'_>, node: &mut Item) { |
303 | + if self.found.is_some() { |
304 | + return; |
305 | + } |
306 | + match node { |
307 | + Item::None => {} |
308 | + Item::Value(value) => { |
309 | + let mut copy = self.state.clone(); |
310 | + copy.append(&mut vec![key.to_string()]); |
311 | + if copy.join(".") == self.target { |
312 | + if let Some(new_value) = &self.replace { |
313 | + *value = v2v(new_value.clone()); |
314 | + } |
315 | + self.found = Some(value.clone()); |
316 | + } |
317 | + self.state.clear(); |
318 | + } |
319 | + Item::Table(_) => { |
320 | + self.state.push(key.to_string()); |
321 | + toml_edit::visit_mut::visit_table_like_kv_mut(self, key, node); |
322 | + } |
323 | + Item::ArrayOfTables(_) => { |
324 | + // arrays are not supported |
325 | + } |
326 | + } |
327 | + } |
328 | + } |
329 | + |
330 | + /// Editor is can perform limited modification Ayllu configuration files. |
331 | + /// It only supports modifying simple key values with a restricted set of |
332 | + /// value types: bool, string, and numbers for the moment. |
333 | + /// TODO: consider expanding this to support more configuration |
334 | + pub struct Editor(pub Option<PathBuf>); |
335 | + |
336 | + impl Editor { |
337 | + fn get_cfg(&self) -> Result<(PathBuf, String), Error> { |
338 | + if let Some(path) = &self.0 { |
339 | + Ok((path.clone(), std::fs::read_to_string(path)?)) |
340 | + } else if let Some((path, str_cfg)) = try_read() { |
341 | + Ok((path, str_cfg)) |
342 | + } else { |
343 | + Err(Error::ConfigNotFound) |
344 | + } |
345 | + } |
346 | + |
347 | + /// Return the configuration value, if no key is specified then return the |
348 | + /// entire configuration. If as_json is true then return a JSON |
349 | + /// representation rather than TOML. |
350 | + pub fn get(&self, key: &str) -> Result<String, Error> { |
351 | + let (_, cfg_str) = self.get_cfg()?; |
352 | + let mut doc = cfg_str.parse::<DocumentMut>()?; |
353 | + let mut writer = Visitor { |
354 | + target: key.to_string(), |
355 | + replace: None, |
356 | + state: vec![], |
357 | + found: None, |
358 | + }; |
359 | + writer.visit_document_mut(&mut doc); |
360 | + if let Some(value) = writer.found { |
361 | + Ok(value.to_string()) |
362 | + } else { |
363 | + Err(Error::KeyNotFound(key.to_string())) |
364 | + } |
365 | + } |
366 | + |
367 | + /// Set a configuration option at the given path. For example to set a |
368 | + /// value such as: |
369 | + /// [builder] |
370 | + /// address = "/tmp/fuu.socket" |
371 | + /// [builder.database] |
372 | + /// migrate = true |
373 | + /// |
374 | + /// You can use the key_path builder.database |
375 | + /// NOTE: That modifying arrays is unsupported and they may only be |
376 | + /// clobbered. |
377 | + /// NOTE: String values must be escaped such as \"fuu\" |
378 | + pub fn set(&self, key_path: &str, value_str: &str) -> Result<(), Error> { |
379 | + // BUG: this is a hack, I can't figure out how to do this properly via |
380 | + // serde / serde_toml, maybe it's not possible, I don't know. |
381 | + let value: toml::value::Value = toml::from_str(&format!("x = {}", value_str))?; |
382 | + let value = value.get("x").unwrap(); |
383 | + let (cfg_path, cfg_str) = self.get_cfg()?; |
384 | + let mut doc = cfg_str.parse::<DocumentMut>()?; |
385 | + let mut writer = Visitor { |
386 | + target: key_path.to_string(), |
387 | + replace: Some(value.clone()), |
388 | + ..Default::default() |
389 | + }; |
390 | + writer.visit_document_mut(&mut doc); |
391 | + std::fs::write(cfg_path, doc.to_string().as_bytes())?; |
392 | + Ok(()) |
393 | + } |
394 | + } |
395 | diff --git a/crates/config/src/error.rs b/crates/config/src/error.rs |
396 | index 980b2fa..968a74d 100644 |
397 | --- a/crates/config/src/error.rs |
398 | +++ b/crates/config/src/error.rs |
399 | @@ -1,70 +1,28 @@ |
400 | - use std::error::Error as StdError; |
401 | - use std::fmt::Display; |
402 | use std::io::Error as IoError; |
403 | |
404 | - use toml::de::Error as TomlError; |
405 | - |
406 | - #[derive(Debug)] |
407 | - pub enum ErrorKind { |
408 | - Io(IoError), |
409 | - Serialization(TomlError), |
410 | + use serde_json::error::Error as JsonError; |
411 | + use toml::{de::Error as TomlDeserializeError, ser::Error as TomlSerializeError}; |
412 | + use toml_edit::TomlError as TomlEditError; |
413 | + |
414 | + /// Configuration error |
415 | + #[derive(thiserror::Error, Debug)] |
416 | + pub enum Error { |
417 | + #[error("Io error: {0}")] |
418 | + Io(#[from] IoError), |
419 | + #[error("De-Serialization (toml) error: {0}")] |
420 | + TomlDeSerialization(#[from] TomlDeserializeError), |
421 | + #[error("Serialization (toml) error: {0}")] |
422 | + TomlSerialization(#[from] TomlSerializeError), |
423 | + #[error("Serialization (json) error: {0}")] |
424 | + JsonSerialization(#[from] JsonError), |
425 | + #[error("Configuration edit failure: {0}")] |
426 | + EditFailed(#[from] TomlEditError), |
427 | + #[error("Configuration file could not be found")] |
428 | ConfigNotFound, |
429 | - Other(String), |
430 | - } |
431 | - |
432 | - #[derive(Debug)] |
433 | - pub struct Error { |
434 | - kind: ErrorKind, |
435 | - } |
436 | - |
437 | - impl Error { |
438 | - pub fn kind(kind: ErrorKind) -> Self { |
439 | - Error { kind } |
440 | - } |
441 | - } |
442 | - |
443 | - impl From<IoError> for Error { |
444 | - fn from(value: IoError) -> Self { |
445 | - Error { |
446 | - kind: ErrorKind::Io(value), |
447 | - } |
448 | - } |
449 | - } |
450 | - |
451 | - impl From<TomlError> for Error { |
452 | - fn from(value: TomlError) -> Self { |
453 | - Error { |
454 | - kind: ErrorKind::Serialization(value), |
455 | - } |
456 | - } |
457 | - } |
458 | - |
459 | - impl From<Box<dyn StdError>> for Error { |
460 | - fn from(value: Box<dyn StdError>) -> Self { |
461 | - Error { |
462 | - kind: ErrorKind::Other(value.to_string()), |
463 | - } |
464 | - } |
465 | - } |
466 | - |
467 | - impl Display for Error { |
468 | - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
469 | - match self.kind { |
470 | - ErrorKind::Io(ref err) => err.fmt(f), |
471 | - ErrorKind::Serialization(ref err) => err.fmt(f), |
472 | - ErrorKind::Other(ref msg) => write!(f, "validation: {}", msg), |
473 | - ErrorKind::ConfigNotFound => write!(f, "failed to detect a config file"), |
474 | - } |
475 | - } |
476 | - } |
477 | - |
478 | - impl StdError for Error { |
479 | - fn source(&self) -> Option<&(dyn StdError + 'static)> { |
480 | - match self.kind { |
481 | - ErrorKind::Io(ref err) => Some(err), |
482 | - ErrorKind::Serialization(ref err) => Some(err), |
483 | - ErrorKind::Other(_) => None, |
484 | - ErrorKind::ConfigNotFound => None, |
485 | - } |
486 | - } |
487 | + #[error("Configuration key could not be found: {0}")] |
488 | + KeyNotFound(String), |
489 | + #[error("Error initializing configuration: {0}")] |
490 | + Initialization(String), |
491 | + #[error("Error validating configuration: {0}")] |
492 | + Validation(String), |
493 | } |
494 | diff --git a/crates/config/src/flags.rs b/crates/config/src/flags.rs |
495 | new file mode 100644 |
496 | index 0000000..8d74d74 |
497 | --- /dev/null |
498 | +++ b/crates/config/src/flags.rs |
499 | @@ -0,0 +1,68 @@ |
500 | + use std::path::PathBuf; |
501 | + |
502 | + use clap::Subcommand; |
503 | + use serde::{de::DeserializeOwned, ser::Serialize}; |
504 | + use serde_json::to_string as to_json_str; |
505 | + use toml::ser::to_string as to_toml_string; |
506 | + |
507 | + use crate::edit::Editor; |
508 | + use crate::error::Error; |
509 | + use crate::reader::{Configurable, Reader}; |
510 | + |
511 | + /// Subcommand for managing configuration across different ayllu binaries. |
512 | + #[derive(Subcommand, Debug)] |
513 | + pub enum Command { |
514 | + /// Retrieve a value from the configuration |
515 | + Get { |
516 | + /// dot separated key used to select a value within the config file |
517 | + key: String, |
518 | + }, |
519 | + /// Set a value in the configuration |
520 | + Set { |
521 | + /// dot separated key used to select a value within the config file |
522 | + key: String, |
523 | + /// string representation of a TOML value to be placed in the config |
524 | + value: String, |
525 | + }, |
526 | + /// Display the current configuration. |
527 | + Display { |
528 | + #[clap(long, default_value = "false")] |
529 | + as_json: bool, |
530 | + }, |
531 | + /// Generate a sample configuration file. |
532 | + Generate, |
533 | + } |
534 | + |
535 | + impl Command { |
536 | + pub fn execute<T>(self, example_cfg: &str, path: Option<PathBuf>) -> Result<(), Error> |
537 | + where |
538 | + T: DeserializeOwned + Configurable + Serialize, |
539 | + { |
540 | + match self { |
541 | + Command::Get { key } => { |
542 | + let value = Editor(path).get(&key)?; |
543 | + println!("{}", value); |
544 | + Ok(()) |
545 | + } |
546 | + Command::Set { key, value } => Editor(path).set(&key, &value), |
547 | + Command::Display { as_json } => { |
548 | + let mut cfg: T = Reader::load(path.as_deref())?; |
549 | + if as_json { |
550 | + cfg.validate() |
551 | + .map_err(|e| Error::Validation(e.to_string()))?; |
552 | + println!("{}", to_json_str(&cfg)?); |
553 | + Ok(()) |
554 | + } else { |
555 | + cfg.validate() |
556 | + .map_err(|e| Error::Validation(e.to_string()))?; |
557 | + println!("{}", to_toml_string(&cfg)?); |
558 | + Ok(()) |
559 | + } |
560 | + } |
561 | + Command::Generate => { |
562 | + print!("{}", example_cfg); |
563 | + Ok(()) |
564 | + } |
565 | + } |
566 | + } |
567 | + } |
568 | diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs |
569 | index 04fb65e..03d898a 100644 |
570 | --- a/crates/config/src/lib.rs |
571 | +++ b/crates/config/src/lib.rs |
572 | @@ -1,5 +1,9 @@ |
573 | + pub use edit::Editor; |
574 | pub use error::Error; |
575 | + pub use flags::Command; |
576 | pub use reader::{data_dir, runtime_dir, Configurable, Reader}; |
577 | |
578 | + mod edit; |
579 | mod error; |
580 | + mod flags; |
581 | mod reader; |
582 | diff --git a/crates/config/src/reader.rs b/crates/config/src/reader.rs |
583 | index abb4889..d8af253 100644 |
584 | --- a/crates/config/src/reader.rs |
585 | +++ b/crates/config/src/reader.rs |
586 | @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; |
587 | use serde::de::DeserializeOwned; |
588 | use toml::from_str; |
589 | |
590 | - use crate::error::{Error, ErrorKind}; |
591 | + use crate::error::Error; |
592 | |
593 | pub trait Configurable { |
594 | /// initialize the configuration (typically set defaults) |
595 | @@ -19,6 +19,32 @@ pub trait Configurable { |
596 | } |
597 | } |
598 | |
599 | + /// attempt to read the configuration from the host |
600 | + pub fn try_read() -> Option<(PathBuf, String)> { |
601 | + let home_config = match env::var("XDG_CONFIG_HOME") { |
602 | + Ok(path) => PathBuf::from_iter([path.as_str(), "ayllu", "config.toml"]), |
603 | + Err(_) => PathBuf::from_iter([ |
604 | + env::var("HOME").unwrap_or(String::from("/")).as_str(), |
605 | + ".config", |
606 | + "ayllu", |
607 | + "config.toml", |
608 | + ]), |
609 | + }; |
610 | + for path in [ |
611 | + &home_config, |
612 | + Path::new("./config.toml"), |
613 | + Path::new("/etc/ayllu/config.toml"), |
614 | + ] { |
615 | + log::info!("reading configuration from {}", path.display()); |
616 | + let ret = std::fs::read_to_string(path); |
617 | + match ret { |
618 | + Ok(r) => return Some((path.to_path_buf(), r)), |
619 | + Err(_) => continue, |
620 | + }; |
621 | + } |
622 | + None |
623 | + } |
624 | + |
625 | /// Reader the global configuration file |
626 | pub struct Reader<T>(T); |
627 | |
628 | @@ -26,52 +52,35 @@ impl<T> Reader<T> |
629 | where |
630 | T: DeserializeOwned + Configurable, |
631 | { |
632 | - fn _try_read() -> Option<String> { |
633 | - let home_config = match env::var("XDG_CONFIG_HOME") { |
634 | - Ok(path) => PathBuf::from_iter([path.as_str(), "ayllu", "config.toml"]), |
635 | - Err(_) => PathBuf::from_iter([ |
636 | - env::var("HOME").unwrap_or(String::from("/")).as_str(), |
637 | - ".config", |
638 | - "ayllu", |
639 | - "config.toml", |
640 | - ]), |
641 | - }; |
642 | - for path in [ |
643 | - &home_config, |
644 | - Path::new("./config.toml"), |
645 | - Path::new("/etc/ayllu/config.toml"), |
646 | - ] { |
647 | - log::info!("reading configuration from {}", path.display()); |
648 | - let ret = std::fs::read_to_string(path); |
649 | - match ret { |
650 | - Ok(r) => return Some(r), |
651 | - Err(_) => continue, |
652 | - }; |
653 | - } |
654 | - None |
655 | - } |
656 | - |
657 | - /// load a configuration from a given path, if the path is not specified |
658 | + /// load a configuration from a given path, if the path is not specified |
659 | /// then attempt to load the config from common defaults. |
660 | pub fn load(path: Option<&Path>) -> Result<T, Error> { |
661 | let config_str = match path { |
662 | Some(path) => std::fs::read_to_string(path)?, |
663 | - None => match Reader::<T>::_try_read() { |
664 | - Some(config_str) => config_str, |
665 | - None => return Err(Error::kind(ErrorKind::ConfigNotFound)), |
666 | + None => match try_read() { |
667 | + Some((_, config_str)) => config_str, |
668 | + None => return Err(Error::ConfigNotFound), |
669 | }, |
670 | }; |
671 | let mut config = from_str::<T>(&config_str)?; |
672 | - config.initialize()?; |
673 | - config.validate()?; |
674 | + config |
675 | + .initialize() |
676 | + .map_err(|e| Error::Initialization(e.to_string()))?; |
677 | + config |
678 | + .validate() |
679 | + .map_err(|e| Error::Validation(e.to_string()))?; |
680 | Ok(config) |
681 | } |
682 | |
683 | /// load the config from a string |
684 | pub fn read(config_str: &str) -> Result<T, Error> { |
685 | let mut config = from_str::<T>(config_str)?; |
686 | - config.initialize()?; |
687 | - config.validate()?; |
688 | + config |
689 | + .initialize() |
690 | + .map_err(|e| Error::Initialization(e.to_string()))?; |
691 | + config |
692 | + .validate() |
693 | + .map_err(|e| Error::Validation(e.to_string()))?; |
694 | Ok(config) |
695 | } |
696 | } |
697 | diff --git a/scripts/build_and_deploy.sh b/scripts/build_and_deploy.sh |
698 | index 51b33ee..0e00c94 100755 |
699 | --- a/scripts/build_and_deploy.sh |
700 | +++ b/scripts/build_and_deploy.sh |
701 | @@ -2,6 +2,7 @@ |
702 | # Internal helper script to build and deploy Ayllu in one command |
703 | |
704 | scripts/build_all_containers.sh |
705 | + scripts/push_all_containers.sh |
706 | |
707 | /usr/lib/podman/quadlet --user "$HOME/.config/systemd/user" |
708 | systemctl --user daemon-reload |
709 | diff --git a/scripts/build_container.sh b/scripts/build_container.sh |
710 | index 0a23bf1..e0664c6 100755 |
711 | --- a/scripts/build_container.sh |
712 | +++ b/scripts/build_container.sh |
713 | @@ -1,11 +1,13 @@ |
714 | #!/bin/sh |
715 | - set -ex |
716 | + # Build a container image and tag it appropriately. For the base ayllu |
717 | + # container it's tag will look like registry.ayllu-forge.org/ayllu/ayllu:$BRANCH |
718 | + # Other containers will have ayllu:$name-$BRANCH |
719 | + set -e |
720 | |
721 | REGISTRY="registry.ayllu-forge.org" |
722 | + IMAGE_PATH="ayllu/ayllu" |
723 | COMMIT_ID="$(git rev-parse HEAD)" |
724 | BRANCH_NAME="$(git branch --show-current)" |
725 | - BASE_BUILD_IMAGE="$REGISTRY/ayllu/base-build:$COMMIT_ID" |
726 | - EXTRA_ARGS="--build-arg=BASE_BUILD_IMAGE=$BASE_BUILD_IMAGE" |
727 | |
728 | usage() { |
729 | printf "USAGE: build_container.sh PATH\n" |
730 | @@ -13,45 +15,28 @@ usage() { |
731 | } |
732 | |
733 | TARGET_DIR="$1" |
734 | - |
735 | [ -z "$TARGET_DIR" ] && usage |
736 | |
737 | - FLAVOR="$(basename "$TARGET_DIR")" |
738 | - case "$FLAVOR" in |
739 | - "base-build") |
740 | - IMAGE_NAME="ayllu/base-build" |
741 | - DETAILED_TAG="$COMMIT_ID" |
742 | - FRIENDLY_TAG="$BRANCH_NAME" |
743 | - ;; |
744 | - "ayllu") |
745 | - IMAGE_NAME="ayllu/ayllu" |
746 | - DETAILED_TAG="$COMMIT_ID" |
747 | - FRIENDLY_TAG="$BRANCH_NAME" |
748 | - ;; |
749 | - "multiuser") |
750 | - IMAGE_NAME="ayllu/ayllu" |
751 | - DETAILED_TAG="$FLAVOR-$COMMIT_ID" |
752 | - FRIENDLY_TAG="$FLAVOR-$BRANCH_NAME" |
753 | - EXTRA_ARGS="${EXTRA_ARGS} --build-arg=BASE_IMAGE=$REGISTRY/$IMAGE_NAME:$COMMIT_ID" |
754 | - ;; |
755 | - "multiuser-mail") |
756 | - IMAGE_NAME="ayllu/ayllu" |
757 | - DETAILED_TAG="$FLAVOR-$COMMIT_ID" |
758 | - FRIENDLY_TAG="$FLAVOR-$BRANCH_NAME" |
759 | - EXTRA_ARGS="${EXTRA_ARGS} --build-arg=BASE_IMAGE=$REGISTRY/$IMAGE_NAME:multiuser-$COMMIT_ID" |
760 | - ;; |
761 | - *) |
762 | - echo "bad container name $FLAVOR" |
763 | - exit 1 |
764 | - ;; |
765 | - esac |
766 | - |
767 | - echo "building container $REGISTRY/$IMAGE_NAME:$DETAILED_TAG" |
768 | + NAME="$(basename "$TARGET_DIR")" |
769 | + TAG_NAME="$NAME-$COMMIT_ID" |
770 | + if [ "$NAME" = "ayllu" ] ; then |
771 | + TAG_NAME="$COMMIT_ID" |
772 | + fi |
773 | |
774 | podman \ |
775 | build --network=host \ |
776 | - -t "$REGISTRY/$IMAGE_NAME:$DETAILED_TAG" \ |
777 | - -f "$TARGET_DIR/Containerfile" ${EXTRA_ARGS} . |
778 | + -t "$REGISTRY/$IMAGE_PATH:$TAG_NAME" \ |
779 | + -f "$TARGET_DIR/Containerfile" \ |
780 | + --build-arg=BUILD_IMAGE="$REGISTRY/$IMAGE_PATH:base-build-$COMMIT_ID" \ |
781 | + --build-arg=BASE_IMAGE="$REGISTRY/$IMAGE_PATH:$COMMIT_ID" \ |
782 | + --build-arg=MULTIUSER_IMAGE="$REGISTRY/$IMAGE_PATH:multiuser-$COMMIT_ID" \ |
783 | + . |
784 | + |
785 | + if [ "$NAME" = "ayllu" ] ; then |
786 | + TARGET_TAG="$REGISTRY/$IMAGE_PATH:$BRANCH_NAME" |
787 | + else |
788 | + TARGET_TAG="$REGISTRY/$IMAGE_PATH:$NAME-$BRANCH_NAME" |
789 | + fi |
790 | |
791 | podman tag \ |
792 | - "$REGISTRY/$IMAGE_NAME:$DETAILED_TAG" "$REGISTRY/$IMAGE_NAME:$FRIENDLY_TAG" |
793 | + "$REGISTRY/$IMAGE_PATH:$TAG_NAME" "$TARGET_TAG" |
794 | diff --git a/scripts/ensure_database.sh b/scripts/ensure_database.sh |
795 | index 2f15919..4fae5b8 100755 |
796 | --- a/scripts/ensure_database.sh |
797 | +++ b/scripts/ensure_database.sh |
798 | @@ -5,7 +5,7 @@ set -ex |
799 | |
800 | COMPONENT="$1" |
801 | |
802 | - if [ ! -n "$COMPONENT" ] ; then |
803 | + if [ -z "$COMPONENT" ] ; then |
804 | COMPONENT="ayllu" |
805 | fi |
806 | |
807 | @@ -14,7 +14,9 @@ DB_URL="sqlite://${DB_PATH}" |
808 | |
809 | mkdir -p "$(dirname "${DB_PATH}")" |
810 | |
811 | - if [ ! -f ${DB_PATH} ]; then |
812 | + echo "$DB_PATH" |
813 | + |
814 | + if [ ! -e "${DB_PATH}" ]; then |
815 | cargo sqlx database create --database-url "${DB_URL}" |
816 | fi |
817 | |
818 | diff --git a/scripts/push_all_containers.sh b/scripts/push_all_containers.sh |
819 | new file mode 100755 |
820 | index 0000000..728706d |
821 | --- /dev/null |
822 | +++ b/scripts/push_all_containers.sh |
823 | @@ -0,0 +1,7 @@ |
824 | + #!/bin/sh |
825 | + set -e |
826 | + |
827 | + scripts/push_container.sh containers/base-build |
828 | + scripts/push_container.sh containers/ayllu |
829 | + scripts/push_container.sh containers/multiuser |
830 | + scripts/push_container.sh containers/multiuser-mail |
831 | diff --git a/scripts/push_container.sh b/scripts/push_container.sh |
832 | index c8be56d..7cbe68d 100755 |
833 | --- a/scripts/push_container.sh |
834 | +++ b/scripts/push_container.sh |
835 | @@ -1,9 +1,16 @@ |
836 | #!/bin/sh |
837 | + # Push a container image to the authenticated Ayllu registry endpoint. Note |
838 | + # that registry.ayllu-forge.org is for enduser consumption without |
839 | + # authentication while registry-auth.ayllu-forge.org is used to securely push |
840 | + # images into the registry and requries credentials. |
841 | + # |
842 | + # Assumes that the image you are pushing has already been built via the |
843 | + # build_container.sh script. |
844 | set -e |
845 | |
846 | REGISTRY="registry.ayllu-forge.org" |
847 | REGISTRY_AUTH="registry-auth.ayllu-forge.org" |
848 | - IMAGE_NAME="ayllu/ayllu" |
849 | + IMAGE_PATH="ayllu/ayllu" |
850 | COMMIT_ID="$(git rev-parse HEAD)" |
851 | BRANCH_NAME="$(git branch --show-current)" |
852 | |
853 | @@ -13,24 +20,19 @@ usage() { |
854 | } |
855 | |
856 | TARGET_DIR="$1" |
857 | - |
858 | [ -z "$TARGET_DIR" ] && usage |
859 | |
860 | - FLAVOR="$(basename "$TARGET_DIR")" |
861 | - if [ "$FLAVOR" = "base" ]; then |
862 | - DETAILED_TAG="$COMMIT_ID" |
863 | - FRIENDLY_TAG="$BRANCH_NAME" |
864 | - else |
865 | - DETAILED_TAG="$FLAVOR-$COMMIT_ID" |
866 | - FRIENDLY_TAG="$FLAVOR-$BRANCH_NAME" |
867 | - fi |
868 | - |
869 | + NAME="$(basename "$TARGET_DIR")" |
870 | podman login "$REGISTRY_AUTH" |
871 | |
872 | - podman tag \ |
873 | - "$REGISTRY/$IMAGE_NAME:$DETAILED_TAG" "$REGISTRY_AUTH/$IMAGE_NAME:$DETAILED_TAG" |
874 | - podman tag \ |
875 | - "$REGISTRY/$IMAGE_NAME:$FRIENDLY_TAG" "$REGISTRY_AUTH/$IMAGE_NAME:$FRIENDLY_TAG" |
876 | - |
877 | - podman push "$REGISTRY_AUTH/$IMAGE_NAME:$DETAILED_TAG" |
878 | - podman push "$REGISTRY_AUTH/$IMAGE_NAME:$FRIENDLY_TAG" |
879 | + if [ "$NAME" = "ayllu" ] ; then |
880 | + podman tag "$REGISTRY/$IMAGE_PATH:$COMMIT_ID" "$REGISTRY_AUTH/$IMAGE_PATH:$COMMIT_ID" |
881 | + podman push "$REGISTRY_AUTH/$IMAGE_PATH:$COMMIT_ID" |
882 | + podman tag "$REGISTRY/$IMAGE_PATH:$BRANCH_NAME" "$REGISTRY_AUTH/$IMAGE_PATH:$BRANCH_NAME" |
883 | + podman push "$REGISTRY_AUTH/$IMAGE_PATH:$BRANCH_NAME" |
884 | + else |
885 | + podman tag "$REGISTRY/$IMAGE_PATH:$COMMIT_ID" "$REGISTRY_AUTH/$IMAGE_PATH:$NAME-$COMMIT_ID" |
886 | + podman push "$REGISTRY_AUTH/$IMAGE_PATH:$NAME-$COMMIT_ID" |
887 | + podman tag "$REGISTRY/$IMAGE_PATH:$BRANCH_NAME" "$REGISTRY_AUTH/$IMAGE_PATH:$NAME-$BRANCH_NAME" |
888 | + podman push "$REGISTRY_AUTH/$IMAGE_PATH:$NAME-$BRANCH_NAME" |
889 | + fi |