fix(config): reject config writes when remote validation fails#142
Open
Episkey-G wants to merge 3 commits into
Open
fix(config): reject config writes when remote validation fails#142Episkey-G wants to merge 3 commits into
Episkey-G wants to merge 3 commits into
Conversation
config add/update 在远程校验失败时仍写盘,把一条「明显报错」的命令变成静默的 配置损坏: - config add 校验失败仍创建 profile,并把用户传入的 region/zone/project 抹成空 - 失败的 config add --active true 会踢掉原有 active profile,坏配置上位, 此后每条命令都用那个密钥为假、region 为空的 profile 且无线索指向病因 - config update 提前落盘 AK/SK,校验失败后 profile 停在半更新状态 - config update 的 project 校验失败会清空 ProjectID 并保存 病根是 HandleError 只报告、不中断控制流,调用方报错后照常往下写盘。三个命令 (config / config add / config update)对此有三种不一致的处理,不像有意设计。 统一为 fail-closed:远程校验失败即整体 return,不写入任何字段。该口径本就是 本仓库的原意 —— ucloud init 一直如此(configure.go:76-94),是 add/update 漂移了。 config update 改在 AggConfig 副本上校验,全部通过后才 UpdateAggConfig。 GetAggConfigByProfile 返回的是 manager map 内条目本身,原地改写会让校验失败后 的半成品被 OAuth token 刷新 handler 连带落盘。 errNoDefaultProject 保持放行:账号有项目但未设默认属良性缺失,一并拒绝会使该类 账号彻底无法配置。getReasonableProject 改用 %w 包装以恢复 errors.Is 链。 顺带修正: - 错误信息报告被检查的那个值。timeout/max-retry 此前恒报 accept 0 —— 后者尤其 隐蔽:%d 对 *int 打印的是指针地址(nil 即 0)而非值,且 go vet 不报(%d 对 指针在 Go 中合法) - max-retry-timesc 拼写 - config update 的 --public-key/--private-key 标注 Required → Optional(二者在 update 中实为可选,仅 profile 是 MarkFlagRequired) 测试:新增 6 个用例覆盖三个命令的失败路径与 errNoDefaultProject 放行,均经 FAIL→PASS 复现闭环验证。注意 CI 的 test job 不含 ./cmd/...,须本地跑 go test ./cmd/... -count=1。 不含 D6(--flag=value 静默失效),已拆为子任务与 root.go 的 os.Args 扫描合并治理。
|
🔴 平台 PR 默认硬拦,需管理员 Approve 放行(或由管理员提交)。判定:改动触及平台/受保护路径(cmd/configure.go, cmd/configure_test.go, cmd/region.go) |
config add 漏了主命令与 config update 都有的 PickResourceID 归一化。 getProjectList 补全吐出的是 org-xxx/Name 形式(project.go:167),用户接受补全后 config add 收到的 --project-id 带斜杠,getReasonableProject 在裸 id 的 map 里查不到, 判为「project does not exist」。 叠加同批的 fail-closed 改动后果被放大:此前该错判会 HandleError 后继续、落盘一个 project 为空的 profile;fail-closed 后直接硬拦——一个真实存在的 project 反而让整条 config add 失败。属既有缺陷(归一化三命令不一致),但正落在本次收紧的路径上,故一并修。 新增 TestConfigAddNormalizesSlashProjectID:--project-id org-123/Default(真实存在) 必须归一化为 org-123 并建成 profile。已验证回退修复后该用例变红(非 tautological)。
config add 的 timeout_sec<=0 本地检查排在 getReasonableRegionZone 远程校验之后, 与主命令(最先检查 timeout)不一致。坏 timeout 会先被喂进校验请求: --timeout-sec 0 → http.Client 变成无超时、照打网关;负值 → 请求瞬间失败。 两种情况下坏 timeout 都先污染了校验请求,用户看到的是 region 错、看不到真正的 timeout 错,还白打一次网络。叠加同批 fail-closed 后症状更糟——region 错直接 return,那个干净的「timeout must be > 0」永远执行不到。 把本地 timeout 检查移到远程校验之前,与主命令对齐:属「连接类配置让自己的校验 请求失败」这一类的一个实例(同类中 timeout 是纯本地约束,可零成本前置消除)。 新增 TestConfigAddChecksTimeoutBeforeValidation:--timeout-sec 0 + 毒网关(被调用 即失败),断言拒绝发生在任何远程调用之前。已验证修复前该用例因 GetRegion 被调用 而红。
Episkey-G
enabled auto-merge
July 17, 2026 16:13
Episkey-G
disabled auto-merge
July 17, 2026 16:13
Episkey-G
enabled auto-merge
July 17, 2026 16:14
This was referenced Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
config add/config update在远程校验失败时仍写盘,把一条明显报错的命令变成静默的配置损坏。均为既有缺陷,与 #141 (channel-key) 无关,git 历史可追溯至更早。config add校验失败仍创建 profile,且把用户传入的--region cn-bj2 --zone cn-bj2-02 --project-id org-xxx全部抹成空config add --active true会把原有 active profile 踢下去,坏配置上位。用户看到报错以为无事发生,此后每条命令都用那个密钥为假、region 为空的 profile,且无任何线索指向病因config update提前落盘 AK/SK,校验失败后 profile 停在「密钥已换、region/project 未换」的半更新状态config update的 project 校验失败会清空ProjectID并保存timeout_sec为 -5 时报accept 0)max-retry-timesc拼写错误config update的--public-key/--private-key误标Required病根:
HandleError只报告、不中断控制流,调用方报错后照常往下写盘。三个命令对「校验失败」有三种不一致的处理,不像有意设计,更像历史叠加:config(主命令)config addconfig updatereturn✅方案
统一为 fail-closed:远程校验失败即整体
return,不写入任何字段。这个口径本就是本仓库的原意 ——
ucloud init一直是 fail-closed(configure.go:76-94),是add/update漂移了。另外,现状 fail-open 落盘的是region='' project_id=''的残缺 profile,本就不可用,所以「离线仍能配」是假象,fail-closed 不损失任何真实能力。三处值得单独说明的决策:
1.
errNoDefaultProject必须放行。 账号「有项目但未设默认」属良性,init 认得并放行。但getReasonableProject此前用%v而非%w包装,切断了errors.Is链 —— 若 fail-closed 无脑 return,这类账号会从「能配」变成彻底无法配置。改用%w并在调用方errors.Is放行,口径与 init 一致。2.
config update改在副本上校验。GetAggConfigByProfile返回的是 manager map 内条目本身,原地改写会让校验失败后的半成品被 OAuth token 刷新 handler 连带落盘。改为draft := *cacheConfig,全部通过后才UpdateAggConfig(&draft)。3. D3 的提前落盘直接删除,无需替代。
BuildClientRuntime完全从传入的AggConfig构造 sdk.Config 与 credential,不读磁盘 —— AK/SK 写进内存即可供校验使用,那次UpdateAggConfig纯属多余。测试
新增 6 个用例,覆盖三个命令的失败路径 +
errNoDefaultProject放行。每条都走完 FAIL→PASS 的复现闭环;主命令那条因写在修复之后,另经临时回退验证其确实会变红,排除 tautological test。复用既有
fakeGatewayServer(httptest 假网关)并扩展为可按 action 指定响应 —— 这使「region 通过但 project 失败」这类组合可精确构造,D4 因此能进单测而非只靠代码审查。D5/D7 用离线实测验证(两处检查都在 region 校验之前,不发网络请求):
max-retry-times这条尤其隐蔽:%d对*int打印的是指针地址(nil 即 0)而非值,且go vet不报 ——%d对指针在 Go 中是合法的,按整数格式化地址。行为变更(用户可见,需入发版说明)
config addconfig add --active trueconfig updateconfig updateconfig(主命令)config(主命令)project_id留空(与 init 一致)成功路径零变化,
go test ./...全绿(与基线逐包一致),go vet干净。评审中追加发现(同批修复)
fail-closed 的前提是「校验失败 == 输入确实坏」。评审时专门反查了「合法输入被校验错判」的场景,修了两个:
D9 —
config add漏归一化 project-id(commit 2)。getProjectList补全吐出的是org-xxx/Name形式,主命令与update都用PickResourceID剥成裸 id,唯独add没有。合法的补全值会被getReasonableProject判「project 不存在」——叠加 fail-closed 后,一个真实存在的 project 反被硬拦。补上归一化,与另两命令对齐。D10 —
config add本地 timeout 检查排在远程校验之后(commit 3)。timeout_sec <= 0是纯本地约束,却排在getReasonableRegionZone之后。坏 timeout 先被喂进校验请求(0→ http.Client 无超时、照打网关;负值 → 请求瞬败),用户看到 region 错、看不到真正的 timeout 错,还白打一次网络。移到远程校验之前,与主命令一致。两者都用测试锁死(D9 归一化用例、D10 用毒网关断言「拒绝发生在任何远程调用之前」),均验证过回退修复即变红。
已知权衡:fail-closed 分不清「资源不存在」与「连接不上」
fail-closed(本 PR 选定)对「连接类配置让自己的校验请求失败」的子类有代价:
--timeout-sec 1过激、网络抖动、base-url 打不通等,会让合法配置也存不进去(尤其config add必须校验、无法跳过)。这是有意接受的权衡——相较之下 fail-open 存的是残缺配置,更糟。「只改元数据不该触发远程校验」的离线痛点由独立任务config-conditional-validation缓解(针对update/主命令的改存量路径;add新建仍需校验)。D10 是这一类中唯一能零成本前置消除的实例(纯本地约束),故并入本 PR。不在本 PR 范围
--flag=value静默失效):root.go:306的 os.Args 手写扫描用精确比较,--base-url=X不匹配故被无视。属用户可见行为变更(尤其--profile=X会从「静默失效」变为「真正生效」,可能让某些当前碰巧能跑的脚本切换 profile),需独立评估,已拆为子任务。ucloud init无条件重置用户自定义 base-url(configure.go:102):已有独立任务。config主命令未采用 draft 副本:属已知残留,落盘行为已由用例守住;单进程 CLI 下内存污染不可观测,若将来引入长驻进程需补。