feat:支持正则匹配轮询分支
This commit is contained in:
parent
14a8cc6a99
commit
00b81bf7ef
@ -13,7 +13,7 @@ login:已支持 token 获取与缓存
|
|||||||
ackSuc/ackFail:已接入回传与本地落库
|
ackSuc/ackFail:已接入回传与本地落库
|
||||||
ConfigCryptoService:已抽出,当前默认透传实现,后续只需替换该服务内算法
|
ConfigCryptoService:已抽出,当前默认透传实现,后续只需替换该服务内算法
|
||||||
Git -> PROD:已支持最小增量推送,删除场景自动回退全量
|
Git -> PROD:已支持最小增量推送,删除场景自动回退全量
|
||||||
Git -> PROD:已改为按“版本分支 + 机场目录 + 模块目录”解析参数
|
Git -> PROD:已改为先按 git.repo.scan-branch-pattern 批量扫描版本分支,再逐个按“版本分支 + 机场目录 + 模块目录”解析参数
|
||||||
PROD -> Git:已按 airportId/appName/fileName 目录结构回写到动态 snapshot 分支
|
PROD -> Git:已按 airportId/appName/fileName 目录结构回写到动态 snapshot 分支
|
||||||
Git -> PROD:sourceVersion/configVersion 已改为 Git 分支名,不再用 commit SHA
|
Git -> PROD:sourceVersion/configVersion 已改为 Git 分支名,不再用 commit SHA
|
||||||
Git -> PROD:baseline 已按版本分支隔离
|
Git -> PROD:baseline 已按版本分支隔离
|
||||||
@ -29,7 +29,9 @@ docs 下设计文档和接口文档已同步更新到当前口径
|
|||||||
Git 仓库约定
|
Git 仓库约定
|
||||||
|
|
||||||
Git -> PROD:
|
Git -> PROD:
|
||||||
- git.repo.scan-branch 直接指向待同步版本分支
|
- 支持两种入口:
|
||||||
|
- git.repo.scan-branch:只同步一个指定版本分支
|
||||||
|
- git.repo.scan-branch-pattern:批量扫描所有匹配分支,当前默认用于 R_XXX_.*
|
||||||
- 分支名本身就是 configVersion
|
- 分支名本身就是 configVersion
|
||||||
- 分支内目录结构必须为:airportId/appName/模块内文件
|
- 分支内目录结构必须为:airportId/appName/模块内文件
|
||||||
- pushConfig 参数映射:
|
- pushConfig 参数映射:
|
||||||
|
|||||||
@ -45,7 +45,9 @@
|
|||||||
当前需求下:
|
当前需求下:
|
||||||
|
|
||||||
- 一个待发布版本对应一个 Git 分支
|
- 一个待发布版本对应一个 Git 分支
|
||||||
- `git.repo.scan-branch` 直接配置为当前待同步版本分支
|
- Git -> PROD 支持:
|
||||||
|
- `git.repo.scan-branch`:单分支同步
|
||||||
|
- `git.repo.scan-branch-pattern`:批量扫描同步
|
||||||
- **分支名本身就是 `configVersion`**
|
- **分支名本身就是 `configVersion`**
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@ -102,13 +104,14 @@ git.repo.snapshot-branch/<configVersion>
|
|||||||
|
|
||||||
流程:
|
流程:
|
||||||
|
|
||||||
1. 拉取 `git.repo.scan-branch`
|
1. 解析 `scan-branch` 或 `scan-branch-pattern`
|
||||||
2. 读取当前 `HEAD revision`
|
2. 逐个拉取命中的版本分支
|
||||||
3. 以 **分支名** 作为业务版本号 `sourceVersion`
|
3. 读取当前 `HEAD revision`
|
||||||
4. 导出该分支工作树
|
4. 以 **分支名** 作为业务版本号 `sourceVersion`
|
||||||
5. 解析所有 `airportId/appName/fileName` 配置项
|
5. 导出该分支工作树
|
||||||
6. 调用生产 `pushConfig`
|
6. 解析所有 `airportId/appName/fileName` 配置项
|
||||||
7. 成功后更新 `sync_task` 和 `sync_checkpoint`
|
7. 调用生产 `pushConfig`
|
||||||
|
8. 成功后更新 `sync_task` 和 `sync_checkpoint`
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
@ -207,6 +210,7 @@ direction + sourceVersion + contentHash
|
|||||||
|
|
||||||
```properties
|
```properties
|
||||||
git.repo.scan-branch=R_XXX_V3.0.3_XXX
|
git.repo.scan-branch=R_XXX_V3.0.3_XXX
|
||||||
|
git.repo.scan-branch-pattern=^R_XXX_.*$
|
||||||
git.repo.snapshot-branch=config-prod-snapshot
|
git.repo.snapshot-branch=config-prod-snapshot
|
||||||
git.repo.commit-message-prefix=sync(prod->git)
|
git.repo.commit-message-prefix=sync(prod->git)
|
||||||
```
|
```
|
||||||
|
|||||||
@ -47,7 +47,8 @@ src/main/resources/
|
|||||||
| `git.repo.remote-uri` | Git 远端地址 |
|
| `git.repo.remote-uri` | Git 远端地址 |
|
||||||
| `git.repo.username` | Git 用户名 |
|
| `git.repo.username` | Git 用户名 |
|
||||||
| `git.repo.password` | Git 密码或 token |
|
| `git.repo.password` | Git 密码或 token |
|
||||||
| `git.repo.scan-branch` | 当前待同步的版本分支名 |
|
| `git.repo.scan-branch` | 当前待同步的单个版本分支名 |
|
||||||
|
| `git.repo.scan-branch-pattern` | 批量扫描的版本分支匹配规则 |
|
||||||
| `git.repo.snapshot-branch` | 动态生产快照分支前缀 |
|
| `git.repo.snapshot-branch` | 动态生产快照分支前缀 |
|
||||||
| `git.repo.commit-message-prefix` | PROD -> Git 提交前缀 |
|
| `git.repo.commit-message-prefix` | PROD -> Git 提交前缀 |
|
||||||
|
|
||||||
@ -84,7 +85,9 @@ src/main/resources/
|
|||||||
|
|
||||||
当前实现约定:
|
当前实现约定:
|
||||||
|
|
||||||
- `git.repo.scan-branch` 直接指向当前待同步版本分支
|
- Git -> PROD 支持两种入口:
|
||||||
|
- `git.repo.scan-branch`:单分支定向同步
|
||||||
|
- `git.repo.scan-branch-pattern`:批量枚举版本分支
|
||||||
- **分支名本身就是 `configVersion`**
|
- **分支名本身就是 `configVersion`**
|
||||||
|
|
||||||
例如:
|
例如:
|
||||||
@ -147,19 +150,20 @@ work/
|
|||||||
|
|
||||||
### 7.1 流程步骤
|
### 7.1 流程步骤
|
||||||
|
|
||||||
1. 拉取 `git.repo.scan-branch`
|
1. 解析单分支或批量匹配规则
|
||||||
2. 读取当前 `HEAD revision`
|
2. 逐个拉取命中的版本分支
|
||||||
3. 以分支名作为 `sourceVersion`
|
3. 读取当前 `HEAD revision`
|
||||||
4. 导出分支工作树到 staging 目录
|
4. 以分支名作为 `sourceVersion`
|
||||||
5. 计算目录内容哈希
|
5. 导出分支工作树到 staging 目录
|
||||||
6. 按 `direction + sourceVersion + contentHash` 做幂等判断
|
6. 计算目录内容哈希
|
||||||
7. 计算本次推送目录:
|
7. 按 `direction + sourceVersion + contentHash` 做幂等判断
|
||||||
|
8. 计算本次推送目录:
|
||||||
- 首次全量
|
- 首次全量
|
||||||
- 删除回退全量
|
- 删除回退全量
|
||||||
- 其余场景最小增量
|
- 其余场景最小增量
|
||||||
8. 遍历目录内所有文件,按路径解析出 `airportId/appName/fileName`
|
9. 遍历目录内所有文件,按路径解析出 `airportId/appName/fileName`
|
||||||
9. 组装 `pushConfig` JSON 数组并提交
|
10. 组装 `pushConfig` JSON 数组并提交
|
||||||
10. 成功后刷新 checkpoint、task 和 baseline
|
11. 成功后刷新 checkpoint、task 和 baseline
|
||||||
|
|
||||||
### 7.2 参数映射规则
|
### 7.2 参数映射规则
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,9 @@
|
|||||||
当前需求下:
|
当前需求下:
|
||||||
|
|
||||||
- **每个待下发版本对应一个 Git 分支**
|
- **每个待下发版本对应一个 Git 分支**
|
||||||
- `git.repo.scan-branch` 指向当前待同步的版本分支
|
- Git -> PROD 支持两种入口:
|
||||||
|
- `git.repo.scan-branch`:指定单个版本分支
|
||||||
|
- `git.repo.scan-branch-pattern`:批量匹配多个版本分支
|
||||||
- **分支名本身就是 `configVersion`**
|
- **分支名本身就是 `configVersion`**
|
||||||
|
|
||||||
例如:
|
例如:
|
||||||
@ -93,7 +95,7 @@ git.repo.snapshot-branch/<configVersion>
|
|||||||
|
|
||||||
流程:
|
流程:
|
||||||
|
|
||||||
1. `prod-agent` 拉取 `git.repo.scan-branch` 指定的版本分支
|
1. `prod-agent` 解析单分支或批量匹配规则,得到待同步版本分支列表
|
||||||
2. 读取当前 `HEAD revision`,仅用于日志和 staging 隔离
|
2. 读取当前 `HEAD revision`,仅用于日志和 staging 隔离
|
||||||
3. 以 **分支名** 作为 `sourceVersion/configVersion`
|
3. 以 **分支名** 作为 `sourceVersion/configVersion`
|
||||||
4. 导出分支工作树
|
4. 导出分支工作树
|
||||||
@ -118,7 +120,7 @@ git.repo.snapshot-branch/<configVersion>
|
|||||||
|
|
||||||
当前代码已经调整为:
|
当前代码已经调整为:
|
||||||
|
|
||||||
- `sourceVersion = git.repo.scan-branch`
|
- `sourceVersion = 当前正在处理的版本分支名`
|
||||||
- 不再使用 `commit SHA` 作为业务版本号
|
- 不再使用 `commit SHA` 作为业务版本号
|
||||||
|
|
||||||
`HEAD revision` 仍然保留,但仅用于:
|
`HEAD revision` 仍然保留,但仅用于:
|
||||||
@ -193,12 +195,14 @@ direction + sourceVersion + contentHash
|
|||||||
|
|
||||||
```properties
|
```properties
|
||||||
git.repo.scan-branch=R_XXX_V3.0.3_XXX
|
git.repo.scan-branch=R_XXX_V3.0.3_XXX
|
||||||
|
git.repo.scan-branch-pattern=^R_XXX_.*$
|
||||||
git.repo.snapshot-branch=config-prod-snapshot
|
git.repo.snapshot-branch=config-prod-snapshot
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
- `scan-branch` 当前应直接配置为待同步版本分支名
|
- `scan-branch` 适合手工定向同步单个版本
|
||||||
|
- `scan-branch-pattern` 适合批量扫描所有待下发版本
|
||||||
- `snapshot-branch` 当前表示动态快照分支前缀
|
- `snapshot-branch` 当前表示动态快照分支前缀
|
||||||
|
|
||||||
### 8.2 生产接口配置
|
### 8.2 生产接口配置
|
||||||
|
|||||||
@ -15,7 +15,8 @@
|
|||||||
|
|
||||||
当前实现约定:
|
当前实现约定:
|
||||||
|
|
||||||
- `git.repo.scan-branch` 指向当前待同步的版本分支
|
- 优先使用 `git.repo.scan-branch-pattern` 批量匹配版本分支
|
||||||
|
- 如需定向同步单个版本,也可以显式配置 `git.repo.scan-branch`
|
||||||
- **分支名本身就是 `configVersion`**
|
- **分支名本身就是 `configVersion`**
|
||||||
- 分支内目录结构必须为:
|
- 分支内目录结构必须为:
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,10 @@ public class GitRepoProperties {
|
|||||||
private String username;
|
private String username;
|
||||||
/** Git 访问密码或 Token。 */
|
/** Git 访问密码或 Token。 */
|
||||||
private String password;
|
private String password;
|
||||||
/** 当前待同步的版本分支,Git -> PROD 只读取此分支。 */
|
/** 当前待同步的单个版本分支。配置为空时,退回使用 scanBranchPattern。 */
|
||||||
private String scanBranch;
|
private String scanBranch;
|
||||||
|
/** Git -> PROD 的版本分支匹配规则,例如 ^R_XXX_.*$ 。 */
|
||||||
|
private String scanBranchPattern;
|
||||||
/** 生产快照分支前缀,PROD -> Git 会写入该前缀下的动态版本分支。 */
|
/** 生产快照分支前缀,PROD -> Git 会写入该前缀下的动态版本分支。 */
|
||||||
private String snapshotBranch;
|
private String snapshotBranch;
|
||||||
/** Git 机器人提交用户名。 */
|
/** Git 机器人提交用户名。 */
|
||||||
@ -69,6 +71,14 @@ public class GitRepoProperties {
|
|||||||
this.scanBranch = scanBranch;
|
this.scanBranch = scanBranch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getScanBranchPattern() {
|
||||||
|
return scanBranchPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScanBranchPattern(String scanBranchPattern) {
|
||||||
|
this.scanBranchPattern = scanBranchPattern;
|
||||||
|
}
|
||||||
|
|
||||||
public String getSnapshotBranch() {
|
public String getSnapshotBranch() {
|
||||||
return snapshotBranch;
|
return snapshotBranch;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,19 +87,73 @@ public class ProdSyncCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拉取当前版本分支,并把配置文件推送到生产接口。
|
* 扫描待同步的版本分支,并逐个把配置文件推送到生产接口。
|
||||||
* 当前约定:分支名本身就是 configVersion。
|
* 当前支持两种模式:
|
||||||
|
* 1. 指定单个 scanBranch
|
||||||
|
* 2. 按 scanBranchPattern 批量匹配多个版本分支
|
||||||
*/
|
*/
|
||||||
public void syncLatestGitToProd() {
|
public void syncLatestGitToProd() {
|
||||||
|
try {
|
||||||
|
List<String> branches = resolveGitToProdBranches();
|
||||||
|
if (branches.isEmpty()) {
|
||||||
|
log.info("PROD git->prod tick. nodeId={}, no matching branches found.", syncProperties.getNodeId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"PROD git->prod tick. nodeId={}, branchCount={}, pushPath={}",
|
||||||
|
syncProperties.getNodeId(),
|
||||||
|
branches.size(),
|
||||||
|
prodApiProperties.getPushPath()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (String branch : branches) {
|
||||||
|
syncSingleGitBranchToProd(branch);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("PROD git->prod sync failed before branch loop completed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取生产配置快照,并按版本写回 Git 快照分支。
|
||||||
|
* 执行顺序是:
|
||||||
|
* 1. 先尝试消费上轮失败的 ACK 定向重拉
|
||||||
|
* 2. 再执行本轮正常的 pullConfig 拉取
|
||||||
|
*/
|
||||||
|
public void syncProdSnapshotToGit() {
|
||||||
|
log.info(
|
||||||
|
"PROD prod->git tick. apiBaseUrl={}, pullPath={}, snapshotBranchPrefix={}",
|
||||||
|
prodApiProperties.getBaseUrl(),
|
||||||
|
prodApiProperties.getPullPath(),
|
||||||
|
gitRepoProperties.getSnapshotBranch()
|
||||||
|
);
|
||||||
|
|
||||||
|
retryFailedProdPulls();
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<ProdPullResult> pullResults = prodConfigApiService.pullConfigSnapshots();
|
||||||
|
for (ProdPullResult pullResult : pullResults) {
|
||||||
|
syncSingleProdSnapshotToGit(pullResult, false);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("PROD prod->git sync failed before new snapshot groups were processed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldSkip(Optional<SyncTask> existing) {
|
||||||
|
return existing.isPresent() && existing.get().getStatus() == SyncStatus.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncSingleGitBranchToProd(String branch) {
|
||||||
String traceId = null;
|
String traceId = null;
|
||||||
try {
|
try {
|
||||||
String branch = gitRepoProperties.getScanBranch();
|
|
||||||
String sourceRevision = gitClientService.prepareRepositoryAndGetHead(branch);
|
String sourceRevision = gitClientService.prepareRepositoryAndGetHead(branch);
|
||||||
String sourceVersion = branch;
|
String sourceVersion = branch;
|
||||||
String stagingKey = buildStagingKey(branch, sourceRevision);
|
String stagingKey = buildStagingKey(branch, sourceRevision);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"PROD git->prod tick. nodeId={}, branch={}, revision={}, pushPath={}",
|
"PROD git->prod branch sync. nodeId={}, branch={}, revision={}, pushPath={}",
|
||||||
syncProperties.getNodeId(),
|
syncProperties.getNodeId(),
|
||||||
branch,
|
branch,
|
||||||
sourceRevision,
|
sourceRevision,
|
||||||
@ -150,40 +204,10 @@ public class ProdSyncCoordinator {
|
|||||||
log.info("Git version pushed to prod successfully. traceId={}, version={}, revision={}",
|
log.info("Git version pushed to prod successfully. traceId={}, version={}, revision={}",
|
||||||
task.getTraceId(), task.getSourceVersion(), sourceRevision);
|
task.getTraceId(), task.getSourceVersion(), sourceRevision);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleFailure(traceId, "PROD git->prod sync failed", e);
|
handleFailure(traceId, "PROD git->prod branch sync failed. branch=" + branch, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 拉取生产配置快照,并按版本写回 Git 快照分支。
|
|
||||||
* 执行顺序是:
|
|
||||||
* 1. 先尝试消费上轮失败的 ACK 定向重拉
|
|
||||||
* 2. 再执行本轮正常的 pullConfig 拉取
|
|
||||||
*/
|
|
||||||
public void syncProdSnapshotToGit() {
|
|
||||||
log.info(
|
|
||||||
"PROD prod->git tick. apiBaseUrl={}, pullPath={}, snapshotBranchPrefix={}",
|
|
||||||
prodApiProperties.getBaseUrl(),
|
|
||||||
prodApiProperties.getPullPath(),
|
|
||||||
gitRepoProperties.getSnapshotBranch()
|
|
||||||
);
|
|
||||||
|
|
||||||
retryFailedProdPulls();
|
|
||||||
|
|
||||||
try {
|
|
||||||
List<ProdPullResult> pullResults = prodConfigApiService.pullConfigSnapshots();
|
|
||||||
for (ProdPullResult pullResult : pullResults) {
|
|
||||||
syncSingleProdSnapshotToGit(pullResult, false);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("PROD prod->git sync failed before new snapshot groups were processed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldSkip(Optional<SyncTask> existing) {
|
|
||||||
return existing.isPresent() && existing.get().getStatus() == SyncStatus.SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理一组已经按 sourceVersion 切分好的生产快照。
|
* 处理一组已经按 sourceVersion 切分好的生产快照。
|
||||||
* retryAttempt=true 表示该组来自 ACK 失败后的定向重拉。
|
* retryAttempt=true 表示该组来自 ACK 失败后的定向重拉。
|
||||||
@ -374,6 +398,26 @@ public class ProdSyncCoordinator {
|
|||||||
return sanitizePathToken(branch) + "-" + sanitizePathToken(sourceRevision);
|
return sanitizePathToken(branch) + "-" + sanitizePathToken(sourceRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析本轮 Git -> PROD 要处理的版本分支列表。
|
||||||
|
* 优先级:
|
||||||
|
* 1. 配置了 scanBranch,则只处理这一个分支
|
||||||
|
* 2. 否则使用 scanBranchPattern 枚举所有匹配分支
|
||||||
|
*/
|
||||||
|
private List<String> resolveGitToProdBranches() throws Exception {
|
||||||
|
if (gitRepoProperties.getScanBranch() != null && !gitRepoProperties.getScanBranch().trim().isEmpty()) {
|
||||||
|
List<String> singleBranch = new ArrayList<String>();
|
||||||
|
singleBranch.add(gitRepoProperties.getScanBranch().trim());
|
||||||
|
return singleBranch;
|
||||||
|
}
|
||||||
|
|
||||||
|
String branchPattern = gitRepoProperties.getScanBranchPattern();
|
||||||
|
if (branchPattern == null || branchPattern.trim().isEmpty()) {
|
||||||
|
throw new IllegalStateException("Missing git.repo.scan-branch or git.repo.scan-branch-pattern");
|
||||||
|
}
|
||||||
|
return gitClientService.listMatchingBranches(branchPattern.trim());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把 snapshot 分支前缀和当前 sourceVersion 组装成最终回写分支。
|
* 把 snapshot 分支前缀和当前 sourceVersion 组装成最终回写分支。
|
||||||
* 例如:config-prod-snapshot/R_XXX_V3.0.3_XXX
|
* 例如:config-prod-snapshot/R_XXX_V3.0.3_XXX
|
||||||
|
|||||||
@ -3,12 +3,12 @@ package com.ftptool.sync.service;
|
|||||||
import com.ftptool.sync.config.GitRepoProperties;
|
import com.ftptool.sync.config.GitRepoProperties;
|
||||||
import com.ftptool.sync.util.FileTreeUtils;
|
import com.ftptool.sync.util.FileTreeUtils;
|
||||||
import org.eclipse.jgit.api.Git;
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.ListBranchCommand;
|
||||||
import org.eclipse.jgit.api.Status;
|
import org.eclipse.jgit.api.Status;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
import org.eclipse.jgit.lib.PersonIdent;
|
import org.eclipse.jgit.lib.PersonIdent;
|
||||||
import org.eclipse.jgit.lib.Ref;
|
import org.eclipse.jgit.lib.Ref;
|
||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||||
import org.eclipse.jgit.transport.RefSpec;
|
import org.eclipse.jgit.transport.RefSpec;
|
||||||
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
|
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
|
||||||
@ -21,6 +21,10 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -57,6 +61,41 @@ public class GitClientService {
|
|||||||
return new File(gitRepoProperties.getLocalPath()).toPath().toAbsolutePath().normalize();
|
return new File(gitRepoProperties.getLocalPath()).toPath().toAbsolutePath().normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出远端所有匹配正则的版本分支名。
|
||||||
|
*/
|
||||||
|
public List<String> listMatchingBranches(String branchRegex) throws IOException, GitAPIException {
|
||||||
|
synchronized (lock) {
|
||||||
|
try (Git git = openOrCloneRepository()) {
|
||||||
|
git.fetch()
|
||||||
|
.setCredentialsProvider(credentialsProvider())
|
||||||
|
.setRemote("origin")
|
||||||
|
.call();
|
||||||
|
|
||||||
|
Pattern pattern = Pattern.compile(branchRegex);
|
||||||
|
List<String> matchedBranches = new ArrayList<String>();
|
||||||
|
List<Ref> remoteBranches = git.branchList()
|
||||||
|
.setListMode(ListBranchCommand.ListMode.REMOTE)
|
||||||
|
.call();
|
||||||
|
for (Ref remoteBranch : remoteBranches) {
|
||||||
|
String shortenedRef = Repository.shortenRefName(remoteBranch.getName());
|
||||||
|
if (!shortenedRef.startsWith("origin/")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String branchName = shortenedRef.substring("origin/".length());
|
||||||
|
if ("HEAD".equals(branchName) || branchName.startsWith("HEAD/")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pattern.matcher(branchName).matches()) {
|
||||||
|
matchedBranches.add(branchName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Collections.sort(matchedBranches);
|
||||||
|
return matchedBranches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出指定分支的工作树快照,供后续打包或哈希计算使用。
|
* 导出指定分支的工作树快照,供后续打包或哈希计算使用。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -13,6 +13,8 @@ sync.jobs.prod-git-to-prod.cron=0 */1 * * * *
|
|||||||
sync.jobs.prod-to-git.cron=20 */2 * * * *
|
sync.jobs.prod-to-git.cron=20 */2 * * * *
|
||||||
|
|
||||||
# 生产接口覆盖示例
|
# 生产接口覆盖示例
|
||||||
|
# 如需批量扫描版本分支,建议在公共配置中使用:
|
||||||
|
# git.repo.scan-branch-pattern=^R_XXX_.*$
|
||||||
prod.api.base-url=https://prod.example.com
|
prod.api.base-url=https://prod.example.com
|
||||||
prod.api.push-path=/pic_bus_manage_monitor/configSync/pushConfig
|
prod.api.push-path=/pic_bus_manage_monitor/configSync/pushConfig
|
||||||
prod.api.pull-path=/pic_bus_manage_monitor/configSync/pullConfig
|
prod.api.pull-path=/pic_bus_manage_monitor/configSync/pullConfig
|
||||||
|
|||||||
@ -44,9 +44,12 @@ git.repo.remote-uri=https://git.example.com/config.git
|
|||||||
git.repo.username=replace-me
|
git.repo.username=replace-me
|
||||||
git.repo.password=replace-me
|
git.repo.password=replace-me
|
||||||
|
|
||||||
# 当前待同步的版本分支。
|
# Git -> PROD 支持两种模式:
|
||||||
# 当前业务约定:分支名本身就是 configVersion。
|
# 1. scan-branch:只同步一个指定版本分支
|
||||||
git.repo.scan-branch=R_XXX_V3.0.3_XXX
|
# 2. scan-branch-pattern:扫描所有匹配的版本分支
|
||||||
|
# 当前默认使用模式 2,匹配所有 R_XXX_.* 版本分支。
|
||||||
|
git.repo.scan-branch=
|
||||||
|
git.repo.scan-branch-pattern=^R_XXX_.*$
|
||||||
|
|
||||||
# 生产快照分支前缀。
|
# 生产快照分支前缀。
|
||||||
# PROD -> Git 实际回写目标为:snapshot-branch/<configVersion>
|
# PROD -> Git 实际回写目标为:snapshot-branch/<configVersion>
|
||||||
|
|||||||
@ -57,7 +57,8 @@ import static org.mockito.Mockito.when;
|
|||||||
"sync.package-temp-dir=${test.work-root}/package",
|
"sync.package-temp-dir=${test.work-root}/package",
|
||||||
"sync.dev-to-prod-staging-dir=${test.work-root}/dev-to-prod",
|
"sync.dev-to-prod-staging-dir=${test.work-root}/dev-to-prod",
|
||||||
"sync.prod-to-dev-staging-dir=${test.work-root}/prod-to-dev",
|
"sync.prod-to-dev-staging-dir=${test.work-root}/prod-to-dev",
|
||||||
"git.repo.scan-branch=config-dev-main",
|
"git.repo.scan-branch=",
|
||||||
|
"git.repo.scan-branch-pattern=^R_XXX_.*$",
|
||||||
"git.repo.snapshot-branch=config-prod-snapshot",
|
"git.repo.snapshot-branch=config-prod-snapshot",
|
||||||
"git.repo.local-path=${test.work-root}/git/config-repo"
|
"git.repo.local-path=${test.work-root}/git/config-repo"
|
||||||
}
|
}
|
||||||
@ -96,8 +97,9 @@ class ProdSyncCoordinatorIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSyncGitToProdAndKeepItIdempotent() throws Exception {
|
void shouldSyncGitToProdAndKeepItIdempotent() throws Exception {
|
||||||
when(gitClientService.prepareRepositoryAndGetHead("config-dev-main")).thenReturn("commit-a");
|
when(gitClientService.listMatchingBranches("^R_XXX_.*$")).thenReturn(Arrays.asList("R_XXX_V3.0.3_XXX"));
|
||||||
when(gitClientService.exportBranchSnapshot(eq("config-dev-main"), any(Path.class)))
|
when(gitClientService.prepareRepositoryAndGetHead("R_XXX_V3.0.3_XXX")).thenReturn("commit-a");
|
||||||
|
when(gitClientService.exportBranchSnapshot(eq("R_XXX_V3.0.3_XXX"), any(Path.class)))
|
||||||
.thenAnswer(invocation -> {
|
.thenAnswer(invocation -> {
|
||||||
Path target = invocation.getArgument(1);
|
Path target = invocation.getArgument(1);
|
||||||
Path configFile = target.resolve("PEK").resolve("monitor").resolve("application.yml");
|
Path configFile = target.resolve("PEK").resolve("monitor").resolve("application.yml");
|
||||||
@ -115,18 +117,45 @@ class ProdSyncCoordinatorIntegrationTest {
|
|||||||
assertEquals(1, tasks.size());
|
assertEquals(1, tasks.size());
|
||||||
SyncTask task = tasks.get(0);
|
SyncTask task = tasks.get(0);
|
||||||
assertEquals(SyncDirection.DEV_TO_PROD, task.getDirection());
|
assertEquals(SyncDirection.DEV_TO_PROD, task.getDirection());
|
||||||
assertEquals("config-dev-main", task.getSourceVersion());
|
assertEquals("R_XXX_V3.0.3_XXX", task.getSourceVersion());
|
||||||
assertEquals("hash-a", task.getContentHash());
|
assertEquals("hash-a", task.getContentHash());
|
||||||
assertEquals(SyncStatus.SUCCESS, task.getStatus());
|
assertEquals(SyncStatus.SUCCESS, task.getStatus());
|
||||||
|
|
||||||
Optional<SyncCheckpoint> checkpoint = syncCheckpointRepository.findByDirection(SyncDirection.DEV_TO_PROD);
|
Optional<SyncCheckpoint> checkpoint = syncCheckpointRepository.findByDirection(SyncDirection.DEV_TO_PROD);
|
||||||
assertTrue(checkpoint.isPresent());
|
assertTrue(checkpoint.isPresent());
|
||||||
assertEquals("config-dev-main", checkpoint.get().getLastSuccessVersion());
|
assertEquals("R_XXX_V3.0.3_XXX", checkpoint.get().getLastSuccessVersion());
|
||||||
assertEquals("hash-a", checkpoint.get().getLastSuccessHash());
|
assertEquals("hash-a", checkpoint.get().getLastSuccessHash());
|
||||||
|
|
||||||
verify(prodConfigApiService, times(1)).pushPackage(any(PackageManifest.class), any(Path.class));
|
verify(prodConfigApiService, times(1)).pushPackage(any(PackageManifest.class), any(Path.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSyncMultipleMatchingGitBranches() throws Exception {
|
||||||
|
when(gitClientService.listMatchingBranches("^R_XXX_.*$"))
|
||||||
|
.thenReturn(Arrays.asList("R_XXX_V3.0.3_XXX", "R_XXX_V3.0.4_XXX"));
|
||||||
|
when(gitClientService.prepareRepositoryAndGetHead("R_XXX_V3.0.3_XXX")).thenReturn("commit-a");
|
||||||
|
when(gitClientService.prepareRepositoryAndGetHead("R_XXX_V3.0.4_XXX")).thenReturn("commit-b");
|
||||||
|
when(gitClientService.exportBranchSnapshot(any(String.class), any(Path.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
String branch = invocation.getArgument(0);
|
||||||
|
Path target = invocation.getArgument(1);
|
||||||
|
Path configFile = target.resolve("PEK").resolve("monitor").resolve(branch + ".yml");
|
||||||
|
Files.createDirectories(configFile.getParent());
|
||||||
|
Files.write(configFile, branch.getBytes("UTF-8"));
|
||||||
|
return target;
|
||||||
|
});
|
||||||
|
when(packageService.calculateDirectoryHash(any(Path.class)))
|
||||||
|
.thenReturn("hash-a")
|
||||||
|
.thenReturn("hash-b");
|
||||||
|
doNothing().when(prodConfigApiService).pushPackage(any(PackageManifest.class), any(Path.class));
|
||||||
|
|
||||||
|
prodSyncCoordinator.syncLatestGitToProd();
|
||||||
|
|
||||||
|
List<SyncTask> tasks = syncTaskRepository.findAll();
|
||||||
|
assertEquals(2, tasks.size());
|
||||||
|
verify(prodConfigApiService, times(2)).pushPackage(any(PackageManifest.class), any(Path.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSyncProdSnapshotToGitAndKeepItIdempotent() throws Exception {
|
void shouldSyncProdSnapshotToGitAndKeepItIdempotent() throws Exception {
|
||||||
Path contentDirectory = Files.createTempDirectory("prod-to-git-");
|
Path contentDirectory = Files.createTempDirectory("prod-to-git-");
|
||||||
@ -292,10 +321,13 @@ class ProdSyncCoordinatorIntegrationTest {
|
|||||||
AtomicInteger exportCounter = new AtomicInteger(0);
|
AtomicInteger exportCounter = new AtomicInteger(0);
|
||||||
List<Path> pushedDirectories = new ArrayList<Path>();
|
List<Path> pushedDirectories = new ArrayList<Path>();
|
||||||
|
|
||||||
when(gitClientService.prepareRepositoryAndGetHead("config-dev-main"))
|
when(gitClientService.listMatchingBranches("^R_XXX_.*$"))
|
||||||
|
.thenReturn(Arrays.asList("R_XXX_V3.0.3_XXX"))
|
||||||
|
.thenReturn(Arrays.asList("R_XXX_V3.0.3_XXX"));
|
||||||
|
when(gitClientService.prepareRepositoryAndGetHead("R_XXX_V3.0.3_XXX"))
|
||||||
.thenReturn("commit-base")
|
.thenReturn("commit-base")
|
||||||
.thenReturn("commit-delta");
|
.thenReturn("commit-delta");
|
||||||
when(gitClientService.exportBranchSnapshot(eq("config-dev-main"), any(Path.class)))
|
when(gitClientService.exportBranchSnapshot(eq("R_XXX_V3.0.3_XXX"), any(Path.class)))
|
||||||
.thenAnswer(invocation -> {
|
.thenAnswer(invocation -> {
|
||||||
Path target = invocation.getArgument(1);
|
Path target = invocation.getArgument(1);
|
||||||
Path fileA = target.resolve("PEK").resolve("monitor").resolve("a.txt");
|
Path fileA = target.resolve("PEK").resolve("monitor").resolve("a.txt");
|
||||||
|
|||||||
@ -0,0 +1,258 @@
|
|||||||
|
package com.ftptool.sync.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ftptool.sync.config.ProdApiProperties;
|
||||||
|
import com.ftptool.sync.config.SyncProperties;
|
||||||
|
import com.ftptool.sync.model.PackageManifest;
|
||||||
|
import com.ftptool.sync.model.ProdPullResult;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||||
|
import org.springframework.test.web.client.MockRestServiceServer;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.client.ExpectedCount.once;
|
||||||
|
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
|
||||||
|
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||||
|
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面向联调的生产接口模拟单元测试。
|
||||||
|
* 这里的 mock 数据和请求格式可以直接拿来对照生产接口联调。
|
||||||
|
*/
|
||||||
|
class ProdConfigApiServiceContractTest {
|
||||||
|
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCallPushConfigWithMockProdApi() throws Exception {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||||
|
|
||||||
|
SyncProperties syncProperties = newSyncProperties("./target/contract-push-work");
|
||||||
|
WorkDirectoryService workDirectoryService = new WorkDirectoryService(syncProperties);
|
||||||
|
workDirectoryService.initialize();
|
||||||
|
|
||||||
|
ProdApiProperties prodApiProperties = new ProdApiProperties();
|
||||||
|
prodApiProperties.setBaseUrl("https://prod.example.com");
|
||||||
|
prodApiProperties.setPushPath("/pic_bus_manage_monitor/configSync/pushConfig");
|
||||||
|
prodApiProperties.setToken("mock-static-token");
|
||||||
|
prodApiProperties.setTokenHeaderName("token");
|
||||||
|
|
||||||
|
ConfigCryptoService configCryptoService = mock(ConfigCryptoService.class);
|
||||||
|
when(configCryptoService.encryptForPush(
|
||||||
|
"PEK", "monitor", "R_XXX_V3.0.3_XXX", "application.yml", "server.port: 8080"
|
||||||
|
)).thenReturn("encrypted-content");
|
||||||
|
|
||||||
|
ProdConfigApiService service = new ProdConfigApiService(
|
||||||
|
prodApiProperties,
|
||||||
|
syncProperties,
|
||||||
|
restTemplate,
|
||||||
|
workDirectoryService,
|
||||||
|
mock(ProdPullAckService.class),
|
||||||
|
configCryptoService
|
||||||
|
);
|
||||||
|
|
||||||
|
Path sourceDirectory = Files.createTempDirectory("contract-push-source-");
|
||||||
|
Path configFile = sourceDirectory.resolve("PEK").resolve("monitor").resolve("application.yml");
|
||||||
|
Files.createDirectories(configFile.getParent());
|
||||||
|
Files.write(configFile, "server.port: 8080".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
server.expect(once(), request -> {
|
||||||
|
JsonNode body = OBJECT_MAPPER.readTree(((MockClientHttpRequest) request).getBodyAsString());
|
||||||
|
assertEquals(1, body.size());
|
||||||
|
assertEquals("PEK", body.get(0).get("airportId").asText());
|
||||||
|
assertEquals("monitor", body.get(0).get("appName").asText());
|
||||||
|
assertEquals("R_XXX_V3.0.3_XXX", body.get(0).get("configVersion").asText());
|
||||||
|
assertEquals("application.yml", body.get(0).get("fileName").asText());
|
||||||
|
assertEquals("encrypted-content", body.get(0).get("configContent").asText());
|
||||||
|
})
|
||||||
|
.andExpect(method(HttpMethod.POST))
|
||||||
|
.andExpect(header("token", "mock-static-token"))
|
||||||
|
.andRespond(withSuccess(
|
||||||
|
"{\"code\":\"0\",\"data\":{\"ackFail\":[]},\"msg\":\"ok\"}",
|
||||||
|
MediaType.APPLICATION_JSON
|
||||||
|
));
|
||||||
|
|
||||||
|
PackageManifest manifest = new PackageManifest();
|
||||||
|
manifest.setTraceId("trace-contract-push");
|
||||||
|
manifest.setSourceVersion("R_XXX_V3.0.3_XXX");
|
||||||
|
|
||||||
|
service.pushPackage(manifest, sourceDirectory);
|
||||||
|
|
||||||
|
verify(configCryptoService).encryptForPush(
|
||||||
|
"PEK", "monitor", "R_XXX_V3.0.3_XXX", "application.yml", "server.port: 8080"
|
||||||
|
);
|
||||||
|
server.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCallPullConfigWithMockProdApi() throws Exception {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||||
|
|
||||||
|
SyncProperties syncProperties = newSyncProperties("./target/contract-pull-work");
|
||||||
|
WorkDirectoryService workDirectoryService = new WorkDirectoryService(syncProperties);
|
||||||
|
workDirectoryService.initialize();
|
||||||
|
|
||||||
|
ProdApiProperties prodApiProperties = new ProdApiProperties();
|
||||||
|
prodApiProperties.setBaseUrl("https://prod.example.com");
|
||||||
|
prodApiProperties.setPullPath("/pic_bus_manage_monitor/configSync/pullConfig");
|
||||||
|
prodApiProperties.setToken("mock-static-token");
|
||||||
|
prodApiProperties.setTokenHeaderName("token");
|
||||||
|
prodApiProperties.setAirportId("PEK");
|
||||||
|
prodApiProperties.setAppName("monitor");
|
||||||
|
prodApiProperties.setPullConfigVersion("R_XXX_V3.0.3_XXX");
|
||||||
|
prodApiProperties.setPullFileName("application.yml");
|
||||||
|
|
||||||
|
ProdPullAckService prodPullAckService = mock(ProdPullAckService.class);
|
||||||
|
when(prodPullAckService.getPendingAckSummary()).thenReturn(
|
||||||
|
new ProdPullAckService.PendingAckSummary(Collections.<String>emptyList(), Collections.<String>emptyList())
|
||||||
|
);
|
||||||
|
|
||||||
|
ConfigCryptoService configCryptoService = mock(ConfigCryptoService.class);
|
||||||
|
when(configCryptoService.decryptAfterPull(
|
||||||
|
"PEK", "monitor", "R_XXX_V3.0.3_XXX", "application.yml", "encrypted-content"
|
||||||
|
)).thenReturn("server.port: 8080");
|
||||||
|
|
||||||
|
ProdConfigApiService service = new ProdConfigApiService(
|
||||||
|
prodApiProperties,
|
||||||
|
syncProperties,
|
||||||
|
restTemplate,
|
||||||
|
workDirectoryService,
|
||||||
|
prodPullAckService,
|
||||||
|
configCryptoService
|
||||||
|
);
|
||||||
|
|
||||||
|
server.expect(once(), request -> {
|
||||||
|
String uri = request.getURI().toString();
|
||||||
|
assertTrue(uri.contains("airportId=PEK"));
|
||||||
|
assertTrue(uri.contains("appName=monitor"));
|
||||||
|
assertTrue(uri.contains("configVersion=R_XXX_V3.0.3_XXX"));
|
||||||
|
assertTrue(uri.contains("fileName=application.yml"));
|
||||||
|
})
|
||||||
|
.andExpect(method(HttpMethod.GET))
|
||||||
|
.andExpect(header("token", "mock-static-token"))
|
||||||
|
.andRespond(withSuccess(
|
||||||
|
"{\"code\":\"0\",\"data\":["
|
||||||
|
+ "{\"id\":\"101\",\"airportId\":\"PEK\",\"appName\":\"monitor\","
|
||||||
|
+ "\"configVersion\":\"R_XXX_V3.0.3_XXX\",\"configContent\":\"encrypted-content\","
|
||||||
|
+ "\"fileName\":\"application.yml\"}"
|
||||||
|
+ "],\"msg\":\"ok\"}",
|
||||||
|
MediaType.APPLICATION_JSON
|
||||||
|
));
|
||||||
|
|
||||||
|
List<ProdPullResult> results = service.pullConfigSnapshots();
|
||||||
|
|
||||||
|
assertEquals(1, results.size());
|
||||||
|
ProdPullResult result = results.get(0);
|
||||||
|
assertEquals("R_XXX_V3.0.3_XXX", result.getSourceVersion());
|
||||||
|
assertEquals(Arrays.asList("101"), result.getPulledConfigIds());
|
||||||
|
Path restoredFile = result.getContentDirectory()
|
||||||
|
.resolve("PEK")
|
||||||
|
.resolve("monitor")
|
||||||
|
.resolve("application.yml");
|
||||||
|
assertTrue(Files.exists(restoredFile));
|
||||||
|
assertEquals("server.port: 8080", new String(Files.readAllBytes(restoredFile), StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
verify(configCryptoService).decryptAfterPull(
|
||||||
|
"PEK", "monitor", "R_XXX_V3.0.3_XXX", "application.yml", "encrypted-content"
|
||||||
|
);
|
||||||
|
server.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLoginThenCallPushConfigWhenStaticTokenMissing() throws Exception {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(false).build();
|
||||||
|
|
||||||
|
SyncProperties syncProperties = newSyncProperties("./target/contract-login-work");
|
||||||
|
WorkDirectoryService workDirectoryService = new WorkDirectoryService(syncProperties);
|
||||||
|
workDirectoryService.initialize();
|
||||||
|
|
||||||
|
ProdApiProperties prodApiProperties = new ProdApiProperties();
|
||||||
|
prodApiProperties.setBaseUrl("https://prod.example.com");
|
||||||
|
prodApiProperties.setLoginPath("/pic_bus_manage_monitor/pam-monitor/login");
|
||||||
|
prodApiProperties.setPushPath("/pic_bus_manage_monitor/configSync/pushConfig");
|
||||||
|
prodApiProperties.setToken("replace-me");
|
||||||
|
prodApiProperties.setTokenHeaderName("token");
|
||||||
|
prodApiProperties.setLoginName("mock-user");
|
||||||
|
prodApiProperties.setLoginPassword("mock-password");
|
||||||
|
|
||||||
|
ConfigCryptoService configCryptoService = mock(ConfigCryptoService.class);
|
||||||
|
when(configCryptoService.encryptForPush(
|
||||||
|
"SHA", "gate", "R_XXX_V3.0.4_XXX", "gate-rule.json", "{\"enabled\":true}"
|
||||||
|
)).thenReturn("encrypted-gate-rule");
|
||||||
|
|
||||||
|
ProdConfigApiService service = new ProdConfigApiService(
|
||||||
|
prodApiProperties,
|
||||||
|
syncProperties,
|
||||||
|
restTemplate,
|
||||||
|
workDirectoryService,
|
||||||
|
mock(ProdPullAckService.class),
|
||||||
|
configCryptoService
|
||||||
|
);
|
||||||
|
|
||||||
|
Path sourceDirectory = Files.createTempDirectory("contract-login-source-");
|
||||||
|
Path configFile = sourceDirectory.resolve("SHA").resolve("gate").resolve("gate-rule.json");
|
||||||
|
Files.createDirectories(configFile.getParent());
|
||||||
|
Files.write(configFile, "{\"enabled\":true}".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
server.expect(once(), request -> {
|
||||||
|
JsonNode body = OBJECT_MAPPER.readTree(((MockClientHttpRequest) request).getBodyAsString());
|
||||||
|
assertEquals("mock-user", body.get("name").asText());
|
||||||
|
assertEquals("mock-password", body.get("password").asText());
|
||||||
|
})
|
||||||
|
.andExpect(method(HttpMethod.POST))
|
||||||
|
.andRespond(withSuccess(
|
||||||
|
"{\"code\":\"0\",\"data\":{\"token\":\"login-token-001\",\"expireTime\":\"2026-12-31 23:59:59\"},\"msg\":\"ok\"}",
|
||||||
|
MediaType.APPLICATION_JSON
|
||||||
|
));
|
||||||
|
|
||||||
|
server.expect(once(), request -> {
|
||||||
|
JsonNode body = OBJECT_MAPPER.readTree(((MockClientHttpRequest) request).getBodyAsString());
|
||||||
|
assertEquals("SHA", body.get(0).get("airportId").asText());
|
||||||
|
assertEquals("gate", body.get(0).get("appName").asText());
|
||||||
|
assertEquals("R_XXX_V3.0.4_XXX", body.get(0).get("configVersion").asText());
|
||||||
|
assertEquals("encrypted-gate-rule", body.get(0).get("configContent").asText());
|
||||||
|
})
|
||||||
|
.andExpect(method(HttpMethod.POST))
|
||||||
|
.andExpect(header("token", "login-token-001"))
|
||||||
|
.andRespond(withSuccess(
|
||||||
|
"{\"code\":\"0\",\"data\":{\"ackFail\":[]},\"msg\":\"ok\"}",
|
||||||
|
MediaType.APPLICATION_JSON
|
||||||
|
));
|
||||||
|
|
||||||
|
PackageManifest manifest = new PackageManifest();
|
||||||
|
manifest.setTraceId("trace-contract-login");
|
||||||
|
manifest.setSourceVersion("R_XXX_V3.0.4_XXX");
|
||||||
|
|
||||||
|
service.pushPackage(manifest, sourceDirectory);
|
||||||
|
|
||||||
|
server.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SyncProperties newSyncProperties(String workDir) {
|
||||||
|
SyncProperties syncProperties = new SyncProperties();
|
||||||
|
syncProperties.setWorkDir(workDir);
|
||||||
|
syncProperties.setPackageTempDir(workDir + "/package");
|
||||||
|
syncProperties.setDevToProdStagingDir(workDir + "/dev-to-prod");
|
||||||
|
syncProperties.setProdToDevStagingDir(workDir + "/prod-to-dev");
|
||||||
|
syncProperties.setPullResponseFileName("prod-config.json");
|
||||||
|
return syncProperties;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user