feat(run): 选择性字段注入、dry-run 预览、默认 JSON 输出
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
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
This commit is contained in:
8
.vscode/tasks.json
vendored
8
.vscode/tasks.json
vendored
@@ -104,9 +104,9 @@
|
|||||||
"dependsOn": "build"
|
"dependsOn": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "test: inject service secrets",
|
"label": "test: run service secrets",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./target/debug/secrets inject -n refining --kind service --name gitea",
|
"command": "./target/debug/secrets run -n refining --kind service --name gitea -- printenv",
|
||||||
"dependsOn": "build"
|
"dependsOn": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
{
|
{
|
||||||
"label": "test: add + delete roundtrip",
|
"label": "test: add + delete roundtrip",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "echo '--- add ---' && ./target/debug/secrets add -n test --kind demo --name roundtrip-test --tag test -m foo=bar -s password=secret123 && echo '--- search metadata ---' && ./target/debug/secrets search -n test && echo '--- inject secrets ---' && ./target/debug/secrets inject -n test --kind demo --name roundtrip-test && echo '--- delete ---' && ./target/debug/secrets delete -n test --kind demo --name roundtrip-test && echo '--- verify deleted ---' && ./target/debug/secrets search -n test",
|
"command": "echo '--- add ---' && ./target/debug/secrets add -n test --kind demo --name roundtrip-test --tag test -m foo=bar -s password=secret123 && echo '--- search metadata ---' && ./target/debug/secrets search -n test && echo '--- run secrets ---' && ./target/debug/secrets run -n test --kind demo --name roundtrip-test -- printenv && echo '--- delete ---' && ./target/debug/secrets delete -n test --kind demo --name roundtrip-test && echo '--- verify deleted ---' && ./target/debug/secrets search -n test",
|
||||||
"dependsOn": "build"
|
"dependsOn": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
{
|
{
|
||||||
"label": "test: add with file secret",
|
"label": "test: add with file secret",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "echo '--- add key from file ---' && ./target/debug/secrets add -n test --kind key --name test-key --tag test -s content=@./test-fixtures/example-key.pem && echo '--- verify metadata ---' && ./target/debug/secrets search -n test --kind key && echo '--- verify inject ---' && ./target/debug/secrets inject -n test --kind key --name test-key && echo '--- cleanup ---' && ./target/debug/secrets delete -n test --kind key --name test-key",
|
"command": "echo '--- add key from file ---' && ./target/debug/secrets add -n test --kind key --name test-key --tag test -s content=@./test-fixtures/example-key.pem && echo '--- verify metadata ---' && ./target/debug/secrets search -n test --kind key && echo '--- verify run ---' && ./target/debug/secrets run -n test --kind key --name test-key -- printenv && echo '--- cleanup ---' && ./target/debug/secrets delete -n test --kind key --name test-key",
|
||||||
"dependsOn": "build"
|
"dependsOn": "build"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
70
AGENTS.md
70
AGENTS.md
@@ -15,7 +15,7 @@
|
|||||||
secrets/
|
secrets/
|
||||||
src/
|
src/
|
||||||
main.rs # CLI 入口,clap 命令定义,auto-migrate,--verbose 全局参数
|
main.rs # CLI 入口,clap 命令定义,auto-migrate,--verbose 全局参数
|
||||||
output.rs # OutputMode 枚举 + TTY 检测(TTY→text,非 TTY→json-compact)
|
output.rs # OutputMode 枚举(默认 json,-o text 供人类使用)
|
||||||
config.rs # 配置读写:~/.config/secrets/config.toml(database_url)
|
config.rs # 配置读写:~/.config/secrets/config.toml(database_url)
|
||||||
db.rs # PgPool 创建 + 建表/索引(DROP+CREATE,含所有表)
|
db.rs # PgPool 创建 + 建表/索引(DROP+CREATE,含所有表)
|
||||||
crypto.rs # AES-256-GCM 加解密、Argon2id 派生、OS 钥匙串
|
crypto.rs # AES-256-GCM 加解密、Argon2id 派生、OS 钥匙串
|
||||||
@@ -30,7 +30,7 @@ secrets/
|
|||||||
update.rs # update 命令:增量更新,secrets 行级 UPSERT/DELETE,CAS 并发保护
|
update.rs # update 命令:增量更新,secrets 行级 UPSERT/DELETE,CAS 并发保护
|
||||||
rollback.rs # rollback 命令:按 entry_version 恢复 entry + secrets
|
rollback.rs # rollback 命令:按 entry_version 恢复 entry + secrets
|
||||||
history.rs # history 命令:查看 entry 变更历史列表
|
history.rs # history 命令:查看 entry 变更历史列表
|
||||||
run.rs # inject / run 命令:仅 secrets 逐字段解密 + key_ref 引用解析(不含 metadata)
|
run.rs # run 命令:仅 secrets 逐字段解密 + key_ref 引用解析(不含 metadata)
|
||||||
upgrade.rs # upgrade 命令:检查、校验摘要并下载最新版本,自动替换二进制
|
upgrade.rs # upgrade 命令:检查、校验摘要并下载最新版本,自动替换二进制
|
||||||
export_cmd.rs # export 命令:批量导出记录,支持 JSON/TOML/YAML,含解密明文
|
export_cmd.rs # export 命令:批量导出记录,支持 JSON/TOML/YAML,含解密明文
|
||||||
import_cmd.rs # import 命令:批量导入记录,冲突检测,dry-run,重新加密写入
|
import_cmd.rs # import 命令:批量导入记录,冲突检测,dry-run,重新加密写入
|
||||||
@@ -157,7 +157,7 @@ secrets add -n refining --kind key --name my-shared-key \
|
|||||||
--tag aliyun --tag hongkong \
|
--tag aliyun --tag hongkong \
|
||||||
-s content=@./keys/my-shared-key.pem
|
-s content=@./keys/my-shared-key.pem
|
||||||
|
|
||||||
# 2. 服务器通过 metadata.key_ref 引用(inject/run 时自动合并 key 的 secrets)
|
# 2. 服务器通过 metadata.key_ref 引用(run 时自动合并 key 的 secrets)
|
||||||
secrets add -n refining --kind server --name i-example0xyz789 \
|
secrets add -n refining --kind server --name i-example0xyz789 \
|
||||||
-m ip=192.0.2.1 -m key_ref=my-shared-key \
|
-m ip=192.0.2.1 -m key_ref=my-shared-key \
|
||||||
-s username=ecs-user
|
-s username=ecs-user
|
||||||
@@ -203,9 +203,9 @@ secrets init # 提示输入主密码,Argon2id 派生主密钥后存入 OS
|
|||||||
**读取一律用 `search`,写入用 `add` / `update`,避免反复查帮助。**
|
**读取一律用 `search`,写入用 `add` / `update`,避免反复查帮助。**
|
||||||
|
|
||||||
输出格式规则:
|
输出格式规则:
|
||||||
- TTY(终端直接运行)→ 默认 `text`
|
- 默认始终输出 `json`(pretty-printed),无论 TTY 还是管道
|
||||||
- 非 TTY(管道/重定向/AI 调用)→ 自动 `json-compact`
|
- 显式 `-o json-compact` → 单行 JSON(管道处理时更紧凑)
|
||||||
- 显式 `-o json` → 美化 JSON
|
- 显式 `-o text` → 人类可读文本格式
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -253,8 +253,7 @@ secrets search -n refining --kind service --name gitea -f metadata.url
|
|||||||
secrets search -n refining --kind service --name gitea \
|
secrets search -n refining --kind service --name gitea \
|
||||||
-f metadata.url -f metadata.default_org
|
-f metadata.url -f metadata.default_org
|
||||||
|
|
||||||
# 需要 secrets 时,改用 inject / run
|
# 需要 secrets 时,改用 run
|
||||||
secrets inject -n refining --kind service --name gitea
|
|
||||||
secrets run -n refining --kind service --name gitea -- printenv
|
secrets run -n refining --kind service --name gitea -- printenv
|
||||||
|
|
||||||
# 模糊关键词搜索
|
# 模糊关键词搜索
|
||||||
@@ -272,7 +271,7 @@ secrets search --tag aliyun --summary
|
|||||||
secrets search -n refining --summary --limit 10 --offset 0
|
secrets search -n refining --summary --limit 10 --offset 0
|
||||||
secrets search -n refining --summary --limit 10 --offset 10
|
secrets search -n refining --summary --limit 10 --offset 10
|
||||||
|
|
||||||
# 管道 / AI 调用(非 TTY 自动 json-compact)
|
# 管道 / AI 调用(默认 json,直接可解析)
|
||||||
secrets search -n refining --kind service | jq '.[].name'
|
secrets search -n refining --kind service | jq '.[].name'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -438,55 +437,42 @@ secrets rollback -n refining --kind service --name gitea --to-version 3
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### inject — 输出临时环境变量
|
|
||||||
|
|
||||||
仅注入 secrets 表中的加密字段(解密后),不含 metadata。敏感值仅打印到 stdout,不持久化、不写入当前 shell。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 参数说明
|
|
||||||
# -n / --namespace refining | ricnsmart
|
|
||||||
# --kind server | service
|
|
||||||
# --name 记录名
|
|
||||||
# --tag 按 tag 过滤(可重复)
|
|
||||||
# --prefix 变量名前缀(留空则以记录 name 作前缀)
|
|
||||||
# -o / --output text(默认 KEY=VALUE)| json | json-compact
|
|
||||||
|
|
||||||
# 打印单条记录的 secrets 变量(KEY=VALUE 格式)
|
|
||||||
secrets inject -n refining --kind service --name gitea
|
|
||||||
|
|
||||||
# 自定义前缀
|
|
||||||
secrets inject -n refining --kind service --name gitea --prefix GITEA
|
|
||||||
|
|
||||||
# JSON 格式(适合管道或脚本解析)
|
|
||||||
secrets inject -n refining --kind service --name gitea -o json
|
|
||||||
|
|
||||||
# eval 注入当前 shell(谨慎使用)
|
|
||||||
eval $(secrets inject -n refining --kind service --name gitea)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### run — 向子进程注入 secrets 并执行命令
|
### run — 向子进程注入 secrets 并执行命令
|
||||||
|
|
||||||
仅注入 secrets 表中的加密字段(解密后),不含 metadata。secrets 仅作用于子进程环境,不修改当前 shell,进程退出码透传。
|
仅注入 secrets 表中的加密字段(解密后),不含 metadata。secrets 仅作用于子进程环境,不修改当前 shell,进程退出码透传。
|
||||||
|
|
||||||
|
使用 `-s/--secret` 指定只注入哪些字段(最小权限原则);使用 `--dry-run` 预览将注入哪些变量名及来源,不执行命令。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 参数说明
|
# 参数说明
|
||||||
# -n / --namespace refining | ricnsmart
|
# -n / --namespace refining | ricnsmart
|
||||||
# --kind server | service
|
# --kind server | service
|
||||||
# --name 记录名
|
# --name 记录名
|
||||||
# --tag 按 tag 过滤(可重复)
|
# --tag 按 tag 过滤(可重复)
|
||||||
|
# -s / --secret 只注入指定字段名(可重复;省略则注入全部)
|
||||||
# --prefix 变量名前缀
|
# --prefix 变量名前缀
|
||||||
# -- <command> 执行的命令及参数
|
# --dry-run 预览变量映射,不执行命令
|
||||||
|
# -o / --output json(默认)| json-compact | text
|
||||||
|
# -- <command> 执行的命令及参数(--dry-run 时可省略)
|
||||||
|
|
||||||
# 向脚本注入单条记录的 secrets
|
# 注入全部 secrets 到脚本
|
||||||
secrets run -n refining --kind service --name gitea -- ./deploy.sh
|
secrets run -n refining --kind service --name gitea -- ./deploy.sh
|
||||||
|
|
||||||
|
# 只注入特定字段(最小化注入范围)
|
||||||
|
secrets run -n refining --kind service --name aliyun \
|
||||||
|
-s access_key_id -s access_key_secret -- aliyun ecs DescribeInstances
|
||||||
|
|
||||||
# 按 tag 批量注入(多条记录合并)
|
# 按 tag 批量注入(多条记录合并)
|
||||||
secrets run --tag production -- env | grep -i token
|
secrets run --tag production -- env | grep -i token
|
||||||
|
|
||||||
# 验证注入了哪些变量
|
# 预览将注入哪些变量(不执行命令,默认 JSON 输出)
|
||||||
secrets run -n refining --kind service --name gitea -- printenv
|
secrets run -n refining --kind service --name gitea --dry-run
|
||||||
|
|
||||||
|
# 配合字段过滤预览
|
||||||
|
secrets run -n refining --kind service --name gitea -s token --dry-run
|
||||||
|
|
||||||
|
# text 模式预览(人类阅读)
|
||||||
|
secrets run -n refining --kind service --name gitea --dry-run -o text
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -616,7 +602,7 @@ secrets --db-url "postgres://..." search -n refining
|
|||||||
- 日志:用户可见输出用 `println!`;调试/运维信息用 `tracing::debug!`/`info!`/`warn!`/`error!`
|
- 日志:用户可见输出用 `println!`;调试/运维信息用 `tracing::debug!`/`info!`/`warn!`/`error!`
|
||||||
- 审计:`add`/`update`/`delete` 成功后调用 `audit::log_tx`,写入 `audit_log` 表;失败只 warn 不中断
|
- 审计:`add`/`update`/`delete` 成功后调用 `audit::log_tx`,写入 `audit_log` 表;失败只 warn 不中断
|
||||||
- 加密:`encrypted` 列存储 AES-256-GCM 密文;`add`/`update`/`search`/`delete` 需主密钥(`secrets init` 后从 OS 钥匙串加载)
|
- 加密:`encrypted` 列存储 AES-256-GCM 密文;`add`/`update`/`search`/`delete` 需主密钥(`secrets init` 后从 OS 钥匙串加载)
|
||||||
- 输出:读命令通过 `OutputMode` 支持 text/json/json-compact/env;写命令 `add` 同样支持 `-o json`
|
- 输出:读命令通过 `OutputMode` 支持 text/json/json-compact;默认始终 `json`(pretty),`-o text` 供人类阅读;写命令 `add` 同样支持 `-o json`
|
||||||
|
|
||||||
## 提交前检查(必须全部通过)
|
## 提交前检查(必须全部通过)
|
||||||
|
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1836,7 +1836,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "secrets"
|
name = "secrets"
|
||||||
version = "0.9.5"
|
version = "0.9.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "secrets"
|
name = "secrets"
|
||||||
version = "0.9.5"
|
version = "0.9.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -64,31 +64,35 @@ secrets search -n refining --kind service --name gitea -f metadata.url
|
|||||||
secrets search -n refining --kind service --name gitea \
|
secrets search -n refining --kind service --name gitea \
|
||||||
-f metadata.url -f metadata.default_org
|
-f metadata.url -f metadata.default_org
|
||||||
|
|
||||||
# 需要 secrets 时,改用 inject / run
|
# 需要 secrets 时,改用 run(只注入 token 字段到子进程)
|
||||||
secrets inject -n refining --kind service --name gitea
|
secrets run -n refining --kind service --name gitea -s token -- ./deploy.sh
|
||||||
secrets run -n refining --kind service --name gitea -- printenv
|
|
||||||
|
# 预览 run 会注入哪些变量(不执行命令)
|
||||||
|
secrets run -n refining --kind service --name gitea --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
`search` 展示 metadata 与 secrets 的字段名,不展示 secret 值本身;需要 secret 值时用 `inject` / `run`(仅注入加密字段,不含 metadata)。
|
`search` 展示 metadata 与 secrets 的字段名,不展示 secret 值本身;需要 secret 值时用 `run`(仅注入加密字段到子进程,不含 metadata)。用 `-s` 指定只注入特定字段,最小化注入范围。
|
||||||
|
|
||||||
### 输出格式
|
### 输出格式
|
||||||
|
|
||||||
| 场景 | 推荐命令 |
|
| 场景 | 推荐命令 |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| AI 解析 / 管道处理 | `-o json` 或 `-o json-compact` |
|
| AI 解析 / 管道处理(默认) | json(pretty-printed) |
|
||||||
| 注入 secrets 到环境变量 | `inject` / `run` |
|
| 管道紧凑格式 | `-o json-compact` |
|
||||||
| 人类查看 | 默认 `text`(TTY 下自动启用) |
|
| 注入 secrets 到子进程环境 | `run` |
|
||||||
| 非 TTY(管道/重定向) | 自动 `json-compact` |
|
| 人类查看 | `-o text` |
|
||||||
|
|
||||||
说明:`text` 输出中的时间会按当前机器本地时区显示;`json/json-compact` 继续使用 UTC(RFC3339 风格)以便脚本和 AI 稳定解析。
|
默认始终输出 JSON,无论是 TTY 还是管道。`text` 输出中时间按本地时区显示;`json/json-compact` 使用 UTC(RFC3339)。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 管道直接 jq 解析(非 TTY 自动 json-compact)
|
# 默认 JSON 输出,直接可 jq 解析
|
||||||
secrets search -n refining --kind service | jq '.[].name'
|
secrets search -n refining --kind service | jq '.[].name'
|
||||||
|
|
||||||
# 需要 secrets 时,使用 inject / run
|
# 需要 secrets 时,使用 run(-s 指定只注入特定字段)
|
||||||
secrets inject -n refining --kind service --name gitea > ~/.config/gitea/secrets.env
|
secrets run -n refining --kind service --name gitea -s token -- ./deploy.sh
|
||||||
secrets run -n refining --kind service --name gitea -- ./deploy.sh
|
|
||||||
|
# 预览 run 会注入哪些变量(不执行命令)
|
||||||
|
secrets run -n refining --kind service --name gitea --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
## 完整命令参考
|
## 完整命令参考
|
||||||
@@ -177,6 +181,16 @@ secrets import backup.json # 导入(冲突时报
|
|||||||
secrets import --force refining.toml # 冲突时覆盖已有记录
|
secrets import --force refining.toml # 冲突时覆盖已有记录
|
||||||
secrets import --dry-run backup.yaml # 预览将要执行的操作(不写入)
|
secrets import --dry-run backup.yaml # 预览将要执行的操作(不写入)
|
||||||
|
|
||||||
|
# ── run ───────────────────────────────────────────────────────────────────────
|
||||||
|
secrets run -n refining --kind service --name gitea -- ./deploy.sh # 注入全部 secrets
|
||||||
|
secrets run -n refining --kind service --name gitea -s token -- ./deploy.sh # 只注入 token 字段
|
||||||
|
secrets run -n refining --kind service --name aliyun \
|
||||||
|
-s access_key_id -s access_key_secret -- aliyun ecs DescribeInstances # 只注入指定字段
|
||||||
|
secrets run --tag production -- env # 按 tag 批量注入
|
||||||
|
secrets run -n refining --kind service --name gitea --dry-run # 预览变量映射
|
||||||
|
secrets run -n refining --kind service --name gitea -s token --dry-run # 过滤后预览
|
||||||
|
secrets run -n refining --kind service --name gitea --dry-run -o text # 人类可读预览
|
||||||
|
|
||||||
# ── 调试 ──────────────────────────────────────────────────────────────────────
|
# ── 调试 ──────────────────────────────────────────────────────────────────────
|
||||||
secrets --verbose search -q mqtt
|
secrets --verbose search -q mqtt
|
||||||
RUST_LOG=secrets=trace secrets search
|
RUST_LOG=secrets=trace secrets search
|
||||||
@@ -193,7 +207,7 @@ RUST_LOG=secrets=trace secrets search
|
|||||||
| entries | name | 人类可读唯一标识 |
|
| entries | name | 人类可读唯一标识 |
|
||||||
| entries | tags | 多维标签,如 `["aliyun","hongkong"]` |
|
| entries | tags | 多维标签,如 `["aliyun","hongkong"]` |
|
||||||
| entries | metadata | 明文描述(ip、desc、domains、key_ref 等) |
|
| entries | metadata | 明文描述(ip、desc、domains、key_ref 等) |
|
||||||
| secrets | field_name | 明文,search 可见,AI 可推断 inject 会生成什么变量 |
|
| secrets | field_name | 明文,search 可见,AI 可推断 run 会注入哪些变量 |
|
||||||
| secrets | encrypted | 仅加密值本身,AES-256-GCM |
|
| secrets | encrypted | 仅加密值本身,AES-256-GCM |
|
||||||
|
|
||||||
`-m` / `--meta` 写入 `metadata`,`-s` / `--secret` 写入 `secrets` 表的独立行。支持 `key=value`、`key=@file`、`key:=<json>`,也支持 `credentials:content@./key.pem` 这类嵌套字段文件写入;删除时支持 `--remove-secret credentials:content`。加解密使用主密钥(由 `secrets init` 设置)。
|
`-m` / `--meta` 写入 `metadata`,`-s` / `--secret` 写入 `secrets` 表的独立行。支持 `key=value`、`key=@file`、`key:=<json>`,也支持 `credentials:content@./key.pem` 这类嵌套字段文件写入;删除时支持 `--remove-secret credentials:content`。加解密使用主密钥(由 `secrets init` 设置)。
|
||||||
@@ -314,7 +328,7 @@ src/
|
|||||||
delete.rs # 删除(CASCADE 删除 secrets)
|
delete.rs # 删除(CASCADE 删除 secrets)
|
||||||
update.rs # 增量更新(tags/metadata + secrets 行级 UPSERT/DELETE)
|
update.rs # 增量更新(tags/metadata + secrets 行级 UPSERT/DELETE)
|
||||||
rollback.rs # rollback / history:按 entry_version 恢复
|
rollback.rs # rollback / history:按 entry_version 恢复
|
||||||
run.rs # inject / run,仅 secrets 逐字段解密 + key_ref 引用解析(不含 metadata)
|
run.rs # run,仅 secrets 逐字段解密 + key_ref 引用解析(不含 metadata)
|
||||||
upgrade.rs # 从 Gitea Release 自更新
|
upgrade.rs # 从 Gitea Release 自更新
|
||||||
export_cmd.rs # export:批量导出,支持 JSON/TOML/YAML,含解密明文
|
export_cmd.rs # export:批量导出,支持 JSON/TOML/YAML,含解密明文
|
||||||
import_cmd.rs # import:批量导入,冲突检测,dry-run,重新加密写入
|
import_cmd.rs # import:批量导入,冲突检测,dry-run,重新加密写入
|
||||||
|
|||||||
@@ -1,45 +1,57 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde_json::Value;
|
use serde_json::json;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::commands::search::{build_injected_env_map, fetch_entries, fetch_secrets_for_entries};
|
use crate::commands::search::{build_injected_env_map, fetch_entries, fetch_secrets_for_entries};
|
||||||
use crate::output::OutputMode;
|
use crate::output::OutputMode;
|
||||||
|
|
||||||
pub struct InjectArgs<'a> {
|
|
||||||
pub namespace: Option<&'a str>,
|
|
||||||
pub kind: Option<&'a str>,
|
|
||||||
pub name: Option<&'a str>,
|
|
||||||
pub tags: &'a [String],
|
|
||||||
pub prefix: &'a str,
|
|
||||||
pub output: OutputMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RunArgs<'a> {
|
pub struct RunArgs<'a> {
|
||||||
pub namespace: Option<&'a str>,
|
pub namespace: Option<&'a str>,
|
||||||
pub kind: Option<&'a str>,
|
pub kind: Option<&'a str>,
|
||||||
pub name: Option<&'a str>,
|
pub name: Option<&'a str>,
|
||||||
pub tags: &'a [String],
|
pub tags: &'a [String],
|
||||||
|
pub secret_fields: &'a [String],
|
||||||
pub prefix: &'a str,
|
pub prefix: &'a str,
|
||||||
|
pub dry_run: bool,
|
||||||
|
pub output: OutputMode,
|
||||||
pub command: &'a [String],
|
pub command: &'a [String],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single environment variable with its origin for dry-run display.
|
||||||
|
pub struct EnvMapping {
|
||||||
|
pub var_name: String,
|
||||||
|
pub source: String,
|
||||||
|
pub field: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CollectArgs<'a> {
|
||||||
|
namespace: Option<&'a str>,
|
||||||
|
kind: Option<&'a str>,
|
||||||
|
name: Option<&'a str>,
|
||||||
|
tags: &'a [String],
|
||||||
|
secret_fields: &'a [String],
|
||||||
|
prefix: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch entries matching the filter and build a flat env map (decrypted secrets only, no metadata).
|
/// Fetch entries matching the filter and build a flat env map (decrypted secrets only, no metadata).
|
||||||
pub async fn collect_env_map(
|
/// If `secret_fields` is non-empty, only those fields are decrypted and included.
|
||||||
|
async fn collect_env_map(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
namespace: Option<&str>,
|
args: &CollectArgs<'_>,
|
||||||
kind: Option<&str>,
|
|
||||||
name: Option<&str>,
|
|
||||||
tags: &[String],
|
|
||||||
prefix: &str,
|
|
||||||
master_key: &[u8; 32],
|
master_key: &[u8; 32],
|
||||||
) -> Result<HashMap<String, String>> {
|
) -> Result<HashMap<String, String>> {
|
||||||
if namespace.is_none() && kind.is_none() && name.is_none() && tags.is_empty() {
|
if args.namespace.is_none()
|
||||||
|
&& args.kind.is_none()
|
||||||
|
&& args.name.is_none()
|
||||||
|
&& args.tags.is_empty()
|
||||||
|
{
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"At least one filter (--namespace, --kind, --name, or --tag) is required for inject/run"
|
"At least one filter (--namespace, --kind, --name, or --tag) is required for run"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let entries = fetch_entries(pool, namespace, kind, name, tags, None).await?;
|
let entries =
|
||||||
|
fetch_entries(pool, args.namespace, args.kind, args.name, args.tags, None).await?;
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
anyhow::bail!("No records matched the given filters.");
|
anyhow::bail!("No records matched the given filters.");
|
||||||
}
|
}
|
||||||
@@ -50,8 +62,17 @@ pub async fn collect_env_map(
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
for entry in &entries {
|
for entry in &entries {
|
||||||
let empty = vec![];
|
let empty = vec![];
|
||||||
let fields = fields_map.get(&entry.id).unwrap_or(&empty);
|
let all_fields = fields_map.get(&entry.id).unwrap_or(&empty);
|
||||||
let row_map = build_injected_env_map(pool, entry, prefix, master_key, fields).await?;
|
let filtered_fields: Vec<_> = if args.secret_fields.is_empty() {
|
||||||
|
all_fields.iter().collect()
|
||||||
|
} else {
|
||||||
|
all_fields
|
||||||
|
.iter()
|
||||||
|
.filter(|f| args.secret_fields.contains(&f.field_name))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let row_map =
|
||||||
|
build_injected_env_map(pool, entry, args.prefix, master_key, &filtered_fields).await?;
|
||||||
for (k, v) in row_map {
|
for (k, v) in row_map {
|
||||||
map.insert(k, v);
|
map.insert(k, v);
|
||||||
}
|
}
|
||||||
@@ -59,64 +80,152 @@ pub async fn collect_env_map(
|
|||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `inject` command: print env vars to stdout.
|
/// Like `collect_env_map` but also returns per-variable source info for dry-run display.
|
||||||
pub async fn run_inject(pool: &PgPool, args: InjectArgs<'_>, master_key: &[u8; 32]) -> Result<()> {
|
async fn collect_env_map_with_source(
|
||||||
let env_map = collect_env_map(
|
pool: &PgPool,
|
||||||
pool,
|
args: &CollectArgs<'_>,
|
||||||
args.namespace,
|
master_key: &[u8; 32],
|
||||||
args.kind,
|
) -> Result<(HashMap<String, String>, Vec<EnvMapping>)> {
|
||||||
args.name,
|
if args.namespace.is_none()
|
||||||
args.tags,
|
&& args.kind.is_none()
|
||||||
args.prefix,
|
&& args.name.is_none()
|
||||||
master_key,
|
&& args.tags.is_empty()
|
||||||
)
|
{
|
||||||
.await?;
|
anyhow::bail!(
|
||||||
|
"At least one filter (--namespace, --kind, --name, or --tag) is required for run"
|
||||||
match args.output {
|
);
|
||||||
OutputMode::Json => {
|
}
|
||||||
let obj: serde_json::Map<String, Value> = env_map
|
let entries =
|
||||||
.into_iter()
|
fetch_entries(pool, args.namespace, args.kind, args.name, args.tags, None).await?;
|
||||||
.map(|(k, v)| (k, Value::String(v)))
|
if entries.is_empty() {
|
||||||
.collect();
|
anyhow::bail!("No records matched the given filters.");
|
||||||
println!("{}", serde_json::to_string_pretty(&Value::Object(obj))?);
|
|
||||||
}
|
|
||||||
OutputMode::JsonCompact => {
|
|
||||||
let obj: serde_json::Map<String, Value> = env_map
|
|
||||||
.into_iter()
|
|
||||||
.map(|(k, v)| (k, Value::String(v)))
|
|
||||||
.collect();
|
|
||||||
println!("{}", serde_json::to_string(&Value::Object(obj))?);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let mut pairs: Vec<(String, String)> = env_map.into_iter().collect();
|
|
||||||
pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
|
||||||
for (k, v) in pairs {
|
|
||||||
println!("{}={}", k, shell_quote(&v));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
let entry_ids: Vec<uuid::Uuid> = entries.iter().map(|e| e.id).collect();
|
||||||
|
let fields_map = fetch_secrets_for_entries(pool, &entry_ids).await?;
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
let mut mappings: Vec<EnvMapping> = Vec::new();
|
||||||
|
|
||||||
|
for entry in &entries {
|
||||||
|
let empty = vec![];
|
||||||
|
let all_fields = fields_map.get(&entry.id).unwrap_or(&empty);
|
||||||
|
let filtered_fields: Vec<_> = if args.secret_fields.is_empty() {
|
||||||
|
all_fields.iter().collect()
|
||||||
|
} else {
|
||||||
|
all_fields
|
||||||
|
.iter()
|
||||||
|
.filter(|f| args.secret_fields.contains(&f.field_name))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_map =
|
||||||
|
build_injected_env_map(pool, entry, args.prefix, master_key, &filtered_fields).await?;
|
||||||
|
|
||||||
|
let source = format!("{}/{}/{}", entry.namespace, entry.kind, entry.name);
|
||||||
|
for field in &filtered_fields {
|
||||||
|
let var_name = format!(
|
||||||
|
"{}_{}",
|
||||||
|
env_prefix_name(&entry.name, args.prefix),
|
||||||
|
field.field_name.to_uppercase().replace(['-', '.'], "_")
|
||||||
|
);
|
||||||
|
if row_map.contains_key(&var_name) {
|
||||||
|
mappings.push(EnvMapping {
|
||||||
|
var_name: var_name.clone(),
|
||||||
|
source: source.clone(),
|
||||||
|
field: field.field_name.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (k, v) in row_map {
|
||||||
|
map.insert(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((map, mappings))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_prefix_name(entry_name: &str, prefix: &str) -> String {
|
||||||
|
let name_part = entry_name.to_uppercase().replace(['-', '.', ' '], "_");
|
||||||
|
if prefix.is_empty() {
|
||||||
|
name_part
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}_{}",
|
||||||
|
prefix.to_uppercase().replace(['-', '.', ' '], "_"),
|
||||||
|
name_part
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `run` command: inject secrets into a child process environment and execute.
|
/// `run` command: inject secrets into a child process environment and execute.
|
||||||
|
/// With `--dry-run`, prints the variable mapping (names and sources only) without executing.
|
||||||
pub async fn run_exec(pool: &PgPool, args: RunArgs<'_>, master_key: &[u8; 32]) -> Result<()> {
|
pub async fn run_exec(pool: &PgPool, args: RunArgs<'_>, master_key: &[u8; 32]) -> Result<()> {
|
||||||
if args.command.is_empty() {
|
if !args.dry_run && args.command.is_empty() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"No command specified. Usage: secrets run [filter flags] -- <command> [args]"
|
"No command specified. Usage: secrets run [filter flags] -- <command> [args]"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let env_map = collect_env_map(
|
let collect = CollectArgs {
|
||||||
pool,
|
namespace: args.namespace,
|
||||||
args.namespace,
|
kind: args.kind,
|
||||||
args.kind,
|
name: args.name,
|
||||||
args.name,
|
tags: args.tags,
|
||||||
args.tags,
|
secret_fields: args.secret_fields,
|
||||||
args.prefix,
|
prefix: args.prefix,
|
||||||
master_key,
|
};
|
||||||
)
|
|
||||||
.await?;
|
if args.dry_run {
|
||||||
|
let (env_map, mappings) = collect_env_map_with_source(pool, &collect, master_key).await?;
|
||||||
|
|
||||||
|
let total_vars = env_map.len();
|
||||||
|
let total_records = {
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
for m in &mappings {
|
||||||
|
seen.insert(&m.source);
|
||||||
|
}
|
||||||
|
seen.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
match args.output {
|
||||||
|
OutputMode::Text => {
|
||||||
|
for m in &mappings {
|
||||||
|
println!("{:<40} <- {} :: {}", m.var_name, m.source, m.field);
|
||||||
|
}
|
||||||
|
println!("---");
|
||||||
|
println!(
|
||||||
|
"{} variable(s) from {} record(s).",
|
||||||
|
total_vars, total_records
|
||||||
|
);
|
||||||
|
}
|
||||||
|
OutputMode::Json | OutputMode::JsonCompact => {
|
||||||
|
let vars: Vec<_> = mappings
|
||||||
|
.iter()
|
||||||
|
.map(|m| {
|
||||||
|
json!({
|
||||||
|
"name": m.var_name,
|
||||||
|
"source": m.source,
|
||||||
|
"field": m.field,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let out = json!({
|
||||||
|
"variables": vars,
|
||||||
|
"total_vars": total_vars,
|
||||||
|
"total_records": total_records,
|
||||||
|
});
|
||||||
|
if args.output == OutputMode::Json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&out)?);
|
||||||
|
} else {
|
||||||
|
println!("{}", serde_json::to_string(&out)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let env_map = collect_env_map(pool, &collect, master_key).await?;
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
vars = env_map.len(),
|
vars = env_map.len(),
|
||||||
@@ -137,7 +246,3 @@ pub async fn run_exec(pool: &PgPool, args: RunArgs<'_>, master_key: &[u8; 32]) -
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shell_quote(s: &str) -> String {
|
|
||||||
format!("'{}'", s.replace('\'', "'\\''"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ pub async fn run(pool: &PgPool, args: SearchArgs<'_>) -> Result<()> {
|
|||||||
fn validate_safe_search_args(fields: &[String]) -> Result<()> {
|
fn validate_safe_search_args(fields: &[String]) -> Result<()> {
|
||||||
if let Some(field) = fields.iter().find(|field| is_secret_field(field)) {
|
if let Some(field) = fields.iter().find(|field| is_secret_field(field)) {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Field '{}' is sensitive. `search -f` only supports metadata.* fields; use `secrets inject` or `secrets run` for secrets.",
|
"Field '{}' is sensitive. `search -f` only supports metadata.* fields; use `secrets run` for secrets.",
|
||||||
field
|
field
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -121,11 +121,11 @@ struct PagedFetchArgs<'a> {
|
|||||||
offset: u32,
|
offset: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A very large limit used when callers need all matching records (export, inject, run).
|
/// A very large limit used when callers need all matching records (export, run).
|
||||||
/// Postgres will stop scanning when this many rows are found; adjust if needed.
|
/// Postgres will stop scanning when this many rows are found; adjust if needed.
|
||||||
pub const FETCH_ALL_LIMIT: u32 = 100_000;
|
pub const FETCH_ALL_LIMIT: u32 = 100_000;
|
||||||
|
|
||||||
/// Fetch entries matching the given filters (used by search, inject, run).
|
/// Fetch entries matching the given filters (used by search, run).
|
||||||
/// `limit` caps the result set; pass `FETCH_ALL_LIMIT` when you need all matching records.
|
/// `limit` caps the result set; pass `FETCH_ALL_LIMIT` when you need all matching records.
|
||||||
pub async fn fetch_entries(
|
pub async fn fetch_entries(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
@@ -319,7 +319,7 @@ pub async fn build_injected_env_map(
|
|||||||
entry: &Entry,
|
entry: &Entry,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
master_key: &[u8; 32],
|
master_key: &[u8; 32],
|
||||||
fields: &[SecretField],
|
fields: &[&SecretField],
|
||||||
) -> Result<HashMap<String, String>> {
|
) -> Result<HashMap<String, String>> {
|
||||||
let effective_prefix = env_prefix(entry, prefix);
|
let effective_prefix = env_prefix(entry, prefix);
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
@@ -456,7 +456,7 @@ fn print_text(entry: &Entry, summary: bool, schema: Option<&[SecretField]>) -> R
|
|||||||
Some(fields) if !fields.is_empty() => {
|
Some(fields) if !fields.is_empty() => {
|
||||||
let schema_str: Vec<String> = fields.iter().map(|f| f.field_name.clone()).collect();
|
let schema_str: Vec<String> = fields.iter().map(|f| f.field_name.clone()).collect();
|
||||||
println!(" secrets: {}", schema_str.join(", "));
|
println!(" secrets: {}", schema_str.join(", "));
|
||||||
println!(" (use `secrets inject` or `secrets run` to get values)");
|
println!(" (use `secrets run` to get values)");
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
104
src/main.rs
104
src/main.rs
@@ -41,8 +41,8 @@ use output::resolve_output_mode;
|
|||||||
# Pipe-friendly (non-TTY defaults to json-compact automatically)
|
# Pipe-friendly (non-TTY defaults to json-compact automatically)
|
||||||
secrets search -n refining --kind service | jq '.[].name'
|
secrets search -n refining --kind service | jq '.[].name'
|
||||||
|
|
||||||
# Inject secrets into environment variables when you really need them
|
# Run a command with secrets injected into its child process environment
|
||||||
secrets inject -n refining --kind service --name gitea"
|
secrets run -n refining --kind service --name gitea -- printenv"
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Database URL, overrides saved config (one-time override)
|
/// Database URL, overrides saved config (one-time override)
|
||||||
@@ -132,7 +132,7 @@ EXAMPLES:
|
|||||||
#[arg(long = "tag")]
|
#[arg(long = "tag")]
|
||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
/// Plaintext metadata: key=value, key:=<json>, key=@file, or nested:path@file.
|
/// Plaintext metadata: key=value, key:=<json>, key=@file, or nested:path@file.
|
||||||
/// Use key_ref=<name> to reference a shared key entry (kind=key); inject/run merge its secrets.
|
/// Use key_ref=<name> to reference a shared key entry (kind=key); run merges its secrets.
|
||||||
#[arg(long = "meta", short = 'm')]
|
#[arg(long = "meta", short = 'm')]
|
||||||
meta: Vec<String>,
|
meta: Vec<String>,
|
||||||
/// Secret entry: key=value, key:=<json>, key=@file, or nested:path@file
|
/// Secret entry: key=value, key:=<json>, key=@file, or nested:path@file
|
||||||
@@ -169,8 +169,7 @@ EXAMPLES:
|
|||||||
secrets search -n refining --kind service --name gitea \\
|
secrets search -n refining --kind service --name gitea \\
|
||||||
-f metadata.url -f metadata.default_org
|
-f metadata.url -f metadata.default_org
|
||||||
|
|
||||||
# Inject decrypted secrets only when needed
|
# Run a command with decrypted secrets only when needed
|
||||||
secrets inject -n refining --kind service --name gitea
|
|
||||||
secrets run -n refining --kind service --name gitea -- printenv
|
secrets run -n refining --kind service --name gitea -- printenv
|
||||||
|
|
||||||
# Paginate large result sets
|
# Paginate large result sets
|
||||||
@@ -392,54 +391,33 @@ EXAMPLES:
|
|||||||
output: Option<String>,
|
output: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Print secrets as environment variables (stdout only, nothing persisted).
|
|
||||||
///
|
|
||||||
/// Outputs KEY=VALUE pairs for all matched records. Safe to pipe or eval.
|
|
||||||
#[command(after_help = "EXAMPLES:
|
|
||||||
# Print env vars for a single service
|
|
||||||
secrets inject -n refining --kind service --name gitea
|
|
||||||
|
|
||||||
# With a custom prefix
|
|
||||||
secrets inject -n refining --kind service --name gitea --prefix GITEA
|
|
||||||
|
|
||||||
# JSON output (all vars as a JSON object)
|
|
||||||
secrets inject -n refining --kind service --name gitea -o json
|
|
||||||
|
|
||||||
# Eval into current shell (use with caution)
|
|
||||||
eval $(secrets inject -n refining --kind service --name gitea)
|
|
||||||
|
|
||||||
# For entries with metadata.key_ref, referenced key's secrets are merged automatically")]
|
|
||||||
Inject {
|
|
||||||
#[arg(short, long)]
|
|
||||||
namespace: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
kind: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
name: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
tag: Vec<String>,
|
|
||||||
/// Prefix to prepend to every variable name (uppercased automatically)
|
|
||||||
#[arg(long, default_value = "")]
|
|
||||||
prefix: String,
|
|
||||||
/// Output format: text/KEY=VALUE (default), json, json-compact
|
|
||||||
#[arg(short, long = "output")]
|
|
||||||
output: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Run a command with secrets injected as environment variables.
|
/// Run a command with secrets injected as environment variables.
|
||||||
///
|
///
|
||||||
/// Secrets are available only to the child process; the current shell
|
/// Secrets are available only to the child process; the current shell
|
||||||
/// environment is not modified. The process exit code is propagated.
|
/// 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:
|
#[command(after_help = "EXAMPLES:
|
||||||
# Run a script with a single service's secrets injected
|
# Run a script with a single service's secrets injected
|
||||||
secrets run -n refining --kind service --name gitea -- ./deploy.sh
|
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)
|
# Run with a tag filter (all matched records merged)
|
||||||
secrets run --tag production -- env | grep GITEA
|
secrets run --tag production -- env | grep GITEA
|
||||||
|
|
||||||
# With prefix
|
# With prefix
|
||||||
secrets run -n refining --kind service --name gitea --prefix GITEA -- printenv
|
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)")]
|
# metadata.key_ref entries get key secrets merged (e.g. server + shared PEM)")]
|
||||||
Run {
|
Run {
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
@@ -450,11 +428,20 @@ EXAMPLES:
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
tag: Vec<String>,
|
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)
|
/// Prefix to prepend to every variable name (uppercased automatically)
|
||||||
#[arg(long, default_value = "")]
|
#[arg(long, default_value = "")]
|
||||||
prefix: String,
|
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
|
/// Command and arguments to execute with injected environment
|
||||||
#[arg(last = true, required = true)]
|
#[arg(last = true)]
|
||||||
command: Vec<String>,
|
command: Vec<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -770,40 +757,24 @@ async fn main() -> Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands::Inject {
|
|
||||||
namespace,
|
|
||||||
kind,
|
|
||||||
name,
|
|
||||||
tag,
|
|
||||||
prefix,
|
|
||||||
output,
|
|
||||||
} => {
|
|
||||||
let master_key = crypto::load_master_key()?;
|
|
||||||
let out = resolve_output_mode(output.as_deref())?;
|
|
||||||
commands::run::run_inject(
|
|
||||||
&pool,
|
|
||||||
commands::run::InjectArgs {
|
|
||||||
namespace: namespace.as_deref(),
|
|
||||||
kind: kind.as_deref(),
|
|
||||||
name: name.as_deref(),
|
|
||||||
tags: &tag,
|
|
||||||
prefix: &prefix,
|
|
||||||
output: out,
|
|
||||||
},
|
|
||||||
&master_key,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Commands::Run {
|
Commands::Run {
|
||||||
namespace,
|
namespace,
|
||||||
kind,
|
kind,
|
||||||
name,
|
name,
|
||||||
tag,
|
tag,
|
||||||
|
secret_fields,
|
||||||
prefix,
|
prefix,
|
||||||
|
dry_run,
|
||||||
|
output,
|
||||||
command,
|
command,
|
||||||
} => {
|
} => {
|
||||||
let master_key = crypto::load_master_key()?;
|
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(
|
commands::run::run_exec(
|
||||||
&pool,
|
&pool,
|
||||||
commands::run::RunArgs {
|
commands::run::RunArgs {
|
||||||
@@ -811,7 +782,10 @@ async fn main() -> Result<()> {
|
|||||||
kind: kind.as_deref(),
|
kind: kind.as_deref(),
|
||||||
name: name.as_deref(),
|
name: name.as_deref(),
|
||||||
tags: &tag,
|
tags: &tag,
|
||||||
|
secret_fields: &secret_fields,
|
||||||
prefix: &prefix,
|
prefix: &prefix,
|
||||||
|
dry_run,
|
||||||
|
output: out,
|
||||||
command: &command,
|
command: &command,
|
||||||
},
|
},
|
||||||
&master_key,
|
&master_key,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use chrono::{DateTime, Local, Utc};
|
use chrono::{DateTime, Local, Utc};
|
||||||
use std::io::IsTerminal;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// Output format for all commands.
|
/// Output format for all commands.
|
||||||
@@ -32,16 +31,12 @@ impl FromStr for OutputMode {
|
|||||||
|
|
||||||
/// Resolve the effective output mode.
|
/// Resolve the effective output mode.
|
||||||
/// - Explicit value from `--output` takes priority.
|
/// - Explicit value from `--output` takes priority.
|
||||||
/// - TTY → text; non-TTY (piped/redirected) → json-compact.
|
/// - Default is always `Json` (AI-first); use `-o text` for human-readable output.
|
||||||
pub fn resolve_output_mode(explicit: Option<&str>) -> anyhow::Result<OutputMode> {
|
pub fn resolve_output_mode(explicit: Option<&str>) -> anyhow::Result<OutputMode> {
|
||||||
if let Some(s) = explicit {
|
if let Some(s) = explicit {
|
||||||
return s.parse();
|
return s.parse();
|
||||||
}
|
}
|
||||||
if std::io::stdout().is_terminal() {
|
Ok(OutputMode::Json)
|
||||||
Ok(OutputMode::Text)
|
|
||||||
} else {
|
|
||||||
Ok(OutputMode::JsonCompact)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a UTC timestamp for local human-readable output.
|
/// Format a UTC timestamp for local human-readable output.
|
||||||
|
|||||||
Reference in New Issue
Block a user