ironflow_cli/commands/
secret.rs1use anyhow::{Context, Result, bail};
7use clap::{Args, Subcommand, value_parser};
8use ironflow_sdk::IronflowClient;
9use ironflow_sdk::client::ApiResponse;
10use ironflow_sdk::types::{
11 KeyVersionsResponse, RotateSecretsRequest, SetSecretRequest, UpdateSecretRequest,
12};
13use serde::Serialize;
14use std::slice;
15use uuid::Uuid;
16
17use crate::confirm::{confirm, resolve_secret_value};
18use crate::output;
19
20#[derive(Debug, Args)]
22pub struct SecretArgs {
23 #[command(subcommand)]
25 pub command: SecretCommands,
26}
27
28#[derive(Debug, Subcommand)]
30pub enum SecretCommands {
31 List,
33 Set {
35 key: String,
37 value: Option<String>,
40 },
41 Update {
43 key: String,
45 value: Option<String>,
47 },
48 Delete {
50 key: String,
52 #[arg(long)]
54 yes: bool,
55 },
56 Rotate(RotateArgs),
61 KeyStatus,
63}
64
65#[derive(Debug, Args)]
67pub struct RotateArgs {
68 #[arg(long, value_parser = value_parser!(i32).range(1..))]
70 pub to_version: Option<i32>,
71
72 #[arg(long, default_value_t = 100, value_parser = value_parser!(i32).range(1..=1000))]
74 pub batch_size: i32,
75}
76
77#[derive(Debug, Serialize)]
79struct RotationSummary {
80 to_version: i32,
82 rotated: u64,
84 failed: u64,
86 batches: u64,
88}
89
90pub async fn execute(client: &IronflowClient, args: &SecretArgs, json_mode: bool) -> Result<()> {
98 match &args.command {
99 SecretCommands::List => {
100 let response = client.list_secrets().await?;
101 output::print_output(json_mode, &response, || {
102 output::secrets_table(&response.data)
103 })?;
104 }
105 SecretCommands::Set { key, value } => {
106 let value = resolve_secret_value(value.as_deref(), "secret value")?;
107 let request: SetSecretRequest = SetSecretRequest::builder()
108 .key(key.clone())
109 .value(value)
110 .try_into()
111 .context("failed to build SetSecretRequest")?;
112
113 let response = client.create_secret(&request).await?;
114 output::print_output(json_mode, &response, || {
115 output::secrets_table(slice::from_ref(&response.data))
116 })?;
117 }
118 SecretCommands::Update { key, value } => {
119 let value = resolve_secret_value(value.as_deref(), "secret value")?;
120 let request: UpdateSecretRequest = UpdateSecretRequest::builder()
121 .value(value)
122 .try_into()
123 .context("failed to build UpdateSecretRequest")?;
124
125 let response = client.update_secret(key, &request).await?;
126 output::print_output(json_mode, &response, || {
127 output::secrets_table(slice::from_ref(&response.data))
128 })?;
129 }
130 SecretCommands::Delete { key, yes } => {
131 confirm(&format!("Delete secret '{key}'?"), *yes)?;
132 client.delete_secret(key).await?;
133 output::report_deletion(json_mode, "secret", key)?;
134 }
135 SecretCommands::Rotate(rotate_args) => rotate(client, rotate_args, json_mode).await?,
136 SecretCommands::KeyStatus => key_status(client, json_mode).await?,
137 }
138 Ok(())
139}
140
141async fn rotate(client: &IronflowClient, args: &RotateArgs, json_mode: bool) -> Result<()> {
143 let mut cursor: Option<Uuid> = None;
144 let mut to_version = args.to_version;
145 let mut rotated = 0u64;
146 let mut failed = 0u64;
147 let mut batches = 0u64;
148
149 loop {
150 let request = RotateSecretsRequest {
151 to_version,
152 batch_size: Some(args.batch_size),
153 after_id: cursor,
154 };
155
156 let response = client.rotate_secrets(&request).await?;
157 let batch = response.data;
158 batches += 1;
159 rotated += batch.rotated as u64;
160 failed += batch.failed as u64;
161
162 to_version = Some(batch.to_version);
166
167 if !json_mode {
168 let done = rotated + failed;
169 println!(
170 "rotating to version {}: {done} done, {} remaining",
171 batch.to_version, batch.remaining
172 );
173 }
174
175 let Some(last_id) = batch.last_id else { break };
177 if batch.remaining == 0 {
178 break;
179 }
180 cursor = Some(last_id);
181 }
182
183 let summary = RotationSummary {
184 to_version: to_version.unwrap_or_default(),
185 rotated,
186 failed,
187 batches,
188 };
189
190 if json_mode {
191 output::print_json(&summary)?;
192 } else {
193 println!(
194 "done: {rotated} secret(s) rotated to version {} in {batches} batch(es)",
195 summary.to_version
196 );
197 }
198
199 if failed > 0 {
200 bail!(
201 "{failed} secret(s) could not be decrypted and were left on their previous key version; check the server logs for the affected keys"
202 );
203 }
204
205 Ok(())
206}
207
208async fn key_status(client: &IronflowClient, json_mode: bool) -> Result<()> {
210 let response = client.secret_key_versions().await?;
211 output::print_output(json_mode, &response, || {
212 output::key_versions_table(&response.data)
213 })?;
214
215 warn_on_missing(&response);
216
217 Ok(())
218}
219
220fn warn_on_missing(response: &ApiResponse<KeyVersionsResponse>) {
222 if response.data.missing.is_empty() {
223 return;
224 }
225
226 let missing = response
227 .data
228 .missing
229 .iter()
230 .map(|v| v.to_string())
231 .collect::<Vec<_>>()
232 .join(", ");
233 eprintln!(
234 "warning: key version(s) {missing} are used by stored secrets but not configured; those secrets cannot be read and the server will refuse to restart"
235 );
236}