Skip to content

Commit b60ed83

Browse files
authored
Config flows (#65)
* add: implement config source and config body labels; drop: remove support for yml extension. * undo * undo * add: edit stub * add: decrypt and edit * add: re-ecnrypt vars after edit * add: implement edit * add: replace describe by show config * update docs * fix: short-circuit edit if no changes fix: fail if config with exact path is missing --------- Co-authored-by: queil <queil@users.noreply.github.com>
1 parent 429ad62 commit b60ed83

12 files changed

+483
-133
lines changed

Cargo.lock

+39-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rooz"
3-
version = "0.90.0"
3+
version = "0.91.0"
44
edition = "2021"
55

66
[dependencies]
@@ -11,6 +11,7 @@ clap = { version = "4.5.16", features = ["derive", "env"] }
1111
clap_complete = "4.5.24"
1212
colored = "2.1.0"
1313
ctrlc = "3.4.5"
14+
edit = "0.1.5"
1415
env_logger = "0.11.5"
1516
futures = "0.3.30"
1617
handlebars = "6.0.0"

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ rooz tmp --image alpine --shell sh
100100
Most of the settings can be configured via:
101101

102102
* environment variables
103-
* a config file in the cloned repository (if any) (`.rooz.toml`, `rooz.yaml`)
103+
* a config file in the cloned repository (if any) (`.rooz.toml`, `.rooz.yaml`)
104104
* a config file specified via `--config` (on `rooz new`) (`toml/yaml`)
105105
:information_source: it can be a local file path or a remote git file like: `git@github.com/my/configs//path/in/repo/config.rooz.yaml`
106106
* cmd-line parameters
@@ -209,7 +209,7 @@ env:
209209
Now, encrypt the value:
210210

211211
```sh
212-
rooz encrypt --config ./examples/encrypt/example-secrets.rooz.yaml --env API_KEY
212+
rooz encrypt --config ./examples/encrypt/example-secrets.rooz.yaml apiKey
213213
```
214214

215215
View the file. If the result looks like the below (things may have got re-ordered
@@ -243,7 +243,7 @@ rooz enter secrets-test
243243

244244
*It's similar to docker-compose but super simple and limited to bare minimum.*
245245

246-
* `rooz` has a limited support for sidecars (containers running along). It is only available via `.rooz.toml`:
246+
* `rooz` has a limited support for sidecars (containers running along). It is only available via `.rooz.toml` / `.rooz.yaml`:
247247

248248
```toml
249249
[sidecars.sql]

src/age_utils.rs

+25-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ pub fn mount(target: &str) -> Mount {
2323
}
2424
}
2525

26+
pub enum Variable {
27+
Secret { value: String },
28+
ClearText { value: String },
29+
}
30+
31+
impl Variable {
32+
pub fn to_string(&self) -> String {
33+
match &self {
34+
Variable::Secret { value } => value.to_string(),
35+
Variable::ClearText { value } => value.to_string(),
36+
}
37+
}
38+
}
39+
2640
impl<'a> WorkspaceApi<'a> {
2741
pub async fn read_age_identity(&self) -> Result<Identity, AnyError> {
2842
let workspace_key = id::random_suffix("tmp");
@@ -104,8 +118,8 @@ pub fn encrypt(plaintext: String, recipient: Recipient) -> Result<String, AnyErr
104118
pub fn decrypt(
105119
identity: &dyn age::Identity,
106120
env_vars: LinkedHashMap<String, String>,
107-
) -> Result<LinkedHashMap<String, String>, AnyError> {
108-
let mut ret = LinkedHashMap::<String, String>::new();
121+
) -> Result<LinkedHashMap<String, Variable>, AnyError> {
122+
let mut ret = LinkedHashMap::<String, Variable>::new();
109123
for (k, v) in env_vars.iter() {
110124
if v.starts_with(SECRET_HEADER) {
111125
let formatted = v.replace("|", "\n");
@@ -126,10 +140,17 @@ pub fn decrypt(
126140

127141
ret.insert(
128142
k.to_string(),
129-
std::str::from_utf8(&decrypted[..])?.to_string(),
143+
Variable::Secret {
144+
value: std::str::from_utf8(&decrypted[..])?.to_string(),
145+
},
130146
);
131147
} else {
132-
ret.insert(k.to_string(), v.to_string());
148+
ret.insert(
149+
k.to_string(),
150+
Variable::ClearText {
151+
value: v.to_string(),
152+
},
153+
);
133154
}
134155
}
135156
Ok(ret)

src/api/sidecar.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ impl<'a> WorkspaceApi<'a> {
5454
let labels = labels
5555
.clone()
5656
.with_container(Some(&name))
57-
.with_role(Some(labels::ROLE_SIDECAR));
57+
.with_role(labels::ROLE_SIDECAR);
5858
let mut ports = HashMap::<String, Option<String>>::new();
5959
RoozCfg::parse_ports(&mut ports, s.ports.clone());
6060

src/api/workspace.rs

+140-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
use std::path::Path;
2-
1+
use age::x25519::Identity;
32
use bollard::{
43
network::ListNetworksOptions,
54
service::{ContainerSummary, Volume},
65
volume::ListVolumesOptions,
76
};
7+
use linked_hash_map::LinkedHashMap;
8+
use std::path::Path;
89

910
use crate::{
10-
age_utils,
11+
age_utils::{self, Variable},
1112
api::WorkspaceApi,
13+
cli::{ConfigPart, WorkParams, WorkspacePersistence},
1214
constants,
1315
labels::{self, Labels, ROLE},
1416
model::{
15-
config::FinalCfg,
17+
config::{ConfigSource, FileFormat, FinalCfg, RoozCfg},
1618
types::{AnyError, ContainerResult, RunSpec, WorkSpec, WorkspaceResult},
1719
volume::{RoozVolume, CACHE_ROLE, WORK_ROLE},
1820
},
@@ -200,11 +202,143 @@ impl<'a> WorkspaceApi<'a> {
200202
Ok(())
201203
}
202204

203-
pub async fn show_config(&self, workspace_key: &str) -> Result<(), AnyError> {
205+
pub async fn show_config(&self, workspace_key: &str, part: ConfigPart) -> Result<(), AnyError> {
206+
let labels = Labels::new(Some(workspace_key), Some(WORK_ROLE));
207+
for c in self.api.container.get_all(&labels).await? {
208+
if let Some(labels) = c.labels {
209+
println!(
210+
"{}",
211+
labels[match part {
212+
ConfigPart::OriginPath => labels::CONFIG_ORIGIN,
213+
ConfigPart::OriginBody => labels::CONFIG_BODY,
214+
ConfigPart::Runtime => labels::RUNTIME_CONFIG,
215+
}]
216+
);
217+
}
218+
}
219+
Ok(())
220+
}
221+
222+
pub fn encrypt(
223+
&self,
224+
identity: Identity,
225+
name: &str,
226+
vars: LinkedHashMap<String, String>,
227+
) -> Result<LinkedHashMap<String, String>, AnyError> {
228+
let encrypted = self.encrypt_value(identity, vars[name].to_string())?;
229+
let mut new_vars = vars.clone();
230+
new_vars.insert(name.to_string(), encrypted);
231+
Ok(new_vars)
232+
}
233+
234+
pub fn encrypt_value(
235+
&self,
236+
identity: Identity,
237+
clear_text: String,
238+
) -> Result<String, AnyError> {
239+
age_utils::encrypt(clear_text, identity.to_public())
240+
}
241+
242+
pub async fn decrypt(
243+
&self,
244+
vars: Option<LinkedHashMap<String, String>>,
245+
) -> Result<LinkedHashMap<String, age_utils::Variable>, AnyError> {
246+
log::debug!("Checking if vars need decryption");
247+
if let Some(vars) = age_utils::needs_decryption(vars.clone()) {
248+
log::debug!("Decrypting vars");
249+
let identity = self.read_age_identity().await?;
250+
age_utils::decrypt(&identity, vars)
251+
} else {
252+
log::debug!("No encrypted vars found");
253+
let mut ret = LinkedHashMap::<String, Variable>::new();
254+
match vars {
255+
Some(vars) => {
256+
for (k, v) in vars {
257+
ret.insert(k, Variable::ClearText { value: v });
258+
}
259+
Ok(ret)
260+
}
261+
None => Ok(ret),
262+
}
263+
}
264+
}
265+
266+
pub fn variables_to_string(
267+
&self,
268+
vars: &LinkedHashMap<String, Variable>,
269+
) -> LinkedHashMap<String, String> {
270+
let mut ret = LinkedHashMap::<String, String>::new();
271+
for (k, v) in vars {
272+
ret.insert(k.clone(), v.to_string());
273+
}
274+
ret
275+
}
276+
277+
pub async fn edit(&self, workspace_key: &str) -> Result<(), AnyError> {
204278
let labels = Labels::new(Some(workspace_key), Some(WORK_ROLE));
205279
for c in self.api.container.get_all(&labels).await? {
206280
if let Some(labels) = c.labels {
207-
println!("{}", labels[labels::RUNTIME_CONFIG]);
281+
let config_source = &labels[labels::CONFIG_ORIGIN];
282+
let format = FileFormat::from_path(config_source);
283+
let config =
284+
RoozCfg::deserialize_config(&labels[labels::CONFIG_BODY], format)?.unwrap();
285+
let decrypted = self.decrypt(config.clone().vars).await?;
286+
let decrypted_config = RoozCfg {
287+
vars: if decrypted.len() > 0 {
288+
Some(self.variables_to_string(&decrypted))
289+
} else {
290+
None
291+
},
292+
..config
293+
};
294+
295+
let decrypted_string = decrypted_config.to_string(format)?;
296+
let edited_string = edit::edit(decrypted_string.clone())?;
297+
298+
if edited_string != decrypted_string {
299+
let edited_config = RoozCfg::from_string(&edited_string, format)?;
300+
let identity = self.read_age_identity().await?;
301+
302+
let mut encrypted_vars = LinkedHashMap::<String, String>::new();
303+
for (k, v) in &decrypted {
304+
let edited_value = &edited_config.clone().vars.unwrap()[k];
305+
match v {
306+
Variable::ClearText { .. } => {
307+
encrypted_vars.insert(k.to_string(), edited_value.to_string())
308+
}
309+
Variable::Secret { .. } => encrypted_vars.insert(
310+
k.to_string(),
311+
self.encrypt_value(identity.clone(), edited_value.to_string())?,
312+
),
313+
};
314+
}
315+
316+
let encrypted_config = RoozCfg {
317+
vars: if encrypted_vars.len() > 0 {
318+
Some(encrypted_vars)
319+
} else {
320+
None
321+
},
322+
..edited_config
323+
};
324+
325+
self.new(
326+
&WorkParams {
327+
..Default::default()
328+
},
329+
Some(ConfigSource::Body {
330+
value: encrypted_config,
331+
origin: config_source.to_string(),
332+
format,
333+
}),
334+
Some(WorkspacePersistence {
335+
name: labels[labels::WORKSPACE_KEY].to_string(),
336+
replace: false,
337+
apply: true,
338+
}),
339+
)
340+
.await?;
341+
}
208342
}
209343
}
210344
Ok(())

0 commit comments

Comments
 (0)