git.delta.rocks / jrsonnet / refs/commits / 7b7c4bb80b01

difftreelog

feat fleet secret regenerate subcommand

puknpnklYaroslav Bolyukin2026-04-18parent: #204755f.patch.diff
in: trunk

1 file changed

modifiedcmds/fleet/src/cmds/secrets.rsdiffbeforeafterboth
49 #[clap(short = 'm', long)]49 #[clap(short = 'm', long)]
50 machine: Vec<String>,50 machine: Vec<String>,
51 },51 },
52 /// Regenerate secret (prune then ensure)
53 Regenerate {
54 /// Secret to regenerate
55 name: String,
56
57 /// Machines to regenerate for - if specified, only those machines
58 #[clap(short = 'm', long)]
59 machine: Vec<String>,
60 },
52 List {},61 List {},
53}62}
5463
170 machine,179 machine,
171 whole_dist,180 whole_dist,
172 } => {181 } => {
173 let mut secrets = config.data.secrets.write().expect("not poisoned");
174 let Some(dists) = secrets.get_mut(&name) else {
175 bail!("secret {name} not found");
176 };
177 if machine.is_empty() && whole_dist {
178 for dist in dists.distributions_mut() {
179 dist.prune("manual prune".to_owned());
180 }
181 } else if machine.is_empty() {
182 let dist = dists
183 .distributions_mut()
184 .exactly_one()
185 .map_err(|e| anyhow!("{e}"))
186 .context(
187 "with no machine specified, there should be exactly one distribution",
188 )?;
189 dist.prune("manual prune".to_owned());182 Self::prune(config, &name, &machine, whole_dist)?;
190 } else if whole_dist {
191 for dist in dists.distributions_mut() {
192 if machine
193 .iter()
194 .any(|m| dist.owners().any(|o| o.as_host() == Some(m.as_str())))
195 {
196 dist.prune(format!(
197 "manual prune of distribution containing {}",
198 machine.join(", ")
199 ));
200 }
201 }
202 } else {
203 let owners: BTreeSet<SecretOwner> =
204 machine.iter().map(SecretOwner::host).collect();
205 for dist in dists.distributions_mut() {
206 dist.prune_owners(&owners, "manual prune".to_owned());
207 }
208 }
209 }183 }
210 Secret::Ensure { name, machine } => {184 Secret::Ensure { name, machine } => {
185 Self::ensure(config, opts, &name, &machine)?;
186 }
211 let hosts: Vec<String> = if machine.is_empty() {187 Secret::Regenerate { name, machine } => {
212 config
213 .list_hosts()?
214 .into_iter()
215 .filter(|h| opts.should_skip(h).ok() != Some(true))
216 .map(|h| h.name)
217 .collect()
218 } else {
219 machine
220 };
221
222 for hostname in &hosts {
223 let nixos_cfg = config.system_config(hostname)?;188 let pruned = Self::prune(config, &name, &machine, true)?;
224 let secrets = nix_go!(nixos_cfg.secrets);189 // In general, this is not correct - already evaluated secret would still be cached after pruning
190 // But as a dedicated CLI subcommand it is safe to assume it was not evaluated yet
225 if secrets.has_field(&name)? {191 Self::ensure(config, opts, &name, &pruned)?;
226 info!("ensuring secret {name} for {hostname}");
227 // Force evaluation of secret parts, triggering __fleetEnsureHostSecret
228 nix_go!(secrets[{ &name }].definition.parts);
229 }
230 }192 }
231 }
232 }193 }
233 Ok(())194 Ok(())
234 }195 }
196
197 fn prune(
198 config: &Config,
199 name: &str,
200 machine: &[String],
201 whole_dist: bool,
202 ) -> Result<Vec<String>> {
203 let mut secrets = config.data.secrets.write().expect("not poisoned");
204 let Some(dists) = secrets.get_mut(name) else {
205 bail!("secret {name} not found");
206 };
207 let owners_before: BTreeSet<String> = dists
208 .owners()
209 .filter_map(|o| o.as_host().map(str::to_owned))
210 .collect();
211
212 if machine.is_empty() && whole_dist {
213 for dist in dists.distributions_mut() {
214 dist.prune("manual prune".to_owned());
215 }
216 } else if machine.is_empty() {
217 let dist = dists
218 .distributions_mut()
219 .exactly_one()
220 .map_err(|e| anyhow!("{e}"))
221 .context("with no machine specified, there should be exactly one distribution")?;
222 dist.prune("manual prune".to_owned());
223 } else if whole_dist {
224 for dist in dists.distributions_mut() {
225 if machine
226 .iter()
227 .any(|m| dist.owners().any(|o| o.as_host() == Some(m.as_str())))
228 {
229 dist.prune(format!(
230 "manual prune of distribution containing {}",
231 machine.join(", ")
232 ));
233 }
234 }
235 } else {
236 let owners: BTreeSet<SecretOwner> = machine.iter().map(SecretOwner::host).collect();
237 for dist in dists.distributions_mut() {
238 dist.prune_owners(&owners, "manual prune".to_owned());
239 }
240 }
241
242 let owners_after: BTreeSet<String> = dists
243 .owners()
244 .filter_map(|o| o.as_host().map(str::to_owned))
245 .collect();
246 Ok(owners_before.difference(&owners_after).cloned().collect())
247 }
248
249 fn ensure(config: &Config, opts: &FleetOpts, name: &str, machine: &[String]) -> Result<()> {
250 let hosts: Vec<String> = if machine.is_empty() {
251 config
252 .list_hosts()?
253 .into_iter()
254 .filter(|h| opts.should_skip(h).ok() != Some(true))
255 .map(|h| h.name)
256 .collect()
257 } else {
258 machine.to_vec()
259 };
260
261 for hostname in &hosts {
262 let nixos_cfg = config.system_config(hostname)?;
263 let secrets = nix_go!(nixos_cfg.secrets);
264 if secrets.has_field(name)? {
265 info!("ensuring secret {name} for {hostname}");
266 // Force evaluation of secret parts, triggering __fleetEnsureHostSecret
267 nix_go!(secrets[{ name }].definition.parts);
268 }
269 }
270 Ok(())
271 }
235}272}
236273