Some checks failed
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 3s
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 1m17s
Secrets CLI - Build & Release / 通知 (push) Successful in 6s
Secrets CLI - Build & Release / 发布草稿 Release (push) Has been cancelled
Secrets CLI - Build & Release / Build (aarch64-apple-darwin) (push) Has started running
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
- tracing + tracing-subscriber,全局 --verbose/-v 与 RUST_LOG 控制 - 新增 audit_log 表,add/update/delete 成功后自动写入审计记录 - 新增 src/audit.rs,审计失败仅 warn 不中断主流程 - 更新 README/AGENTS.md,补充 verbose、audit_log 说明 - .vscode/tasks.json 增加 verbose/update/audit 测试任务 Made-with: Cursor
113 lines
2.8 KiB
Rust
113 lines
2.8 KiB
Rust
use anyhow::Result;
|
|
use sqlx::PgPool;
|
|
|
|
use crate::models::Secret;
|
|
|
|
pub async fn run(
|
|
pool: &PgPool,
|
|
namespace: Option<&str>,
|
|
kind: Option<&str>,
|
|
tag: Option<&str>,
|
|
query: Option<&str>,
|
|
show_secrets: bool,
|
|
) -> Result<()> {
|
|
let mut conditions: Vec<String> = Vec::new();
|
|
let mut idx: i32 = 1;
|
|
|
|
if namespace.is_some() {
|
|
conditions.push(format!("namespace = ${}", idx));
|
|
idx += 1;
|
|
}
|
|
if kind.is_some() {
|
|
conditions.push(format!("kind = ${}", idx));
|
|
idx += 1;
|
|
}
|
|
if tag.is_some() {
|
|
conditions.push(format!("tags @> ARRAY[${}]", idx));
|
|
idx += 1;
|
|
}
|
|
if query.is_some() {
|
|
conditions.push(format!(
|
|
"(name ILIKE ${i} OR namespace ILIKE ${i} OR kind ILIKE ${i} OR metadata::text ILIKE ${i} OR EXISTS (SELECT 1 FROM unnest(tags) t WHERE t ILIKE ${i}))",
|
|
i = idx
|
|
));
|
|
}
|
|
|
|
let where_clause = if conditions.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!("WHERE {}", conditions.join(" AND "))
|
|
};
|
|
|
|
let sql = format!(
|
|
"SELECT * FROM secrets {} ORDER BY namespace, kind, name",
|
|
where_clause
|
|
);
|
|
|
|
tracing::debug!(sql, "executing search query");
|
|
|
|
let mut q = sqlx::query_as::<_, Secret>(&sql);
|
|
if let Some(v) = namespace {
|
|
q = q.bind(v);
|
|
}
|
|
if let Some(v) = kind {
|
|
q = q.bind(v);
|
|
}
|
|
if let Some(v) = tag {
|
|
q = q.bind(v);
|
|
}
|
|
if let Some(v) = query {
|
|
q = q.bind(format!("%{}%", v));
|
|
}
|
|
|
|
let rows = q.fetch_all(pool).await?;
|
|
|
|
if rows.is_empty() {
|
|
println!("No records found.");
|
|
return Ok(());
|
|
}
|
|
|
|
for row in &rows {
|
|
println!("[{}/{}] {}", row.namespace, row.kind, row.name,);
|
|
println!(" id: {}", row.id);
|
|
|
|
if !row.tags.is_empty() {
|
|
println!(" tags: [{}]", row.tags.join(", "));
|
|
}
|
|
|
|
if row.metadata.as_object().is_some_and(|m| !m.is_empty()) {
|
|
println!(
|
|
" metadata: {}",
|
|
serde_json::to_string_pretty(&row.metadata)?
|
|
);
|
|
}
|
|
|
|
if show_secrets {
|
|
println!(
|
|
" secrets: {}",
|
|
serde_json::to_string_pretty(&row.encrypted)?
|
|
);
|
|
} else {
|
|
let keys: Vec<String> = row
|
|
.encrypted
|
|
.as_object()
|
|
.map(|m| m.keys().cloned().collect())
|
|
.unwrap_or_default();
|
|
if !keys.is_empty() {
|
|
println!(
|
|
" secrets: [{}] (--show-secrets to reveal)",
|
|
keys.join(", ")
|
|
);
|
|
}
|
|
}
|
|
|
|
println!(
|
|
" created: {}",
|
|
row.created_at.format("%Y-%m-%d %H:%M:%S UTC")
|
|
);
|
|
println!();
|
|
}
|
|
println!("{} record(s) found.", rows.len());
|
|
Ok(())
|
|
}
|