diff --git a/.gitea/workflows/secrets.yml b/.gitea/workflows/secrets.yml index 4c09584..6f6de14 100644 --- a/.gitea/workflows/secrets.yml +++ b/.gitea/workflows/secrets.yml @@ -17,6 +17,7 @@ permissions: env: BINARY_NAME: secrets + SECRETS_UPGRADE_URL: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/latest CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6ed125f..32bce8d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -142,7 +142,7 @@ { "label": "test: add with file secret", "type": "shell", - "command": "echo '--- add key from file ---' && ./target/debug/secrets add -n test --kind key --name test-key --tag test -s content=@./refining/keys/Vultr && 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 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", "dependsOn": "build" } ] diff --git a/AGENTS.md b/AGENTS.md index 54c77d3..9ad6698 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,9 +145,9 @@ secrets_history ( |------|--------|------| | `namespace` | 项目/团队隔离 | `refining`, `ricnsmart` | | `kind` | 记录类型 | `server`, `service`, `key` | -| `name` | 唯一标识名 | `i-uf63f2uookgs5uxmrdyc`, `gitea` | +| `name` | 唯一标识名 | `i-example0abcd1234efgh`, `gitea` | | `tags` | 多维分类标签 | `["aliyun","hongkong","ricn"]` | -| `metadata` | 明文非敏感信息 | `{"ip":"47.243.154.187","desc":"Grafana","key_ref":"ricn-hk-260127"}` | +| `metadata` | 明文非敏感信息 | `{"ip":"192.0.2.1","desc":"Grafana","key_ref":"my-shared-key"}` | | `secrets.field_name` | 加密字段名(明文) | `"username"`, `"token"`, `"ssh_key"` | | `secrets.field_type` | 值类型(明文) | `"string"`, `"number"`, `"boolean"`, `"json"` | | `secrets.value_len` | 原始值字符数(明文) | `4`(root),`40`(token),`4096`(PEM) | @@ -159,17 +159,17 @@ secrets_history ( ```bash # 1. 存共享 PEM -secrets add -n refining --kind key --name ricn-hk-260127 \ +secrets add -n refining --kind key --name my-shared-key \ --tag aliyun --tag hongkong \ - -s content=@./keys/ricn-hk-260127.pem + -s content=@./keys/my-shared-key.pem # 2. 服务器通过 metadata.key_ref 引用(inject/run 时自动合并 key 的 secrets) -secrets add -n refining --kind server --name i-j6c39dmtkr26vztii0ox \ - -m ip=47.243.154.187 -m key_ref=ricn-hk-260127 \ +secrets add -n refining --kind server --name i-example0xyz789 \ + -m ip=192.0.2.1 -m key_ref=my-shared-key \ -s username=ecs-user # 3. 轮换只需更新 key 记录,所有引用服务器自动生效 -secrets update -n refining --kind key --name ricn-hk-260127 \ +secrets update -n refining --kind key --name my-shared-key \ -s content=@./keys/new-key.pem ``` @@ -231,7 +231,7 @@ secrets init # 参数说明(带典型值) # -n / --namespace refining | ricnsmart # --kind server | service -# --name gitea | i-uf63f2uookgs5uxmrdyc | mqtt +# --name gitea | i-example0abcd1234efgh | mqtt # --tag aliyun | hongkong | production # -q / --query mqtt | grafana | gitea (模糊匹配 name/namespace/kind/tags/metadata) # secrets schema search 默认展示 secrets 字段名、类型与长度(无需 master_key) @@ -249,7 +249,7 @@ secrets search --sort updated --limit 10 --summary # 精确定位单条记录 secrets search -n refining --kind service --name gitea -secrets search -n refining --kind server --name i-uf63f2uookgs5uxmrdyc +secrets search -n refining --kind server --name i-example0abcd1234efgh # 精确定位并获取完整内容(secrets 保持加密占位) secrets search -n refining --kind service --name gitea -o json @@ -266,7 +266,7 @@ secrets run -n refining --kind service --name gitea -- printenv # 模糊关键词搜索 secrets search -q mqtt secrets search -q grafana -secrets search -q 47.117 +secrets search -q 192.0.2 # 按条件过滤 secrets search -n refining --kind service @@ -290,31 +290,31 @@ secrets search -n refining --kind service | jq '.[].name' # 参数说明(带典型值) # -n / --namespace refining | ricnsmart # --kind server | service -# --name gitea | i-uf63f2uookgs5uxmrdyc +# --name gitea | i-example0abcd1234efgh # --tag aliyun | hongkong(可重复) -# -m / --meta ip=47.117.131.22 | desc="Aliyun ECS" | url=https://... | tls:cert@./cert.pem(可重复) +# -m / --meta ip=10.0.0.1 | desc="ECS" | url=https://... | tls:cert@./cert.pem(可重复) # -s / --secret token= | ssh_key=@./key.pem | password=secret123 | credentials:content@./key.pem(可重复) # 添加服务器 -secrets add -n refining --kind server --name i-uf63f2uookgs5uxmrdyc \ +secrets add -n refining --kind server --name i-example0abcd1234efgh \ --tag aliyun --tag shanghai \ - -m ip=47.117.131.22 -m desc="Aliyun Shanghai ECS" \ - -s username=root -s ssh_key=@./keys/voson_shanghai_e.pem + -m ip=10.0.0.1 -m desc="Aliyun Shanghai ECS" \ + -s username=root -s ssh_key=@./keys/deploy-key.pem # 添加服务凭据 secrets add -n refining --kind service --name gitea \ --tag gitea \ - -m url=https://gitea.refining.dev -m default_org=refining -m username=voson \ + -m url=https://code.example.com -m default_org=refining -m username=voson \ -s token= -s runner_token= # 从文件读取 token secrets add -n ricnsmart --kind service --name mqtt \ - -m host=mqtt.ricnsmart.com -m port=1883 \ + -m host=mqtt.example.com -m port=1883 \ -s password=@./mqtt_password.txt # 多行文件直接写入嵌套 secret 字段 -secrets add -n refining --kind server --name i-uf63f2uookgs5uxmrdyc \ - -s credentials:content@./keys/voson_shanghai_e.pem +secrets add -n refining --kind server --name i-example0abcd1234efgh \ + -s credentials:content@./keys/deploy-key.pem # 使用类型化值(key:=)存储非字符串类型 secrets add -n refining --kind service --name prometheus \ @@ -334,7 +334,7 @@ secrets add -n refining --kind service --name prometheus \ # 参数说明(带典型值) # -n / --namespace refining | ricnsmart # --kind server | service -# --name gitea | i-uf63f2uookgs5uxmrdyc +# --name gitea | i-example0abcd1234efgh # --add-tag production | backup(不影响已有 tag,可重复) # --remove-tag staging | deprecated(可重复) # -m / --meta ip=10.0.0.1 | desc="新描述" | credentials:username=root(新增或覆盖,可重复) @@ -343,7 +343,7 @@ secrets add -n refining --kind service --name prometheus \ # --remove-secret old_password | deprecated_key | credentials:content(删除 secret 字段,可重复) # 更新单个 metadata 字段 -secrets update -n refining --kind server --name i-uf63f2uookgs5uxmrdyc \ +secrets update -n refining --kind server --name i-example0abcd1234efgh \ -m ip=10.0.0.1 # 轮换 token @@ -360,11 +360,11 @@ secrets update -n refining --kind service --name mqtt \ --remove-meta old_port --remove-secret old_password # 从文件更新嵌套 secret 字段 -secrets update -n refining --kind server --name i-uf63f2uookgs5uxmrdyc \ - -s credentials:content@./keys/voson_shanghai_e.pem +secrets update -n refining --kind server --name i-example0abcd1234efgh \ + -s credentials:content@./keys/deploy-key.pem # 删除嵌套字段 -secrets update -n refining --kind server --name i-uf63f2uookgs5uxmrdyc \ +secrets update -n refining --kind server --name i-example0abcd1234efgh \ --remove-secret credentials:content # 移除 tag @@ -379,7 +379,7 @@ secrets update -n refining --kind service --name gitea --remove-tag staging # 参数说明(带典型值) # -n / --namespace refining | ricnsmart # --kind server | service -# --name gitea | i-uf63f2uookgs5uxmrdyc(必须精确匹配) +# --name gitea | i-example0abcd1234efgh(必须精确匹配) # 删除服务凭据 secrets delete -n refining --kind service --name legacy-mqtt @@ -484,7 +484,9 @@ secrets run -n refining --kind service --name gitea -- printenv ### upgrade — 自动更新 CLI 二进制 -从 Gitea Release 下载最新版本,校验对应 `.sha256` 摘要后替换当前二进制,无需数据库连接或主密钥。 +从 Release 服务器下载最新版本,校验对应 `.sha256` 摘要后替换当前二进制,无需数据库连接或主密钥。 + +**配置方式**:`SECRETS_UPGRADE_URL` 必填。优先用**构建时**:`SECRETS_UPGRADE_URL=https://... cargo build`,CI 已自动注入。或**运行时**:写在 `.env` 或 `export` 后执行。 ```bash # 检查是否有新版本(不下载) @@ -504,7 +506,7 @@ secrets upgrade # 参数说明 # -n / --namespace refining | ricnsmart # --kind server | service -# --name gitea | i-uf63f2uookgs5uxmrdyc +# --name gitea | i-example0abcd1234efgh # --tag aliyun | production(可重复) # -q / --query 模糊关键词 # --file 输出文件路径,格式由扩展名推断(.json / .toml / .yaml / .yml) @@ -664,5 +666,6 @@ cargo fmt -- --check && cargo clippy -- -D warnings && cargo test |------|------| | `RUST_LOG` | 日志级别,如 `secrets=debug`、`secrets=trace`(默认 warn) | | `USER` | 审计日志 actor 字段来源,Shell 自动设置,通常无需手动配置 | +| `SECRETS_UPGRADE_URL` | upgrade 的 Release API 地址。构建时(cargo build)或运行时(.env/export) | 数据库连接通过 `secrets config set-db` 持久化到 `~/.config/secrets/config.toml`,不支持环境变量。 diff --git a/Cargo.lock b/Cargo.lock index fc9d1bf..1dc995c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1836,7 +1836,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "secrets" -version = "0.9.1" +version = "0.9.2" dependencies = [ "aes-gcm", "anyhow", @@ -1844,6 +1844,7 @@ dependencies = [ "chrono", "clap", "dirs", + "dotenvy", "flate2", "keyring", "rand 0.10.0", diff --git a/Cargo.toml b/Cargo.toml index 029d877..bbf240c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secrets" -version = "0.9.1" +version = "0.9.2" edition = "2024" [dependencies] @@ -10,6 +10,7 @@ argon2 = { version = "^0.5.3", features = ["std"] } chrono = { version = "^0.4.44", features = ["serde"] } clap = { version = "^4.6.0", features = ["derive"] } dirs = "^6.0.0" +dotenvy = "^0.15" flate2 = "^1.1.9" keyring = { version = "^3.6.3", features = ["apple-native", "windows-native", "linux-native"] } rand = "^0.10.0" diff --git a/README.md b/README.md index 1309a2d..624a5b7 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ secrets search -n refining --summary --limit 10 --offset 10 # 翻页 # ── add ────────────────────────────────────────────────────────────────────── secrets add -n refining --kind server --name my-server \ --tag aliyun --tag shanghai \ - -m ip=47.117.131.22 -m desc="Aliyun Shanghai ECS" \ + -m ip=10.0.0.1 -m desc="Example ECS" \ -s username=root -s ssh_key=@./keys/server.pem # 多行文件直接写入嵌套 secret 字段 @@ -136,7 +136,7 @@ secrets add -n refining --kind service --name deploy-bot \ secrets add -n refining --kind service --name gitea \ --tag gitea \ - -m url=https://gitea.refining.dev -m default_org=refining \ + -m url=https://code.example.com -m default_org=myorg \ -s token= # ── update ─────────────────────────────────────────────────────────────────── @@ -158,7 +158,7 @@ secrets config path # 打印配置文件路径 # ── upgrade ────────────────────────────────────────────────────────────────── secrets upgrade --check # 仅检查是否有新版本 -secrets upgrade # 下载、校验 SHA-256 并安装最新版(从 Gitea Release) +secrets upgrade # 下载、校验 SHA-256 并安装最新版(可通过 SECRETS_UPGRADE_URL 自托管) # ── export ──────────────────────────────────────────────────────────────────── secrets export --file backup.json # 全量导出到 JSON @@ -203,12 +203,12 @@ RUST_LOG=secrets=trace secrets search | 目标值 | 写法示例 | 实际存入 | |------|------|------| -| 普通字符串 | `-m url=https://gitea.refining.dev` | `"https://gitea.refining.dev"` | +| 普通字符串 | `-m url=https://code.example.com` | `"https://code.example.com"` | | 文件内容字符串 | `-m notes=@./service-notes.txt` | `"..."` | | 布尔值 | `-m enabled:=true` | `true` | | 数字 | `-m port:=3000` | `3000` | | `null` | `-m deprecated_at:=null` | `null` | -| 数组 | `-m domains:='["gitea.refining.dev","git.refining.dev"]'` | `["gitea.refining.dev","git.refining.dev"]` | +| 数组 | `-m domains:='["code.example.com","git.example.com"]'` | `["code.example.com","git.example.com"]` | | 对象 | `-m tls:='{"enabled":true,"redirect_http":true}'` | `{"enabled":true,"redirect_http":true}` | | 嵌套路径 + JSON | `-m deploy:strategy:='{"type":"rolling","batch":2}'` | `{"deploy":{"strategy":{"type":"rolling","batch":2}}}` | @@ -223,10 +223,10 @@ RUST_LOG=secrets=trace secrets search ```bash secrets add -n refining --kind service --name gitea \ - -m url=https://gitea.refining.dev \ + -m url=https://code.example.com \ -m port:=3000 \ -m enabled:=true \ - -m domains:='["gitea.refining.dev","git.refining.dev"]' \ + -m domains:='["code.example.com","git.example.com"]' \ -m tls:='{"enabled":true,"redirect_http":true}' ``` diff --git a/src/commands/search.rs b/src/commands/search.rs index a2e7cc4..fe277aa 100644 --- a/src/commands/search.rs +++ b/src/commands/search.rs @@ -542,7 +542,7 @@ mod tests { kind: "service".to_string(), name: "gitea.main".to_string(), tags: vec!["prod".to_string()], - metadata: json!({"url": "https://gitea.refining.dev", "enabled": true}), + metadata: json!({"url": "https://code.example.com", "enabled": true}), version: 1, created_at: Utc::now(), updated_at: Utc::now(), @@ -579,7 +579,7 @@ mod tests { assert_eq!( map.get("GITEA_MAIN_URL").map(String::as_str), - Some("https://gitea.refining.dev") + Some("https://code.example.com") ); assert_eq!( map.get("GITEA_MAIN_ENABLED").map(String::as_str), diff --git a/src/commands/upgrade.rs b/src/commands/upgrade.rs index 5a50d53..c8ce7ad 100644 --- a/src/commands/upgrade.rs +++ b/src/commands/upgrade.rs @@ -5,10 +5,26 @@ use sha2::{Digest, Sha256}; use std::io::{Cursor, Read, Write}; use std::time::Duration; -const GITEA_API: &str = "https://gitea.refining.dev/api/v1/repos/refining/secrets/releases/latest"; - const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// Build-time config via `option_env!("SECRETS_UPGRADE_URL")`. Set during `cargo build`, e.g.: +/// SECRETS_UPGRADE_URL=https://... cargo build --release +const BUILD_UPGRADE_URL: Option<&'static str> = option_env!("SECRETS_UPGRADE_URL"); + +fn upgrade_api_url() -> Result { + if let Some(url) = BUILD_UPGRADE_URL.filter(|s| !s.trim().is_empty()) { + return Ok(url.to_string()); + } + let url = std::env::var("SECRETS_UPGRADE_URL").context( + "SECRETS_UPGRADE_URL is not set at build or runtime. Set it when building: \ + SECRETS_UPGRADE_URL=https://... cargo build, or export before running secrets upgrade.", + )?; + if url.trim().is_empty() { + anyhow::bail!("SECRETS_UPGRADE_URL is empty."); + } + Ok(url) +} + #[derive(Debug, Deserialize)] struct Release { tag_name: String, @@ -186,13 +202,14 @@ pub async fn run(check_only: bool) -> Result<()> { .build() .context("failed to build HTTP client")?; + let api_url = upgrade_api_url()?; let release: Release = client - .get(GITEA_API) + .get(&api_url) .send() .await - .context("failed to fetch release info from Gitea")? + .context("failed to fetch release info")? .error_for_status() - .context("Gitea API returned an error")? + .context("release API returned an error")? .json() .await .context("failed to parse release JSON")?; diff --git a/src/main.rs b/src/main.rs index ac92040..b478657 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,11 @@ 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; @@ -76,25 +81,25 @@ EXAMPLES: # Add a server secrets add -n refining --kind server --name my-server \\ --tag aliyun --tag shanghai \\ - -m ip=47.117.131.22 -m desc=\"Aliyun Shanghai ECS\" \\ + -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://gitea.refining.dev -m default_org=refining \\ + -m url=https://code.example.com -m default_org=myorg \\ -s token= # Add typed JSON metadata secrets add -n refining --kind service --name gitea \\ -m port:=3000 \\ -m enabled:=true \\ - -m domains:='[\"gitea.refining.dev\",\"git.refining.dev\"]' \\ + -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.ricnsmart.com -m port=1883 \\ + -m host=mqtt.example.com -m port=1883 \\ -s password=@./mqtt_password.txt # Add typed JSON secrets @@ -114,7 +119,7 @@ EXAMPLES: /// Kind of record: server, service, key, ... #[arg(long)] kind: String, - /// Human-readable unique name, e.g. gitea, i-uf63f2uookgs5uxmrdyc + /// Human-readable unique name, e.g. gitea, i-example0abcd1234efgh #[arg(long)] name: String, /// Tag for categorization (repeatable), e.g. --tag aliyun --tag hongkong @@ -177,7 +182,7 @@ EXAMPLES: /// Filter by kind, e.g. server, service #[arg(long)] kind: Option, - /// Exact name filter, e.g. gitea, i-uf63f2uookgs5uxmrdyc + /// Exact name filter, e.g. gitea, i-example0abcd1234efgh #[arg(long)] name: Option, /// Filter by tag, e.g. --tag aliyun (repeatable for AND intersection) @@ -423,8 +428,8 @@ EXAMPLES: /// Check for a newer version and update the binary in-place. /// - /// Downloads the latest release from Gitea and replaces the current binary. - /// No database connection or master key required. + /// 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 @@ -530,6 +535,7 @@ enum ConfigAction { #[tokio::main] async fn main() -> Result<()> { + load_dotenv(); let cli = Cli::parse(); let filter = if cli.verbose { diff --git a/test-fixtures/example-key.pem b/test-fixtures/example-key.pem new file mode 100644 index 0000000..d2a80bd --- /dev/null +++ b/test-fixtures/example-key.pem @@ -0,0 +1,3 @@ +-----BEGIN EXAMPLE KEY PLACEHOLDER----- +This file is for local dev/testing. Replace with a real key when needed. +-----END EXAMPLE KEY PLACEHOLDER-----