修复校验任务幂等键并更新双版本数据库回归进度
This commit is contained in:
parent
24a9683c43
commit
4ab3f93beb
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 业务范围
|
## 业务范围
|
||||||
|
|
||||||
证书申请、当前证书部署、证书导入及申请/部署手动重试先持久化任务,再交给后台工作线程执行。浏览器断线不取消已接收工作,前端通过任务接口确认最终结果。提交成功、证书保存成功、逐服务器推送成功、整体任务成功是不同层级,不能混用。
|
证书申请、当前证书部署、证书导入、本地证书校验及申请/部署手动重试先持久化任务,再交给后台工作线程执行。浏览器断线不取消已接收工作,前端通过任务接口确认最终结果。提交成功、证书保存成功、本地校验成功、逐服务器推送成功、整体任务成功是不同层级,不能混用。
|
||||||
|
|
||||||
## 接口与权限
|
## 接口与权限
|
||||||
|
|
||||||
@ -15,12 +15,14 @@
|
|||||||
| POST /api/v1/domains/:id/issue | 提交申请及关联部署,无请求体 | 可见域名、证书管理、部署执行、DNS 凭据使用权限;202 |
|
| POST /api/v1/domains/:id/issue | 提交申请及关联部署,无请求体 | 可见域名、证书管理、部署执行、DNS 凭据使用权限;202 |
|
||||||
| POST /api/v1/domains/:id/deploy | 提交已有证书部署,无请求体 | 可见域名、部署执行权限;202 |
|
| POST /api/v1/domains/:id/deploy | 提交已有证书部署,无请求体 | 可见域名、部署执行权限;202 |
|
||||||
| POST /api/v1/domains/:id/certificate/import | 上传证书后提交导入及部署任务 | 可见域名、证书管理、部署执行权限;不要求 DNS 凭据权限;202 |
|
| POST /api/v1/domains/:id/certificate/import | 上传证书后提交导入及部署任务 | 可见域名、证书管理、部署执行权限;不要求 DNS 凭据权限;202 |
|
||||||
|
| POST /api/v1/domains/:id/certificate/validate | 提交当前本地证书校验,无请求体 | 可见域名、私钥读取权限;不额外要求证书管理、DNS 或部署执行权限;202 |
|
||||||
| POST /api/v1/tasks/:id/retry | 同一任务新增一次尝试,无请求体 | 重试、证书管理、部署执行权限;申请类还检查 DNS 凭据;202 |
|
| POST /api/v1/tasks/:id/retry | 同一任务新增一次尝试,无请求体 | 重试、证书管理、部署执行权限;申请类还检查 DNS 凭据;202 |
|
||||||
| DELETE /api/v1/tasks/:id | 请求取消 | 任务可见及取消权限;后台任务返回 202,非异步历史任务可能返回 200 |
|
| DELETE /api/v1/tasks/:id | 请求取消 | 任务可见及取消权限;后台任务返回 202,非异步历史任务可能返回 200 |
|
||||||
| GET /api/v1/tasks | 当前身份可见任务列表 | 支持 domain_id 和 limit;返回 200 与 items 数组 |
|
| GET /api/v1/tasks | 当前身份可见任务列表 | 支持 domain_id 和 limit;返回 200 与 items 数组 |
|
||||||
| GET /api/v1/tasks/:id | 单个任务最新状态 | 返回 200 与任务对象本身,不包 task 字段 |
|
| GET /api/v1/tasks/:id | 单个任务最新状态 | 返回 200 与任务对象本身,不包 task 字段 |
|
||||||
| GET /api/v1/tasks/:id/attempts | 任务尝试历史 | 返回 200 与 items 数组 |
|
| GET /api/v1/tasks/:id/attempts | 任务尝试历史 | 返回 200 与 items 数组 |
|
||||||
| GET /api/v1/tasks/:id/deployment-results | 逐尝试的逐服务器结果与完整报告 | 返回 200、items 和 reports 数组;按任务、最近尝试编号及尝试 ID 精确匹配 |
|
| GET /api/v1/tasks/:id/deployment-results | 逐尝试的逐服务器结果与完整报告 | 返回 200、items 和 reports 数组;按任务、最近尝试编号及尝试 ID 精确匹配 |
|
||||||
|
| GET /api/v1/tasks/:id/validation-results | 读取本地校验公开元数据 | 任务读取权限;返回 200 与 items 数组,仓储查询实时复核可见范围 |
|
||||||
|
|
||||||
列表 limit 默认 50,有效范围为 1~200,越界由仓储回退至 50。不存在和无权访问的任务都返回 403,不能根据接口泄露其他项目资源是否存在。
|
列表 limit 默认 50,有效范围为 1~200,越界由仓储回退至 50。不存在和无权访问的任务都返回 403,不能根据接口泄露其他项目资源是否存在。
|
||||||
|
|
||||||
@ -56,7 +58,7 @@
|
|||||||
|
|
||||||
### 幂等与错误
|
### 幂等与错误
|
||||||
|
|
||||||
- 首次申请和部署支持 Idempotency-Key:长度 8~128,只允许英文字母、数字、连字符、下划线及英文句点。前端当前为每次明确操作生成新键,网络重发若需要去重必须复用原键。
|
- 首次申请、部署、导入和校验支持 Idempotency-Key:长度 8~128,只允许英文字母、数字、连字符、下划线及英文句点。校验前端使用 certificate-validate 前缀,不将接口路径中的斜线写入请求头。前端当前为每次明确操作生成新键,网络重发若需要去重必须复用原键。
|
||||||
- 同一域名、同一键命中已结束任务返回 200、task 和 replayed: true;命中活动任务返回 409、task 和 replayed: true,不再次执行。跨操作复用同一键返回 409。
|
- 同一域名、同一键命中已结束任务返回 200、task 和 replayed: true;命中活动任务返回 409、task 和 replayed: true,不再次执行。跨操作复用同一键返回 409。
|
||||||
- 同类活动任务、仍在收尾、成功任务重试或不支持的任务类型返回 409。部署的 partial_success 允许重试;申请类只允许 failed 或 cancelled。
|
- 同类活动任务、仍在收尾、成功任务重试或不支持的任务类型返回 409。部署的 partial_success 允许重试;申请类只允许 failed 或 cancelled。
|
||||||
- 取消 202 只说明接收取消意图,状态为 cancelling;不能提前显示已取消或允许重试。已经完成的外部操作仍可最终成功,并提示取消未撤销该操作。
|
- 取消 202 只说明接收取消意图,状态为 cancelling;不能提前显示已取消或允许重试。已经完成的外部操作仍可最终成功,并提示取消未撤销该操作。
|
||||||
@ -71,6 +73,7 @@
|
|||||||
| TaskAttempt | id、task_id、attempt_number、stage、status、message、scope_type、project_id、created_at;可选 started_at、finished_at |
|
| TaskAttempt | id、task_id、attempt_number、stage、status、message、scope_type、project_id、created_at;可选 started_at、finished_at |
|
||||||
| TaskDeploymentResult | task_id、attempt_id、attempt_number、result_index、server_id、server、status、message、created_at;可选 backup_id |
|
| TaskDeploymentResult | task_id、attempt_id、attempt_number、result_index、server_id、server、status、message、created_at;可选 backup_id |
|
||||||
| TaskDeploymentReport | task_id、attempt_id、attempt_number、report、created_at;report 保留汇总、逐目标结果、verification_status 与 recovery_status |
|
| TaskDeploymentReport | task_id、attempt_id、attempt_number、report、created_at;report 保留汇总、逐目标结果、verification_status 与 recovery_status |
|
||||||
|
| TaskValidationResult | task_id、attempt_id、attempt_number、validation、created_at;validation 包含 domains、serial_number、fingerprint_sha256、not_before、not_after、chain_length |
|
||||||
|
|
||||||
编号和时间为字符串,时间使用带时区的 RFC3339 表示;attempt_number 与 result_index 为整数。Task 与 TaskAttempt 的状态含义如下:
|
编号和时间为字符串,时间使用带时区的 RFC3339 表示;attempt_number 与 result_index 为整数。Task 与 TaskAttempt 的状态含义如下:
|
||||||
|
|
||||||
@ -119,6 +122,17 @@ deployment-results 响应保留旧 items,新增 reports;每次尝试独立
|
|||||||
|
|
||||||
## 执行及前端交互边界
|
## 执行及前端交互边界
|
||||||
|
|
||||||
|
### 本地证书校验
|
||||||
|
|
||||||
|
- 提交返回 202,task_type 为 certificate_validate。提交与取得执行槽之后均复核私钥读取权限和域名归属;响应断开不取消已接收工作,不返回证书或私钥路径。
|
||||||
|
- 只有成功校验才保存 validation;失败、排队取消、关停、执行异常不生成通过结果。校验结果、任务、尝试和完成审计共用一个事务,不生成空的部署报告。
|
||||||
|
- validation 中 domains 为字符串数组,serial_number 和 fingerprint_sha256 为字符串,not_before 和 not_after 为带时区时间,chain_length 为整数。结果读取形式为 items 数组,每条记录关联原 task_id、attempt_id 和 attempt_number。
|
||||||
|
- 前端仅为校验任务额外读取 validation-results;任务与最近一次尝试均为 success,且三个关联字段全部一致时展示结果。切换、关闭详情和撤权后清除缓存;历史成功任务没有详情时明确说明未留存,不推断当前证书仍然有效。
|
||||||
|
- 结果只证明执行时的本地证书、私钥匹配及信任链校验,不代表远端部署、HTTPS 或生产验收通过。
|
||||||
|
- 校验不接入通用手动重试入口,POST /tasks/:id/retry 返回 409。用户需要重新校验时,应从域名校验入口明确创建新任务。
|
||||||
|
|
||||||
|
### 公共执行约束
|
||||||
|
|
||||||
- 当前后台最多接收 32 个在途任务,4 个执行槽,总执行预算 30 分钟;这些是实现限制,不是保证耗时。
|
- 当前后台最多接收 32 个在途任务,4 个执行槽,总执行预算 30 分钟;这些是实现限制,不是保证耗时。
|
||||||
- 队列满或关闭时拒绝执行,并尝试保存失败终态;持久化失败保留中断状态,不伪造已完成。
|
- 队列满或关闭时拒绝执行,并尝试保存失败终态;持久化失败保留中断状态,不伪造已完成。
|
||||||
- 服务关闭停止接收、发送取消信号并等待工作线程收尾。启动恢复将中断记录标为失败,不自动重放外部签发、部署。
|
- 服务关闭停止接收、发送取消信号并等待工作线程收尾。启动恢复将中断记录标为失败,不自动重放外部签发、部署。
|
||||||
@ -153,5 +167,6 @@ deployment-results 响应保留旧 items,新增 reports;每次尝试独立
|
|||||||
- [x] HTTP 异步完成审计、逐服务器结果与任务终态同事务,已覆盖三类保存失败整体回滚;其他工作流及三库实机复审仍待完成。
|
- [x] HTTP 异步完成审计、逐服务器结果与任务终态同事务,已覆盖三类保存失败整体回滚;其他工作流及三库实机复审仍待完成。
|
||||||
- [x] 后台证书共用执行器新增五处故障回滚、每次重试报告、等待取消及无活动尝试收尾回归;应用、仓储、HTTP 三包原生 Linux 测试、全后端测试及 go vet 均通过,未正式部署。
|
- [x] 后台证书共用执行器新增五处故障回滚、每次重试报告、等待取消及无活动尝试收尾回归;应用、仓储、HTTP 三包原生 Linux 测试、全后端测试及 go vet 均通过,未正式部署。
|
||||||
- [x] 生产导入入口及前端任务跟踪已接入异步执行;专项回归通过,未正式部署。
|
- [x] 生产导入入口及前端任务跟踪已接入异步执行;专项回归通过,未正式部署。
|
||||||
- [ ] 版本恢复、校验及手动续期等其他长操作尚未全部异步化;当前历史版本恢复的真实实现仍拒绝恢复,不能用模拟工作流测试替代功能验收。
|
- [x] 校验入口、独立结果持久化和详情展示已接入异步任务;前端幂等键契约缺陷已先复现后修复,最新 266 项测试及生产构建通过,尚未正式部署。
|
||||||
|
- [ ] 版本恢复及手动续期等其他长操作尚未全部异步化;当前历史版本恢复的真实实现仍拒绝恢复,不能用模拟工作流测试替代功能验收。
|
||||||
- [ ] 正式构建部署后验证超过反代等待窗口的真实操作、断线查询、取消回滚和服务重启;本说明不替代生产验收。
|
- [ ] 正式构建部署后验证超过反代等待窗口的真实操作、断线查询、取消回滚和服务重启;本说明不替代生产验收。
|
||||||
|
|||||||
@ -1,5 +1,29 @@
|
|||||||
# Linux 生产流程测试记录
|
# Linux 生产流程测试记录
|
||||||
|
|
||||||
|
## 2026年10月2日20:34 证书校验后台任务及幂等契约回归(未部署)
|
||||||
|
|
||||||
|
### 代码与隔离边界
|
||||||
|
|
||||||
|
- [x] 接续 fce5a84 校验开发批次,修复前精确提交推送 24a9683;没有暂存测试归档、日志、密钥或凭据。本批只使用原生 Go/前端用例,没有创建辅助验收脚本。
|
||||||
|
- [x] 最终后端增量 validation-async-second-20261002.tar 的本机与双机 SHA256 均为 3f6d01008441f2053b7098732bf622aab16b6d9a34ee513967ff1deaa437227b。第一台额外使用 fce5a84 源码基线归档,SHA256 为 a82b68293c7ce1d0a8155e08164a3a6ad2b6ec57df8465731da1a6fad673aca2;均只包含源码,不是正式发布包。
|
||||||
|
- [x] 密码 SSH/SCP 沿用严格主机密钥校验,未启用公钥登录。第二台复用 /var/tmp/scert-async-20261002-1556/source/backend;第一台使用 /var/tmp/scert-report-race-20261002-1801/validation-source/backend。测试以 nobody 执行,GOMAXPROCS=1、GOMEMLIMIT=384MiB、-p 1。
|
||||||
|
|
||||||
|
### 后端与实库证据
|
||||||
|
|
||||||
|
- [!] validation-second-full.log:第一次全量测试在 SSH 被重置后收到 Hangup;重连确认没有测试进程才重新启动。此记录不记为通过,未覆盖原始日志。
|
||||||
|
- [x] 第二台 validation-final-full-vet.log:go test -p 1 ./... -count=1 与 go vet -p 1 ./... 完成,VALIDATION_FINAL_FULL_EXIT=0、VALIDATION_FINAL_VET_EXIT=0。外部实库默认跳过,不把此结果当成三库完整验收。
|
||||||
|
- [x] 第二台 validation-final-repeat.log:TestAsyncValidation、TestAsyncExecutionRechecksQueuedAuthorization、TestTaskValidation 重复二十次通过,VALIDATION_FINAL_REPEAT_EXIT=0;HTTP 包 158.647 秒,仓储包 16.395 秒。覆盖独立结果、无伪报告、失败审计原子回滚、撤权与排队取消等路径,不替代竞争检测。
|
||||||
|
- [x] 第一台 validation-mysql21.log:既有 /usr/sbin/mysqld 8.0.46 启动独立空实例,仅监听 127.0.0.1:42647,严格 TLS。TestMySQLRealTaskValidationMigrationRecovery 八场景通过,VALIDATION_MYSQL21_EXIT=0,45.369 秒。
|
||||||
|
- [x] 第一台 validation-mysql57.log:复用既有 MySQL 5.7.44 程序,独立空实例仅监听 127.0.0.1:44333,严格 TLS;同一八场景通过,VALIDATION_MYSQL57_EXIT=0,21.575 秒。覆盖建表前后恢复、已有详情保持,以及字段容量、主键顺序、空值、引擎和额外字段冲突拒绝。
|
||||||
|
- [x] 两个独立 MySQL 测试实例均已退出;正式 MySQL 进程仍为 1092。第一台 Scert、Nginx、MySQL、Xmail 和第二台 Scert、Nginx、MySQL 均 active。第二台磁盘剩余约 774MB,未在该机新增数据库实例或擅自清理产物。
|
||||||
|
|
||||||
|
### 前端证据与未关闭项
|
||||||
|
|
||||||
|
- [x] .runtime/validation-async-frontend-second-20261002.log:上一轮组件注册修复后 263 项通过,生产构建通过。但审查随后发现校验幂等键含斜线,旧测试只判断请求头存在,未覆盖后端合法字符契约。
|
||||||
|
- [x] .runtime/validation-idempotency-contract-before-20261002.log:补充断言后四个相关测试中两个失败,明确复现 certificate/validate 前缀问题;申请、部署对照用例通过。修复使用 certificate-validate 前缀,没有放宽后端输入限制。
|
||||||
|
- [x] .runtime/validation-async-frontend-final-20261002.log:最终 266 项全部通过,零失败、零跳过;.runtime/validation-async-build-final-20261002.log:最终生产构建退出零。新详情组件包含真实 Vue 模板编译和服务端渲染回归,不称为浏览器实机验证。
|
||||||
|
- [!] 未替换线上程序、未清空线上数据库、未新增公网监听。本批不是正式包或生产部署验收;PostgreSQL 实库、三库首次 Web、历史恢复、其他长操作、升级、通知重置、备份恢复和完整生产门禁仍未关闭。
|
||||||
|
|
||||||
## 2026年10月2日19:34 证书导入异步自动回归(未部署)
|
## 2026年10月2日19:34 证书导入异步自动回归(未部署)
|
||||||
|
|
||||||
- [x] 修改前确认 main 工作区干净,09616e7 基线推送返回已同步。第二台复用已核验主机指纹的密码 SSH 会话;隔离路径 /var/tmp/scert-async-20261002-1556/source/backend,不触碰正式安装或线上数据。
|
- [x] 修改前确认 main 工作区干净,09616e7 基线推送返回已同步。第二台复用已核验主机指纹的密码 SSH 会话;隔离路径 /var/tmp/scert-async-20261002-1556/source/backend,不触碰正式安装或线上数据。
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
# Scert 整改任务执行清单
|
# Scert 整改任务执行清单
|
||||||
|
|
||||||
## 2026年10月2日 证书校验后台任务执行计划
|
## 2026年10月2日20:34 证书校验后台任务回归完成,等待复审
|
||||||
|
|
||||||
本批需求沿用已确认的 P0-10、P1-04、P2-03 清单:先开发和自动回归,再统一生产部署;不处理 Windows。修改前已提交并推送 fce5a84。
|
本批需求沿用已确认的 P0-10、P1-04、P2-03 清单:先开发和自动回归,再统一生产部署;不处理 Windows。修改前已提交并推送 fce5a84。
|
||||||
|
|
||||||
- [~] 校验入口接入持久化后台任务;提交和执行前均检查现有私钥读取权限,不误加部署或 DNS 权限。
|
- [x] 20:25 补充审查发现前端校验幂等键含路径分隔符。提交推送 24a9683 修复前基线后,新增契约断言先复现两个失败,再改用 certificate-validate 前缀;保留后端字符与长度限制。申请、部署、校验三种真实组合式提交均验证每次新操作使用独立合法键。
|
||||||
- [ ] 校验元数据独立持久化,与任务、尝试和审计终态同事务提交;不伪装部署报告,不保存文件路径或私钥。
|
|
||||||
- [ ] 前端跟踪校验任务,在任务详情展示校验域名、指纹、有效期、序列号和链长度;刷新与撤权保持真实状态。
|
- [x] 校验入口接入持久化后台任务;提交和执行前均检查现有私钥读取权限,不误加部署、证书管理或 DNS 权限。队列、取消、关停和异常回收复用公共工作线程,不重复创建任务。
|
||||||
- [ ] 补充异步生命周期、事务故障回滚、实时权限和前端回归;核对迁移兼容性并更新接口说明及测试记录。
|
- [x] 校验元数据独立持久化,与任务、尝试和审计终态同事务提交;失败或取消不保存通过结果,不伪装部署报告,不保存文件路径或私钥。新增三库第 21 号迁移。
|
||||||
|
- [x] 前端跟踪校验任务,在任务详情展示校验域名、指纹、有效期、序列号和链长度;按任务及最近成功尝试精确匹配,切换、关闭及撤权清空缓存,明确本地校验不等于远端 HTTPS 验收。
|
||||||
|
- [x] 第二台原生 Linux 全后端测试及 go vet 通过;校验生命周期、排队授权和仓储事务专项重复二十次通过。首次全量测试因 SSH 断开收到 Hangup,确认进程退出后重新执行,未覆盖失败日志。
|
||||||
|
- [x] 第一台独立 MySQL 8.0.46 与 5.7.44 的第 21 号迁移各通过八个恢复/冲突场景;只监听回环,严格 TLS,测试后独立实例退出。此结果不替代 PostgreSQL 实库或三库首次 Web 流程。
|
||||||
|
- [x] 最新前端 266 项全部通过、零跳过,生产构建通过;真实模板编译与服务端渲染覆盖新结果组件。已同步本清单、整改清单、Linux 测试记录及异步接口说明。
|
||||||
- [!] 本批通过不关闭历史恢复、其他长操作、升级、通知重置、三库、备份恢复及最终生产门禁。
|
- [!] 本批通过不关闭历史恢复、其他长操作、升级、通知重置、三库、备份恢复及最终生产门禁。
|
||||||
|
- [!] 本批未正式部署、未清理线上数据库、未创建辅助验收脚本;竞争检测仍无本批通过证据。第二台磁盘约剩 774MB,后续正式构建前必须重新评估容量,不据此擅自删除文件。
|
||||||
|
|
||||||
## 2026年10月2日19:34 证书导入异步化回归完成,等待复审
|
## 2026年10月2日19:34 证书导入异步化回归完成,等待复审
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,15 @@
|
|||||||
# Scert 整改、合并与优化清单
|
# Scert 整改、合并与优化清单
|
||||||
|
|
||||||
|
## 2026年10月2日20:34 当前开发进度:证书校验后台化完成待复审
|
||||||
|
|
||||||
|
当前开发批次对应 P0-10/P1-04/P2-03;不是仍停留在 P0-03 的代码开发,也不代表这些整项已经复审通过。下方带时间的记录保留为历史证据,最新状态以本入口和执行清单为准。原 P0/P1/P2/P3 范围不缩减,Windows 按本轮要求不处理。
|
||||||
|
|
||||||
|
- [x] 校验生产入口返回持久化后台任务,复用排队、取消、关停及执行前实时权限;只保留原有私钥读取权限要求,不误加 DNS、证书管理或部署权限。
|
||||||
|
- [x] 三库第 21 号迁移及独立公开校验结果存储已实现;成功结果、任务、尝试和审计同事务,不暴露私钥路径,不将校验伪装成部署报告。
|
||||||
|
- [x] 前端读取校验详情,按最近成功尝试展示域名、指纹、有效期等信息;补齐前端幂等键非法斜线缺陷,先复现再修复。最新前端 266 项及生产构建通过。
|
||||||
|
- [x] 原生 Linux 全后端、go vet 及相关专项二十次重复回归通过;第一台独立 MySQL 8.0.46、5.7.44 均通过新增迁移的八类恢复与冲突场景,未触碰线上数据库。
|
||||||
|
- [!] 尚未正式部署或进行浏览器生产验收;历史版本恢复、手动续期等长操作、升级重启 502、通知重置、三库首次 Web、备份恢复、监控及 P3 继续保留。普通重复回归不替代竞争检测。
|
||||||
|
|
||||||
## 2026年10月2日19:34 证书导入异步化开发进度
|
## 2026年10月2日19:34 证书导入异步化开发进度
|
||||||
|
|
||||||
- [x] P2-03 子项:生产导入入口返回后台任务,复用任务队列、实时权限复核及事务收尾,不重复创建任务。上传文件在请求结束后保留到工作线程完成,并覆盖拒绝、取消、关停及异常回收。
|
- [x] P2-03 子项:生产导入入口返回后台任务,复用任务队列、实时权限复核及事务收尾,不重复创建任务。上传文件在请求结束后保留到工作线程完成,并覆盖拒绝、取消、关停及异常回收。
|
||||||
@ -1757,7 +1767,7 @@ cancelled 已取消
|
|||||||
|
|
||||||
**状态:** `[~] 进行中`
|
**状态:** `[~] 进行中`
|
||||||
|
|
||||||
**2026年10月2日19:34 子项进度:** 申请、当前证书部署、申请/部署重试及生产证书导入已接入 HTTP 异步任务;导入增加上传生命周期、执行前授权、幂等找回和保存结果持久化,自动回归通过待复审。校验、历史恢复及其余列出的长操作不因本批通过而关闭,整项继续进行中。
|
**2026年10月2日20:34 子项进度:** 申请、当前证书部署、申请/部署重试、生产证书导入及本地证书校验已接入 HTTP 异步任务;导入具有上传生命周期和保存标志,校验具有独立元数据及最近尝试详情,均复核执行前权限。最新自动回归通过待复审,尚未统一生产部署;手动续期、历史恢复及其余列出的长操作不因本批通过而关闭,整项继续进行中。
|
||||||
|
|
||||||
以下操作不能长期阻塞 HTTP 请求:
|
以下操作不能长期阻塞 HTTP 请求:
|
||||||
|
|
||||||
|
|||||||
@ -590,7 +590,8 @@ export function useDashboard() {
|
|||||||
const options = { method: "POST" };
|
const options = { method: "POST" };
|
||||||
if (action === "issue" || action === "deploy" || action === "certificate/validate") {
|
if (action === "issue" || action === "deploy" || action === "certificate/validate") {
|
||||||
const suffix = crypto.randomUUID ? crypto.randomUUID() : createRandomSuffix();
|
const suffix = crypto.randomUUID ? crypto.randomUUID() : createRandomSuffix();
|
||||||
options.headers = { "Idempotency-Key": `${action}-${item.id}-${suffix}` };
|
const prefix = action === "certificate/validate" ? "certificate-validate" : action;
|
||||||
|
options.headers = { "Idempotency-Key": `${prefix}-${item.id}-${suffix}` };
|
||||||
}
|
}
|
||||||
const response = await fetchAdmin(`/api/v1/domains/${item.id}/${action}`, options);
|
const response = await fetchAdmin(`/api/v1/domains/${item.id}/${action}`, options);
|
||||||
const result = await response.json().catch(() => ({}));
|
const result = await response.json().catch(() => ({}));
|
||||||
|
|||||||
@ -41,7 +41,7 @@ test("校验任务提交后读取独立详情,页面重开可恢复,撤权
|
|||||||
const { state } = await mountComposable(t, useDashboard, async (path, options = {}) => {
|
const { state } = await mountComposable(t, useDashboard, async (path, options = {}) => {
|
||||||
if (path.endsWith("/certificate/validate")) {
|
if (path.endsWith("/certificate/validate")) {
|
||||||
submitted++;
|
submitted++;
|
||||||
assert.ok(options.headers["Idempotency-Key"]);
|
assert.match(options.headers["Idempotency-Key"], /^[A-Za-z0-9._-]{8,128}$/);
|
||||||
return Response.json({ task, accepted: true }, { status: 202 });
|
return Response.json({ task, accepted: true }, { status: 202 });
|
||||||
}
|
}
|
||||||
if (path === `/api/v1/tasks/${task.id}`) return Response.json(task);
|
if (path === `/api/v1/tasks/${task.id}`) return Response.json(task);
|
||||||
@ -59,6 +59,7 @@ test("校验任务提交后读取独立详情,页面重开可恢复,撤权
|
|||||||
});
|
});
|
||||||
await state.validateDomain({ id: "domain-1", name: "example.test" });
|
await state.validateDomain({ id: "domain-1", name: "example.test" });
|
||||||
assert.equal(submitted, 1);
|
assert.equal(submitted, 1);
|
||||||
|
assert.equal(state.error.value, "");
|
||||||
assert.equal(state.showTaskDetails.value, true);
|
assert.equal(state.showTaskDetails.value, true);
|
||||||
assert.match(state.notice.value, /任务已提交/);
|
assert.match(state.notice.value, /任务已提交/);
|
||||||
assert.equal(state.taskValidationResult.value, null);
|
assert.equal(state.taskValidationResult.value, null);
|
||||||
@ -158,6 +159,31 @@ test("完整报告无逐目标结果仍可读取,执行成功不能覆盖任
|
|||||||
assert.equal(latestDeploymentReport(initialTask, attempts, [], reports), null);
|
assert.equal(latestDeploymentReport(initialTask, attempts, [], reports), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const [method, type, prefix] of [
|
||||||
|
["issueDomain", "certificate_issue", "issue"],
|
||||||
|
["deployDomain", "certificate_deploy", "deploy"],
|
||||||
|
["validateDomain", "certificate_validate", "certificate-validate"],
|
||||||
|
]) {
|
||||||
|
test(`域名操作 ${prefix} 生成符合后端契约且每次独立的幂等键`, async (t) => {
|
||||||
|
const posts = [];
|
||||||
|
const item = { id: "2fd1f484-0adc-4a14-a592-fd9342dc5720", name: "example.test" };
|
||||||
|
const task = { ...initialTask, task_type: type, domain_id: item.id };
|
||||||
|
const { state } = await mountComposable(t, useDashboard,
|
||||||
|
taskFetcher(() => task, (options) => posts.push(options)));
|
||||||
|
await state[method](item);
|
||||||
|
await state[method](item);
|
||||||
|
assert.equal(posts.length, 2);
|
||||||
|
const keys = posts.map((options) => options.headers["Idempotency-Key"]);
|
||||||
|
for (const key of keys) {
|
||||||
|
assert.match(key, /^[A-Za-z0-9._-]{8,128}$/);
|
||||||
|
assert.ok(key.startsWith(`${prefix}-${item.id}-`));
|
||||||
|
}
|
||||||
|
assert.notEqual(keys[0], keys[1]);
|
||||||
|
assert.equal(state.error.value, "");
|
||||||
|
assert.match(state.notice.value, /任务已提交/);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function taskFetcher(readTask, onPost = () => {}) {
|
function taskFetcher(readTask, onPost = () => {}) {
|
||||||
return async (path, options = {}) => {
|
return async (path, options = {}) => {
|
||||||
const task = readTask();
|
const task = readTask();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user