Some checks failed
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 3s
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 2m20s
Secrets CLI - Build & Release / Build (macOS aarch64 + x86_64) (push) Successful in 1m4s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 1m13s
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Secrets CLI - Build & Release / 发布草稿 Release (push) Has been cancelled
- run 新增 -s/--secret 字段过滤,只注入指定字段到子进程(最小权限) - run 新增 --dry-run 模式,输出变量名与来源映射,不执行命令、不暴露值 - run 新增 -o 参数,dry-run 默认 JSON 输出 - 默认输出格式改为始终 json,移除 TTY 自动切换逻辑,-o text 供人类使用 - build_injected_env_map 签名从 &[SecretField] 改为 &[&SecretField] - 更新 AGENTS.md、README.md、.vscode/tasks.json - version: 0.9.5 → 0.9.6 Made-with: Cursor
854 lines
29 KiB
Rust
854 lines
29 KiB
Rust
mod audit;
|
|
mod commands;
|
|
mod config;
|
|
mod crypto;
|
|
mod db;
|
|
mod models;
|
|
mod output;
|
|
|
|
use anyhow::Result;
|
|
|
|
/// Load .env from current or parent directories (best-effort, no error if missing).
|
|
fn load_dotenv() {
|
|
let _ = dotenvy::dotenv();
|
|
}
|
|
use clap::{Parser, Subcommand};
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
use output::resolve_output_mode;
|
|
|
|
#[derive(Parser)]
|
|
#[command(
|
|
name = "secrets",
|
|
version,
|
|
about = "Secrets & config manager backed by PostgreSQL — optimised for AI agents",
|
|
after_help = "QUICK START:
|
|
# 1. Configure database (once per device)
|
|
secrets config set-db \"postgres://postgres:<password>@<host>:<port>/secrets\"
|
|
|
|
# 2. Initialize master key (once per device)
|
|
secrets init
|
|
|
|
# Discover what namespaces / kinds exist
|
|
secrets search --summary --limit 20
|
|
|
|
# Precise lookup (JSON output for easy parsing)
|
|
secrets search -n refining --kind service --name gitea -o json
|
|
|
|
# Extract a single metadata field directly
|
|
secrets search -n refining --kind service --name gitea -f metadata.url
|
|
|
|
# Pipe-friendly (non-TTY defaults to json-compact automatically)
|
|
secrets search -n refining --kind service | jq '.[].name'
|
|
|
|
# Run a command with secrets injected into its child process environment
|
|
secrets run -n refining --kind service --name gitea -- printenv"
|
|
)]
|
|
struct Cli {
|
|
/// Database URL, overrides saved config (one-time override)
|
|
#[arg(long, global = true, default_value = "")]
|
|
db_url: String,
|
|
|
|
/// Enable verbose debug output
|
|
#[arg(long, short, global = true)]
|
|
verbose: bool,
|
|
|
|
#[command(subcommand)]
|
|
command: Commands,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Commands {
|
|
/// Initialize master key on this device (run once per device).
|
|
///
|
|
/// Prompts for a master password, derives a key with Argon2id, and stores
|
|
/// it in the OS Keychain. Use the same password on every device.
|
|
///
|
|
/// NOTE: Run `secrets config set-db <URL>` first if database is not configured.
|
|
#[command(after_help = "PREREQUISITE:
|
|
Database must be configured first. Run: secrets config set-db <DATABASE_URL>
|
|
|
|
EXAMPLES:
|
|
# First device: generates a new Argon2id salt and stores master key
|
|
secrets init
|
|
|
|
# Subsequent devices: reuses existing salt from the database
|
|
secrets init")]
|
|
Init,
|
|
|
|
/// Add or update a record (upsert). Use -m for plaintext metadata, -s for secrets.
|
|
#[command(after_help = "EXAMPLES:
|
|
# Add a server
|
|
secrets add -n refining --kind server --name my-server \\
|
|
--tag aliyun --tag shanghai \\
|
|
-m ip=10.0.0.1 -m desc=\"Example ECS\" \\
|
|
-s username=root -s ssh_key=@./keys/server.pem
|
|
|
|
# Add a service credential
|
|
secrets add -n refining --kind service --name gitea \\
|
|
--tag gitea \\
|
|
-m url=https://code.example.com -m default_org=myorg \\
|
|
-s token=<token>
|
|
|
|
# Add typed JSON metadata
|
|
secrets add -n refining --kind service --name gitea \\
|
|
-m port:=3000 \\
|
|
-m enabled:=true \\
|
|
-m domains:='[\"code.example.com\",\"git.example.com\"]' \\
|
|
-m tls:='{\"enabled\":true,\"redirect_http\":true}'
|
|
|
|
# Add with token read from a file
|
|
secrets add -n ricnsmart --kind service --name mqtt \\
|
|
-m host=mqtt.example.com -m port=1883 \\
|
|
-s password=@./mqtt_password.txt
|
|
|
|
# Add typed JSON secrets
|
|
secrets add -n refining --kind service --name deploy-bot \\
|
|
-s enabled:=true \\
|
|
-s retry_count:=3 \\
|
|
-s scopes:='[\"repo\",\"workflow\"]' \\
|
|
-s extra:='{\"region\":\"ap-east-1\",\"verify_tls\":true}'
|
|
|
|
# Write a multiline file into a nested secret field
|
|
secrets add -n refining --kind server --name my-server \\
|
|
-s credentials:content@./keys/server.pem
|
|
|
|
# Shared PEM (key_ref): store key once, reference from multiple servers
|
|
secrets add -n refining --kind key --name my-shared-key \\
|
|
--tag aliyun -s content=@./keys/shared.pem
|
|
secrets add -n refining --kind server --name i-abc123 \\
|
|
-m ip=10.0.0.1 -m key_ref=my-shared-key -s username=ecs-user")]
|
|
Add {
|
|
/// Namespace, e.g. refining, ricnsmart
|
|
#[arg(short, long)]
|
|
namespace: String,
|
|
/// Kind of record: server, service, key, ...
|
|
#[arg(long)]
|
|
kind: String,
|
|
/// Human-readable unique name, e.g. gitea, i-example0abcd1234efgh
|
|
#[arg(long)]
|
|
name: String,
|
|
/// Tag for categorization (repeatable), e.g. --tag aliyun --tag hongkong
|
|
#[arg(long = "tag")]
|
|
tags: Vec<String>,
|
|
/// Plaintext metadata: key=value, key:=<json>, key=@file, or nested:path@file.
|
|
/// Use key_ref=<name> to reference a shared key entry (kind=key); run merges its secrets.
|
|
#[arg(long = "meta", short = 'm')]
|
|
meta: Vec<String>,
|
|
/// Secret entry: key=value, key:=<json>, key=@file, or nested:path@file
|
|
#[arg(long = "secret", short = 's')]
|
|
secrets: Vec<String>,
|
|
/// Output format: text (default on TTY), json, json-compact
|
|
#[arg(short, long = "output")]
|
|
output: Option<String>,
|
|
},
|
|
|
|
/// Search / read records. This is the primary read command for AI agents.
|
|
///
|
|
/// Supports fuzzy search (-q), exact lookup (--name), field extraction (-f),
|
|
/// summary view (--summary), pagination (--limit / --offset), and structured
|
|
/// output (-o json / json-compact). When stdout is not a TTY, output
|
|
/// defaults to json-compact automatically.
|
|
#[command(after_help = "EXAMPLES:
|
|
# Discover all records (summary, safe default limit)
|
|
secrets search --summary --limit 20
|
|
|
|
# Filter by namespace and kind
|
|
secrets search -n refining --kind service
|
|
|
|
# Exact lookup — returns 0 or 1 record
|
|
secrets search -n refining --kind service --name gitea
|
|
|
|
# Fuzzy keyword search (matches name, namespace, kind, tags, metadata)
|
|
secrets search -q mqtt
|
|
|
|
# Extract a single metadata field value
|
|
secrets search -n refining --kind service --name gitea -f metadata.url
|
|
|
|
# Multiple fields at once
|
|
secrets search -n refining --kind service --name gitea \\
|
|
-f metadata.url -f metadata.default_org
|
|
|
|
# Run a command with decrypted secrets only when needed
|
|
secrets run -n refining --kind service --name gitea -- printenv
|
|
|
|
# Paginate large result sets
|
|
secrets search -n refining --summary --limit 10 --offset 0
|
|
secrets search -n refining --summary --limit 10 --offset 10
|
|
|
|
# Sort by most recently updated
|
|
secrets search --sort updated --limit 5 --summary
|
|
|
|
# Non-TTY / pipe: output is json-compact by default
|
|
secrets search -n refining --kind service | jq '.[].name'")]
|
|
Search {
|
|
/// Filter by namespace, e.g. refining, ricnsmart
|
|
#[arg(short, long)]
|
|
namespace: Option<String>,
|
|
/// Filter by kind, e.g. server, service
|
|
#[arg(long)]
|
|
kind: Option<String>,
|
|
/// Exact name filter, e.g. gitea, i-example0abcd1234efgh
|
|
#[arg(long)]
|
|
name: Option<String>,
|
|
/// Filter by tag, e.g. --tag aliyun (repeatable for AND intersection)
|
|
#[arg(long)]
|
|
tag: Vec<String>,
|
|
/// Fuzzy keyword (matches name, namespace, kind, tags, metadata text)
|
|
#[arg(short, long)]
|
|
query: Option<String>,
|
|
/// Extract metadata field value(s) directly: metadata.<key> (repeatable)
|
|
#[arg(short = 'f', long = "field")]
|
|
fields: Vec<String>,
|
|
/// Return lightweight summary only (namespace, kind, name, tags, desc, updated_at)
|
|
#[arg(long)]
|
|
summary: bool,
|
|
/// Maximum number of records to return [default: 50]
|
|
#[arg(long, default_value = "50")]
|
|
limit: u32,
|
|
/// Skip this many records (for pagination)
|
|
#[arg(long, default_value = "0")]
|
|
offset: u32,
|
|
/// Sort order: name (default), updated, created
|
|
#[arg(long, default_value = "name")]
|
|
sort: String,
|
|
/// Output format: text (default on TTY), json, json-compact
|
|
#[arg(short, long = "output")]
|
|
output: Option<String>,
|
|
},
|
|
|
|
/// Delete one record precisely, or bulk-delete by namespace.
|
|
///
|
|
/// With --name: deletes exactly that record (--kind also required).
|
|
/// Without --name: bulk-deletes all records matching namespace + optional --kind.
|
|
/// Use --dry-run to preview bulk deletes before committing.
|
|
#[command(after_help = "EXAMPLES:
|
|
# Delete a single record (exact match)
|
|
secrets delete -n refining --kind service --name legacy-mqtt
|
|
|
|
# Preview what a bulk delete would remove (no writes)
|
|
secrets delete -n refining --dry-run
|
|
|
|
# Bulk-delete all records in a namespace
|
|
secrets delete -n ricnsmart
|
|
|
|
# Bulk-delete only server records in a namespace
|
|
secrets delete -n ricnsmart --kind server
|
|
|
|
# JSON output
|
|
secrets delete -n refining --kind service -o json")]
|
|
Delete {
|
|
/// Namespace, e.g. refining
|
|
#[arg(short, long)]
|
|
namespace: String,
|
|
/// Kind filter, e.g. server, service (required with --name; optional for bulk)
|
|
#[arg(long)]
|
|
kind: Option<String>,
|
|
/// Exact name of the record to delete (omit for bulk delete)
|
|
#[arg(long)]
|
|
name: Option<String>,
|
|
/// Preview what would be deleted without making any changes (bulk mode only)
|
|
#[arg(long)]
|
|
dry_run: bool,
|
|
/// Output format: text (default on TTY), json, json-compact
|
|
#[arg(short, long = "output")]
|
|
output: Option<String>,
|
|
},
|
|
|
|
/// Incrementally update an existing record (merge semantics; record must exist).
|
|
///
|
|
/// Only the fields you pass are changed — everything else is preserved.
|
|
/// Use --add-tag / --remove-tag to modify tags without touching other fields.
|
|
#[command(after_help = "EXAMPLES:
|
|
# Update a single metadata field (all other fields unchanged)
|
|
secrets update -n refining --kind server --name my-server -m ip=10.0.0.1
|
|
|
|
# Rotate a secret token
|
|
secrets update -n refining --kind service --name gitea -s token=<new-token>
|
|
|
|
# Update typed JSON metadata
|
|
secrets update -n refining --kind service --name gitea \\
|
|
-m deploy:strategy:='{\"type\":\"rolling\",\"batch\":2}' \\
|
|
-m runtime:max_open_conns:=20
|
|
|
|
# Add a tag and rotate password at the same time
|
|
secrets update -n refining --kind service --name gitea \\
|
|
--add-tag production -s token=<new-token>
|
|
|
|
# Remove a deprecated metadata field and a stale secret key
|
|
secrets update -n refining --kind service --name mqtt \\
|
|
--remove-meta old_port --remove-secret old_password
|
|
|
|
# Remove a nested field
|
|
secrets update -n refining --kind server --name my-server \\
|
|
--remove-secret credentials:content
|
|
|
|
# Remove a tag
|
|
secrets update -n refining --kind service --name gitea --remove-tag staging
|
|
|
|
# Update a nested secret field from a file
|
|
secrets update -n refining --kind server --name my-server \\
|
|
-s credentials:content@./keys/server.pem
|
|
|
|
# Update nested typed JSON fields
|
|
secrets update -n refining --kind service --name deploy-bot \\
|
|
-s auth:config:='{\"issuer\":\"gitea\",\"rotate\":true}' \\
|
|
-s auth:retry:=5
|
|
|
|
# Rotate shared PEM (all servers with key_ref=my-shared-key get the new key)
|
|
secrets update -n refining --kind key --name my-shared-key \\
|
|
-s content=@./keys/new-shared.pem")]
|
|
Update {
|
|
/// Namespace, e.g. refining, ricnsmart
|
|
#[arg(short, long)]
|
|
namespace: String,
|
|
/// Kind of record: server, service, key, ...
|
|
#[arg(long)]
|
|
kind: String,
|
|
/// Human-readable unique name
|
|
#[arg(long)]
|
|
name: String,
|
|
/// Add a tag (repeatable; does not affect existing tags)
|
|
#[arg(long = "add-tag")]
|
|
add_tags: Vec<String>,
|
|
/// Remove a tag (repeatable)
|
|
#[arg(long = "remove-tag")]
|
|
remove_tags: Vec<String>,
|
|
/// Set or overwrite a metadata field: key=value, key:=<json>, key=@file, or nested:path@file.
|
|
/// Use key_ref=<name> to reference a shared key entry (kind=key).
|
|
#[arg(long = "meta", short = 'm')]
|
|
meta: Vec<String>,
|
|
/// Delete a metadata field by key or nested path, e.g. old_port or credentials:content
|
|
#[arg(long = "remove-meta")]
|
|
remove_meta: Vec<String>,
|
|
/// Set or overwrite a secret field: key=value, key:=<json>, key=@file, or nested:path@file
|
|
#[arg(long = "secret", short = 's')]
|
|
secrets: Vec<String>,
|
|
/// Delete a secret field by key or nested path, e.g. old_password or credentials:content
|
|
#[arg(long = "remove-secret")]
|
|
remove_secrets: Vec<String>,
|
|
/// Output format: text (default on TTY), json, json-compact
|
|
#[arg(short, long = "output")]
|
|
output: Option<String>,
|
|
},
|
|
|
|
/// Manage CLI configuration (database connection, etc.)
|
|
#[command(after_help = "EXAMPLES:
|
|
# Configure the database URL (run once per device; persisted to config file)
|
|
secrets config set-db \"postgres://postgres:<password>@<host>:<port>/secrets\"
|
|
|
|
# Show current config (password is masked)
|
|
secrets config show
|
|
|
|
# Print path to the config file
|
|
secrets config path")]
|
|
Config {
|
|
#[command(subcommand)]
|
|
action: ConfigAction,
|
|
},
|
|
|
|
/// Show the change history for a record.
|
|
#[command(after_help = "EXAMPLES:
|
|
# Show last 20 versions for a service record
|
|
secrets history -n refining --kind service --name gitea
|
|
|
|
# Show last 5 versions
|
|
secrets history -n refining --kind service --name gitea --limit 5")]
|
|
History {
|
|
#[arg(short, long)]
|
|
namespace: String,
|
|
#[arg(long)]
|
|
kind: String,
|
|
#[arg(long)]
|
|
name: String,
|
|
/// Number of history entries to show [default: 20]
|
|
#[arg(long, default_value = "20")]
|
|
limit: u32,
|
|
/// Output format: text (default on TTY), json, json-compact
|
|
#[arg(short, long = "output")]
|
|
output: Option<String>,
|
|
},
|
|
|
|
/// Roll back a record to a previous version.
|
|
#[command(after_help = "EXAMPLES:
|
|
# Roll back to the most recent snapshot (undo last change)
|
|
secrets rollback -n refining --kind service --name gitea
|
|
|
|
# Roll back to a specific version number
|
|
secrets rollback -n refining --kind service --name gitea --to-version 3")]
|
|
Rollback {
|
|
#[arg(short, long)]
|
|
namespace: String,
|
|
#[arg(long)]
|
|
kind: String,
|
|
#[arg(long)]
|
|
name: String,
|
|
/// Target version to restore. Omit to restore the most recent snapshot.
|
|
#[arg(long)]
|
|
to_version: Option<i64>,
|
|
/// Output format: text (default on TTY), json, json-compact
|
|
#[arg(short, long = "output")]
|
|
output: Option<String>,
|
|
},
|
|
|
|
/// Run a command with secrets injected as environment variables.
|
|
///
|
|
/// Secrets are available only to the child process; the current shell
|
|
/// environment is not modified. The process exit code is propagated.
|
|
///
|
|
/// Use -s/--secret to inject only specific fields. Use --dry-run to preview
|
|
/// which variables would be injected without executing the command.
|
|
#[command(after_help = "EXAMPLES:
|
|
# Run a script with a single service's secrets injected
|
|
secrets run -n refining --kind service --name gitea -- ./deploy.sh
|
|
|
|
# Inject only specific fields (minimal exposure)
|
|
secrets run -n refining --kind service --name aliyun \\
|
|
-s access_key_id -s access_key_secret -- aliyun ecs DescribeInstances
|
|
|
|
# Run with a tag filter (all matched records merged)
|
|
secrets run --tag production -- env | grep GITEA
|
|
|
|
# With prefix
|
|
secrets run -n refining --kind service --name gitea --prefix GITEA -- printenv
|
|
|
|
# Preview which variables would be injected (no command executed)
|
|
secrets run -n refining --kind service --name gitea --dry-run
|
|
|
|
# Preview with field filter and JSON output
|
|
secrets run -n refining --kind service --name gitea -s token --dry-run -o json
|
|
|
|
# metadata.key_ref entries get key secrets merged (e.g. server + shared PEM)")]
|
|
Run {
|
|
#[arg(short, long)]
|
|
namespace: Option<String>,
|
|
#[arg(long)]
|
|
kind: Option<String>,
|
|
#[arg(long)]
|
|
name: Option<String>,
|
|
#[arg(long)]
|
|
tag: Vec<String>,
|
|
/// Only inject these secret field names (repeatable). Omit to inject all fields.
|
|
#[arg(long = "secret", short = 's')]
|
|
secret_fields: Vec<String>,
|
|
/// Prefix to prepend to every variable name (uppercased automatically)
|
|
#[arg(long, default_value = "")]
|
|
prefix: String,
|
|
/// Preview variables that would be injected without executing the command
|
|
#[arg(long)]
|
|
dry_run: bool,
|
|
/// Output format for --dry-run: json (default), json-compact, text
|
|
#[arg(short, long = "output")]
|
|
output: Option<String>,
|
|
/// Command and arguments to execute with injected environment
|
|
#[arg(last = true)]
|
|
command: Vec<String>,
|
|
},
|
|
|
|
/// Check for a newer version and update the binary in-place.
|
|
///
|
|
/// Downloads the latest release and replaces the current binary. No database connection or master key required.
|
|
/// Release URL defaults to the upstream server; override via SECRETS_UPGRADE_URL for self-hosted or fork.
|
|
#[command(after_help = "EXAMPLES:
|
|
# Check for updates only (no download)
|
|
secrets upgrade --check
|
|
|
|
# Download and install the latest version
|
|
secrets upgrade")]
|
|
Upgrade {
|
|
/// Only check if a newer version is available; do not download
|
|
#[arg(long)]
|
|
check: bool,
|
|
},
|
|
|
|
/// Export records to a file (JSON, TOML, or YAML).
|
|
///
|
|
/// Decrypts and exports all matched records. Requires master key unless --no-secrets is used.
|
|
#[command(after_help = "EXAMPLES:
|
|
# Export everything to JSON
|
|
secrets export --file backup.json
|
|
|
|
# Export a specific namespace to TOML
|
|
secrets export -n refining --file refining.toml
|
|
|
|
# Export a specific kind
|
|
secrets export -n refining --kind service --file services.yaml
|
|
|
|
# Export by tag
|
|
secrets export --tag production --file prod.json
|
|
|
|
# Export schema only (no decryption needed)
|
|
secrets export --no-secrets --file schema.json
|
|
|
|
# Print to stdout in YAML
|
|
secrets export -n refining --format yaml")]
|
|
Export {
|
|
/// Filter by namespace
|
|
#[arg(short, long)]
|
|
namespace: Option<String>,
|
|
/// Filter by kind, e.g. server, service
|
|
#[arg(long)]
|
|
kind: Option<String>,
|
|
/// Exact name filter
|
|
#[arg(long)]
|
|
name: Option<String>,
|
|
/// Filter by tag (repeatable)
|
|
#[arg(long)]
|
|
tag: Vec<String>,
|
|
/// Fuzzy keyword search
|
|
#[arg(short, long)]
|
|
query: Option<String>,
|
|
/// Output file path (format inferred from extension: .json / .toml / .yaml / .yml)
|
|
#[arg(long)]
|
|
file: Option<String>,
|
|
/// Explicit format: json, toml, or yaml (overrides file extension; required for stdout)
|
|
#[arg(long)]
|
|
format: Option<String>,
|
|
/// Omit secrets from output (no master key required)
|
|
#[arg(long)]
|
|
no_secrets: bool,
|
|
},
|
|
|
|
/// Import records from a file (JSON, TOML, or YAML).
|
|
///
|
|
/// Reads an export file and inserts or updates entries. Requires master key to re-encrypt secrets.
|
|
#[command(after_help = "EXAMPLES:
|
|
# Import a JSON backup (conflict = error by default)
|
|
secrets import backup.json
|
|
|
|
# Import and overwrite existing records
|
|
secrets import --force refining.toml
|
|
|
|
# Preview what would be imported (no writes)
|
|
secrets import --dry-run backup.yaml
|
|
|
|
# JSON output for the import summary
|
|
secrets import backup.json -o json")]
|
|
Import {
|
|
/// Input file path (format inferred from extension: .json / .toml / .yaml / .yml)
|
|
file: String,
|
|
/// Overwrite existing records on conflict (default: error and abort)
|
|
#[arg(long)]
|
|
force: bool,
|
|
/// Preview operations without writing to the database
|
|
#[arg(long)]
|
|
dry_run: bool,
|
|
/// Output format: text (default on TTY), json, json-compact
|
|
#[arg(short, long = "output")]
|
|
output: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum ConfigAction {
|
|
/// Save database URL to config file (~/.config/secrets/config.toml)
|
|
SetDb {
|
|
/// PostgreSQL connection string, e.g. postgres://user:pass@<host>:<port>/dbname
|
|
url: String,
|
|
},
|
|
/// Show current configuration (password masked)
|
|
Show,
|
|
/// Print path to the config file
|
|
Path,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
load_dotenv();
|
|
let cli = Cli::parse();
|
|
|
|
let filter = if cli.verbose {
|
|
EnvFilter::new("secrets=debug")
|
|
} else {
|
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("secrets=warn"))
|
|
};
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(filter)
|
|
.with_target(false)
|
|
.init();
|
|
|
|
// config subcommand needs no database or master key
|
|
if let Commands::Config { action } = cli.command {
|
|
return commands::config::run(action).await;
|
|
}
|
|
|
|
// upgrade needs no database or master key either
|
|
if let Commands::Upgrade { check } = cli.command {
|
|
return commands::upgrade::run(check).await;
|
|
}
|
|
|
|
let db_url = config::resolve_db_url(&cli.db_url)?;
|
|
let pool = db::create_pool(&db_url).await?;
|
|
db::migrate(&pool).await?;
|
|
|
|
// init needs a pool but sets up the master key — handle before loading it
|
|
if let Commands::Init = cli.command {
|
|
return commands::init::run(&pool).await;
|
|
}
|
|
|
|
// All remaining commands require the master key from the OS Keychain,
|
|
// except delete which operates on plaintext metadata only.
|
|
|
|
match cli.command {
|
|
Commands::Init | Commands::Config { .. } | Commands::Upgrade { .. } => unreachable!(),
|
|
|
|
Commands::Add {
|
|
namespace,
|
|
kind,
|
|
name,
|
|
tags,
|
|
meta,
|
|
secrets,
|
|
output,
|
|
} => {
|
|
let master_key = crypto::load_master_key()?;
|
|
let _span =
|
|
tracing::info_span!("cmd", command = "add", %namespace, %kind, %name).entered();
|
|
let out = resolve_output_mode(output.as_deref())?;
|
|
commands::add::run(
|
|
&pool,
|
|
commands::add::AddArgs {
|
|
namespace: &namespace,
|
|
kind: &kind,
|
|
name: &name,
|
|
tags: &tags,
|
|
meta_entries: &meta,
|
|
secret_entries: &secrets,
|
|
output: out,
|
|
},
|
|
&master_key,
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Commands::Search {
|
|
namespace,
|
|
kind,
|
|
name,
|
|
tag,
|
|
query,
|
|
fields,
|
|
summary,
|
|
limit,
|
|
offset,
|
|
sort,
|
|
output,
|
|
} => {
|
|
let _span = tracing::info_span!("cmd", command = "search").entered();
|
|
let out = resolve_output_mode(output.as_deref())?;
|
|
commands::search::run(
|
|
&pool,
|
|
commands::search::SearchArgs {
|
|
namespace: namespace.as_deref(),
|
|
kind: kind.as_deref(),
|
|
name: name.as_deref(),
|
|
tags: &tag,
|
|
query: query.as_deref(),
|
|
fields: &fields,
|
|
summary,
|
|
limit,
|
|
offset,
|
|
sort: &sort,
|
|
output: out,
|
|
},
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Commands::Delete {
|
|
namespace,
|
|
kind,
|
|
name,
|
|
dry_run,
|
|
output,
|
|
} => {
|
|
let _span =
|
|
tracing::info_span!("cmd", command = "delete", %namespace, ?kind, ?name).entered();
|
|
let out = resolve_output_mode(output.as_deref())?;
|
|
commands::delete::run(
|
|
&pool,
|
|
commands::delete::DeleteArgs {
|
|
namespace: &namespace,
|
|
kind: kind.as_deref(),
|
|
name: name.as_deref(),
|
|
dry_run,
|
|
output: out,
|
|
},
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Commands::Update {
|
|
namespace,
|
|
kind,
|
|
name,
|
|
add_tags,
|
|
remove_tags,
|
|
meta,
|
|
remove_meta,
|
|
secrets,
|
|
remove_secrets,
|
|
output,
|
|
} => {
|
|
let master_key = crypto::load_master_key()?;
|
|
let _span =
|
|
tracing::info_span!("cmd", command = "update", %namespace, %kind, %name).entered();
|
|
let out = resolve_output_mode(output.as_deref())?;
|
|
commands::update::run(
|
|
&pool,
|
|
commands::update::UpdateArgs {
|
|
namespace: &namespace,
|
|
kind: &kind,
|
|
name: &name,
|
|
add_tags: &add_tags,
|
|
remove_tags: &remove_tags,
|
|
meta_entries: &meta,
|
|
remove_meta: &remove_meta,
|
|
secret_entries: &secrets,
|
|
remove_secrets: &remove_secrets,
|
|
output: out,
|
|
},
|
|
&master_key,
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Commands::History {
|
|
namespace,
|
|
kind,
|
|
name,
|
|
limit,
|
|
output,
|
|
} => {
|
|
let out = resolve_output_mode(output.as_deref())?;
|
|
commands::history::run(
|
|
&pool,
|
|
commands::history::HistoryArgs {
|
|
namespace: &namespace,
|
|
kind: &kind,
|
|
name: &name,
|
|
limit,
|
|
output: out,
|
|
},
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Commands::Rollback {
|
|
namespace,
|
|
kind,
|
|
name,
|
|
to_version,
|
|
output,
|
|
} => {
|
|
let master_key = crypto::load_master_key()?;
|
|
let out = resolve_output_mode(output.as_deref())?;
|
|
commands::rollback::run(
|
|
&pool,
|
|
commands::rollback::RollbackArgs {
|
|
namespace: &namespace,
|
|
kind: &kind,
|
|
name: &name,
|
|
to_version,
|
|
output: out,
|
|
},
|
|
&master_key,
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Commands::Run {
|
|
namespace,
|
|
kind,
|
|
name,
|
|
tag,
|
|
secret_fields,
|
|
prefix,
|
|
dry_run,
|
|
output,
|
|
command,
|
|
} => {
|
|
let master_key = crypto::load_master_key()?;
|
|
let out = resolve_output_mode(output.as_deref())?;
|
|
if !dry_run && command.is_empty() {
|
|
anyhow::bail!(
|
|
"No command specified. Usage: secrets run [filter flags] -- <command> [args]"
|
|
);
|
|
}
|
|
commands::run::run_exec(
|
|
&pool,
|
|
commands::run::RunArgs {
|
|
namespace: namespace.as_deref(),
|
|
kind: kind.as_deref(),
|
|
name: name.as_deref(),
|
|
tags: &tag,
|
|
secret_fields: &secret_fields,
|
|
prefix: &prefix,
|
|
dry_run,
|
|
output: out,
|
|
command: &command,
|
|
},
|
|
&master_key,
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Commands::Export {
|
|
namespace,
|
|
kind,
|
|
name,
|
|
tag,
|
|
query,
|
|
file,
|
|
format,
|
|
no_secrets,
|
|
} => {
|
|
let master_key = if no_secrets {
|
|
None
|
|
} else {
|
|
Some(crypto::load_master_key()?)
|
|
};
|
|
let _span = tracing::info_span!("cmd", command = "export").entered();
|
|
commands::export_cmd::run(
|
|
&pool,
|
|
commands::export_cmd::ExportArgs {
|
|
namespace: namespace.as_deref(),
|
|
kind: kind.as_deref(),
|
|
name: name.as_deref(),
|
|
tags: &tag,
|
|
query: query.as_deref(),
|
|
file: file.as_deref(),
|
|
format: format.as_deref(),
|
|
no_secrets,
|
|
},
|
|
master_key.as_ref(),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Commands::Import {
|
|
file,
|
|
force,
|
|
dry_run,
|
|
output,
|
|
} => {
|
|
let master_key = crypto::load_master_key()?;
|
|
let _span = tracing::info_span!("cmd", command = "import").entered();
|
|
let out = resolve_output_mode(output.as_deref())?;
|
|
commands::import_cmd::run(
|
|
&pool,
|
|
commands::import_cmd::ImportArgs {
|
|
file: &file,
|
|
force,
|
|
dry_run,
|
|
output: out,
|
|
},
|
|
&master_key,
|
|
)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|