From ae72769741eb04f19b30e00ce7b1a8e74d42f185 Mon Sep 17 00:00:00 2001 From: waner Date: Wed, 29 Apr 2026 16:45:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A4=87=E4=BB=BD=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 44 +-- docs/deployment/tms-deployment.md | 24 ++ .../resource-restore/apply-resource-backup.sh | 181 ++++++++++ scripts/resource-restore/replay-mq.sh | 107 ++++++ scripts/resource-restore/start-runner.sh | 48 --- .../config/ResourceBackupProperties.java | 101 ++++-- .../controller/ResourceBackupController.java | 7 - .../ResourceEnvelopeCryptoServiceImpl.java | 24 +- .../ResourceDiscoveryServiceImpl.java | 54 +-- .../ResourceRestoreTaskDetailResponse.java | 10 +- .../dto/support/BackupResourceKind.java | 7 + .../support/ConfiguredFileBackupResource.java | 59 ++++ .../dto/support/DiscoveredResourceItem.java | 36 ++ .../backup/enums/ResourceControlPhase.java | 5 +- .../backup/enums/ResourceTaskStatus.java | 3 +- .../packagex/MqReplayContextBuilder.java | 77 +++++ .../packagex/ResourcePackageServiceImpl.java | 46 ++- .../ResourcePayloadExtractionResult.java | 43 +++ .../packagex/ResourcePayloadExtractor.java | 327 ++++++++++++++++++ .../LightweightRestoreApplierLauncher.java | 108 ++++++ .../ProcessBuilderRestoreCommandInvoker.java} | 8 +- .../RestoreCommandInvoker.java} | 4 +- .../restore/RestoreStatusFileReader.java | 82 +++++ .../ConsistencyVerifyRunnerStepExecutor.java | 111 ------ .../DatabaseRestoreRunnerStepExecutor.java | 135 -------- .../PayloadArchiveRunnerStepExecutor.java | 198 ----------- .../ProcessResourceRestoreRunnerLauncher.java | 41 --- .../backup/runner/ResourceRestoreRunner.java | 156 --------- .../runner/ResourceRestoreRunnerLauncher.java | 8 - .../runner/ResourceRestoreRunnerMain.java | 38 -- .../backup/runner/RestoreStateReconciler.java | 60 ---- .../backup/runner/RunnerStallDetector.java | 64 ---- .../backup/runner/RunnerStepContext.java | 13 - .../runner/RunnerStepExecutionResult.java | 37 -- .../backup/runner/RunnerStepExecutor.java | 8 - .../ServiceCommandRunnerStepExecutor.java | 82 ----- .../model/ResourceRestoreHandoffSnapshot.java | 77 ----- .../impl/ResourcePrecheckServiceImpl.java | 9 +- .../impl/ResourceRestoreServiceImpl.java | 77 +++-- .../impl/ResourceTaskQueryServiceImpl.java | 138 +++----- .../backup/support/file/HandoffFileStore.java | 52 --- .../file/ResourceRestoreStateFileStore.java | 190 ---------- .../file/ResourceRestoreStateSnapshot.java | 160 --------- .../modules/cert/dto/EntityCreateRequest.java | 5 +- .../modules/cert/dto/EntityGenP10Request.java | 3 +- .../modules/cert/dto/EntityItemResponse.java | 6 +- .../modules/cert/dto/EntityListRequest.java | 4 +- .../modules/cert/dto/EntityUpdateRequest.java | 4 +- .../tms/modules/cert/entity/KeyEntity.java | 6 +- .../modules/cert/enums/EntityCertMode.java | 34 ++ .../tms/modules/cert/enums/EntityStatus.java | 35 ++ .../cert/repository/KeyEntityRepository.java | 2 +- .../impl/KeyEntityRepositoryImpl.java | 19 +- .../cert/service/CertificateService.java | 3 + .../modules/cert/service/EntityService.java | 65 +++- src/main/resources/application.yml | 22 +- .../backup/ResourceBackupDtoContractTest.java | 14 +- .../backup/ResourceBackupPropertiesTest.java | 45 +++ .../ResourceBackupControllerTest.java | 22 +- .../ResourceRestoreControllerTest.java | 18 +- .../ResourceEnvelopeCryptoServiceTest.java | 24 +- .../ResourceDiscoveryServiceTest.java | 22 +- .../packagex/MqReplayContextBuilderTest.java | 46 +++ .../packagex/ResourcePackageServiceTest.java | 44 ++- .../ResourcePayloadExtractorTest.java | 124 +++++++ .../ResourceTaskRepositoryTest.java | 16 +- .../ResourceTaskSchemaContractTest.java | 8 +- ...ApplyResourceBackupScriptContractTest.java | 154 +++++++++ ...LightweightRestoreApplierLauncherTest.java | 67 ++++ .../restore/MqReplayScriptContractTest.java | 74 ++++ .../RestoreCommandInvokerTest.java} | 12 +- .../restore/RestoreStatusFileReaderTest.java | 33 ++ ...nsistencyVerifyRunnerStepExecutorTest.java | 122 ------- ...DatabaseRestoreRunnerStepExecutorTest.java | 196 ----------- .../PayloadArchiveRunnerStepExecutorTest.java | 177 ---------- ...cessResourceRestoreRunnerLauncherTest.java | 71 ---- .../runner/ResourceRestoreRunnerTest.java | 148 -------- .../runner/RestoreStateReconcilerTest.java | 62 ---- .../runner/RunnerStallDetectorTest.java | 53 --- .../ServiceCommandRunnerStepExecutorTest.java | 145 -------- ...esourcePrecheckLightweightServiceTest.java | 81 +++++ .../ResourceRestoreHandoffServiceTest.java | 124 +++++-- .../ResourceRestoreSupportServiceTest.java | 241 ------------- .../service/ResourceTaskQueryServiceTest.java | 269 ++++++-------- .../ResourceRestoreStateFileStoreTest.java | 35 -- .../cert/service/EntityServiceTest.java | 184 ++++++++++ 86 files changed, 2648 insertions(+), 3250 deletions(-) create mode 100755 scripts/resource-restore/apply-resource-backup.sh create mode 100755 scripts/resource-restore/replay-mq.sh delete mode 100755 scripts/resource-restore/start-runner.sh create mode 100644 src/main/java/com/cisd/tms/modules/backup/dto/support/BackupResourceKind.java create mode 100644 src/main/java/com/cisd/tms/modules/backup/dto/support/ConfiguredFileBackupResource.java create mode 100644 src/main/java/com/cisd/tms/modules/backup/packagex/MqReplayContextBuilder.java create mode 100644 src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractionResult.java create mode 100644 src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractor.java create mode 100644 src/main/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncher.java rename src/main/java/com/cisd/tms/modules/backup/{runner/ProcessBuilderRunnerCommandInvoker.java => restore/ProcessBuilderRestoreCommandInvoker.java} (74%) rename src/main/java/com/cisd/tms/modules/backup/{runner/RunnerCommandInvoker.java => restore/RestoreCommandInvoker.java} (59%) create mode 100644 src/main/java/com/cisd/tms/modules/backup/restore/RestoreStatusFileReader.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/ConsistencyVerifyRunnerStepExecutor.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/DatabaseRestoreRunnerStepExecutor.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/PayloadArchiveRunnerStepExecutor.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/ProcessResourceRestoreRunnerLauncher.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunner.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerLauncher.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerMain.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/RestoreStateReconciler.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/RunnerStallDetector.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepContext.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepExecutionResult.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepExecutor.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/ServiceCommandRunnerStepExecutor.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/runner/model/ResourceRestoreHandoffSnapshot.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/support/file/HandoffFileStore.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateFileStore.java delete mode 100644 src/main/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateSnapshot.java create mode 100644 src/main/java/com/cisd/tms/modules/cert/enums/EntityCertMode.java create mode 100644 src/main/java/com/cisd/tms/modules/cert/enums/EntityStatus.java create mode 100644 src/test/java/com/cisd/tms/modules/backup/ResourceBackupPropertiesTest.java create mode 100644 src/test/java/com/cisd/tms/modules/backup/packagex/MqReplayContextBuilderTest.java create mode 100644 src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractorTest.java create mode 100644 src/test/java/com/cisd/tms/modules/backup/restore/ApplyResourceBackupScriptContractTest.java create mode 100644 src/test/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncherTest.java create mode 100644 src/test/java/com/cisd/tms/modules/backup/restore/MqReplayScriptContractTest.java rename src/test/java/com/cisd/tms/modules/backup/{runner/RunnerCommandInvokerTest.java => restore/RestoreCommandInvokerTest.java} (54%) create mode 100644 src/test/java/com/cisd/tms/modules/backup/restore/RestoreStatusFileReaderTest.java delete mode 100644 src/test/java/com/cisd/tms/modules/backup/runner/ConsistencyVerifyRunnerStepExecutorTest.java delete mode 100644 src/test/java/com/cisd/tms/modules/backup/runner/DatabaseRestoreRunnerStepExecutorTest.java delete mode 100644 src/test/java/com/cisd/tms/modules/backup/runner/PayloadArchiveRunnerStepExecutorTest.java delete mode 100644 src/test/java/com/cisd/tms/modules/backup/runner/ProcessResourceRestoreRunnerLauncherTest.java delete mode 100644 src/test/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerTest.java delete mode 100644 src/test/java/com/cisd/tms/modules/backup/runner/RestoreStateReconcilerTest.java delete mode 100644 src/test/java/com/cisd/tms/modules/backup/runner/RunnerStallDetectorTest.java delete mode 100644 src/test/java/com/cisd/tms/modules/backup/runner/ServiceCommandRunnerStepExecutorTest.java create mode 100644 src/test/java/com/cisd/tms/modules/backup/service/ResourcePrecheckLightweightServiceTest.java delete mode 100644 src/test/java/com/cisd/tms/modules/backup/service/ResourceRestoreSupportServiceTest.java delete mode 100644 src/test/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateFileStoreTest.java diff --git a/README.md b/README.md index df3bbdd..572acc4 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,6 @@ Open: ## Resource Backup / Restore 最小调用顺序: -- `GET /api/v1/resource-backups/keyset/status` - `POST /api/v1/resource-backups` - `GET /api/v1/resource-backups/{taskId}` - `GET /api/v1/resource-backups/{taskId}/download` @@ -237,54 +236,48 @@ Open: - `GET /api/v1/resource-restores/{taskId}/steps` - `GET /api/v1/resource-restores/{taskId}/steps/{stepNo}/log` +前后端字段、状态枚举和联调流程见 `docs/openapi/resource-backup-restore-frontend-integration.md`。 + 当前实现边界: - 资源备份包输出为 `.tmsbak`,包内包含 `manifest.json`、`envelope.json`、`payload.enc`、`signature.sig` - 当前第一版备份/恢复只覆盖最小资源集: - `TMS_CONFIG` - - `TMS_DB_DUMP` - - `CMEP_DB_DUMP` + - `TMS_DB` + - `CMEP_DB` - `CP_CONFIG` - `RECEIVER_LICENSE` + - `MQ_REPLAY` - 预检阶段只做包头解析、指纹匹配和验签,不提前解密完整 payload -- 恢复任务创建后会生成 `handoff.json`、`restore-state.json`,并进入 `HANDOFF_READY` -- `ResourceRestoreRunner` 当前已具备接管、步骤循环、`step-output/*.json`、心跳刷新、`RECONCILE_PENDING` 推进能力 -- runner 现已通过外部脚本启动独立进程,不再运行在 TMS Web 进程内 -- 当前已落地的恢复步骤: - - `STOP_SERVICES` - - `RESTORE_FILES` - - `RESTORE_DATABASES` - - `START_SERVICES` - - `CONSISTENCY_VERIFY` -- `RESTORE_FILES` 当前只回写关键配置/License 文件,不做目录级全量恢复 -- `RESTORE_DATABASES` 当前恢复: +- 恢复任务创建时 Java 完成验签、PCIe 解密、payload 展开和恢复路径白名单校验 +- Java 会生成 shell 友好的 `restore-files.tsv`、`restore-databases.tsv`、`restore-mq.tsv` +- 创建恢复任务后启动 `apply-resource-backup.sh --work-dir `,由 shell 执行停服务、文件覆盖、SQL 导入、MQ replay、起服务和健康检查 +- shell 侧状态写入 `status.json`,日志写入 `restore.log` +- `FILE` 回写 manifest 明确声明的配置/License 文件 +- `DATABASE` 恢复: - `db/TMS.sql` - `db/CMEP.sql`(包内存在时) -- `CONSISTENCY_VERIFY` 当前只检查: - - `RESTORE_FILES` 成功 - - `RESTORE_DATABASES` 成功 - - `START_SERVICES` 成功 - - 健康检查地址返回 `UP` +- `MQ_REPLAY` 通过 `replay-mq.sh` 消费 `mq/mq-restore-context.json`,也可用 `MQ_REPLAY_COMMAND` 委派给现场脚本 +- Java 启动恢复脚本前会在工作目录生成 `restore-env.sh`,传递数据库连接、恢复脚本路径、MQ replay 脚本路径和健康检查配置 关键配置: - `tms.backup.output-dir` - `tms.backup.precheck-store-dir` - `tms.backup.restore-task-root-dir` - `tms.backup.tms-script-path` -- `tms.backup.runner-launch-script-path` +- `tms.backup.restore-apply-script-path` +- `tms.backup.allowed-restore-roots` - `tms.backup.mysqldump-path` - `tms.backup.mysql-path` - `tms.backup.restore-db-script-path` +- `tms.backup.mq-replay-script-path` - `tms.backup.tms-database-name` - `tms.backup.cmep-database-name` - `tms.backup.health-check-url` - `tms.backup.health-check-timeout-seconds` -- `tms.backup.runner-auto-launch-enabled` -- `tms.backup.runner-heartbeat-timeout-seconds` 恢复前置条件: - LMK、IK、数据存储加密密钥对、数据真实性保护密钥对需先通过 UKey 恢复 -- 默认不自动拉起本机 runner;如需在现场机器自动执行,显式开启 `tms.backup.runner-auto-launch-enabled=true` -- `/home/tms/bin/resource-restore/start-runner.sh` 和 `/home/tms/bin/resource-restore/restore-db.sh` 必须已部署并可执行 +- `/home/tms/bin/resource-restore/apply-resource-backup.sh`、`restore-db.sh`、`replay-mq.sh` 必须已部署并可执行 - `mysql` / `mysqldump` 客户端必须可用 当前明确不在第一版恢复范围内: @@ -292,8 +285,7 @@ Open: - `organization.json` - 已发布 web 静态资源 - `/home/tms/uploads` -- RabbitMQ / TLQ / CFMQ -- `CMEP` / 机构库 +- 机构库 - 在线恢复 / 增量恢复 / 回滚 - 升级执行日志统一写入 `tms.upgrade.log-dir` diff --git a/docs/deployment/tms-deployment.md b/docs/deployment/tms-deployment.md index 49577bb..1d39b51 100644 --- a/docs/deployment/tms-deployment.md +++ b/docs/deployment/tms-deployment.md @@ -874,3 +874,27 @@ bash -n /home/tms/bin/start_standard_apps.sh bash -n /home/tms/bin/start_standard_nginx.sh bash -n /home/tms/bin/check_standard_runtime.sh ``` + +如启用当前第一版资源备份恢复能力,再额外检查: + +```bash +ls -l /home/tms/bin/resource-restore +bash -n /home/tms/bin/resource-restore/apply-resource-backup.sh +bash -n /home/tms/bin/resource-restore/restore-db.sh +bash -n /home/tms/bin/resource-restore/replay-mq.sh +which mysql +which mysqldump +``` + +当前第一版资源恢复只覆盖: +- `TMS_CONFIG` +- `TMS_DB` +- `CMEP_DB` +- `CP_CONFIG` +- `RECEIVER_LICENSE` +- `MQ_REPLAY` + +当前第一版明确不覆盖: +- nginx / web / uploads +- RabbitMQ / TLQ / CFMQ 运行时文件 +- 机构库 diff --git a/scripts/resource-restore/apply-resource-backup.sh b/scripts/resource-restore/apply-resource-backup.sh new file mode 100755 index 0000000..3a11adc --- /dev/null +++ b/scripts/resource-restore/apply-resource-backup.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +set -euo pipefail + +WORK_DIR="" +while [[ $# -gt 0 ]]; do + case "$1" in + --work-dir) + WORK_DIR="${2:-}" + shift 2 + ;; + *) + echo "[ERROR] unsupported argument: $1" >&2 + exit 1 + ;; + esac +done + +if [[ -z "${WORK_DIR}" ]]; then + echo "[ERROR] --work-dir is required" >&2 + exit 1 +fi + +PAYLOAD_DIR="${WORK_DIR}/payload" +STATUS_FILE="${WORK_DIR}/status.json" +LOG_FILE="${WORK_DIR}/restore.log" +MANIFEST="${PAYLOAD_DIR}/payload-manifest.json" +FILES_PLAN="${WORK_DIR}/restore-files.tsv" +DATABASES_PLAN="${WORK_DIR}/restore-databases.tsv" +MQ_PLAN="${WORK_DIR}/restore-mq.tsv" +ENV_FILE="${WORK_DIR}/restore-env.sh" + +json_escape() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + value="${value//$'\n'/ }" + printf '%s' "${value}" +} + +write_status() { + local status="$1" + local phase="$2" + local message="$3" + printf '{"status":"%s","phase":"%s","message":"%s","updatedAt":"%s"}\n' \ + "$(json_escape "${status}")" \ + "$(json_escape "${phase}")" \ + "$(json_escape "${message}")" \ + "$(date '+%Y-%m-%dT%H:%M:%S%z')" > "${STATUS_FILE}" +} + +log() { + printf '[%s] %s\n' "$(date '+%F %T')" "$1" >> "${LOG_FILE}" +} + +fail() { + local message="$1" + local phase="${2:-FAILED}" + log "[ERROR] ${message}" + write_status "FAILED" "${phase}" "${message}" + exit 1 +} + +require_env() { + local name="$1" + local value="${!name:-}" + if [[ -z "${value}" ]]; then + fail "required environment variable ${name} is empty" "${2:-PRECHECK}" + fi +} + +restore_files() { + [[ -f "${FILES_PLAN}" ]] || return 0 + while IFS=$'\t' read -r entry_path restore_path || [[ -n "${entry_path:-}" ]]; do + [[ -n "${entry_path:-}" ]] || continue + [[ -n "${restore_path:-}" ]] || fail "restore file target is empty for ${entry_path}" "FILE" + local source_file="${PAYLOAD_DIR}/${entry_path}" + [[ -f "${source_file}" ]] || fail "restore source file not found: ${entry_path}" "FILE" + mkdir -p "$(dirname "${restore_path}")" + cp -f "${source_file}" "${restore_path}" + log "restored file: ${restore_path}" + done < "${FILES_PLAN}" +} + +restore_databases() { + [[ -f "${DATABASES_PLAN}" ]] || return 0 + [[ -s "${DATABASES_PLAN}" ]] || return 0 + local script="${DB_RESTORE_SCRIPT_PATH:-}" + [[ -n "${script}" && -x "${script}" ]] || fail "DB_RESTORE_SCRIPT_PATH is not executable" "DATABASE" + require_env "DB_HOST" "DATABASE" + require_env "DB_PORT" "DATABASE" + require_env "DB_USER" "DATABASE" + local mysql_path="${MYSQL_PATH:-mysql}" + local db_password="${DB_PASSWORD:-}" + while IFS=$'\t' read -r entry_path db_name || [[ -n "${entry_path:-}" ]]; do + [[ -n "${entry_path:-}" ]] || continue + [[ -n "${db_name:-}" ]] || fail "database name is empty for ${entry_path}" "DATABASE" + local sql_file="${PAYLOAD_DIR}/${entry_path}" + [[ -f "${sql_file}" ]] || fail "sql file not found: ${entry_path}" "DATABASE" + "${script}" \ + --mysql-path "${mysql_path}" \ + --host "${DB_HOST}" \ + --port "${DB_PORT}" \ + --username "${DB_USER}" \ + --password "${db_password}" \ + --db-name "${db_name}" \ + --sql-file "${sql_file}" >> "${LOG_FILE}" 2>&1 + log "restored database: ${db_name}" + done < "${DATABASES_PLAN}" +} + +replay_mq() { + [[ -f "${MQ_PLAN}" ]] || return 0 + [[ -s "${MQ_PLAN}" ]] || return 0 + local script="${MQ_REPLAY_SCRIPT_PATH:-}" + [[ -n "${script}" && -x "${script}" ]] || fail "MQ_REPLAY_SCRIPT_PATH is not executable" "MQ_REPLAY" + while IFS= read -r entry_path || [[ -n "${entry_path:-}" ]]; do + [[ -n "${entry_path:-}" ]] || continue + local context_file="${PAYLOAD_DIR}/${entry_path}" + [[ -f "${context_file}" ]] || fail "mq replay context not found: ${entry_path}" "MQ_REPLAY" + "${script}" --context-file "${context_file}" --work-dir "${WORK_DIR}" >> "${LOG_FILE}" 2>&1 + log "mq replay applied" + done < "${MQ_PLAN}" +} + +health_check() { + local url="${HEALTH_CHECK_URL:-}" + [[ -n "${url}" ]] || { + log "health check skipped: HEALTH_CHECK_URL is empty" + return 0 + } + command -v curl >/dev/null 2>&1 || fail "curl command not found for health check" "HEALTH_CHECK" + local timeout="${HEALTH_CHECK_TIMEOUT_SECONDS:-5}" + local response + response="$(curl -fsS --max-time "${timeout}" "${url}")" || fail "health check request failed" "HEALTH_CHECK" + if [[ "${response}" != *"UP"* ]]; then + fail "health check did not return UP" "HEALTH_CHECK" + fi + log "health check passed" +} + +mkdir -p "${WORK_DIR}" +touch "${LOG_FILE}" +chmod 700 "${WORK_DIR}" || true + +if [[ -f "${ENV_FILE}" ]]; then + # shellcheck disable=SC1090 + source "${ENV_FILE}" +fi + +[[ -f "${MANIFEST}" ]] || fail "payload manifest not found" "PRECHECK" + +write_status "RUNNING" "SERVICE_STOP" "stopping services" +log "SERVICE_STOP" +if [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then + "${TMS_SCRIPT_PATH}" stop >> "${LOG_FILE}" 2>&1 || true +fi + +write_status "RUNNING" "FILE" "restoring files" +log "FILE" +restore_files + +write_status "RUNNING" "DATABASE" "restoring databases" +log "DATABASE" +restore_databases + +write_status "RUNNING" "MQ_REPLAY" "replaying mq configuration" +log "MQ_REPLAY" +replay_mq + +write_status "RUNNING" "SERVICE_START" "starting services" +log "SERVICE_START" +if [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then + "${TMS_SCRIPT_PATH}" start >> "${LOG_FILE}" 2>&1 || true +fi + +write_status "RUNNING" "HEALTH_CHECK" "checking health" +log "HEALTH_CHECK" +health_check + +write_status "SUCCESS" "DONE" "restore applied" +log "DONE" diff --git a/scripts/resource-restore/replay-mq.sh b/scripts/resource-restore/replay-mq.sh new file mode 100755 index 0000000..083c7a3 --- /dev/null +++ b/scripts/resource-restore/replay-mq.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONTEXT_FILE="" +WORK_DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --context-file) + CONTEXT_FILE="${2:-}" + shift 2 + ;; + --work-dir) + WORK_DIR="${2:-}" + shift 2 + ;; + *) + echo "[ERROR] unsupported argument: $1" >&2 + exit 1 + ;; + esac +done + +if [[ -z "${CONTEXT_FILE}" || ! -f "${CONTEXT_FILE}" ]]; then + echo "[ERROR] --context-file is required" >&2 + exit 1 +fi + +WORK_DIR="${WORK_DIR:-$(dirname "${CONTEXT_FILE}")}" +mkdir -p "${WORK_DIR}" +LOG_FILE="${WORK_DIR}/mq-replay.log" +touch "${LOG_FILE}" + +log() { + printf '[%s] %s\n' "$(date '+%F %T')" "$1" >> "${LOG_FILE}" +} + +json_text() { + local key="$1" + sed -nE 's/.*"'"${key}"'"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p' "${CONTEXT_FILE}" | head -n 1 +} + +find_ctl() { + local configured="${RABBITMQ_CTL_COMMAND:-}" + if [[ -n "${configured}" ]]; then + printf '%s' "${configured}" + return 0 + fi + for candidate in rabbitmqctl /usr/local/rabbitmq/sbin/rabbitmqctl /usr/local/rabbitmq-server/sbin/rabbitmqctl /usr/sbin/rabbitmqctl /usr/lib/rabbitmq/bin/rabbitmqctl; do + if command -v "${candidate}" >/dev/null 2>&1; then + command -v "${candidate}" + return 0 + fi + if [[ -x "${candidate}" ]]; then + printf '%s' "${candidate}" + return 0 + fi + done + return 1 +} + +if [[ -n "${MQ_REPLAY_COMMAND:-}" ]]; then + log "delegating mq replay to configured command" + "${MQ_REPLAY_COMMAND}" --context-file "${CONTEXT_FILE}" --work-dir "${WORK_DIR}" >> "${LOG_FILE}" 2>&1 + exit 0 +fi + +MQ_TYPE="$(json_text mqType)" +USER_NAME="$(json_text channelUsername)" +USER_PASS="$(json_text channelPassword)" +ORG_CODE="$(json_text orgCode)" +VHOST="${RABBIT_VHOST:-/RQ}" + +case "${MQ_TYPE}" in + RABBITMQ|RABBITMQ_TLQ|RABBITMQ_CFMQ) + ;; + *) + log "mq replay skipped for mqType=${MQ_TYPE}" + exit 0 + ;; +esac + +if [[ -z "${USER_NAME}" || -z "${USER_PASS}" ]]; then + echo "[ERROR] mq replay context missing channel account" >&2 + exit 1 +fi + +CTL="$(find_ctl)" || { + echo "[ERROR] rabbitmqctl not found" >&2 + exit 1 +} + +"${CTL}" status >> "${LOG_FILE}" 2>&1 || log "rabbitmqctl status failed, continue replay" +"${CTL}" add_vhost "${VHOST}" >> "${LOG_FILE}" 2>&1 || true +"${CTL}" add_user "${USER_NAME}" "${USER_PASS}" >> "${LOG_FILE}" 2>&1 || "${CTL}" change_password "${USER_NAME}" "${USER_PASS}" >> "${LOG_FILE}" 2>&1 +"${CTL}" set_permissions -p "${VHOST}" "${USER_NAME}" ".*" ".*" ".*" >> "${LOG_FILE}" 2>&1 +log "rabbitmq user and permissions replayed" + +if [[ -n "${RABBIT_QUEUE_REPLAY_SCRIPT_PATH:-}" && -x "${RABBIT_QUEUE_REPLAY_SCRIPT_PATH}" ]]; then + STAGED_QUEUE_SCRIPT="${WORK_DIR}/$(basename "${RABBIT_QUEUE_REPLAY_SCRIPT_PATH}")" + cp -f "${RABBIT_QUEUE_REPLAY_SCRIPT_PATH}" "${STAGED_QUEUE_SCRIPT}" + if [[ -n "${ORG_CODE}" ]]; then + sed -i.bak "s/AAAABBBBXXX/${ORG_CODE}/g" "${STAGED_QUEUE_SCRIPT}" || true + fi + bash "${STAGED_QUEUE_SCRIPT}" >> "${LOG_FILE}" 2>&1 + log "rabbitmq queue script replayed" +fi diff --git a/scripts/resource-restore/start-runner.sh b/scripts/resource-restore/start-runner.sh deleted file mode 100755 index b3aaa34..0000000 --- a/scripts/resource-restore/start-runner.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -HANDOFF_FILE="" -while [[ $# -gt 0 ]]; do - case "$1" in - --handoff-file) - HANDOFF_FILE="${2:-}" - shift 2 - ;; - *) - echo "[ERROR] unsupported argument: $1" >&2 - exit 1 - ;; - esac -done - -if [[ -z "${HANDOFF_FILE}" ]]; then - echo "[ERROR] --handoff-file is required" >&2 - exit 1 -fi - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -SOURCE_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -APP_HOME="${APP_HOME:-/home/tms}" -CONF_DIR="${CONF_DIR:-${APP_HOME}/config}" -JAVA_BIN="${JAVA_BIN:-java}" -RUNNER_JAR_PATH="${RUNNER_JAR_PATH:-${APP_HOME}/tms-framework.jar}" -RUNNER_LOG_DIR="${RUNNER_LOG_DIR:-${APP_HOME}/logs}" -RUNNER_STDOUT_LOG="${RUNNER_STDOUT_LOG:-${RUNNER_LOG_DIR}/resource-restore-runner.out.log}" - -if [[ -f "${SOURCE_ROOT}/target/tms-framework.jar" ]]; then - RUNNER_JAR_PATH="${SOURCE_ROOT}/target/tms-framework.jar" -fi - -mkdir -p "${RUNNER_LOG_DIR}" - -nohup "${JAVA_BIN}" \ - -cp "${RUNNER_JAR_PATH}" \ - -Dloader.main=com.cisd.tms.modules.backup.runner.ResourceRestoreRunnerMain \ - org.springframework.boot.loader.launch.PropertiesLauncher \ - --spring.main.web-application-type=none \ - --spring.main.banner-mode=off \ - --spring.config.additional-location="optional:file:${CONF_DIR}/" \ - --tms.backup.runner-handoff-file="${HANDOFF_FILE}" \ - >> "${RUNNER_STDOUT_LOG}" 2>&1 & - -echo $! diff --git a/src/main/java/com/cisd/tms/modules/backup/config/ResourceBackupProperties.java b/src/main/java/com/cisd/tms/modules/backup/config/ResourceBackupProperties.java index 1304e1b..435efeb 100644 --- a/src/main/java/com/cisd/tms/modules/backup/config/ResourceBackupProperties.java +++ b/src/main/java/com/cisd/tms/modules/backup/config/ResourceBackupProperties.java @@ -1,5 +1,8 @@ package com.cisd.tms.modules.backup.config; +import com.cisd.tms.modules.backup.dto.support.BackupResourceKind; +import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource; +import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "tms.backup") @@ -9,18 +12,24 @@ public class ResourceBackupProperties { private String precheckStoreDir = "/home/tms/tmp/resource-restore-precheck"; private String restoreTaskRootDir = "/home/tms/tmp/resource-restore"; private String tmsScriptPath = "/home/tms/scripts/tms.sh"; - private String runnerLaunchScriptPath = "/home/tms/bin/resource-restore/start-runner.sh"; private String mysqldumpPath = "mysqldump"; private String mysqlPath = "mysql"; private String restoreDbScriptPath = "/home/tms/bin/resource-restore/restore-db.sh"; + private String mqReplayScriptPath = "/home/tms/bin/resource-restore/replay-mq.sh"; private String tmsDatabaseName = ""; private String cmepDatabaseName = "CMEP"; private String healthCheckUrl = "http://127.0.0.1:8080/actuator/health"; private int healthCheckTimeoutSeconds = 5; - private boolean runnerAutoLaunchEnabled = false; - private int runnerHeartbeatTimeoutSeconds = 300; private int storageEncKeyIndex = 1; private int authenticitySignKeyIndex = 1; + private String restoreApplyScriptPath = "/home/tms/bin/resource-restore/apply-resource-backup.sh"; + private List allowedRestoreRoots = List.of( + "/home/tms/config", + "/home/cmep4i", + "/home/cmep4d", + "/home/tms/tmp/resource-restore" + ); + private List fileResources = List.of(); public String getOutputDir() { return outputDir; @@ -54,14 +63,6 @@ public class ResourceBackupProperties { this.tmsScriptPath = tmsScriptPath; } - public String getRunnerLaunchScriptPath() { - return runnerLaunchScriptPath; - } - - public void setRunnerLaunchScriptPath(String runnerLaunchScriptPath) { - this.runnerLaunchScriptPath = runnerLaunchScriptPath; - } - public String getMysqldumpPath() { return mysqldumpPath; } @@ -86,6 +87,14 @@ public class ResourceBackupProperties { this.restoreDbScriptPath = restoreDbScriptPath; } + public String getMqReplayScriptPath() { + return mqReplayScriptPath; + } + + public void setMqReplayScriptPath(String mqReplayScriptPath) { + this.mqReplayScriptPath = mqReplayScriptPath; + } + public String getTmsDatabaseName() { return tmsDatabaseName; } @@ -118,22 +127,6 @@ public class ResourceBackupProperties { this.healthCheckTimeoutSeconds = healthCheckTimeoutSeconds; } - public boolean getRunnerAutoLaunchEnabled() { - return runnerAutoLaunchEnabled; - } - - public void setRunnerAutoLaunchEnabled(boolean runnerAutoLaunchEnabled) { - this.runnerAutoLaunchEnabled = runnerAutoLaunchEnabled; - } - - public int getRunnerHeartbeatTimeoutSeconds() { - return runnerHeartbeatTimeoutSeconds; - } - - public void setRunnerHeartbeatTimeoutSeconds(int runnerHeartbeatTimeoutSeconds) { - this.runnerHeartbeatTimeoutSeconds = runnerHeartbeatTimeoutSeconds; - } - public int getStorageEncKeyIndex() { return storageEncKeyIndex; } @@ -149,4 +142,58 @@ public class ResourceBackupProperties { public void setAuthenticitySignKeyIndex(int authenticitySignKeyIndex) { this.authenticitySignKeyIndex = authenticitySignKeyIndex; } + + public String getRestoreApplyScriptPath() { + return restoreApplyScriptPath; + } + + public void setRestoreApplyScriptPath(String restoreApplyScriptPath) { + this.restoreApplyScriptPath = restoreApplyScriptPath; + } + + public List getAllowedRestoreRoots() { + return allowedRestoreRoots; + } + + public void setAllowedRestoreRoots(List allowedRestoreRoots) { + this.allowedRestoreRoots = allowedRestoreRoots; + } + + public List getFileResources() { + return fileResources; + } + + public void setFileResources(List fileResources) { + this.fileResources = fileResources; + } + + public List getDefaultFileResources() { + ConfiguredFileBackupResource tmsConfig = fileResource( + "TMS_CONFIG", + "DIR", + "/home/tms/config", + "/home/tms/config", + true + ); + ConfiguredFileBackupResource cpConfig = fileResource("CP_CONFIG", "FILE", "", "", true); + ConfiguredFileBackupResource receiverLicense = fileResource("RECEIVER_LICENSE", "FILE", "", "", true); + return List.of(tmsConfig, cpConfig, receiverLicense); + } + + private static ConfiguredFileBackupResource fileResource( + String code, + String mode, + String path, + String restorePath, + boolean required + ) { + ConfiguredFileBackupResource resource = new ConfiguredFileBackupResource(); + resource.setCode(code); + resource.setKind(BackupResourceKind.FILE.name()); + resource.setMode(mode); + resource.setPath(path); + resource.setRestorePath(restorePath); + resource.setRequired(required); + return resource; + } } diff --git a/src/main/java/com/cisd/tms/modules/backup/controller/ResourceBackupController.java b/src/main/java/com/cisd/tms/modules/backup/controller/ResourceBackupController.java index 64801ba..4e3d5ac 100644 --- a/src/main/java/com/cisd/tms/modules/backup/controller/ResourceBackupController.java +++ b/src/main/java/com/cisd/tms/modules/backup/controller/ResourceBackupController.java @@ -4,7 +4,6 @@ import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest; import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse; import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse; -import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse; import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse; import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse; import com.cisd.tms.modules.backup.service.ResourceBackupService; @@ -39,12 +38,6 @@ public class ResourceBackupController { private final ResourceBackupService resourceBackupService; - @GetMapping("/keyset/status") - @Operation(summary = "查询资源备份密钥状态", description = "返回资源备份恢复前置密钥状态和公钥指纹。") - public ApiResponse keysetStatus() { - return ApiResponse.success(resourceBackupService.getKeysetStatus()); - } - @PostMapping @ReplayProtected @Operation(summary = "创建资源备份任务", description = "受理资源备份请求并返回备份任务号。") diff --git a/src/main/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceImpl.java b/src/main/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceImpl.java index acedbc2..75f994b 100644 --- a/src/main/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceImpl.java @@ -7,8 +7,8 @@ import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import java.nio.charset.StandardCharsets; import java.security.Security; +import java.util.Arrays; import java.util.Base64; import java.util.LinkedHashMap; import java.util.Map; @@ -75,6 +75,8 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto return sealedPayload; } catch (Exception ex) { throw new IllegalStateException("failed to seal resource payload", ex); + } finally { + wipe(sm4Key); } } @@ -128,7 +130,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto decryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex()); decryptRequest.setCipherBlob(wrappedDek); decryptRequest.setOutBufferSize(dekLength); - byte[] sm4Key = normalize(pcieCryptoService.sdfeInternalDecryptEcc(decryptRequest)); + byte[] sm4Key = normalizeAndWipe(pcieCryptoService.sdfeInternalDecryptEcc(decryptRequest)); byte[] iv = Base64.getDecoder().decode(String.valueOf(envelope.get("ivBase64"))); try { Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER); @@ -136,6 +138,8 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto return cipher.doFinal(cipherBytes); } catch (Exception ex) { throw new IllegalStateException("failed to unseal resource payload", ex); + } finally { + wipe(sm4Key); } } @@ -165,6 +169,16 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto return normalized; } + private static byte[] normalizeAndWipe(BackupDataResult result) { + try { + return normalize(result); + } finally { + if (result != null) { + wipe(result.getData()); + } + } + } + private static int parseInt(Object value, int defaultValue) { if (value == null) { return defaultValue; @@ -182,4 +196,10 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto private static String buildFingerprint(String prefix, int keyIndex) { return prefix + ":" + keyIndex; } + + private static void wipe(byte[] bytes) { + if (bytes != null) { + Arrays.fill(bytes, (byte) 0); + } + } } diff --git a/src/main/java/com/cisd/tms/modules/backup/discovery/ResourceDiscoveryServiceImpl.java b/src/main/java/com/cisd/tms/modules/backup/discovery/ResourceDiscoveryServiceImpl.java index fb30062..bf82495 100644 --- a/src/main/java/com/cisd/tms/modules/backup/discovery/ResourceDiscoveryServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/backup/discovery/ResourceDiscoveryServiceImpl.java @@ -28,24 +28,15 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService { } List items = new ArrayList<>(); - items.add(item("FILE", "TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), true)); - items.add(item("DB", "TMS_DB_DUMP", "TMS", true)); + items.add(file("TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), initExecutorProperties.getTmsConfigDir(), "DIR", true)); + items.add(file("CP_CONFIG", resolveCpConfigPath(productType), resolveCpConfigPath(productType), "FILE", true)); + items.add(file("RECEIVER_LICENSE", resolveReceiverLicensePath(productType), resolveReceiverLicensePath(productType), "FILE", true)); + items.add(database("TMS_DB", "TMS", "db/TMS.sql", true)); if (!"DIRECT".equals(productType)) { // 第一版标准版迁移需要把收发器业务库一起带走,DIRECT 暂时不要求这份库。 - items.add(item("DB", "CMEP_DB_DUMP", "CMEP", true)); - } - items.add(item("FILE", "CP_CONFIG", resolveCpConfigPath(productType), true)); - items.add(item("FILE", "RECEIVER_LICENSE", resolveReceiverLicensePath(productType), true)); - - if (mqType.startsWith("RABBITMQ")) { - items.add(item("MQ", "RABBITMQ_DEFINITIONS", initExecutorProperties.getRabbitmqAdminCommand(), true)); - } - if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { - items.add(item("FILE", "TLQ_LICENSE", resolveTlqLicensePath(productType), true)); - } - if ("RABBITMQ_CFMQ".equals(mqType)) { - items.add(item("FILE", "CFMQ_CONFIG", initExecutorProperties.getCfmqTargetDir(), true)); + items.add(database("CMEP_DB", "CMEP", "db/CMEP.sql", true)); } + items.add(mqReplay(true)); return items; } @@ -63,18 +54,33 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService { return initExecutorProperties.getStandardReceiverLicenseTargetPath(); } - private String resolveTlqLicensePath(String productType) { - if ("DIRECT".equals(productType)) { - return initExecutorProperties.getDirectTlqLicenseTargetPath(); - } - return initExecutorProperties.getStandardTlqLicenseTargetPath(); - } - - private static DiscoveredResourceItem item(String type, String code, String path, boolean required) { + private static DiscoveredResourceItem file(String code, String path, String restorePath, String mode, boolean required) { DiscoveredResourceItem item = new DiscoveredResourceItem(); - item.setResourceType(type); + item.setResourceType("FILE"); item.setResourceCode(code); item.setResolvedPath(path); + item.setRestorePath(restorePath); + item.setMode(mode); + item.setRequired(required); + return item; + } + + private static DiscoveredResourceItem database(String code, String databaseName, String entryPath, boolean required) { + DiscoveredResourceItem item = new DiscoveredResourceItem(); + item.setResourceType("DATABASE"); + item.setResourceCode(code); + item.setDatabaseName(databaseName); + item.setResolvedPath(databaseName); + item.setEntryPath(entryPath); + item.setRequired(required); + return item; + } + + private static DiscoveredResourceItem mqReplay(boolean required) { + DiscoveredResourceItem item = new DiscoveredResourceItem(); + item.setResourceType("MQ_REPLAY"); + item.setResourceCode("MQ_REPLAY"); + item.setEntryPath("mq/mq-restore-context.json"); item.setRequired(required); return item; } diff --git a/src/main/java/com/cisd/tms/modules/backup/dto/response/ResourceRestoreTaskDetailResponse.java b/src/main/java/com/cisd/tms/modules/backup/dto/response/ResourceRestoreTaskDetailResponse.java index 8116fb1..d73aa35 100644 --- a/src/main/java/com/cisd/tms/modules/backup/dto/response/ResourceRestoreTaskDetailResponse.java +++ b/src/main/java/com/cisd/tms/modules/backup/dto/response/ResourceRestoreTaskDetailResponse.java @@ -21,7 +21,7 @@ public class ResourceRestoreTaskDetailResponse { private String currentStepCode; private Integer currentStepNo; private String lastHeartbeatAt; - private String runnerStatePath; + private String statusFilePath; private Boolean orphaned; private List cleanupWarnings; private List warnings; @@ -155,12 +155,12 @@ public class ResourceRestoreTaskDetailResponse { this.lastHeartbeatAt = lastHeartbeatAt; } - public String getRunnerStatePath() { - return runnerStatePath; + public String getStatusFilePath() { + return statusFilePath; } - public void setRunnerStatePath(String runnerStatePath) { - this.runnerStatePath = runnerStatePath; + public void setStatusFilePath(String statusFilePath) { + this.statusFilePath = statusFilePath; } public Boolean getOrphaned() { diff --git a/src/main/java/com/cisd/tms/modules/backup/dto/support/BackupResourceKind.java b/src/main/java/com/cisd/tms/modules/backup/dto/support/BackupResourceKind.java new file mode 100644 index 0000000..f79701f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/backup/dto/support/BackupResourceKind.java @@ -0,0 +1,7 @@ +package com.cisd.tms.modules.backup.dto.support; + +public enum BackupResourceKind { + FILE, + DATABASE, + MQ_REPLAY +} diff --git a/src/main/java/com/cisd/tms/modules/backup/dto/support/ConfiguredFileBackupResource.java b/src/main/java/com/cisd/tms/modules/backup/dto/support/ConfiguredFileBackupResource.java new file mode 100644 index 0000000..d1c7637 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/backup/dto/support/ConfiguredFileBackupResource.java @@ -0,0 +1,59 @@ +package com.cisd.tms.modules.backup.dto.support; + +public class ConfiguredFileBackupResource { + + private String code; + private String kind = BackupResourceKind.FILE.name(); + private String mode = "FILE"; + private String path; + private String restorePath; + private boolean required = true; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getRestorePath() { + return restorePath; + } + + public void setRestorePath(String restorePath) { + this.restorePath = restorePath; + } + + public boolean isRequired() { + return required; + } + + public void setRequired(boolean required) { + this.required = required; + } +} diff --git a/src/main/java/com/cisd/tms/modules/backup/dto/support/DiscoveredResourceItem.java b/src/main/java/com/cisd/tms/modules/backup/dto/support/DiscoveredResourceItem.java index 5baa990..d718c8e 100644 --- a/src/main/java/com/cisd/tms/modules/backup/dto/support/DiscoveredResourceItem.java +++ b/src/main/java/com/cisd/tms/modules/backup/dto/support/DiscoveredResourceItem.java @@ -5,6 +5,10 @@ public class DiscoveredResourceItem { private String resourceType; private String resourceCode; private String resolvedPath; + private String restorePath; + private String mode; + private String databaseName; + private String entryPath; private boolean required; public String getResourceType() { @@ -31,6 +35,38 @@ public class DiscoveredResourceItem { this.resolvedPath = resolvedPath; } + public String getRestorePath() { + return restorePath; + } + + public void setRestorePath(String restorePath) { + this.restorePath = restorePath; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public String getDatabaseName() { + return databaseName; + } + + public void setDatabaseName(String databaseName) { + this.databaseName = databaseName; + } + + public String getEntryPath() { + return entryPath; + } + + public void setEntryPath(String entryPath) { + this.entryPath = entryPath; + } + public boolean isRequired() { return required; } diff --git a/src/main/java/com/cisd/tms/modules/backup/enums/ResourceControlPhase.java b/src/main/java/com/cisd/tms/modules/backup/enums/ResourceControlPhase.java index 87d3c5c..158d7bf 100644 --- a/src/main/java/com/cisd/tms/modules/backup/enums/ResourceControlPhase.java +++ b/src/main/java/com/cisd/tms/modules/backup/enums/ResourceControlPhase.java @@ -5,8 +5,7 @@ package com.cisd.tms.modules.backup.enums; */ public enum ResourceControlPhase { TMS_PRECHECK, - HANDOFF_READY, - RUNNER_OWNED, - RECONCILE_PENDING, + JAVA_EXTRACT, + SHELL_APPLY, DONE } diff --git a/src/main/java/com/cisd/tms/modules/backup/enums/ResourceTaskStatus.java b/src/main/java/com/cisd/tms/modules/backup/enums/ResourceTaskStatus.java index 781bafb..5c71880 100644 --- a/src/main/java/com/cisd/tms/modules/backup/enums/ResourceTaskStatus.java +++ b/src/main/java/com/cisd/tms/modules/backup/enums/ResourceTaskStatus.java @@ -8,8 +8,7 @@ public enum ResourceTaskStatus { PRECHECKING, PRECHECK_FAILED, READY, - HANDOFF, - RUNNING, + APPLYING, VERIFYING, SUCCESS, FAILED, diff --git a/src/main/java/com/cisd/tms/modules/backup/packagex/MqReplayContextBuilder.java b/src/main/java/com/cisd/tms/modules/backup/packagex/MqReplayContextBuilder.java new file mode 100644 index 0000000..1ce2d78 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/backup/packagex/MqReplayContextBuilder.java @@ -0,0 +1,77 @@ +package com.cisd.tms.modules.backup.packagex; + +import com.cisd.tms.modules.init.entity.InitTaskEntity; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.stereotype.Component; + +@Component +public class MqReplayContextBuilder { + + private final ObjectMapper objectMapper; + + public MqReplayContextBuilder(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public byte[] build(InitTaskEntity task) { + if (task == null || isBlank(task.getInitPlanJson())) { + throw new IllegalArgumentException("initPlanJson is empty"); + } + try { + JsonNode root = objectMapper.readTree(task.getInitPlanJson()); + JsonNode request = root.path("request"); + JsonNode nodes = request.path("nodes"); + JsonNode mq = request.path("mq"); + + Map payload = new LinkedHashMap<>(); + payload.put("schemaVersion", "1.0"); + payload.put("productType", firstNonBlank(text(root, "productType"), task.getProductType())); + payload.put("mqType", firstNonBlank(text(mq, "mqType"), text(root, "mqType"), task.getMqType())); + payload.put("orgCodeType", text(request, "orgCodeType")); + payload.put("orgCode", firstNonBlank(text(request, "orgCode"), text(root, "orgCode"), task.getOrgCode())); + payload.put("orgNameCn", text(request, "orgNameCn")); + payload.put("orgNameEn", text(request, "orgNameEn")); + payload.put("deployMode", text(request, "deployMode")); + + Map nodeValues = new LinkedHashMap<>(); + nodeValues.put("node01Ip", text(nodes, "node01Ip")); + nodeValues.put("node02Ip", text(nodes, "node02Ip")); + nodeValues.put("node03Ip", text(nodes, "node03Ip")); + nodeValues.put("node04Ip", text(nodes, "node04Ip")); + payload.put("nodes", nodeValues); + + Map mqValues = new LinkedHashMap<>(); + mqValues.put("channelUsername", text(mq, "channelUsername")); + mqValues.put("channelPassword", text(mq, "channelPassword")); + mqValues.put("cipsGatewayHost", text(mq, "cipsGatewayHost")); + mqValues.put("cfgZipResourceCode", ""); + mqValues.put("cfmqConfigResourceCode", ""); + payload.put("mq", mqValues); + + return objectMapper.writeValueAsBytes(payload); + } catch (Exception ex) { + throw new IllegalArgumentException("failed to build mq replay context", ex); + } + } + + private static String text(JsonNode node, String field) { + JsonNode value = node == null ? null : node.path(field); + return value == null || value.isMissingNode() || value.isNull() ? "" : value.asText(""); + } + + private static String firstNonBlank(String... values) { + for (String value : values) { + if (!isBlank(value)) { + return value.trim(); + } + } + return ""; + } + + private static boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceImpl.java b/src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceImpl.java index ce67a24..0334d1a 100644 --- a/src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceImpl.java @@ -37,6 +37,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService { private final ResourceBackupProperties resourceBackupProperties; private final ResourceEnvelopeCryptoService resourceEnvelopeCryptoService; private final DatabaseBackupCollector databaseBackupCollector; + private final MqReplayContextBuilder mqReplayContextBuilder; private final ObjectMapper objectMapper; public ResourcePackageServiceImpl( @@ -44,10 +45,21 @@ public class ResourcePackageServiceImpl implements ResourcePackageService { ResourceEnvelopeCryptoService resourceEnvelopeCryptoService, DatabaseBackupCollector databaseBackupCollector, ObjectMapper objectMapper + ) { + this(resourceBackupProperties, resourceEnvelopeCryptoService, databaseBackupCollector, new MqReplayContextBuilder(objectMapper), objectMapper); + } + + public ResourcePackageServiceImpl( + ResourceBackupProperties resourceBackupProperties, + ResourceEnvelopeCryptoService resourceEnvelopeCryptoService, + DatabaseBackupCollector databaseBackupCollector, + MqReplayContextBuilder mqReplayContextBuilder, + ObjectMapper objectMapper ) { this.resourceBackupProperties = resourceBackupProperties; this.resourceEnvelopeCryptoService = resourceEnvelopeCryptoService; this.databaseBackupCollector = databaseBackupCollector; + this.mqReplayContextBuilder = mqReplayContextBuilder; this.objectMapper = objectMapper; } @@ -225,7 +237,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService { resourceEntry.put("resolvedPath", item.getResolvedPath()); resourceEntry.put("required", item.isRequired()); - int collectedFiles = collectResourceItem(zipOutputStream, item, resourceEntry); + int collectedFiles = collectResourceItem(zipOutputStream, initTask, item, resourceEntry); if (collectedFiles > 0) { collectedCount++; } @@ -243,18 +255,26 @@ public class ResourcePackageServiceImpl implements ResourcePackageService { private int collectResourceItem( ZipOutputStream zipOutputStream, + InitTaskEntity initTask, DiscoveredResourceItem item, Map resourceEntry ) throws IOException { - if ("DB".equalsIgnoreCase(trim(item.getResourceType()))) { + if ("DATABASE".equalsIgnoreCase(trim(item.getResourceType())) || "DB".equalsIgnoreCase(trim(item.getResourceType()))) { return collectDatabaseItem(zipOutputStream, item, resourceEntry); } + if ("MQ_REPLAY".equalsIgnoreCase(trim(item.getResourceType()))) { + return collectMqReplayItem(zipOutputStream, initTask, item, resourceEntry); + } if (!"FILE".equalsIgnoreCase(trim(item.getResourceType()))) { assertOptionalResource(item, "RESOURCE_TYPE_NOT_SUPPORTED_YET"); resourceEntry.put("collected", Boolean.FALSE); resourceEntry.put("skipReason", "RESOURCE_TYPE_NOT_SUPPORTED_YET"); return 0; } + resourceEntry.put("kind", "FILE"); + resourceEntry.put("mode", trim(item.getMode()).isEmpty() ? "FILE" : trim(item.getMode())); + resourceEntry.put("sourcePath", item.getResolvedPath()); + resourceEntry.put("restorePath", trim(item.getRestorePath()).isEmpty() ? item.getResolvedPath() : item.getRestorePath()); Path resolvedPath = toResolvedPath(item.getResolvedPath()); if (resolvedPath == null || !Files.exists(resolvedPath)) { assertOptionalResource(item, "PATH_NOT_FOUND"); @@ -328,8 +348,10 @@ public class ResourcePackageServiceImpl implements ResourcePackageService { fileEntry.put("contentSm3", sm3Hex(content)); resourceEntry.put("collected", Boolean.TRUE); + resourceEntry.put("kind", "DATABASE"); resourceEntry.put("fileCount", 1); resourceEntry.put("entryPath", entryPath); + resourceEntry.put("database", databaseName); resourceEntry.put("databaseName", databaseName); resourceEntry.put("files", List.of(fileEntry)); return 1; @@ -341,12 +363,28 @@ public class ResourcePackageServiceImpl implements ResourcePackageService { } } + private int collectMqReplayItem( + ZipOutputStream zipOutputStream, + InitTaskEntity initTask, + DiscoveredResourceItem item, + Map resourceEntry + ) throws IOException { + byte[] contextBytes = mqReplayContextBuilder.build(initTask); + String entryPath = trim(item.getEntryPath()).isEmpty() ? "mq/mq-restore-context.json" : trim(item.getEntryPath()); + addZipEntry(zipOutputStream, entryPath, contextBytes); + resourceEntry.put("kind", "MQ_REPLAY"); + resourceEntry.put("collected", Boolean.TRUE); + resourceEntry.put("entryPath", entryPath); + resourceEntry.put("contentSm3", sm3Hex(contextBytes)); + return 1; + } + private DatabaseBackupCollector.CollectedDatabaseDump resolveDatabaseDump(DiscoveredResourceItem item) { String resourceCode = trim(item.getResourceCode()); - if ("TMS_DB_DUMP".equalsIgnoreCase(resourceCode)) { + if ("TMS_DB".equalsIgnoreCase(resourceCode) || "TMS_DB_DUMP".equalsIgnoreCase(resourceCode)) { return databaseBackupCollector.collectTmsDatabaseDump(); } - if ("CMEP_DB_DUMP".equalsIgnoreCase(resourceCode)) { + if ("CMEP_DB".equalsIgnoreCase(resourceCode) || "CMEP_DB_DUMP".equalsIgnoreCase(resourceCode)) { return databaseBackupCollector.collectCmepDatabaseDump(); } return null; diff --git a/src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractionResult.java b/src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractionResult.java new file mode 100644 index 0000000..65a387a --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractionResult.java @@ -0,0 +1,43 @@ +package com.cisd.tms.modules.backup.packagex; + +import java.nio.file.Path; + +public class ResourcePayloadExtractionResult { + + private Path workDir; + private Path payloadDir; + private Path manifestPath; + private Path restoreContextPath; + + public Path getWorkDir() { + return workDir; + } + + public void setWorkDir(Path workDir) { + this.workDir = workDir; + } + + public Path getPayloadDir() { + return payloadDir; + } + + public void setPayloadDir(Path payloadDir) { + this.payloadDir = payloadDir; + } + + public Path getManifestPath() { + return manifestPath; + } + + public void setManifestPath(Path manifestPath) { + this.manifestPath = manifestPath; + } + + public Path getRestoreContextPath() { + return restoreContextPath; + } + + public void setRestoreContextPath(Path restoreContextPath) { + this.restoreContextPath = restoreContextPath; + } +} diff --git a/src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractor.java b/src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractor.java new file mode 100644 index 0000000..e9eca53 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractor.java @@ -0,0 +1,327 @@ +package com.cisd.tms.modules.backup.packagex; + +import com.cisd.tms.modules.backup.config.ResourceBackupProperties; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.springframework.stereotype.Component; + +@Component +public class ResourcePayloadExtractor { + + private final ResourcePackageService resourcePackageService; + private final ObjectMapper objectMapper; + private final ResourceBackupProperties properties; + + public ResourcePayloadExtractor( + ResourcePackageService resourcePackageService, + ObjectMapper objectMapper, + ResourceBackupProperties properties + ) { + this.resourcePackageService = resourcePackageService; + this.objectMapper = objectMapper; + this.properties = properties; + } + + public ResourcePayloadExtractionResult extract(Path packagePath, Path workDir) { + Path normalizedWorkDir = workDir.normalize(); + Path payloadDir = normalizedWorkDir.resolve("payload").normalize(); + ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackage(packagePath); + byte[] payloadArchiveBytes = Optional.ofNullable(inspectResult.getPayloadArchiveBytes()) + .orElseThrow(() -> new IllegalStateException("payload archive bytes missing")); + extractPayloadArchive(payloadArchiveBytes, payloadDir); + + Path manifestPath = payloadDir.resolve("payload-manifest.json").normalize(); + if (!Files.isRegularFile(manifestPath)) { + throw new IllegalStateException("payload manifest missing: " + manifestPath); + } + Map manifest = readManifest(manifestPath); + validateRestoreTargets(payloadDir, manifest); + writeShellPlans(normalizedWorkDir, manifest); + Path restoreContextPath = writeRestoreContext(packagePath, normalizedWorkDir, payloadDir, manifestPath, manifest); + + ResourcePayloadExtractionResult result = new ResourcePayloadExtractionResult(); + result.setWorkDir(normalizedWorkDir); + result.setPayloadDir(payloadDir); + result.setManifestPath(manifestPath); + result.setRestoreContextPath(restoreContextPath); + return result; + } + + private void extractPayloadArchive(byte[] payloadArchiveBytes, Path payloadDir) { + try { + Files.createDirectories(payloadDir); + try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(payloadArchiveBytes))) { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + Path target = resolveSafePayloadTarget(payloadDir, entry.getName()); + if (entry.isDirectory()) { + Files.createDirectories(target); + } else { + Files.createDirectories(target.getParent()); + Files.write(target, readAllBytes(zipInputStream)); + } + zipInputStream.closeEntry(); + } + } + } catch (IOException ex) { + throw new IllegalStateException("failed to extract payload archive", ex); + } + } + + private Path resolveSafePayloadTarget(Path payloadDir, String entryName) { + Path target = payloadDir.resolve(entryName).normalize(); + if (!target.startsWith(payloadDir)) { + throw new IllegalStateException("payload archive entry escapes payload dir: " + entryName); + } + return target; + } + + private Map readManifest(Path manifestPath) { + try { + return objectMapper.readValue(manifestPath.toFile(), new TypeReference>() { + }); + } catch (IOException ex) { + throw new IllegalStateException("failed to read payload manifest: " + manifestPath, ex); + } + } + + private void validateRestoreTargets(Path payloadDir, Map manifest) { + List allowedRoots = allowedRestoreRoots(); + Object resourcesValue = manifest.get("resources"); + if (!(resourcesValue instanceof List resources)) { + return; + } + for (Object resourceObject : resources) { + if (!(resourceObject instanceof Map rawResource)) { + continue; + } + Map resource = castMap(rawResource); + if (!"FILE".equalsIgnoreCase(kindOf(resource))) { + continue; + } + String resourceRestorePath = firstText(resource, "restorePath", "resolvedPath"); + validateAllowedRestorePath(resourceRestorePath, allowedRoots); + validateFileEntries(payloadDir, resource, resourceRestorePath, allowedRoots); + } + } + + private void writeShellPlans(Path workDir, Map manifest) { + List fileLines = new ArrayList<>(); + List databaseLines = new ArrayList<>(); + List mqLines = new ArrayList<>(); + Object resourcesValue = manifest.get("resources"); + if (resourcesValue instanceof List resources) { + for (Object resourceObject : resources) { + if (!(resourceObject instanceof Map rawResource)) { + continue; + } + Map resource = castMap(rawResource); + String kind = kindOf(resource); + if ("FILE".equalsIgnoreCase(kind)) { + appendFilePlanLines(fileLines, resource); + } else if ("DATABASE".equalsIgnoreCase(kind) || "DB".equalsIgnoreCase(kind)) { + String entryPath = firstText(resource, "entryPath"); + String databaseName = normalizeDatabaseName(firstText(resource, "databaseName", "database", "resourceCode", "code")); + if (!entryPath.isEmpty()) { + databaseLines.add(entryPath + "\t" + databaseName); + } + } else if ("MQ_REPLAY".equalsIgnoreCase(kind)) { + String entryPath = firstText(resource, "entryPath"); + if (!entryPath.isEmpty()) { + mqLines.add(entryPath); + } + } + } + } + try { + Files.writeString(workDir.resolve("restore-files.tsv"), String.join(System.lineSeparator(), fileLines) + + (fileLines.isEmpty() ? "" : System.lineSeparator())); + Files.writeString(workDir.resolve("restore-databases.tsv"), String.join(System.lineSeparator(), databaseLines) + + (databaseLines.isEmpty() ? "" : System.lineSeparator())); + Files.writeString(workDir.resolve("restore-mq.tsv"), String.join(System.lineSeparator(), mqLines) + + (mqLines.isEmpty() ? "" : System.lineSeparator())); + } catch (IOException ex) { + throw new IllegalStateException("failed to write shell restore plans", ex); + } + } + + private void appendFilePlanLines(List fileLines, Map resource) { + String resourceRestorePath = firstText(resource, "restorePath", "resolvedPath"); + Object filesValue = resource.get("files"); + if (!(filesValue instanceof List files)) { + return; + } + for (Object fileObject : files) { + if (!(fileObject instanceof Map rawFile)) { + continue; + } + Map fileEntry = castMap(rawFile); + String entryPath = firstText(fileEntry, "entryPath"); + if (entryPath.isEmpty()) { + continue; + } + String restorePath = firstText(fileEntry, "restorePath"); + if (restorePath.isEmpty()) { + restorePath = inferFileRestorePath(resource, resourceRestorePath, fileEntry); + } + if (!restorePath.isEmpty()) { + fileLines.add(entryPath + "\t" + restorePath); + } + } + } + + private void validateFileEntries( + Path payloadDir, + Map resource, + String resourceRestorePath, + List allowedRoots + ) { + Object filesValue = resource.get("files"); + if (!(filesValue instanceof List files)) { + return; + } + for (Object fileObject : files) { + if (!(fileObject instanceof Map rawFile)) { + continue; + } + Map fileEntry = castMap(rawFile); + validatePayloadEntryPath(payloadDir, firstText(fileEntry, "entryPath")); + String fileRestorePath = firstText(fileEntry, "restorePath"); + if (fileRestorePath.isEmpty()) { + fileRestorePath = inferFileRestorePath(resource, resourceRestorePath, fileEntry); + } + validateAllowedRestorePath(fileRestorePath, allowedRoots); + } + } + + private void validatePayloadEntryPath(Path payloadDir, String entryPath) { + if (entryPath.isEmpty()) { + return; + } + Path sourceFile = payloadDir.resolve(entryPath).normalize(); + if (!sourceFile.startsWith(payloadDir)) { + throw new IllegalStateException("payload manifest entry escapes payload dir: " + entryPath); + } + } + + private String inferFileRestorePath(Map resource, String resourceRestorePath, Map fileEntry) { + if (resourceRestorePath.isEmpty()) { + return ""; + } + String relativeName = firstText(fileEntry, "relativeName"); + String mode = firstText(resource, "mode"); + if (!relativeName.isEmpty() && "DIR".equalsIgnoreCase(mode)) { + return Path.of(resourceRestorePath).resolve(relativeName).normalize().toString(); + } + return resourceRestorePath; + } + + private void validateAllowedRestorePath(String restorePath, List allowedRoots) { + if (restorePath.isEmpty()) { + return; + } + Path target = Path.of(restorePath).normalize().toAbsolutePath(); + boolean allowed = allowedRoots.stream().anyMatch(target::startsWith); + if (!allowed) { + throw new IllegalStateException("restore path is not allowed: " + restorePath); + } + } + + private List allowedRestoreRoots() { + List configuredRoots = properties.getAllowedRestoreRoots(); + if (configuredRoots == null || configuredRoots.isEmpty()) { + return List.of(); + } + List roots = new ArrayList<>(); + for (String configuredRoot : configuredRoots) { + String root = trim(configuredRoot); + if (!root.isEmpty()) { + roots.add(Path.of(root).normalize().toAbsolutePath()); + } + } + return roots; + } + + private Path writeRestoreContext( + Path packagePath, + Path workDir, + Path payloadDir, + Path manifestPath, + Map manifest + ) { + try { + Files.createDirectories(workDir); + Path restoreContextPath = workDir.resolve("restore-context.json").normalize(); + Map context = new LinkedHashMap<>(); + context.put("schemaVersion", "1.0"); + context.put("createdAt", Instant.now().toString()); + context.put("packagePath", packagePath.normalize().toString()); + context.put("workDir", workDir.toString()); + context.put("payloadDir", payloadDir.toString()); + context.put("manifestPath", manifestPath.toString()); + context.put("payloadManifest", manifest); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(restoreContextPath.toFile(), context); + return restoreContextPath; + } catch (IOException ex) { + throw new IllegalStateException("failed to write restore context", ex); + } + } + + private static Map castMap(Map rawMap) { + Map result = new LinkedHashMap<>(); + rawMap.forEach((key, value) -> result.put(String.valueOf(key), value)); + return result; + } + + private static String kindOf(Map resource) { + String kind = firstText(resource, "kind"); + if (!kind.isEmpty()) { + return kind; + } + return firstText(resource, "resourceType"); + } + + private static String firstText(Map map, String... keys) { + for (String key : keys) { + Object value = map.get(key); + if (value != null) { + String text = trim(String.valueOf(value)); + if (!text.isEmpty() && !"null".equalsIgnoreCase(text)) { + return text; + } + } + } + return ""; + } + + private static String normalizeDatabaseName(String value) { + if ("TMS_DB".equalsIgnoreCase(value) || "TMS_DB_DUMP".equalsIgnoreCase(value)) { + return "TMS"; + } + if ("CMEP_DB".equalsIgnoreCase(value) || "CMEP_DB_DUMP".equalsIgnoreCase(value)) { + return "CMEP"; + } + return value; + } + + private static byte[] readAllBytes(InputStream inputStream) throws IOException { + return inputStream.readAllBytes(); + } + + private static String trim(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncher.java b/src/main/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncher.java new file mode 100644 index 0000000..e7a508c --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncher.java @@ -0,0 +1,108 @@ +package com.cisd.tms.modules.backup.restore; + +import com.cisd.tms.modules.backup.config.ResourceBackupProperties; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class LightweightRestoreApplierLauncher { + + private final ResourceBackupProperties properties; + private final RestoreCommandInvoker invoker; + private final String datasourceUrl; + private final String datasourceUsername; + private final String datasourcePassword; + + public LightweightRestoreApplierLauncher( + ResourceBackupProperties properties, + RestoreCommandInvoker invoker, + @Value("${spring.datasource.url:}") String datasourceUrl, + @Value("${spring.datasource.username:}") String datasourceUsername, + @Value("${spring.datasource.password:}") String datasourcePassword + ) { + this.properties = properties; + this.invoker = invoker; + this.datasourceUrl = datasourceUrl; + this.datasourceUsername = datasourceUsername; + this.datasourcePassword = datasourcePassword; + } + + public void launch(Path workDir) { + Path normalizedWorkDir = workDir.toAbsolutePath().normalize(); + writeRestoreEnv(normalizedWorkDir); + Path logFile = normalizedWorkDir.resolve("restore-launch.log"); + List command = List.of( + "/bin/bash", + properties.getRestoreApplyScriptPath(), + "--work-dir", + normalizedWorkDir.toString() + ); + int exitCode = invoker.run(command, logFile, normalizedWorkDir); + if (exitCode != 0) { + throw new IllegalStateException("failed to launch lightweight restore apply script, exitCode=" + exitCode); + } + } + + private void writeRestoreEnv(Path workDir) { + try { + Files.createDirectories(workDir); + Map values = new LinkedHashMap<>(); + MysqlTarget target = parseMysqlTarget(datasourceUrl); + values.put("TMS_SCRIPT_PATH", trim(properties.getTmsScriptPath())); + values.put("DB_RESTORE_SCRIPT_PATH", trim(properties.getRestoreDbScriptPath())); + values.put("MQ_REPLAY_SCRIPT_PATH", trim(properties.getMqReplayScriptPath())); + values.put("MYSQL_PATH", trim(properties.getMysqlPath()).isEmpty() ? "mysql" : trim(properties.getMysqlPath())); + values.put("DB_HOST", target.host()); + values.put("DB_PORT", target.port()); + values.put("DB_USER", trim(datasourceUsername)); + values.put("DB_PASSWORD", trim(datasourcePassword)); + values.put("HEALTH_CHECK_URL", trim(properties.getHealthCheckUrl())); + values.put("HEALTH_CHECK_TIMEOUT_SECONDS", String.valueOf(properties.getHealthCheckTimeoutSeconds())); + + StringBuilder builder = new StringBuilder("# generated by TMS resource restore\n"); + for (Map.Entry entry : values.entrySet()) { + builder.append("export ") + .append(entry.getKey()) + .append("=") + .append(quoteShell(entry.getValue())) + .append(System.lineSeparator()); + } + Files.writeString(workDir.resolve("restore-env.sh"), builder.toString()); + } catch (IOException ex) { + throw new IllegalStateException("failed to write lightweight restore env", ex); + } + } + + private static MysqlTarget parseMysqlTarget(String datasourceUrl) { + String url = trim(datasourceUrl); + if (url.startsWith("jdbc:mysql://")) { + try { + URI uri = URI.create(url.substring("jdbc:".length()).split("\\?", 2)[0]); + String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost(); + int port = uri.getPort() <= 0 ? 3306 : uri.getPort(); + return new MysqlTarget(host, String.valueOf(port)); + } catch (IllegalArgumentException ignore) { + return new MysqlTarget("", ""); + } + } + return new MysqlTarget("", ""); + } + + private static String quoteShell(String value) { + return "'" + trim(value).replace("'", "'\\''") + "'"; + } + + private static String trim(String value) { + return value == null ? "" : value.trim(); + } + + private record MysqlTarget(String host, String port) { + } +} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/ProcessBuilderRunnerCommandInvoker.java b/src/main/java/com/cisd/tms/modules/backup/restore/ProcessBuilderRestoreCommandInvoker.java similarity index 74% rename from src/main/java/com/cisd/tms/modules/backup/runner/ProcessBuilderRunnerCommandInvoker.java rename to src/main/java/com/cisd/tms/modules/backup/restore/ProcessBuilderRestoreCommandInvoker.java index 4eb831b..385a096 100644 --- a/src/main/java/com/cisd/tms/modules/backup/runner/ProcessBuilderRunnerCommandInvoker.java +++ b/src/main/java/com/cisd/tms/modules/backup/restore/ProcessBuilderRestoreCommandInvoker.java @@ -1,4 +1,4 @@ -package com.cisd.tms.modules.backup.runner; +package com.cisd.tms.modules.backup.restore; import java.io.IOException; import java.nio.file.Files; @@ -7,7 +7,7 @@ import java.util.List; import org.springframework.stereotype.Component; @Component -public class ProcessBuilderRunnerCommandInvoker implements RunnerCommandInvoker { +public class ProcessBuilderRestoreCommandInvoker implements RestoreCommandInvoker { @Override public int run(List command, Path logFile, Path workingDirectory) { @@ -27,10 +27,10 @@ public class ProcessBuilderRunnerCommandInvoker implements RunnerCommandInvoker Process process = pb.start(); return process.waitFor(); } catch (IOException ex) { - throw new IllegalStateException("failed to start runner command: " + String.join(" ", command), ex); + throw new IllegalStateException("failed to start restore command: " + String.join(" ", command), ex); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); - throw new IllegalStateException("runner command interrupted: " + String.join(" ", command), ex); + throw new IllegalStateException("restore command interrupted: " + String.join(" ", command), ex); } } } diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/RunnerCommandInvoker.java b/src/main/java/com/cisd/tms/modules/backup/restore/RestoreCommandInvoker.java similarity index 59% rename from src/main/java/com/cisd/tms/modules/backup/runner/RunnerCommandInvoker.java rename to src/main/java/com/cisd/tms/modules/backup/restore/RestoreCommandInvoker.java index 42b6f95..b8bc686 100644 --- a/src/main/java/com/cisd/tms/modules/backup/runner/RunnerCommandInvoker.java +++ b/src/main/java/com/cisd/tms/modules/backup/restore/RestoreCommandInvoker.java @@ -1,9 +1,9 @@ -package com.cisd.tms.modules.backup.runner; +package com.cisd.tms.modules.backup.restore; import java.nio.file.Path; import java.util.List; -public interface RunnerCommandInvoker { +public interface RestoreCommandInvoker { int run(List command, Path logFile, Path workingDirectory); } diff --git a/src/main/java/com/cisd/tms/modules/backup/restore/RestoreStatusFileReader.java b/src/main/java/com/cisd/tms/modules/backup/restore/RestoreStatusFileReader.java new file mode 100644 index 0000000..1727af2 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/backup/restore/RestoreStatusFileReader.java @@ -0,0 +1,82 @@ +package com.cisd.tms.modules.backup.restore; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import org.springframework.stereotype.Component; + +@Component +public class RestoreStatusFileReader { + + private static final int MAX_LOG_BYTES = 64 * 1024; + + private final ObjectMapper objectMapper; + + public RestoreStatusFileReader(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public RestoreStatusSnapshot read(Path workDir) { + if (workDir == null) { + return empty(); + } + Path statusFile = workDir.resolve("status.json").normalize(); + Path logFile = workDir.resolve("restore.log").normalize(); + Map status = readStatus(statusFile); + return new RestoreStatusSnapshot( + text(status.get("status")), + text(status.get("phase")), + text(status.get("message")), + text(status.get("updatedAt")), + readTail(logFile) + ); + } + + private Map readStatus(Path statusFile) { + if (!Files.isRegularFile(statusFile)) { + return Map.of(); + } + try { + return objectMapper.readValue(statusFile.toFile(), new TypeReference>() { + }); + } catch (IOException ex) { + throw new IllegalStateException("failed to read restore status file: " + statusFile, ex); + } + } + + private String readTail(Path logFile) { + if (!Files.isRegularFile(logFile)) { + return ""; + } + try { + byte[] bytes = Files.readAllBytes(logFile); + if (bytes.length <= MAX_LOG_BYTES) { + return new String(bytes, StandardCharsets.UTF_8); + } + return new String(bytes, bytes.length - MAX_LOG_BYTES, MAX_LOG_BYTES, StandardCharsets.UTF_8); + } catch (IOException ex) { + throw new IllegalStateException("failed to read restore log file: " + logFile, ex); + } + } + + private static RestoreStatusSnapshot empty() { + return new RestoreStatusSnapshot("", "", "", "", ""); + } + + private static String text(Object value) { + return value == null ? "" : String.valueOf(value); + } + + public record RestoreStatusSnapshot( + String status, + String phase, + String message, + String updatedAt, + String log + ) { + } +} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/ConsistencyVerifyRunnerStepExecutor.java b/src/main/java/com/cisd/tms/modules/backup/runner/ConsistencyVerifyRunnerStepExecutor.java deleted file mode 100644 index 43fe5ae..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/ConsistencyVerifyRunnerStepExecutor.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class ConsistencyVerifyRunnerStepExecutor implements RunnerStepExecutor { - - private static final List REQUIRED_SUCCESS_STEPS = List.of( - "RESTORE_FILES", - "RESTORE_DATABASES", - "START_SERVICES" - ); - - private final ResourceBackupProperties resourceBackupProperties; - private final ObjectMapper objectMapper; - private final HttpClient httpClient; - - @Autowired - public ConsistencyVerifyRunnerStepExecutor( - ResourceBackupProperties resourceBackupProperties, - ObjectMapper objectMapper - ) { - this(resourceBackupProperties, objectMapper, HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(5)) - .build()); - } - - ConsistencyVerifyRunnerStepExecutor( - ResourceBackupProperties resourceBackupProperties, - ObjectMapper objectMapper, - HttpClient httpClient - ) { - this.resourceBackupProperties = resourceBackupProperties; - this.objectMapper = objectMapper; - this.httpClient = httpClient; - } - - @Override - public boolean supports(String stepCode) { - return "CONSISTENCY_VERIFY".equals(stepCode); - } - - @Override - public RunnerStepExecutionResult execute(RunnerStepContext context) { - Path stepOutputDir = context.taskDir().resolve("step-output"); - for (String requiredStep : REQUIRED_SUCCESS_STEPS) { - assertStepSucceeded(stepOutputDir, requiredStep); - } - verifyHealthEndpoint(); - return RunnerStepExecutionResult.success("required steps succeeded and health endpoint is UP"); - } - - private void assertStepSucceeded(Path stepOutputDir, String stepCode) { - try { - Path stepFile = resolveRequiredStepOutput(stepOutputDir, stepCode); - Map payload = objectMapper.readValue(stepFile.toFile(), new TypeReference>() { - }); - String result = String.valueOf(payload.get("result")); - if (!"SUCCESS".equalsIgnoreCase(result)) { - throw new IllegalStateException("required step is not successful: " + stepCode); - } - } catch (IOException ex) { - throw new IllegalStateException("failed to read step output for " + stepCode, ex); - } - } - - private Path resolveRequiredStepOutput(Path stepOutputDir, String stepCode) throws IOException { - if (!Files.isDirectory(stepOutputDir)) { - throw new IllegalStateException("step output directory missing: " + stepOutputDir); - } - try (var pathStream = Files.list(stepOutputDir)) { - return pathStream - .filter(path -> path.getFileName().toString().endsWith("-" + stepCode + ".json")) - .findFirst() - .orElseThrow(() -> new IllegalStateException("step output missing for " + stepCode)); - } - } - - private void verifyHealthEndpoint() { - String healthUrl = resourceBackupProperties.getHealthCheckUrl(); - try { - HttpRequest request = HttpRequest.newBuilder(URI.create(healthUrl)) - .timeout(Duration.ofSeconds(resourceBackupProperties.getHealthCheckTimeoutSeconds())) - .GET() - .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() != 200 || !response.body().contains("\"status\":\"UP\"")) { - throw new IllegalStateException("health endpoint is not UP: statusCode=" + response.statusCode()); - } - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("health verification interrupted", ex); - } catch (Exception ex) { - throw new IllegalStateException("failed to verify health endpoint: " + healthUrl, ex); - } - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/DatabaseRestoreRunnerStepExecutor.java b/src/main/java/com/cisd/tms/modules/backup/runner/DatabaseRestoreRunnerStepExecutor.java deleted file mode 100644 index 6ac43d4..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/DatabaseRestoreRunnerStepExecutor.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -public class DatabaseRestoreRunnerStepExecutor implements RunnerStepExecutor { - - private final ResourceBackupProperties resourceBackupProperties; - private final RunnerCommandInvoker runnerCommandInvoker; - private final String datasourceUrl; - private final String datasourceUsername; - private final String datasourcePassword; - - public DatabaseRestoreRunnerStepExecutor( - ResourceBackupProperties resourceBackupProperties, - RunnerCommandInvoker runnerCommandInvoker, - @Value("${spring.datasource.url:}") String datasourceUrl, - @Value("${spring.datasource.username:}") String datasourceUsername, - @Value("${spring.datasource.password:}") String datasourcePassword - ) { - this.resourceBackupProperties = resourceBackupProperties; - this.runnerCommandInvoker = runnerCommandInvoker; - this.datasourceUrl = datasourceUrl; - this.datasourceUsername = datasourceUsername; - this.datasourcePassword = datasourcePassword; - } - - @Override - public boolean supports(String stepCode) { - return "RESTORE_DATABASES".equals(stepCode); - } - - @Override - public RunnerStepExecutionResult execute(RunnerStepContext context) { - if (!supports(context.stepCode())) { - throw new IllegalArgumentException("unsupported runner step: " + context.stepCode()); - } - List restoredDatabases = new ArrayList<>(); - restoreDatabase(context, "TMS.sql", resourceBackupProperties.getTmsDatabaseName(), "TMS", true, restoredDatabases); - // 标准版第一版还需要恢复收发器业务库;DIRECT 当前没有这份库时允许跳过。 - restoreDatabase(context, "CMEP.sql", resourceBackupProperties.getCmepDatabaseName(), "CMEP", false, restoredDatabases); - return RunnerStepExecutionResult.success("restored databases " + String.join(",", restoredDatabases)); - } - - private void restoreDatabase( - RunnerStepContext context, - String sqlFileName, - String configuredDatabaseName, - String fallbackDatabaseName, - boolean required, - List restoredDatabases - ) { - Path sqlFile = context.taskDir().resolve("staging").resolve("payload").resolve("db").resolve(sqlFileName).normalize(); - if (!Files.isRegularFile(sqlFile)) { - if (required) { - throw new IllegalStateException("restore database sql file missing: " + sqlFile); - } - return; - } - MysqlTarget target = resolveTarget(configuredDatabaseName, fallbackDatabaseName); - List command = new ArrayList<>(); - command.add("/bin/bash"); - command.add(resourceBackupProperties.getRestoreDbScriptPath()); - command.add("--mysql-path"); - command.add(resourceBackupProperties.getMysqlPath()); - command.add("--host"); - command.add(target.host()); - command.add("--port"); - command.add(String.valueOf(target.port())); - command.add("--username"); - command.add(target.username()); - command.add("--password"); - command.add(target.password()); - command.add("--db-name"); - command.add(target.databaseName()); - command.add("--sql-file"); - command.add(sqlFile.toString()); - - int exitCode = runnerCommandInvoker.run(command, resolveLogFile(context), context.taskDir()); - if (exitCode != 0) { - throw new IllegalStateException("runner step RESTORE_DATABASES failed for " + target.databaseName() + ", exitCode=" + exitCode); - } - restoredDatabases.add(target.databaseName()); - } - - private MysqlTarget resolveTarget(String configuredDatabaseName, String fallbackDatabaseName) { - String url = trim(datasourceUrl); - if (url.isEmpty()) { - throw new IllegalStateException("spring.datasource.url is required for database restore"); - } - if (!url.startsWith("jdbc:mysql://")) { - throw new IllegalStateException("unsupported datasource url for restore: " + url); - } - try { - String normalizedUrl = url.substring("jdbc:".length()); - String[] segments = normalizedUrl.split("\\?", 2); - URI uri = URI.create(segments[0]); - String databaseName = trim(configuredDatabaseName); - if (databaseName.isEmpty()) { - String path = trim(uri.getPath()); - databaseName = path.startsWith("/") ? path.substring(1) : path; - } - if (databaseName.isEmpty()) { - databaseName = fallbackDatabaseName; - } - return new MysqlTarget( - uri.getHost() == null ? "127.0.0.1" : uri.getHost(), - uri.getPort() <= 0 ? 3306 : uri.getPort(), - trim(datasourceUsername), - trim(datasourcePassword), - databaseName - ); - } catch (IllegalArgumentException ex) { - throw new IllegalStateException("invalid datasource url for restore: " + url, ex); - } - } - - private Path resolveLogFile(RunnerStepContext context) { - return context.taskDir().resolve("steps").resolve(String.format("%02d-%s.log", context.stepNo(), context.stepCode())); - } - - private static String trim(String value) { - return value == null ? "" : value.trim(); - } - - private record MysqlTarget(String host, int port, String username, String password, String databaseName) { - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/PayloadArchiveRunnerStepExecutor.java b/src/main/java/com/cisd/tms/modules/backup/runner/PayloadArchiveRunnerStepExecutor.java deleted file mode 100644 index b9f8159..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/PayloadArchiveRunnerStepExecutor.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult; -import com.cisd.tms.modules.backup.packagex.ResourcePackageService; -import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot; -import com.cisd.tms.modules.backup.support.file.HandoffFileStore; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import org.springframework.stereotype.Component; - -@Component -public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor { - - private static final Set RESTORE_FILE_RESOURCE_CODES = - Set.of("TMS_CONFIG", "CP_CONFIG", "RECEIVER_LICENSE"); - - private final HandoffFileStore handoffFileStore; - private final ResourcePackageService resourcePackageService; - private final ObjectMapper objectMapper; - - public PayloadArchiveRunnerStepExecutor( - HandoffFileStore handoffFileStore, - ResourcePackageService resourcePackageService, - ObjectMapper objectMapper - ) { - this.handoffFileStore = handoffFileStore; - this.resourcePackageService = resourcePackageService; - this.objectMapper = objectMapper; - } - - @Override - public boolean supports(String stepCode) { - return "RESTORE_FILES".equals(stepCode); - } - - @Override - public RunnerStepExecutionResult execute(RunnerStepContext context) { - if ("RESTORE_FILES".equals(context.stepCode())) { - return restoreFiles(context); - } - throw new IllegalArgumentException("unsupported runner step: " + context.stepCode()); - } - - private RunnerStepExecutionResult restoreFiles(RunnerStepContext context) { - ResourceRestoreHandoffSnapshot handoff = readRequiredHandoff(context.handoffFile()); - Path packagePath = resolveRequiredPackagePath(handoff); - ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackage(packagePath); - byte[] payloadArchiveBytes = Optional.ofNullable(inspectResult.getPayloadArchiveBytes()) - .orElseThrow(() -> new IllegalStateException("payload archive bytes missing")); - Path payloadDir = context.taskDir().resolve("staging").resolve("payload").normalize(); - extractPayloadArchive(payloadArchiveBytes, payloadDir); - - Path payloadManifestFile = payloadDir.resolve("payload-manifest.json"); - int resourceCount = readResourceCount(payloadManifestFile); - int restoredFileCount = restoreSupportedFiles(payloadDir, payloadManifestFile); - return RunnerStepExecutionResult.success( - "payload extracted to staging, resourceCount=" + resourceCount + ", restoredFileCount=" + restoredFileCount - ); - } - - private ResourceRestoreHandoffSnapshot readRequiredHandoff(Path handoffFile) { - return handoffFileStore.read(handoffFile) - .orElseThrow(() -> new IllegalStateException("restore handoff file not found: " + handoffFile)); - } - - private Path resolveRequiredPackagePath(ResourceRestoreHandoffSnapshot handoff) { - String packagePath = trim(handoff.getPackagePath()); - if (packagePath.isEmpty()) { - throw new IllegalStateException("restore handoff packagePath is empty"); - } - Path resolved = Path.of(packagePath).normalize(); - if (!Files.isRegularFile(resolved)) { - throw new IllegalStateException("restore package file not found: " + resolved); - } - return resolved; - } - - private void extractPayloadArchive(byte[] payloadArchiveBytes, Path payloadDir) { - try { - Files.createDirectories(payloadDir); - try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(payloadArchiveBytes))) { - ZipEntry entry; - while ((entry = zipInputStream.getNextEntry()) != null) { - Path target = resolveSafePayloadTarget(payloadDir, entry.getName()); - if (entry.isDirectory()) { - Files.createDirectories(target); - continue; - } - Files.createDirectories(target.getParent()); - // 备份包来自外部输入,即使已经验签,也要防御 zip-slip,确保只落到 staging/payload 下。 - Files.write(target, readAllBytes(zipInputStream)); - } - } - } catch (IOException ex) { - throw new IllegalStateException("failed to extract payload archive", ex); - } - } - - private Path resolveSafePayloadTarget(Path payloadDir, String entryName) { - Path target = payloadDir.resolve(entryName).normalize(); - if (!target.startsWith(payloadDir)) { - throw new IllegalStateException("payload archive entry escapes staging dir: " + entryName); - } - return target; - } - - private int readResourceCount(Path manifestFile) { - try { - Map manifest = objectMapper.readValue(manifestFile.toFile(), new TypeReference>() { - }); - Object resourcesValue = manifest.get("resources"); - if (resourcesValue instanceof List resources) { - return resources.size(); - } - return 0; - } catch (IOException ex) { - throw new IllegalStateException("failed to read payload manifest: " + manifestFile, ex); - } - } - - private int restoreSupportedFiles(Path payloadDir, Path manifestFile) { - try { - Map manifest = objectMapper.readValue(manifestFile.toFile(), new TypeReference>() { - }); - Object resourcesValue = manifest.get("resources"); - if (!(resourcesValue instanceof List resources)) { - return 0; - } - int restoredFileCount = 0; - for (Object resourceObject : resources) { - if (!(resourceObject instanceof Map rawResource)) { - continue; - } - Map resource = castMap(rawResource); - String resourceCode = String.valueOf(resource.get("resourceCode")); - if (!RESTORE_FILE_RESOURCE_CODES.contains(resourceCode)) { - continue; - } - Object filesValue = resource.get("files"); - if (!(filesValue instanceof List rawFiles)) { - continue; - } - for (Object fileObject : rawFiles) { - if (!(fileObject instanceof Map rawFile)) { - continue; - } - Map fileEntry = castMap(rawFile); - Path sourceFile = payloadDir.resolve(String.valueOf(fileEntry.get("entryPath"))).normalize(); - if (!sourceFile.startsWith(payloadDir) || !Files.isRegularFile(sourceFile)) { - throw new IllegalStateException("restored file source missing: " + sourceFile); - } - Path targetFile = resolveTargetFile(resourceCode, String.valueOf(resource.get("resolvedPath")), String.valueOf(fileEntry.get("relativeName"))); - Files.createDirectories(targetFile.getParent()); - // 第一版只回写最关键的配置/license 文件,不做目录合并或增量策略。 - Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); - restoredFileCount++; - } - } - return restoredFileCount; - } catch (IOException ex) { - throw new IllegalStateException("failed to restore payload files", ex); - } - } - - private Path resolveTargetFile(String resourceCode, String resolvedPath, String relativeName) { - Path targetPath = Path.of(resolvedPath).normalize(); - if ("TMS_CONFIG".equals(resourceCode)) { - return targetPath.resolve(relativeName).normalize(); - } - return targetPath; - } - - private static Map castMap(Map rawMap) { - Map result = new java.util.LinkedHashMap<>(); - rawMap.forEach((key, value) -> result.put(String.valueOf(key), value)); - return result; - } - - private static byte[] readAllBytes(InputStream inputStream) throws IOException { - return inputStream.readAllBytes(); - } - - private static String trim(String value) { - return value == null ? "" : value.trim(); - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/ProcessResourceRestoreRunnerLauncher.java b/src/main/java/com/cisd/tms/modules/backup/runner/ProcessResourceRestoreRunnerLauncher.java deleted file mode 100644 index 1e597c4..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/ProcessResourceRestoreRunnerLauncher.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import java.nio.file.Path; -import java.util.List; -import org.springframework.stereotype.Component; - -@Component -public class ProcessResourceRestoreRunnerLauncher implements ResourceRestoreRunnerLauncher { - - private final ResourceBackupProperties resourceBackupProperties; - private final RunnerCommandInvoker runnerCommandInvoker; - - public ProcessResourceRestoreRunnerLauncher( - ResourceBackupProperties resourceBackupProperties, - RunnerCommandInvoker runnerCommandInvoker - ) { - this.resourceBackupProperties = resourceBackupProperties; - this.runnerCommandInvoker = runnerCommandInvoker; - } - - @Override - public void launch(Path handoffFile) { - if (!resourceBackupProperties.getRunnerAutoLaunchEnabled()) { - return; - } - Path normalizedHandoffFile = handoffFile.normalize().toAbsolutePath(); - Path taskDir = normalizedHandoffFile.getParent(); - Path launcherLog = taskDir.resolve("runner-launch.log"); - List command = List.of( - "/bin/bash", - resourceBackupProperties.getRunnerLaunchScriptPath(), - "--handoff-file", - normalizedHandoffFile.toString() - ); - int exitCode = runnerCommandInvoker.run(command, launcherLog, taskDir); - if (exitCode != 0) { - throw new IllegalStateException("failed to launch external restore runner, exitCode=" + exitCode); - } - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunner.java b/src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunner.java deleted file mode 100644 index d6500e8..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunner.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot; -import com.cisd.tms.modules.backup.support.file.HandoffFileStore; -import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.springframework.stereotype.Component; - -@Component -public class ResourceRestoreRunner { - - private static final String INITIAL_STEP_CODE = "RESTORE_LOCK"; - private static final int INITIAL_STEP_NO = 1; - - private final HandoffFileStore handoffFileStore; - private final ResourceRestoreStateFileStore resourceRestoreStateFileStore; - private final ObjectMapper objectMapper; - private final List runnerStepExecutors; - - public ResourceRestoreRunner( - HandoffFileStore handoffFileStore, - ResourceRestoreStateFileStore resourceRestoreStateFileStore, - ObjectMapper objectMapper, - List runnerStepExecutors - ) { - this.handoffFileStore = handoffFileStore; - this.resourceRestoreStateFileStore = resourceRestoreStateFileStore; - this.objectMapper = objectMapper; - this.runnerStepExecutors = runnerStepExecutors; - } - - public void run(Path handoffFile) { - ResourceRestoreHandoffSnapshot handoff = handoffFileStore.read(handoffFile) - .orElseThrow(() -> new IllegalStateException("restore handoff file not found: " + handoffFile)); - Path stateFile = Path.of(handoff.getRunnerStatePath()).normalize(); - String runnerInstanceId = nextRunnerInstanceId(); - long runnerPid = ProcessHandle.current().pid(); - resourceRestoreStateFileStore.claimOwnership(stateFile, runnerInstanceId, runnerPid, INITIAL_STEP_CODE, INITIAL_STEP_NO); - - List restorePlan = readRestorePlan(handoff.getRestorePlanJson()); - try { - for (int i = 0; i < restorePlan.size(); i++) { - String stepCode = restorePlan.get(i); - int stepNo = i + 1; - if (INITIAL_STEP_CODE.equals(stepCode)) { - continue; - } - resourceRestoreStateFileStore.markStepRunning(stateFile, stepCode, stepNo, runnerInstanceId); - RunnerStepExecutor executor = resolveExecutor(stepCode); - RunnerStepContext context = new RunnerStepContext( - handoff.getTaskId(), - stepCode, - stepNo, - handoffFile.getParent(), - handoffFile, - stateFile - ); - RunnerStepExecutionResult result = executor.execute(context); - writeStepOutput(context, result); - resourceRestoreStateFileStore.touchHeartbeat(stateFile, runnerInstanceId); - } - String lastStepCode = restorePlan.isEmpty() ? INITIAL_STEP_CODE : restorePlan.get(restorePlan.size() - 1); - int lastStepNo = restorePlan.isEmpty() ? INITIAL_STEP_NO : restorePlan.size(); - resourceRestoreStateFileStore.markReconcilePending(stateFile, "SUCCESS", null, null, lastStepCode, lastStepNo, runnerInstanceId); - } catch (RuntimeException ex) { - int failedStepNo = detectFailedStepNo(handoff, stateFile); - String failedStepCode = detectFailedStepCode(handoff, stateFile); - writeStepFailureOutput(handoffFile.getParent(), failedStepCode, failedStepNo, ex); - resourceRestoreStateFileStore.markReconcilePending( - stateFile, - "FAILED", - "RUNNER_EXECUTION_FAILED", - ex.getMessage(), - failedStepCode, - failedStepNo, - runnerInstanceId - ); - throw ex; - } - } - - private String nextRunnerInstanceId() { - return "RRUN-" + UUID.randomUUID(); - } - - private List readRestorePlan(String restorePlanJson) { - if (restorePlanJson == null || restorePlanJson.isBlank()) { - return List.of(INITIAL_STEP_CODE); - } - try { - return objectMapper.readValue(restorePlanJson, new TypeReference>() { - }); - } catch (Exception ex) { - throw new IllegalStateException("failed to parse restore plan json", ex); - } - } - - private RunnerStepExecutor resolveExecutor(String stepCode) { - return runnerStepExecutors.stream() - .filter(executor -> executor.supports(stepCode)) - .findFirst() - .orElseThrow(() -> new IllegalStateException("no runner step executor for step: " + stepCode)); - } - - private int detectFailedStepNo(ResourceRestoreHandoffSnapshot handoff, Path stateFile) { - return resourceRestoreStateFileStore.read(stateFile) - .map(snapshot -> snapshot.getCurrentStepNo() == null ? INITIAL_STEP_NO : snapshot.getCurrentStepNo()) - .orElse(INITIAL_STEP_NO); - } - - private String detectFailedStepCode(ResourceRestoreHandoffSnapshot handoff, Path stateFile) { - return resourceRestoreStateFileStore.read(stateFile) - .map(snapshot -> snapshot.getCurrentStepCode() == null || snapshot.getCurrentStepCode().isBlank() - ? INITIAL_STEP_CODE - : snapshot.getCurrentStepCode()) - .orElse(INITIAL_STEP_CODE); - } - - private void writeStepOutput(RunnerStepContext context, RunnerStepExecutionResult result) { - try { - Path outputDir = context.taskDir().resolve("step-output"); - Files.createDirectories(outputDir); - Path outputFile = outputDir.resolve(String.format("%02d-%s.json", context.stepNo(), context.stepCode())); - Map json = new LinkedHashMap<>(); - json.put("stepCode", context.stepCode()); - json.put("result", result.getResult()); - json.put("summary", result.getSummary()); - objectMapper.writeValue(outputFile.toFile(), json); - } catch (IOException ex) { - throw new IllegalStateException("failed to write step output for " + context.stepCode(), ex); - } - } - - private void writeStepFailureOutput(Path taskDir, String stepCode, int stepNo, RuntimeException ex) { - try { - Path outputDir = taskDir.resolve("step-output"); - Files.createDirectories(outputDir); - Path outputFile = outputDir.resolve(String.format("%02d-%s.json", stepNo, stepCode)); - Map json = new LinkedHashMap<>(); - json.put("stepCode", stepCode); - json.put("result", "FAILED"); - json.put("summary", ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage()); - objectMapper.writeValue(outputFile.toFile(), json); - } catch (IOException ioException) { - throw new IllegalStateException("failed to write failed step output for " + stepCode, ioException); - } - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerLauncher.java b/src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerLauncher.java deleted file mode 100644 index d7604c2..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerLauncher.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import java.nio.file.Path; - -public interface ResourceRestoreRunnerLauncher { - - void launch(Path handoffFile); -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerMain.java b/src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerMain.java deleted file mode 100644 index ad8e759..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerMain.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.TmsApplication; -import java.nio.file.Path; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.env.Environment; - -public final class ResourceRestoreRunnerMain { - - private ResourceRestoreRunnerMain() { - } - - public static void main(String[] args) { - ConfigurableApplicationContext context = new SpringApplicationBuilder(TmsApplication.class) - .web(WebApplicationType.NONE) - .logStartupInfo(false) - .run(args); - int exitCode = 0; - try { - Environment environment = context.getEnvironment(); - String handoffFile = environment.getProperty("tms.backup.runner-handoff-file", ""); - if (handoffFile == null || handoffFile.isBlank()) { - throw new IllegalStateException("tms.backup.runner-handoff-file is required"); - } - context.getBean(ResourceRestoreRunner.class).run(Path.of(handoffFile)); - } catch (RuntimeException ex) { - exitCode = 1; - throw ex; - } finally { - final int finalExitCode = exitCode; - int springExitCode = SpringApplication.exit(context, () -> finalExitCode); - System.exit(springExitCode); - } - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/RestoreStateReconciler.java b/src/main/java/com/cisd/tms/modules/backup/runner/RestoreStateReconciler.java deleted file mode 100644 index 25a9405..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/RestoreStateReconciler.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository; -import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; -import java.time.LocalDateTime; -import java.util.function.Supplier; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class RestoreStateReconciler { - - private final ResourceRestoreTaskRepository resourceRestoreTaskRepository; - private final ResourceRestoreStateFileStore resourceRestoreStateFileStore; - private final Supplier nowSupplier; - - @Autowired - public RestoreStateReconciler( - ResourceRestoreTaskRepository resourceRestoreTaskRepository, - ResourceRestoreStateFileStore resourceRestoreStateFileStore - ) { - this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, LocalDateTime::now); - } - - RestoreStateReconciler( - ResourceRestoreTaskRepository resourceRestoreTaskRepository, - ResourceRestoreStateFileStore resourceRestoreStateFileStore, - Supplier nowSupplier - ) { - this.resourceRestoreTaskRepository = resourceRestoreTaskRepository; - this.resourceRestoreStateFileStore = resourceRestoreStateFileStore; - this.nowSupplier = nowSupplier; - } - - public void reconcileOnce() { - for (ResourceRestoreTaskEntity entity : resourceRestoreTaskRepository.findAll()) { - if (entity.getRunnerStatePath() == null || entity.getRunnerStatePath().isBlank()) { - continue; - } - java.nio.file.Path stateFile = java.nio.file.Path.of(entity.getRunnerStatePath()).normalize(); - resourceRestoreStateFileStore.read(stateFile).ifPresent(snapshot -> { - if (!"RECONCILE_PENDING".equals(snapshot.getControlPhase())) { - return; - } - entity.setStatus(snapshot.getStatus()); - entity.setErrorCode(blankToNull(snapshot.getErrorCode())); - entity.setErrorMessage(blankToNull(snapshot.getErrorMessage())); - entity.setFinishTime(nowSupplier.get()); - entity.setUpdateTime(nowSupplier.get()); - resourceRestoreTaskRepository.update(entity); - resourceRestoreStateFileStore.markDone(stateFile); - }); - } - } - - private static String blankToNull(String value) { - return value == null || value.isBlank() ? null : value; - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStallDetector.java b/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStallDetector.java deleted file mode 100644 index 630188b..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStallDetector.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository; -import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; -import java.nio.file.Path; -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.function.Supplier; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class RunnerStallDetector { - - private final ResourceRestoreTaskRepository resourceRestoreTaskRepository; - private final ResourceRestoreStateFileStore resourceRestoreStateFileStore; - private final ResourceBackupProperties resourceBackupProperties; - private final Supplier nowSupplier; - - @Autowired - public RunnerStallDetector( - ResourceRestoreTaskRepository resourceRestoreTaskRepository, - ResourceRestoreStateFileStore resourceRestoreStateFileStore, - ResourceBackupProperties resourceBackupProperties - ) { - this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, resourceBackupProperties, LocalDateTime::now); - } - - RunnerStallDetector( - ResourceRestoreTaskRepository resourceRestoreTaskRepository, - ResourceRestoreStateFileStore resourceRestoreStateFileStore, - ResourceBackupProperties resourceBackupProperties, - Supplier nowSupplier - ) { - this.resourceRestoreTaskRepository = resourceRestoreTaskRepository; - this.resourceRestoreStateFileStore = resourceRestoreStateFileStore; - this.resourceBackupProperties = resourceBackupProperties; - this.nowSupplier = nowSupplier; - } - - public void detectOnce() { - for (ResourceRestoreTaskEntity entity : resourceRestoreTaskRepository.findAll()) { - if (entity.getRunnerStatePath() == null || entity.getRunnerStatePath().isBlank()) { - continue; - } - Path stateFile = Path.of(entity.getRunnerStatePath()).normalize(); - resourceRestoreStateFileStore.read(stateFile).ifPresent(snapshot -> { - if (!"RUNNER_OWNED".equals(snapshot.getControlPhase())) { - return; - } - if (snapshot.getLastHeartbeatAt() == null || snapshot.getLastHeartbeatAt().isBlank()) { - return; - } - LocalDateTime heartbeat = LocalDateTime.parse(snapshot.getLastHeartbeatAt()); - if (Duration.between(heartbeat, nowSupplier.get()).getSeconds() <= resourceBackupProperties.getRunnerHeartbeatTimeoutSeconds()) { - return; - } - resourceRestoreStateFileStore.markStalled(stateFile, "RUNNER_STALLED", "restore runner heartbeat timed out"); - }); - } - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepContext.java b/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepContext.java deleted file mode 100644 index 8df9e77..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepContext.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import java.nio.file.Path; - -public record RunnerStepContext( - String taskId, - String stepCode, - int stepNo, - Path taskDir, - Path handoffFile, - Path stateFile -) { -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepExecutionResult.java b/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepExecutionResult.java deleted file mode 100644 index d507261..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepExecutionResult.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -public class RunnerStepExecutionResult { - - private String result; - private String summary; - - public static RunnerStepExecutionResult success(String summary) { - RunnerStepExecutionResult result = new RunnerStepExecutionResult(); - result.setResult("SUCCESS"); - result.setSummary(summary); - return result; - } - - public static RunnerStepExecutionResult failed(String summary) { - RunnerStepExecutionResult result = new RunnerStepExecutionResult(); - result.setResult("FAILED"); - result.setSummary(summary); - return result; - } - - public String getResult() { - return result; - } - - public void setResult(String result) { - this.result = result; - } - - public String getSummary() { - return summary; - } - - public void setSummary(String summary) { - this.summary = summary; - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepExecutor.java b/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepExecutor.java deleted file mode 100644 index cf5d762..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/RunnerStepExecutor.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -public interface RunnerStepExecutor { - - boolean supports(String stepCode); - - RunnerStepExecutionResult execute(RunnerStepContext context); -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/ServiceCommandRunnerStepExecutor.java b/src/main/java/com/cisd/tms/modules/backup/runner/ServiceCommandRunnerStepExecutor.java deleted file mode 100644 index 3348cec..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/ServiceCommandRunnerStepExecutor.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import com.cisd.tms.modules.init.config.InitExecutorProperties; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import org.springframework.stereotype.Component; - -@Component -public class ServiceCommandRunnerStepExecutor implements RunnerStepExecutor { - - private final ResourceBackupProperties resourceBackupProperties; - private final InitExecutorProperties initExecutorProperties; - private final RunnerCommandInvoker runnerCommandInvoker; - - public ServiceCommandRunnerStepExecutor( - ResourceBackupProperties resourceBackupProperties, - InitExecutorProperties initExecutorProperties, - RunnerCommandInvoker runnerCommandInvoker - ) { - this.resourceBackupProperties = resourceBackupProperties; - this.initExecutorProperties = initExecutorProperties; - this.runnerCommandInvoker = runnerCommandInvoker; - } - - @Override - public boolean supports(String stepCode) { - return "STOP_SERVICES".equals(stepCode) || "START_SERVICES".equals(stepCode); - } - - @Override - public RunnerStepExecutionResult execute(RunnerStepContext context) { - if (!supports(context.stepCode())) { - throw new IllegalArgumentException("unsupported runner step: " + context.stepCode()); - } - List> commands = resolveCommands(context.stepCode()); - int executedCount = 0; - for (List command : commands) { - int exitCode = runnerCommandInvoker.run(command, resolveLogFile(context), context.taskDir()); - if (exitCode != 0) { - throw new IllegalStateException("runner step " + context.stepCode() + " failed, exitCode=" + exitCode); - } - executedCount++; - } - return RunnerStepExecutionResult.success("executed " + executedCount + " command(s)"); - } - - private List> resolveCommands(String stepCode) { - return switch (stepCode) { - case "STOP_SERVICES" -> buildStopCommands(); - case "START_SERVICES" -> buildStartCommands(); - default -> throw new IllegalArgumentException("unsupported runner step: " + stepCode); - }; - } - - private List> buildStopCommands() { - List> commands = new ArrayList<>(); - commands.add(List.of("bash", resourceBackupProperties.getTmsScriptPath(), "stop")); - appendShellCommand(commands, initExecutorProperties.getStandardAppStopCommand()); - appendShellCommand(commands, initExecutorProperties.getStandardRabbitStopCommand()); - return commands; - } - - private List> buildStartCommands() { - List> commands = new ArrayList<>(); - appendShellCommand(commands, initExecutorProperties.getStandardAppStartCommand()); - commands.add(List.of("bash", resourceBackupProperties.getTmsScriptPath(), "start")); - return commands; - } - - private void appendShellCommand(List> commands, String shellCommand) { - if (shellCommand == null || shellCommand.isBlank()) { - return; - } - commands.add(List.of("/bin/zsh", "-lc", shellCommand.trim())); - } - - private Path resolveLogFile(RunnerStepContext context) { - return context.taskDir().resolve("steps").resolve(String.format("%02d-%s.log", context.stepNo(), context.stepCode())); - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/runner/model/ResourceRestoreHandoffSnapshot.java b/src/main/java/com/cisd/tms/modules/backup/runner/model/ResourceRestoreHandoffSnapshot.java deleted file mode 100644 index e55c4c0..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/runner/model/ResourceRestoreHandoffSnapshot.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.cisd.tms.modules.backup.runner.model; - -public class ResourceRestoreHandoffSnapshot { - - private String taskId; - private String precheckId; - private String backupId; - private String packageFileId; - private String packagePath; - private String stagingDir; - private String runnerStatePath; - private String restorePlanJson; - - public String getTaskId() { - return taskId; - } - - public void setTaskId(String taskId) { - this.taskId = taskId; - } - - public String getPrecheckId() { - return precheckId; - } - - public void setPrecheckId(String precheckId) { - this.precheckId = precheckId; - } - - public String getBackupId() { - return backupId; - } - - public void setBackupId(String backupId) { - this.backupId = backupId; - } - - public String getPackageFileId() { - return packageFileId; - } - - public void setPackageFileId(String packageFileId) { - this.packageFileId = packageFileId; - } - - public String getPackagePath() { - return packagePath; - } - - public void setPackagePath(String packagePath) { - this.packagePath = packagePath; - } - - public String getStagingDir() { - return stagingDir; - } - - public void setStagingDir(String stagingDir) { - this.stagingDir = stagingDir; - } - - public String getRunnerStatePath() { - return runnerStatePath; - } - - public void setRunnerStatePath(String runnerStatePath) { - this.runnerStatePath = runnerStatePath; - } - - public String getRestorePlanJson() { - return restorePlanJson; - } - - public void setRestorePlanJson(String restorePlanJson) { - this.restorePlanJson = restorePlanJson; - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourcePrecheckServiceImpl.java b/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourcePrecheckServiceImpl.java index 3584821..6d56162 100644 --- a/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourcePrecheckServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourcePrecheckServiceImpl.java @@ -57,14 +57,7 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService { response.setPayloadDecrypted(false); response.setWarnings(List.of()); response.setBlockingIssues(List.of()); - response.setRestorePlan(List.of( - "RESTORE_LOCK", - "STOP_SERVICES", - "RESTORE_FILES", - "RESTORE_DATABASES", - "START_SERVICES", - "CONSISTENCY_VERIFY" - )); + response.setRestorePlan(List.of("JAVA_EXTRACT", "SHELL_APPLY")); response.setRestoreScopeSummary(buildScopeSummary(inspectResult)); response.setExpiresAt(LocalDateTime.now().plusMinutes(30).toString()); resourcePrecheckStateStore.save(new ResourcePrecheckContext(request.getFileId(), response)); diff --git a/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceRestoreServiceImpl.java b/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceRestoreServiceImpl.java index 7117d62..5de7def 100644 --- a/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceRestoreServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceRestoreServiceImpl.java @@ -6,20 +6,21 @@ import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest; import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse; import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse; import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.cisd.tms.modules.backup.runner.ResourceRestoreRunnerLauncher; +import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractor; import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository; +import com.cisd.tms.modules.backup.restore.LightweightRestoreApplierLauncher; import com.cisd.tms.modules.backup.service.ResourceRestoreService; import com.cisd.tms.modules.backup.support.ResourcePrecheckContext; import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore; import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator; import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import com.cisd.tms.modules.backup.support.file.HandoffFileStore; -import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; import com.cisd.tms.modules.file.entity.FileRecordEntity; import com.cisd.tms.modules.file.repository.FileRecordRepository; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.time.DateTimeException; import java.time.LocalDateTime; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -32,10 +33,9 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService { private final FileRecordRepository fileRecordRepository; private final ResourcePrecheckStateStore resourcePrecheckStateStore; private final ResourceTaskIdGenerator resourceTaskIdGenerator; - private final HandoffFileStore handoffFileStore; - private final ResourceRestoreStateFileStore resourceRestoreStateFileStore; private final ResourceBackupProperties resourceBackupProperties; - private final ResourceRestoreRunnerLauncher resourceRestoreRunnerLauncher; + private final ResourcePayloadExtractor resourcePayloadExtractor; + private final LightweightRestoreApplierLauncher lightweightRestoreApplierLauncher; @Override @@ -47,6 +47,7 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService { ResourcePrecheckContext context = resourcePrecheckStateStore.find(request.getPrecheckId()) .orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck not found")); ResourceRestorePrecheckResponse precheck = context.getResponse(); + validatePrecheck(precheck); if (!trim(request.getConfirmBackupId()).equals(trim(precheck.getBackupId()))) { throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "confirmed backup id does not match precheck result"); } @@ -59,36 +60,31 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService { String taskId = resourceTaskIdGenerator.nextRestoreTaskId(); LocalDateTime now = LocalDateTime.now(); + Path taskDir = Path.of(resourceBackupProperties.getRestoreTaskRootDir()).resolve(taskId).normalize(); + createDirectories(taskDir); + resourcePayloadExtractor.extract(Path.of(packagePath), taskDir); + ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity(); entity.setTaskId(taskId); entity.setPrecheckId(precheck.getPrecheckId()); entity.setBackupId(precheck.getBackupId()); - // 恢复任务创建完成后,就应该已经进入“等待 runner 接管”的交接态, - // 前端和运维看到的状态不能还停留在 PRECHECKED。 - entity.setStatus("HANDOFF"); + // 轻量恢复模型下,Java 已完成包验签/解密/展开,后续由 shell apply 脚本接管实际覆盖/导入。 + entity.setStatus("APPLYING"); entity.setSourceDeviceId("UNKNOWN"); entity.setSourceProductType(precheck.getSourceProductType()); entity.setTargetProductType(precheck.getTargetProductType()); entity.setPackageFileId(context.getFileId()); - Path taskDir = Path.of(resourceBackupProperties.getRestoreTaskRootDir()).resolve(taskId).normalize(); - // 恢复 runner 的工作目录和状态文件路径在任务创建时就固定下来, - // 这样后续即使 TMS 重启,控制面路径也不会漂移。 - // 这里必须把真实备份包路径写进任务和 handoff,后续 runner 才能真正解包, - // 否则它只能拿到 fileId,却不知道恢复时去哪里取浏览器上传/下载回来的备份文件。 entity.setPackagePath(packagePath); - entity.setStagingDir(taskDir.resolve("staging").toString()); - entity.setRunnerStatePath(taskDir.resolve("restore-state.json").toString()); + entity.setStagingDir(taskDir.resolve("payload").toString()); + entity.setRunnerStatePath(taskDir.resolve("status.json").toString()); entity.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex()); entity.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex()); - entity.setRestorePlanJson(joinAsJsonArray(precheck.getRestorePlan())); + entity.setRestorePlanJson("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]"); entity.setCreateBy("SYSTEM"); entity.setCreateTime(now); entity.setUpdateTime(now); resourceRestoreTaskRepository.save(entity); - handoffFileStore.write(taskDir, entity); - resourceRestoreStateFileStore.initialize(taskDir, entity); - resourceRestoreStateFileStore.markHandoffReady(taskDir.resolve("restore-state.json"), "RESTORE_LOCK", 1); - resourceRestoreRunnerLauncher.launch(taskDir.resolve("handoff.json")); + lightweightRestoreApplierLauncher.launch(taskDir); CreateResourceRestoreResponse response = new CreateResourceRestoreResponse(); response.setTaskId(taskId); @@ -97,23 +93,34 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService { return response; } - private static String joinAsJsonArray(List items) { - if (items == null || items.isEmpty()) { - return "[]"; + private static void createDirectories(Path taskDir) { + try { + Files.createDirectories(taskDir); + } catch (IOException ex) { + throw new IllegalStateException("failed to create restore task dir: " + taskDir, ex); } - // 这里先用最轻量的 JSON 数组序列化,避免为了一个占位字段引入额外 DTO。 - StringBuilder builder = new StringBuilder("["); - for (int i = 0; i < items.size(); i++) { - if (i > 0) { - builder.append(','); - } - builder.append('"').append(items.get(i)).append('"'); - } - builder.append(']'); - return builder.toString(); } private static String trim(String value) { return value == null ? "" : value.trim(); } + + private static void validatePrecheck(ResourceRestorePrecheckResponse precheck) { + if (precheck == null) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck not found"); + } + if (!Boolean.TRUE.equals(precheck.getCompatible())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck is not compatible"); + } + String expiresAt = trim(precheck.getExpiresAt()); + if (!expiresAt.isEmpty()) { + try { + if (LocalDateTime.parse(expiresAt).isBefore(LocalDateTime.now())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck expired"); + } + } catch (DateTimeException ex) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck expiresAt is invalid"); + } + } + } } diff --git a/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceTaskQueryServiceImpl.java b/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceTaskQueryServiceImpl.java index acae85c..1474edd 100644 --- a/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceTaskQueryServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceTaskQueryServiceImpl.java @@ -7,17 +7,13 @@ import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse; import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse; import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository; +import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader; +import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader.RestoreStatusSnapshot; import com.cisd.tms.modules.backup.service.ResourceTaskQueryService; -import com.cisd.tms.modules.backup.support.ResourceTaskStepLogService; -import com.cisd.tms.modules.backup.support.file.FileSystemResourceTaskStepLogService; -import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; -import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateSnapshot; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Map; import java.util.List; import lombok.RequiredArgsConstructor; @@ -28,9 +24,8 @@ import org.springframework.stereotype.Service; public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService { private final ResourceRestoreTaskRepository resourceRestoreTaskRepository; - private final ResourceRestoreStateFileStore resourceRestoreStateFileStore; private final ObjectMapper objectMapper; - private final ResourceTaskStepLogService resourceTaskStepLogService; + private final RestoreStatusFileReader restoreStatusFileReader; @Override public ResourceRestoreTaskDetailResponse getRestoreTask(String taskId) { @@ -48,7 +43,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService { response.setTargetProductType(entity.getTargetProductType()); response.setPackageFileId(entity.getPackageFileId()); response.setPackagePathVisible(Boolean.FALSE); - response.setRunnerStatePath(entity.getRunnerStatePath()); + response.setStatusFilePath(entity.getRunnerStatePath()); response.setRestorePlan(readStringList(entity.getRestorePlanJson())); response.setConsistencyReport(readObject(entity.getConsistencyReportJson())); response.setErrorCode(entity.getErrorCode()); @@ -57,7 +52,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService { response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString()); response.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString()); response.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString()); - mergeRunnerState(response, entity); + mergeLightweightStatus(response, entity); return response; } @@ -66,8 +61,8 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService { ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId)) .orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId))); List restorePlan = readStringList(entity.getRestorePlanJson()); - ResourceRestoreStateSnapshot snapshot = resourceRestoreStateFileStore.read(resolveStatePath(entity.getRunnerStatePath())).orElse(null); Path taskDir = resolveTaskDir(entity); + RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(taskDir); List responses = new ArrayList<>(); for (int i = 0; i < restorePlan.size(); i++) { String stepCode = restorePlan.get(i); @@ -75,7 +70,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService { ResourceTaskStepItemResponse item = new ResourceTaskStepItemResponse(); item.setStepNo(stepNo); item.setStepCode(stepCode); - applyStepState(item, taskDir, snapshot, stepCode, stepNo); + applyLightweightStepState(item, snapshot, stepCode); responses.add(item); } return responses; @@ -87,38 +82,36 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService { .orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId))); List restorePlan = readStringList(entity.getRestorePlanJson()); String stepCode = stepNo > 0 && stepNo <= restorePlan.size() ? restorePlan.get(stepNo - 1) : ""; - return resourceTaskStepLogService.read(resolveTaskDir(entity), trim(taskId), stepNo, stepCode); + RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity)); + ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse(); + response.setTaskId(trim(taskId)); + response.setStepNo(stepNo); + response.setStepCode(stepCode); + response.setContent(snapshot.log()); + response.setTruncated(false); + return response; } private static String trim(String value) { return value == null ? "" : value.trim(); } - private void mergeRunnerState(ResourceRestoreTaskDetailResponse response, ResourceRestoreTaskEntity entity) { - // 恢复任务详情以数据库记录为基线,再用 runner 状态文件覆盖实时字段。 - // 这样即使 runner 尚未接管,前端也能看到稳定的任务基本信息。 - resourceRestoreStateFileStore.read(resolveStatePath(entity.getRunnerStatePath())) - .ifPresent(snapshot -> applySnapshot(response, snapshot)); + private void mergeLightweightStatus(ResourceRestoreTaskDetailResponse response, ResourceRestoreTaskEntity entity) { + RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity)); + if (trim(snapshot.status()).isEmpty()) { + return; + } + response.setStatus(defaultIfBlank(snapshot.status(), response.getStatus())); + response.setControlPhase(defaultIfBlank(snapshot.phase(), response.getControlPhase())); + response.setStateSource("SHELL_STATUS"); + response.setCurrentStepCode(defaultIfBlank(snapshot.phase(), response.getCurrentStepCode())); + response.setCurrentStepNo(currentStepNo(snapshot.phase(), response.getRestorePlan())); + response.setLastHeartbeatAt(defaultIfBlank(snapshot.updatedAt(), response.getLastHeartbeatAt())); + response.setErrorMessage(defaultIfBlank(snapshot.message(), response.getErrorMessage())); } - private void applySnapshot(ResourceRestoreTaskDetailResponse response, ResourceRestoreStateSnapshot snapshot) { - response.setStatus(defaultIfBlank(snapshot.getStatus(), response.getStatus())); - response.setControlPhase(defaultIfBlank(snapshot.getControlPhase(), response.getControlPhase())); - response.setStateSource(defaultIfBlank(snapshot.getStateSource(), response.getStateSource())); - response.setCurrentStepCode(defaultIfBlank(snapshot.getCurrentStepCode(), response.getCurrentStepCode())); - response.setCurrentStepNo(snapshot.getCurrentStepNo() == null ? response.getCurrentStepNo() : snapshot.getCurrentStepNo()); - response.setLastHeartbeatAt(defaultIfBlank(snapshot.getLastHeartbeatAt(), response.getLastHeartbeatAt())); - response.setOrphaned(snapshot.getOrphaned() == null ? response.getOrphaned() : snapshot.getOrphaned()); - response.setCleanupWarnings(defaultIfNull(snapshot.getCleanupWarnings(), response.getCleanupWarnings())); - response.setWarnings(defaultIfNull(snapshot.getWarnings(), response.getWarnings())); - response.setBlockingIssues(defaultIfNull(snapshot.getBlockingIssues(), response.getBlockingIssues())); - response.setConsistencyReport(snapshot.getConsistencyReport() == null ? response.getConsistencyReport() : snapshot.getConsistencyReport()); - response.setErrorCode(defaultIfBlank(snapshot.getErrorCode(), response.getErrorCode())); - response.setErrorMessage(defaultIfBlank(snapshot.getErrorMessage(), response.getErrorMessage())); - } - - private Path resolveStatePath(String runnerStatePath) { - return trim(runnerStatePath).isEmpty() ? null : Path.of(runnerStatePath).normalize(); + private Path resolveStatePath(String statusFilePath) { + return trim(statusFilePath).isEmpty() ? null : Path.of(statusFilePath).normalize(); } private Path resolveTaskDir(ResourceRestoreTaskEntity entity) { @@ -126,58 +119,29 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService { return statePath == null ? null : statePath.getParent(); } - private void applyStepState( - ResourceTaskStepItemResponse item, - Path taskDir, - ResourceRestoreStateSnapshot snapshot, - String stepCode, - int stepNo - ) { - Map stepOutput = readStepOutput(taskDir, stepNo, stepCode); - item.setLogAvailable(taskDir != null && Files.isRegularFile(taskDir.resolve("steps").resolve(String.format("%02d-%s.log", stepNo, stepCode)))); - if (stepOutput != null) { - item.setStatus(stringValue(stepOutput.get("result"), "UNKNOWN")); - item.setMessage(stringValue(stepOutput.get("summary"), "")); + private void applyLightweightStepState(ResourceTaskStepItemResponse item, RestoreStatusSnapshot snapshot, String stepCode) { + item.setLogAvailable(!trim(snapshot.log()).isEmpty()); + if (trim(snapshot.status()).isEmpty()) { + item.setStatus("PENDING"); return; } - if (snapshot != null && stepNo == safeInt(snapshot.getCurrentStepNo())) { - if ("RUNNER_OWNED".equals(snapshot.getControlPhase())) { - item.setStatus("RUNNING"); - } else if ("RECONCILE_PENDING".equals(snapshot.getControlPhase()) && "FAILED".equals(snapshot.getStatus())) { - item.setStatus("FAILED"); - item.setMessage(defaultIfBlank(snapshot.getErrorMessage(), "")); - } else { - item.setStatus("PENDING"); - } + if ("JAVA_EXTRACT".equals(stepCode)) { + item.setStatus("SUCCESS"); + return; + } + if ("SHELL_APPLY".equals(stepCode)) { + item.setStatus(snapshot.status()); + item.setMessage(snapshot.message()); + return; + } + if (stepCode.equals(snapshot.phase())) { + item.setStatus(snapshot.status()); + item.setMessage(snapshot.message()); return; } item.setStatus("PENDING"); } - private Map readStepOutput(Path taskDir, int stepNo, String stepCode) { - if (taskDir == null) { - return null; - } - Path outputFile = taskDir.resolve("step-output").resolve(String.format("%02d-%s.json", stepNo, stepCode)); - if (!Files.isRegularFile(outputFile)) { - return null; - } - try { - return objectMapper.readValue(outputFile.toFile(), new TypeReference>() { - }); - } catch (Exception ex) { - throw new IllegalStateException("failed to parse resource task step output", ex); - } - } - - private static int safeInt(Integer value) { - return value == null ? -1 : value; - } - - private static String stringValue(Object value, String defaultValue) { - return value == null ? defaultValue : String.valueOf(value); - } - private List readStringList(String json) { if (trim(json).isEmpty()) { return List.of(); @@ -205,7 +169,15 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService { return trim(candidate).isEmpty() ? fallback : candidate; } - private static T defaultIfNull(T candidate, T fallback) { - return candidate == null ? fallback : candidate; + private static Integer currentStepNo(String phase, List restorePlan) { + if (restorePlan == null || restorePlan.isEmpty()) { + return null; + } + for (int i = 0; i < restorePlan.size(); i++) { + if (restorePlan.get(i).equals(phase)) { + return i + 1; + } + } + return restorePlan.contains("SHELL_APPLY") ? restorePlan.indexOf("SHELL_APPLY") + 1 : null; } } diff --git a/src/main/java/com/cisd/tms/modules/backup/support/file/HandoffFileStore.java b/src/main/java/com/cisd/tms/modules/backup/support/file/HandoffFileStore.java deleted file mode 100644 index fd147e5..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/support/file/HandoffFileStore.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.cisd.tms.modules.backup.support.file; - -import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; -import org.springframework.stereotype.Component; - -@Component -public class HandoffFileStore { - - private final ObjectMapper objectMapper; - - public HandoffFileStore(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - - public void write(Path taskDir, ResourceRestoreTaskEntity entity) { - try { - Files.createDirectories(taskDir); - Path file = taskDir.resolve("handoff.json"); - Map json = new LinkedHashMap<>(); - json.put("taskId", entity.getTaskId()); - json.put("precheckId", entity.getPrecheckId()); - json.put("backupId", entity.getBackupId()); - json.put("packageFileId", entity.getPackageFileId()); - json.put("packagePath", entity.getPackagePath()); - json.put("stagingDir", entity.getStagingDir()); - json.put("runnerStatePath", entity.getRunnerStatePath()); - json.put("restorePlanJson", entity.getRestorePlanJson()); - objectMapper.writeValue(file.toFile(), json); - } catch (IOException ex) { - throw new IllegalStateException("failed to write restore handoff file", ex); - } - } - - public Optional read(Path handoffFile) { - if (handoffFile == null || !Files.isRegularFile(handoffFile)) { - return Optional.empty(); - } - try { - return Optional.ofNullable(objectMapper.readValue(handoffFile.toFile(), ResourceRestoreHandoffSnapshot.class)); - } catch (IOException ex) { - throw new IllegalStateException("failed to read restore handoff file", ex); - } - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateFileStore.java b/src/main/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateFileStore.java deleted file mode 100644 index eb513e3..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateFileStore.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.cisd.tms.modules.backup.support.file; - -import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.UUID; -import org.springframework.stereotype.Component; - -@Component -public class ResourceRestoreStateFileStore { - - private final ObjectMapper objectMapper; - - public ResourceRestoreStateFileStore(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - - public void initialize(Path taskDir, ResourceRestoreTaskEntity entity) { - try { - Files.createDirectories(taskDir); - Map json = new LinkedHashMap<>(); - json.put("taskId", entity.getTaskId()); - json.put("status", entity.getStatus()); - json.put("controlPhase", "PRECHECKED"); - json.put("stateSource", "DB"); - json.put("runnerInstanceId", ""); - json.put("runnerPid", 0); - json.put("ownershipToken", UUID.randomUUID().toString()); - json.put("orphaned", false); - objectMapper.writeValue(taskDir.resolve("restore-state.json").toFile(), json); - } catch (IOException ex) { - throw new IllegalStateException("failed to initialize restore state file", ex); - } - } - - public java.util.Optional read(Path stateFile) { - if (stateFile == null || !Files.isRegularFile(stateFile)) { - return java.util.Optional.empty(); - } - try { - return java.util.Optional.ofNullable(objectMapper.readValue(stateFile.toFile(), ResourceRestoreStateSnapshot.class)); - } catch (IOException ex) { - throw new IllegalStateException("failed to read restore state file", ex); - } - } - - public void markHandoffReady(Path stateFile, String currentStepCode, Integer currentStepNo) { - try { - Map json = readJson(stateFile); - json.put("status", "HANDOFF"); - json.put("controlPhase", "HANDOFF_READY"); - json.put("stateSource", "DB"); - json.put("currentStepCode", currentStepCode); - json.put("currentStepNo", currentStepNo); - json.put("orphaned", false); - objectMapper.writeValue(stateFile.toFile(), json); - } catch (IOException ex) { - throw new IllegalStateException("failed to mark restore state as handoff ready", ex); - } - } - - public void claimOwnership(Path stateFile, String runnerInstanceId, long runnerPid, String currentStepCode, Integer currentStepNo) { - try { - Map json = readJson(stateFile); - String controlPhase = String.valueOf(json.getOrDefault("controlPhase", "")); - if (!"HANDOFF_READY".equals(controlPhase)) { - throw new IllegalStateException("restore state is not handoff ready: " + controlPhase); - } - json.put("status", "RUNNING"); - json.put("controlPhase", "RUNNER_OWNED"); - json.put("stateSource", "RUNNER_FILE"); - json.put("runnerInstanceId", runnerInstanceId); - json.put("runnerPid", runnerPid); - json.put("currentStepCode", currentStepCode); - json.put("currentStepNo", currentStepNo); - json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString()); - json.put("orphaned", false); - objectMapper.writeValue(stateFile.toFile(), json); - } catch (IOException ex) { - throw new IllegalStateException("failed to claim restore runner ownership", ex); - } - } - - public void markStepRunning(Path stateFile, String currentStepCode, Integer currentStepNo, String runnerInstanceId) { - updateOwnedState(stateFile, runnerInstanceId, json -> { - json.put("status", "RUNNING"); - json.put("controlPhase", "RUNNER_OWNED"); - json.put("stateSource", "RUNNER_FILE"); - json.put("currentStepCode", currentStepCode); - json.put("currentStepNo", currentStepNo); - json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString()); - }, "failed to mark restore step running"); - } - - public void touchHeartbeat(Path stateFile, String runnerInstanceId) { - updateOwnedState(stateFile, runnerInstanceId, json -> - json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString()), "failed to touch restore heartbeat"); - } - - public void markReconcilePending( - Path stateFile, - String status, - String errorCode, - String errorMessage, - String currentStepCode, - Integer currentStepNo, - String runnerInstanceId - ) { - updateOwnedState(stateFile, runnerInstanceId, json -> { - json.put("status", status); - json.put("controlPhase", "RECONCILE_PENDING"); - json.put("stateSource", "RUNNER_FILE"); - json.put("currentStepCode", currentStepCode); - json.put("currentStepNo", currentStepNo); - json.put("errorCode", errorCode == null ? "" : errorCode); - json.put("errorMessage", errorMessage == null ? "" : errorMessage); - json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString()); - }, "failed to mark restore reconcile pending"); - } - - private void updateOwnedState( - Path stateFile, - String runnerInstanceId, - JsonMutator mutator, - String errorMessage - ) { - try { - Map json = readJson(stateFile); - String currentRunnerInstanceId = String.valueOf(json.getOrDefault("runnerInstanceId", "")); - if (!runnerInstanceId.equals(currentRunnerInstanceId)) { - throw new IllegalStateException("restore runner ownership changed: " + currentRunnerInstanceId); - } - mutator.accept(json); - objectMapper.writeValue(stateFile.toFile(), json); - } catch (IOException ex) { - throw new IllegalStateException(errorMessage, ex); - } - } - - public void markDone(Path stateFile) { - try { - Map json = readJson(stateFile); - json.put("controlPhase", "DONE"); - json.put("stateSource", "DB"); - json.put("lastHeartbeatAt", LocalDateTime.now().toString()); - objectMapper.writeValue(stateFile.toFile(), json); - } catch (IOException ex) { - throw new IllegalStateException("failed to mark restore state done", ex); - } - } - - public void markStalled(Path stateFile, String errorCode, String errorMessage) { - try { - Map json = readJson(stateFile); - if (!"RUNNER_OWNED".equals(String.valueOf(json.getOrDefault("controlPhase", "")))) { - return; - } - json.put("status", "FAILED"); - json.put("controlPhase", "RECONCILE_PENDING"); - json.put("stateSource", "RUNNER_FILE"); - json.put("orphaned", true); - json.put("errorCode", errorCode); - json.put("errorMessage", errorMessage); - json.put("lastHeartbeatAt", LocalDateTime.now().toString()); - objectMapper.writeValue(stateFile.toFile(), json); - } catch (IOException ex) { - throw new IllegalStateException("failed to mark stalled restore state", ex); - } - } - - @FunctionalInterface - private interface JsonMutator { - void accept(Map json); - } - - private Map readJson(Path stateFile) throws IOException { - if (stateFile == null || !Files.isRegularFile(stateFile)) { - throw new IllegalStateException("restore state file not found: " + stateFile); - } - return objectMapper.readValue( - stateFile.toFile(), - objectMapper.getTypeFactory().constructMapType(LinkedHashMap.class, String.class, Object.class) - ); - } -} diff --git a/src/main/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateSnapshot.java b/src/main/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateSnapshot.java deleted file mode 100644 index bf6c570..0000000 --- a/src/main/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateSnapshot.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.cisd.tms.modules.backup.support.file; - -import java.util.List; - -public class ResourceRestoreStateSnapshot { - - private String taskId; - private String status; - private String controlPhase; - private String stateSource; - private String runnerInstanceId; - private Long runnerPid; - private String ownershipToken; - private String currentStepCode; - private Integer currentStepNo; - private String lastHeartbeatAt; - private Boolean orphaned; - private List cleanupWarnings; - private List warnings; - private List blockingIssues; - private Object consistencyReport; - private String errorCode; - private String errorMessage; - - public String getTaskId() { - return taskId; - } - - public void setTaskId(String taskId) { - this.taskId = taskId; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } - - public String getControlPhase() { - return controlPhase; - } - - public void setControlPhase(String controlPhase) { - this.controlPhase = controlPhase; - } - - public String getStateSource() { - return stateSource; - } - - public void setStateSource(String stateSource) { - this.stateSource = stateSource; - } - - public String getRunnerInstanceId() { - return runnerInstanceId; - } - - public void setRunnerInstanceId(String runnerInstanceId) { - this.runnerInstanceId = runnerInstanceId; - } - - public Long getRunnerPid() { - return runnerPid; - } - - public void setRunnerPid(Long runnerPid) { - this.runnerPid = runnerPid; - } - - public String getOwnershipToken() { - return ownershipToken; - } - - public void setOwnershipToken(String ownershipToken) { - this.ownershipToken = ownershipToken; - } - - public String getCurrentStepCode() { - return currentStepCode; - } - - public void setCurrentStepCode(String currentStepCode) { - this.currentStepCode = currentStepCode; - } - - public Integer getCurrentStepNo() { - return currentStepNo; - } - - public void setCurrentStepNo(Integer currentStepNo) { - this.currentStepNo = currentStepNo; - } - - public String getLastHeartbeatAt() { - return lastHeartbeatAt; - } - - public void setLastHeartbeatAt(String lastHeartbeatAt) { - this.lastHeartbeatAt = lastHeartbeatAt; - } - - public Boolean getOrphaned() { - return orphaned; - } - - public void setOrphaned(Boolean orphaned) { - this.orphaned = orphaned; - } - - public List getCleanupWarnings() { - return cleanupWarnings; - } - - public void setCleanupWarnings(List cleanupWarnings) { - this.cleanupWarnings = cleanupWarnings; - } - - public List getWarnings() { - return warnings; - } - - public void setWarnings(List warnings) { - this.warnings = warnings; - } - - public List getBlockingIssues() { - return blockingIssues; - } - - public void setBlockingIssues(List blockingIssues) { - this.blockingIssues = blockingIssues; - } - - public Object getConsistencyReport() { - return consistencyReport; - } - - public void setConsistencyReport(Object consistencyReport) { - this.consistencyReport = consistencyReport; - } - - public String getErrorCode() { - return errorCode; - } - - public void setErrorCode(String errorCode) { - this.errorCode = errorCode; - } - - public String getErrorMessage() { - return errorMessage; - } - - public void setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; - } -} diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateRequest.java index da7301a..ba5d36b 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateRequest.java @@ -8,9 +8,10 @@ import java.util.List; @Data public class EntityCreateRequest { @NotBlank - private String entityName; + private String orgCode; + @NotBlank + private String orgName; @NotBlank private String algoType; private List allowIps; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityGenP10Request.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityGenP10Request.java index c710ea3..7dad1d8 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityGenP10Request.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityGenP10Request.java @@ -8,8 +8,9 @@ import lombok.Data; public class EntityGenP10Request { @NotNull private Long id; + @NotNull + private String certMode; @Valid @NotNull private Subject subject; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityItemResponse.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityItemResponse.java index 73e6750..cdc6a2a 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityItemResponse.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityItemResponse.java @@ -8,12 +8,14 @@ import java.util.List; @Data public class EntityItemResponse { private Long id; - private String entityName; + private String orgCode; + private String orgName; private String algoType; private Integer keyLen; private Integer keyIdx; private String subject; private List allowIps; + private String status; + private String statusName; private LocalDateTime createTime; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityListRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityListRequest.java index bf22f21..3817891 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityListRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityListRequest.java @@ -6,7 +6,7 @@ import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = true) public class EntityListRequest extends CommonPageRequest { - private String keyword; + private String orgCode; private String algoType; + private String status; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityUpdateRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityUpdateRequest.java index 4825c76..b7f160b 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityUpdateRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityUpdateRequest.java @@ -9,7 +9,7 @@ import java.util.List; public class EntityUpdateRequest { @NotNull private Long id; - private String entityName; + private String orgCode; + private String orgName; private List allowIps; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/entity/KeyEntity.java b/src/main/java/com/cisd/tms/modules/cert/entity/KeyEntity.java index 2e04ca3..3587cfc 100644 --- a/src/main/java/com/cisd/tms/modules/cert/entity/KeyEntity.java +++ b/src/main/java/com/cisd/tms/modules/cert/entity/KeyEntity.java @@ -12,7 +12,8 @@ import lombok.experimental.Accessors; @TableName("t_key_entity") public class KeyEntity extends BaseEntity { - private String entityName; + private String orgCode; + private String orgName; private String algoType; private Integer keyLen; private Integer keyIdx; @@ -20,8 +21,9 @@ public class KeyEntity extends BaseEntity { private String publicKey; private String p10Data; private String subjectDn; + private String certMode; + private String status; private String allowIps; private String signData; private String createBy; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/enums/EntityCertMode.java b/src/main/java/com/cisd/tms/modules/cert/enums/EntityCertMode.java new file mode 100644 index 0000000..e612fe0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/enums/EntityCertMode.java @@ -0,0 +1,34 @@ +package com.cisd.tms.modules.cert.enums; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; + +import java.util.Arrays; + +public enum EntityCertMode { + SINGLE("SINGLE", "单证"), + DOUBLE("DOUBLE", "双证"); + + private final String code; + private final String displayName; + + EntityCertMode(String code, String displayName) { + this.code = code; + this.displayName = displayName; + } + + public String getCode() { + return code; + } + + public String getDisplayName() { + return displayName; + } + + public static EntityCertMode requireValid(String code) { + return Arrays.stream(values()) + .filter(value -> value.code.equalsIgnoreCase(code)) + .findFirst() + .orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "invalid entity cert mode")); + } +} diff --git a/src/main/java/com/cisd/tms/modules/cert/enums/EntityStatus.java b/src/main/java/com/cisd/tms/modules/cert/enums/EntityStatus.java new file mode 100644 index 0000000..695c880 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/enums/EntityStatus.java @@ -0,0 +1,35 @@ +package com.cisd.tms.modules.cert.enums; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; + +import java.util.Arrays; + +public enum EntityStatus { + CREATED("CREATED", "已创建"), + P10_GENERATED("P10_GENERATED", "已生成P10请求"), + CERT_IMPORTED("CERT_IMPORTED", "已导入证书"); + + private final String code; + private final String displayName; + + EntityStatus(String code, String displayName) { + this.code = code; + this.displayName = displayName; + } + + public String getCode() { + return code; + } + + public String getDisplayName() { + return displayName; + } + + public static EntityStatus requireValid(String code) { + return Arrays.stream(values()) + .filter(value -> value.code.equalsIgnoreCase(code)) + .findFirst() + .orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "invalid entity status")); + } +} diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/KeyEntityRepository.java b/src/main/java/com/cisd/tms/modules/cert/repository/KeyEntityRepository.java index 9fab4e4..cb9ccd2 100644 --- a/src/main/java/com/cisd/tms/modules/cert/repository/KeyEntityRepository.java +++ b/src/main/java/com/cisd/tms/modules/cert/repository/KeyEntityRepository.java @@ -13,5 +13,5 @@ public interface KeyEntityRepository { Optional findByKeyIdx(Integer keyIdx); List findUsedKeyIdx(); Page page(EntityListRequest request); + void updateStatus(Long id, String status); } - diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/impl/KeyEntityRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/cert/repository/impl/KeyEntityRepositoryImpl.java index 4a11658..0cb87a4 100644 --- a/src/main/java/com/cisd/tms/modules/cert/repository/impl/KeyEntityRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/cert/repository/impl/KeyEntityRepositoryImpl.java @@ -1,6 +1,7 @@ package com.cisd.tms.modules.cert.repository.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.cisd.tms.modules.cert.dto.EntityListRequest; import com.cisd.tms.modules.cert.entity.KeyEntity; @@ -62,12 +63,14 @@ public class KeyEntityRepositoryImpl implements KeyEntityRepository { @Override public Page page(EntityListRequest request) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - if (StringUtils.hasText(request.getKeyword())) { - wrapper.and(w -> w.like(KeyEntity::getEntityName, request.getKeyword()) - .or().like(KeyEntity::getSubjectDn, request.getKeyword())); + if (StringUtils.hasText(request.getOrgCode())) { + wrapper.like(KeyEntity::getOrgCode, request.getOrgCode()); } if (StringUtils.hasText(request.getAlgoType())) { - wrapper.eq(KeyEntity::getAlgoType, request.getAlgoType()); + wrapper.eq(KeyEntity::getAlgoType, request.getAlgoType().toUpperCase()); + } + if (StringUtils.hasText(request.getStatus())) { + wrapper.eq(KeyEntity::getStatus, request.getStatus().toUpperCase()); } wrapper.orderByDesc(KeyEntity::getCreateTime); long total = mapper.selectCount(wrapper); @@ -78,5 +81,11 @@ public class KeyEntityRepositoryImpl implements KeyEntityRepository { page.setRecords(records); return page; } -} + @Override + public void updateStatus(Long id, String status) { + mapper.update(null, new LambdaUpdateWrapper() + .eq(KeyEntity::getId, id) + .set(KeyEntity::getStatus, status)); + } +} diff --git a/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java b/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java index 6dcf7bc..977f747 100644 --- a/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java +++ b/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java @@ -92,6 +92,9 @@ public class CertificateService { entity.setCreateTime(LocalDateTime.now()); entity.setUpdateTime(LocalDateTime.now()); certificateRepository.save(entity); + if (finalEntityId != null) { + entityService.markCertificateImported(finalEntityId); + } return new ImportCertificateResponse(entity.getId(), entity.getSubjectDn(), entity.getValidTo()); } catch (BizException ex) { throw ex; diff --git a/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java b/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java index ad1d0ed..59ba0d0 100644 --- a/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java +++ b/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java @@ -16,6 +16,8 @@ import com.cisd.tms.modules.cert.dto.EntityListRequest; import com.cisd.tms.modules.cert.dto.EntityUpdateRequest; import com.cisd.tms.modules.cert.dto.Subject; import com.cisd.tms.modules.cert.entity.KeyEntity; +import com.cisd.tms.modules.cert.enums.EntityCertMode; +import com.cisd.tms.modules.cert.enums.EntityStatus; import com.cisd.tms.modules.cert.repository.CertificateRepository; import com.cisd.tms.modules.cert.repository.KeyEntityRepository; import com.cisd.tms.modules.cert.support.Sm2KeySupport; @@ -39,11 +41,13 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Base64; import java.util.List; +import java.util.Locale; @Service public class EntityService { private static final String SM2 = "SM2"; + private static final String RSA = "RSA"; private static final byte[] DEFAULT_SM2_USER_ID = "1234567812345678".getBytes(StandardCharsets.UTF_8); private final KeyEntityRepository keyEntityRepository; @@ -72,26 +76,33 @@ public class EntityService { @Transactional public EntityCreateResponse create(EntityCreateRequest request) { - if (!SM2.equalsIgnoreCase(request.getAlgoType())) { - throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "only SM2 is supported"); - } + String algoType = normalizeAlgoType(request.getAlgoType()); int keyIdx = resolveNextKeyIdx(); + int keyLen = SM2.equals(algoType) ? 256 : 2048; GenerateKeyPairRequest generateRequest = new GenerateKeyPairRequest(); - generateRequest.setKeyBits(256); + generateRequest.setKeyBits(keyLen); generateRequest.setKeyIndex(keyIdx); generateRequest.setKeyType(PcieUserKeyType.SIGN.getCode()); - pcieCryptoService.generateKeyPairEcc(generateRequest); + if (SM2.equals(algoType)) { + pcieCryptoService.generateKeyPairEcc(generateRequest); + } else { + pcieCryptoService.generateKeyPairRsa(generateRequest); + } try { BackupDataResult backup = pcieCryptoService.backupUserKey(keyIdx, PcieUserKeyType.SIGN.getCode(), 4096); - BackupDataResult publicKey = pcieCryptoService.exportSignPublicKeyEcc(keyIdx); + BackupDataResult publicKey = SM2.equals(algoType) + ? pcieCryptoService.exportSignPublicKeyEcc(keyIdx) + : pcieCryptoService.exportSignPublicKeyRsa(keyIdx); KeyEntity entity = new KeyEntity() - .setEntityName(request.getEntityName()) - .setAlgoType(SM2) - .setKeyLen(256) + .setOrgCode(request.getOrgCode()) + .setOrgName(request.getOrgName()) + .setAlgoType(algoType) + .setKeyLen(keyLen) .setKeyIdx(keyIdx) .setBackupData(Base64.getEncoder().encodeToString(backup.getData())) .setPublicKey(Base64.getEncoder().encodeToString(publicKey.getData())) + .setStatus(EntityStatus.CREATED.getCode()) .setAllowIps(writeAllowIps(request.getAllowIps())) .setCreateBy("system"); entity.setCreateTime(LocalDateTime.now()); @@ -110,8 +121,11 @@ public class EntityService { @Transactional public void update(EntityUpdateRequest request) { KeyEntity entity = getById(request.getId()); - if (StringUtils.hasText(request.getEntityName())) { - entity.setEntityName(request.getEntityName()); + if (StringUtils.hasText(request.getOrgCode())) { + entity.setOrgCode(request.getOrgCode()); + } + if (StringUtils.hasText(request.getOrgName())) { + entity.setOrgName(request.getOrgName()); } if (request.getAllowIps() != null) { entity.setAllowIps(writeAllowIps(request.getAllowIps())); @@ -132,6 +146,10 @@ public class EntityService { @Transactional public String generateP10(EntityGenP10Request request) { KeyEntity entity = getById(request.getId()); + if (!SM2.equalsIgnoreCase(entity.getAlgoType())) { + throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "only SM2 P10 is supported"); + } + EntityCertMode certMode = EntityCertMode.requireValid(request.getCertMode()); Subject subject = request.getSubject(); X500Name subjectName = subject.toX500Name(); String subjectDn = subjectName.toString(); @@ -142,6 +160,8 @@ public class EntityService { String pem = encodePkcs10Pem(csr); entity.setSubjectDn(subjectDn); entity.setP10Data(pem); + entity.setCertMode(certMode.getCode()); + entity.setStatus(EntityStatus.P10_GENERATED.getCode()); keyEntityRepository.save(entity); return pem; } @@ -156,19 +176,40 @@ public class EntityService { .orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "entity not found")); } + @Transactional + public void markCertificateImported(Long id) { + keyEntityRepository.updateStatus(id, EntityStatus.CERT_IMPORTED.getCode()); + } + private EntityItemResponse toItem(KeyEntity entity) { EntityItemResponse response = new EntityItemResponse(); response.setId(entity.getId()); - response.setEntityName(entity.getEntityName()); + response.setOrgCode(entity.getOrgCode()); + response.setOrgName(entity.getOrgName()); response.setAlgoType(entity.getAlgoType()); response.setKeyLen(entity.getKeyLen()); response.setKeyIdx(entity.getKeyIdx()); response.setSubject(entity.getSubjectDn()); response.setAllowIps(readAllowIps(entity.getAllowIps())); + response.setStatus(entity.getStatus()); + if (StringUtils.hasText(entity.getStatus())) { + response.setStatusName(EntityStatus.requireValid(entity.getStatus()).getDisplayName()); + } response.setCreateTime(entity.getCreateTime()); return response; } + private String normalizeAlgoType(String algoType) { + if (!StringUtils.hasText(algoType)) { + throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "algoType is required"); + } + String normalized = algoType.toUpperCase(Locale.ROOT); + if (!SM2.equals(normalized) && !RSA.equals(normalized)) { + throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "algoType must be SM2 or RSA"); + } + return normalized; + } + private int resolveNextKeyIdx() { List used = keyEntityRepository.findUsedKeyIdx(); for (int idx = 2; idx <= 8; idx++) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 20a4485..aefab02 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -184,16 +184,24 @@ tms: backup: # 资源备份包输出目录;第一版先在本机生成可见的 .tmsbak 文件。 output-dir: ${TMS_BACKUP_OUTPUT_DIR:/home/tms/tmp/resource-backup-packages} - # 恢复 runner 启停 TMS 时使用的脚本路径。 + # 轻量恢复 apply 脚本启停 TMS 时使用的脚本路径。 tms-script-path: ${TMS_BACKUP_TMS_SCRIPT_PATH:/home/tms/scripts/tms.sh} - # 外部恢复 runner 启动脚本路径;创建恢复任务后由主进程调用该脚本拉起独立 runner。 - runner-launch-script-path: ${TMS_BACKUP_RUNNER_LAUNCH_SCRIPT_PATH:/home/tms/bin/resource-restore/start-runner.sh} + # 轻量恢复 apply 脚本路径;Java 完成验签/解密/展开后调用该脚本执行覆盖、SQL、MQ replay。 + restore-apply-script-path: ${TMS_BACKUP_RESTORE_APPLY_SCRIPT_PATH:/home/tms/bin/resource-restore/apply-resource-backup.sh} + # 文件恢复白名单根路径;manifest 中 FILE 恢复路径必须落在这些目录下。 + allowed-restore-roots: + - ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_TMS_CONFIG:/home/tms/config} + - ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_CMEP4I:/home/cmep4i} + - ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_CMEP4D:/home/cmep4d} + - ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_WORK:/home/tms/tmp/resource-restore} # 导出 TMS 库时使用的 mysqldump 命令;现场可按实际安装路径覆盖。 mysqldump-path: ${TMS_BACKUP_MYSQLDUMP_PATH:mysqldump} # 恢复 TMS 库时使用的 mysql 客户端命令。 mysql-path: ${TMS_BACKUP_MYSQL_PATH:mysql} - # 数据库恢复脚本路径;runner 在 RESTORE_DATABASES 阶段调用。 + # 数据库恢复脚本路径;apply 脚本在 RESTORE_DATABASES 阶段调用。 restore-db-script-path: ${TMS_BACKUP_RESTORE_DB_SCRIPT_PATH:/home/tms/bin/resource-restore/restore-db.sh} + # MQ replay 脚本路径;apply 脚本在 MQ_REPLAY 阶段调用。 + mq-replay-script-path: ${TMS_BACKUP_MQ_REPLAY_SCRIPT_PATH:/home/tms/bin/resource-restore/replay-mq.sh} # 备份 TMS 库名;为空时默认从 spring.datasource.url 中解析。 tms-database-name: ${TMS_BACKUP_TMS_DATABASE_NAME:} # 备份/恢复标准收发器业务库名;第一版默认使用 CMEP。 @@ -202,10 +210,6 @@ tms: health-check-url: ${TMS_BACKUP_HEALTH_CHECK_URL:http://127.0.0.1:8080/actuator/health} # 健康检查请求超时(秒)。 health-check-timeout-seconds: ${TMS_BACKUP_HEALTH_CHECK_TIMEOUT_SECONDS:5} - # 是否在创建恢复任务后自动拉起本机 runner;默认关闭,避免开发环境误触停机恢复链。 - runner-auto-launch-enabled: ${TMS_BACKUP_RUNNER_AUTO_LAUNCH_ENABLED:false} - # 恢复 runner 心跳失联超时(秒)。 - runner-heartbeat-timeout-seconds: ${TMS_BACKUP_RUNNER_HEARTBEAT_TIMEOUT_SECONDS:300} mk: init-identify: # SDFE_InitIdentify 旧 PIN(Base64 编码);主密钥初始化接口会读取该值。 @@ -217,7 +221,7 @@ tms: internal-token: ${TMS_INTERNAL_TOKEN:change-me-internal-token} internal-auth: # 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。 - enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:false} + enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:true} # 关闭内部鉴权后,注入请求上下文的调试角色。 debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:SUPER_ADMIN} # 关闭内部鉴权后,注入请求上下文的调试认证等级。 diff --git a/src/test/java/com/cisd/tms/modules/backup/ResourceBackupDtoContractTest.java b/src/test/java/com/cisd/tms/modules/backup/ResourceBackupDtoContractTest.java index 9f065d4..74b5b14 100644 --- a/src/test/java/com/cisd/tms/modules/backup/ResourceBackupDtoContractTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/ResourceBackupDtoContractTest.java @@ -47,16 +47,16 @@ class ResourceBackupDtoContractTest { void shouldSerializeRestoreTaskDetailControlFields() throws Exception { ResourceRestoreTaskDetailResponse response = new ResourceRestoreTaskDetailResponse(); response.setTaskId("RRST-TASK-20260408-000001"); - response.setStatus("RUNNING"); - response.setControlPhase("RUNNER_OWNED"); - response.setStateSource("RUNNER_FILE"); - response.setCurrentStepCode("RESTORE_DATABASES"); + response.setStatus("APPLYING"); + response.setControlPhase("SHELL_APPLY"); + response.setStateSource("SHELL_STATUS"); + response.setCurrentStepCode("SHELL_APPLY"); String json = objectMapper.writeValueAsString(response); Assertions.assertTrue(json.contains("\"taskId\":\"RRST-TASK-20260408-000001\"")); - Assertions.assertTrue(json.contains("\"controlPhase\":\"RUNNER_OWNED\"")); - Assertions.assertTrue(json.contains("\"stateSource\":\"RUNNER_FILE\"")); - Assertions.assertTrue(json.contains("\"currentStepCode\":\"RESTORE_DATABASES\"")); + Assertions.assertTrue(json.contains("\"controlPhase\":\"SHELL_APPLY\"")); + Assertions.assertTrue(json.contains("\"stateSource\":\"SHELL_STATUS\"")); + Assertions.assertTrue(json.contains("\"currentStepCode\":\"SHELL_APPLY\"")); } } diff --git a/src/test/java/com/cisd/tms/modules/backup/ResourceBackupPropertiesTest.java b/src/test/java/com/cisd/tms/modules/backup/ResourceBackupPropertiesTest.java new file mode 100644 index 0000000..4a2c51e --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/backup/ResourceBackupPropertiesTest.java @@ -0,0 +1,45 @@ +package com.cisd.tms.modules.backup; + +import com.cisd.tms.modules.backup.config.ResourceBackupProperties; +import com.cisd.tms.modules.backup.dto.support.BackupResourceKind; +import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ResourceBackupPropertiesTest { + + @Test + void shouldExposeDefaultLightweightRestoreSettings() { + ResourceBackupProperties properties = new ResourceBackupProperties(); + + Assertions.assertEquals( + "/home/tms/bin/resource-restore/apply-resource-backup.sh", + properties.getRestoreApplyScriptPath() + ); + Assertions.assertEquals( + "/home/tms/bin/resource-restore/replay-mq.sh", + properties.getMqReplayScriptPath() + ); + Assertions.assertTrue(properties.getAllowedRestoreRoots().contains("/home/tms/config")); + Assertions.assertTrue(properties.getDefaultFileResources().stream() + .anyMatch(item -> "TMS_CONFIG".equals(item.getCode()) + && BackupResourceKind.FILE.name().equals(item.getKind()))); + } + + @Test + void shouldAcceptAdditionalFileResources() { + ResourceBackupProperties properties = new ResourceBackupProperties(); + ConfiguredFileBackupResource custom = new ConfiguredFileBackupResource(); + custom.setCode("CUSTOM_CERT"); + custom.setKind(BackupResourceKind.FILE.name()); + custom.setMode("FILE"); + custom.setPath("/home/tms/certs/custom.crt"); + custom.setRestorePath("/home/tms/certs/custom.crt"); + custom.setRequired(false); + + properties.setFileResources(java.util.List.of(custom)); + + Assertions.assertEquals("CUSTOM_CERT", properties.getFileResources().get(0).getCode()); + Assertions.assertFalse(properties.getFileResources().get(0).isRequired()); + } +} diff --git a/src/test/java/com/cisd/tms/modules/backup/controller/ResourceBackupControllerTest.java b/src/test/java/com/cisd/tms/modules/backup/controller/ResourceBackupControllerTest.java index a4a3024..92ab21a 100644 --- a/src/test/java/com/cisd/tms/modules/backup/controller/ResourceBackupControllerTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/controller/ResourceBackupControllerTest.java @@ -5,7 +5,6 @@ import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest; import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse; import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse; import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse; -import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse; import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse; import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse; import com.cisd.tms.modules.backup.service.ResourceBackupService; @@ -40,12 +39,6 @@ class ResourceBackupControllerTest { ResourceRestoreService restoreService = Mockito.mock(ResourceRestoreService.class); ResourceTaskQueryService taskQueryService = Mockito.mock(ResourceTaskQueryService.class); - ResourceKeysetStatusResponse keysetStatus = new ResourceKeysetStatusResponse(); - keysetStatus.setLmkReady(true); - keysetStatus.setStorageEncKeyReady(true); - keysetStatus.setStorageEncKeyFingerprint("SM3(KEYSET)"); - Mockito.when(backupService.getKeysetStatus()).thenReturn(keysetStatus); - CreateResourceBackupResponse createBackup = new CreateResourceBackupResponse(); createBackup.setTaskId("RBKP-TASK-20260408-000001"); createBackup.setStatus("PENDING"); @@ -79,9 +72,9 @@ class ResourceBackupControllerTest { ResourceRestoreTaskDetailResponse restoreDetail = new ResourceRestoreTaskDetailResponse(); restoreDetail.setTaskId("RRST-TASK-20260408-000001"); - restoreDetail.setStatus("RUNNING"); - restoreDetail.setControlPhase("RUNNER_OWNED"); - restoreDetail.setStateSource("RUNNER_FILE"); + restoreDetail.setStatus("APPLYING"); + restoreDetail.setControlPhase("SHELL_APPLY"); + restoreDetail.setStateSource("SHELL_STATUS"); Mockito.when(taskQueryService.getRestoreTask("RRST-TASK-20260408-000001")).thenReturn(restoreDetail); MockMvc mockMvc = MockMvcBuilders.standaloneSetup( @@ -89,11 +82,6 @@ class ResourceBackupControllerTest { new ResourceRestoreController(precheckService, restoreService, taskQueryService) ).build(); - mockMvc.perform(get("/api/v1/resource-backups/keyset/status")) - .andExpect(status().isOk()) - .andExpect(content().string(org.hamcrest.Matchers.containsString("\"lmkReady\":true"))) - .andExpect(content().string(org.hamcrest.Matchers.containsString("\"storageEncKeyFingerprint\":\"SM3(KEYSET)\""))); - mockMvc.perform(post("/api/v1/resource-backups") .contentType(MediaType.APPLICATION_JSON) .content("{\"includeUpgradeHistory\":true,\"includeAuditBackups\":false,\"remark\":\"before hardware replacement\"}")) @@ -124,7 +112,7 @@ class ResourceBackupControllerTest { mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260408-000001")) .andExpect(status().isOk()) - .andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"RUNNER_OWNED\""))) - .andExpect(content().string(org.hamcrest.Matchers.containsString("\"stateSource\":\"RUNNER_FILE\""))); + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"SHELL_APPLY\""))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"stateSource\":\"SHELL_STATUS\""))); } } diff --git a/src/test/java/com/cisd/tms/modules/backup/controller/ResourceRestoreControllerTest.java b/src/test/java/com/cisd/tms/modules/backup/controller/ResourceRestoreControllerTest.java index 0c683a6..341074d 100644 --- a/src/test/java/com/cisd/tms/modules/backup/controller/ResourceRestoreControllerTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/controller/ResourceRestoreControllerTest.java @@ -37,26 +37,26 @@ class ResourceRestoreControllerTest { CreateResourceRestoreResponse create = new CreateResourceRestoreResponse(); create.setTaskId("RRST-TASK-20260414-000001"); - create.setStatus("HANDOFF"); + create.setStatus("APPLYING"); Mockito.when(restoreService.createRestoreTask(Mockito.any(CreateResourceRestoreRequest.class))).thenReturn(create); ResourceRestoreTaskDetailResponse detail = new ResourceRestoreTaskDetailResponse(); detail.setTaskId("RRST-TASK-20260414-000001"); - detail.setControlPhase("RECONCILE_PENDING"); - detail.setStateSource("RUNNER_FILE"); + detail.setControlPhase("SHELL_APPLY"); + detail.setStateSource("SHELL_STATUS"); Mockito.when(taskQueryService.getRestoreTask("RRST-TASK-20260414-000001")).thenReturn(detail); ResourceTaskStepItemResponse step = new ResourceTaskStepItemResponse(); step.setStepNo(2); - step.setStepCode("STOP_SERVICES"); + step.setStepCode("SHELL_APPLY"); step.setStatus("SUCCESS"); Mockito.when(taskQueryService.getRestoreTaskSteps("RRST-TASK-20260414-000001")).thenReturn(List.of(step)); ResourceTaskStepLogResponse log = new ResourceTaskStepLogResponse(); log.setTaskId("RRST-TASK-20260414-000001"); log.setStepNo(2); - log.setStepCode("STOP_SERVICES"); - log.setContent("stop ok"); + log.setStepCode("SHELL_APPLY"); + log.setContent("apply ok"); log.setTruncated(false); Mockito.when(taskQueryService.getRestoreTaskStepLog("RRST-TASK-20260414-000001", 2)).thenReturn(log); @@ -78,14 +78,14 @@ class ResourceRestoreControllerTest { mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001")) .andExpect(status().isOk()) - .andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"RECONCILE_PENDING\""))); + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"SHELL_APPLY\""))); mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001/steps")) .andExpect(status().isOk()) - .andExpect(content().string(org.hamcrest.Matchers.containsString("\"stepCode\":\"STOP_SERVICES\""))); + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"stepCode\":\"SHELL_APPLY\""))); mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001/steps/2/log")) .andExpect(status().isOk()) - .andExpect(content().string(org.hamcrest.Matchers.containsString("\"content\":\"stop ok\""))); + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"content\":\"apply ok\""))); } } diff --git a/src/test/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceTest.java index 913e6a8..f90b87a 100644 --- a/src/test/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceTest.java @@ -7,6 +7,7 @@ import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.modules.backup.config.ResourceBackupProperties; +import java.util.Arrays; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -23,16 +24,23 @@ class ResourceEnvelopeCryptoServiceTest { properties.setAuthenticitySignKeyIndex(8); AtomicReference capturedDek = new AtomicReference<>(); + AtomicReference decryptableDek = new AtomicReference<>(); + AtomicReference decryptResultDek = new AtomicReference<>(); Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class))) .thenAnswer(invocation -> { EccInternalEncryptRequest request = invocation.getArgument(0); capturedDek.set(request.getData()); + decryptableDek.set(Arrays.copyOf(request.getData(), request.getData().length)); return backup(new byte[]{0x11, 0x22, 0x33}); }); Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any(EccInternalSignRequest.class))) .thenReturn(backup(new byte[]{0x44, 0x55, 0x66})); Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class))) - .thenAnswer(invocation -> backup(capturedDek.get())); + .thenAnswer(invocation -> { + byte[] decryptedDek = Arrays.copyOf(decryptableDek.get(), decryptableDek.get().length); + decryptResultDek.set(decryptedDek); + return backup(decryptedDek); + }); ResourceEnvelopeCryptoServiceImpl service = new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, properties); byte[] payload = "payload-data".getBytes(java.nio.charset.StandardCharsets.UTF_8); @@ -60,6 +68,8 @@ class ResourceEnvelopeCryptoServiceTest { ArgumentCaptor decryptCaptor = ArgumentCaptor.forClass(EccInternalDecryptRequest.class); Mockito.verify(pcieCryptoService).sdfeInternalDecryptEcc(decryptCaptor.capture()); Assertions.assertEquals(7, decryptCaptor.getValue().getKeyIndex()); + Assertions.assertTrue(isAllZero(capturedDek.get()), "generated SM4 DEK should be wiped after sealing"); + Assertions.assertTrue(isAllZero(decryptResultDek.get()), "decrypted SM4 DEK result buffer should be wiped after unsealing"); } private static BackupDataResult backup(byte[] data) { @@ -68,4 +78,16 @@ class ResourceEnvelopeCryptoServiceTest { result.setLength(data.length); return result; } + + private static boolean isAllZero(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return false; + } + for (byte value : bytes) { + if (value != 0) { + return false; + } + } + return true; + } } diff --git a/src/test/java/com/cisd/tms/modules/backup/discovery/ResourceDiscoveryServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/discovery/ResourceDiscoveryServiceTest.java index a609df3..511e2f7 100644 --- a/src/test/java/com/cisd/tms/modules/backup/discovery/ResourceDiscoveryServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/discovery/ResourceDiscoveryServiceTest.java @@ -16,11 +16,18 @@ class ResourceDiscoveryServiceTest { List items = service.discover(sampleInitTask("ENTERPRISE", "RABBITMQ")); Assertions.assertTrue(containsCode(items, "TMS_CONFIG")); - Assertions.assertTrue(containsCode(items, "TMS_DB_DUMP")); - Assertions.assertTrue(containsCode(items, "CMEP_DB_DUMP")); + Assertions.assertTrue(containsCode(items, "TMS_DB")); + Assertions.assertTrue(containsCode(items, "CMEP_DB")); Assertions.assertTrue(containsCode(items, "CP_CONFIG")); Assertions.assertTrue(containsCode(items, "RECEIVER_LICENSE")); - Assertions.assertTrue(containsCode(items, "RABBITMQ_DEFINITIONS")); + Assertions.assertTrue(containsCode(items, "MQ_REPLAY")); + Assertions.assertFalse(containsCode(items, "RABBITMQ_DEFINITIONS")); + Assertions.assertFalse(containsCode(items, "TLQ_LICENSE")); + Assertions.assertFalse(containsCode(items, "CFMQ_CONFIG")); + Assertions.assertTrue(items.stream().allMatch(item -> + "FILE".equals(item.getResourceType()) + || "DATABASE".equals(item.getResourceType()) + || "MQ_REPLAY".equals(item.getResourceType()))); Assertions.assertFalse(containsCode(items, "NGINX_CONFIG")); Assertions.assertFalse(containsCode(items, "WEB_STATIC")); Assertions.assertFalse(containsCode(items, "ORGANIZATION_JSON")); @@ -35,10 +42,11 @@ class ResourceDiscoveryServiceTest { Assertions.assertTrue(hasPath(items, "CP_CONFIG", "/opt/cpconfig.cfg")); Assertions.assertTrue(hasPath(items, "RECEIVER_LICENSE", "/home/cmep4d/cmep/license/cmep.license")); - Assertions.assertTrue(hasPath(items, "TLQ_LICENSE", "/home/tlq/TLQ8/license.dat")); - Assertions.assertTrue(containsCode(items, "RABBITMQ_DEFINITIONS")); - Assertions.assertTrue(hasRequired(items, "TMS_DB_DUMP")); - Assertions.assertFalse(containsCode(items, "CMEP_DB_DUMP")); + Assertions.assertFalse(containsCode(items, "TLQ_LICENSE")); + Assertions.assertFalse(containsCode(items, "RABBITMQ_DEFINITIONS")); + Assertions.assertTrue(hasRequired(items, "TMS_DB")); + Assertions.assertTrue(hasRequired(items, "MQ_REPLAY")); + Assertions.assertFalse(containsCode(items, "CMEP_DB")); } private static InitTaskEntity sampleInitTask(String productType, String mqType) { diff --git a/src/test/java/com/cisd/tms/modules/backup/packagex/MqReplayContextBuilderTest.java b/src/test/java/com/cisd/tms/modules/backup/packagex/MqReplayContextBuilderTest.java new file mode 100644 index 0000000..747d284 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/backup/packagex/MqReplayContextBuilderTest.java @@ -0,0 +1,46 @@ +package com.cisd.tms.modules.backup.packagex; + +import com.cisd.tms.modules.init.entity.InitTaskEntity; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class MqReplayContextBuilderTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final MqReplayContextBuilder builder = new MqReplayContextBuilder(objectMapper); + + @Test + void shouldBuildMqReplayContextFromInitPlanJson() throws Exception { + InitTaskEntity task = new InitTaskEntity(); + task.setProductType("INDIRECT"); + task.setMqType("RABBITMQ_TLQ"); + task.setOrgCode("BKCHCNBJXXX"); + task.setInitPlanJson(""" + {"productType":"INDIRECT","orgCode":"BKCHCNBJXXX","mqType":"RABBITMQ_TLQ", + "request":{"orgCodeType":"BIC","orgCode":"BKCHCNBJXXX","orgNameCn":"中国银行", + "orgNameEn":"Bank of China","deployMode":"DUAL", + "nodes":{"node01Ip":"192.168.1.11","node02Ip":"192.168.1.12"}, + "mq":{"mqType":"RABBITMQ_TLQ","channelUsername":"tms","channelPassword":"Secret123!"}}} + """); + + byte[] json = builder.build(task); + JsonNode root = objectMapper.readTree(json); + + Assertions.assertEquals("INDIRECT", root.path("productType").asText()); + Assertions.assertEquals("RABBITMQ_TLQ", root.path("mqType").asText()); + Assertions.assertEquals("tms", root.path("mq").path("channelUsername").asText()); + Assertions.assertEquals("Secret123!", root.path("mq").path("channelPassword").asText()); + } + + @Test + void shouldRejectMissingInitPlanJson() { + InitTaskEntity task = new InitTaskEntity(); + task.setProductType("INDIRECT"); + + IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> builder.build(task)); + + Assertions.assertTrue(ex.getMessage().contains("initPlanJson is empty")); + } +} diff --git a/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceTest.java index 1b03364..cb12f55 100644 --- a/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceTest.java @@ -58,8 +58,9 @@ class ResourcePackageServiceTest { sampleInitTask(), List.of( resource("TMS_CONFIG", payloadFile.toString()), - databaseResource("TMS_DB_DUMP", "TMS"), - databaseResource("CMEP_DB_DUMP", "CMEP") + databaseResource("TMS_DB", "TMS", "db/TMS.sql"), + databaseResource("CMEP_DB", "CMEP", "db/CMEP.sql"), + mqReplayResource() ), sampleRequest() ); @@ -71,7 +72,7 @@ class ResourcePackageServiceTest { Assertions.assertFalse(result.getPackageHash().isBlank()); Assertions.assertFalse(result.getStorageEncKeyFingerprint().isBlank()); Assertions.assertFalse(result.getAuthenticityKeyFingerprint().isBlank()); - Assertions.assertTrue(result.getManifestSummaryJson().contains("\"resourceCount\":3")); + Assertions.assertTrue(result.getManifestSummaryJson().contains("\"resourceCount\":4")); try (ZipFile zipFile = new ZipFile(packagePath.toFile())) { Assertions.assertNotNull(zipFile.getEntry("manifest.json")); @@ -89,13 +90,18 @@ class ResourcePackageServiceTest { @SuppressWarnings("unchecked") java.util.List> resources = (java.util.List>) inspectResult.getPayload().get("resources"); - Assertions.assertEquals(3, resources.size()); + Assertions.assertEquals(4, resources.size()); Assertions.assertEquals(Boolean.TRUE, resources.get(0).get("collected")); Assertions.assertTrue(String.valueOf(resources.get(0).get("entryPath")).contains("application.yml")); - Assertions.assertEquals("TMS_DB_DUMP", resources.get(1).get("resourceCode")); + Assertions.assertEquals("TMS_DB", resources.get(1).get("resourceCode")); + Assertions.assertEquals("DATABASE", resources.get(1).get("kind")); Assertions.assertEquals("db/TMS.sql", resources.get(1).get("entryPath")); - Assertions.assertEquals("CMEP_DB_DUMP", resources.get(2).get("resourceCode")); + Assertions.assertEquals("CMEP_DB", resources.get(2).get("resourceCode")); + Assertions.assertEquals("DATABASE", resources.get(2).get("kind")); Assertions.assertEquals("db/CMEP.sql", resources.get(2).get("entryPath")); + Assertions.assertEquals("MQ_REPLAY", resources.get(3).get("resourceCode")); + Assertions.assertEquals("MQ_REPLAY", resources.get(3).get("kind")); + Assertions.assertEquals("mq/mq-restore-context.json", resources.get(3).get("entryPath")); try (ZipInputStream zipInputStream = new ZipInputStream(new java.io.ByteArrayInputStream(inspectResult.getPayloadArchiveBytes()))) { java.util.Map entries = new java.util.LinkedHashMap<>(); @@ -109,6 +115,8 @@ class ResourcePackageServiceTest { Assertions.assertTrue(entries.get("db/TMS.sql").contains("INSERT INTO t_demo")); Assertions.assertTrue(entries.containsKey("db/CMEP.sql")); Assertions.assertTrue(entries.get("db/CMEP.sql").contains("INSERT INTO c_demo")); + Assertions.assertTrue(entries.containsKey("mq/mq-restore-context.json")); + Assertions.assertTrue(entries.get("mq/mq-restore-context.json").contains("\"channelUsername\":\"tms\"")); } } @@ -155,6 +163,13 @@ class ResourcePackageServiceTest { entity.setMqType("RABBITMQ"); entity.setOrgCode("AAAABBBBXXX"); entity.setDeployMode("SINGLE"); + entity.setInitPlanJson(""" + {"productType":"ENTERPRISE","orgCode":"AAAABBBBXXX","mqType":"RABBITMQ", + "request":{"orgCodeType":"BIC","orgCode":"AAAABBBBXXX","orgNameCn":"测试机构", + "orgNameEn":"Test Bank","deployMode":"SINGLE", + "nodes":{"node01Ip":"127.0.0.1"}, + "mq":{"mqType":"RABBITMQ","channelUsername":"tms","channelPassword":"Secret123!"}}} + """); return entity; } @@ -167,11 +182,22 @@ class ResourcePackageServiceTest { return item; } - private static DiscoveredResourceItem databaseResource(String code, String path) { + private static DiscoveredResourceItem databaseResource(String code, String databaseName, String entryPath) { DiscoveredResourceItem item = new DiscoveredResourceItem(); - item.setResourceType("DB"); + item.setResourceType("DATABASE"); item.setResourceCode(code); - item.setResolvedPath(path); + item.setResolvedPath(databaseName); + item.setDatabaseName(databaseName); + item.setEntryPath(entryPath); + item.setRequired(true); + return item; + } + + private static DiscoveredResourceItem mqReplayResource() { + DiscoveredResourceItem item = new DiscoveredResourceItem(); + item.setResourceType("MQ_REPLAY"); + item.setResourceCode("MQ_REPLAY"); + item.setEntryPath("mq/mq-restore-context.json"); item.setRequired(true); return item; } diff --git a/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractorTest.java b/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractorTest.java new file mode 100644 index 0000000..e8aeaa8 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePayloadExtractorTest.java @@ -0,0 +1,124 @@ +package com.cisd.tms.modules.backup.packagex; + +import com.cisd.tms.modules.backup.config.ResourceBackupProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +class ResourcePayloadExtractorTest { + + @TempDir + Path tempDir; + + @Test + void shouldExtractPayloadAndWriteRestoreContext() throws Exception { + ResourcePackageService packageService = Mockito.mock(ResourcePackageService.class); + ResourcePackageInspectResult inspectResult = new ResourcePackageInspectResult(); + inspectResult.setPayloadArchiveBytes(payloadArchive( + """ + {"schemaVersion":"2.0","resources":[ + {"code":"TMS_CONFIG","kind":"FILE","restorePath":"%s/config", + "files":[{"entryPath":"files/TMS_CONFIG/application.yml","restorePath":"%s/config/application.yml"}]}, + {"code":"TMS_DB","kind":"DATABASE","entryPath":"db/TMS.sql"}, + {"code":"MQ_REPLAY","kind":"MQ_REPLAY","entryPath":"mq/mq-restore-context.json"} + ]} + """.formatted(tempDir, tempDir), + "files/TMS_CONFIG/application.yml", + "server.port=8080" + )); + Mockito.when(packageService.inspectPackage(Mockito.any(Path.class))).thenReturn(inspectResult); + + ResourceBackupProperties properties = new ResourceBackupProperties(); + properties.setAllowedRestoreRoots(List.of(tempDir.toString())); + ResourcePayloadExtractor extractor = new ResourcePayloadExtractor(packageService, new ObjectMapper(), properties); + Path workDir = tempDir.resolve("restore"); + + ResourcePayloadExtractionResult result = extractor.extract(tempDir.resolve("backup.tmsbak"), workDir); + + Assertions.assertEquals(workDir.resolve("payload"), result.getPayloadDir()); + Assertions.assertTrue(Files.exists(result.getPayloadDir().resolve("payload-manifest.json"))); + Assertions.assertTrue(Files.exists(result.getPayloadDir().resolve("files/TMS_CONFIG/application.yml"))); + Assertions.assertTrue(Files.exists(workDir.resolve("restore-context.json"))); + Assertions.assertEquals( + "files/TMS_CONFIG/application.yml\t" + tempDir.resolve("config/application.yml") + System.lineSeparator(), + Files.readString(workDir.resolve("restore-files.tsv")) + ); + Assertions.assertEquals( + "db/TMS.sql\tTMS" + System.lineSeparator(), + Files.readString(workDir.resolve("restore-databases.tsv")) + ); + Assertions.assertEquals( + "mq/mq-restore-context.json" + System.lineSeparator(), + Files.readString(workDir.resolve("restore-mq.tsv")) + ); + } + + @Test + void shouldRejectPayloadEntryEscapingTargetDirectory() throws Exception { + ResourcePackageService packageService = Mockito.mock(ResourcePackageService.class); + ResourcePackageInspectResult inspectResult = new ResourcePackageInspectResult(); + inspectResult.setPayloadArchiveBytes(payloadArchive("{}", "../escaped.txt", "bad")); + Mockito.when(packageService.inspectPackage(Mockito.any(Path.class))).thenReturn(inspectResult); + + ResourceBackupProperties properties = new ResourceBackupProperties(); + properties.setAllowedRestoreRoots(List.of(tempDir.toString())); + ResourcePayloadExtractor extractor = new ResourcePayloadExtractor(packageService, new ObjectMapper(), properties); + + IllegalStateException ex = Assertions.assertThrows( + IllegalStateException.class, + () -> extractor.extract(tempDir.resolve("backup.tmsbak"), tempDir.resolve("restore")) + ); + + Assertions.assertTrue(ex.getMessage().contains("escapes payload dir")); + } + + @Test + void shouldRejectFileRestorePathOutsideAllowedRoots() throws Exception { + ResourcePackageService packageService = Mockito.mock(ResourcePackageService.class); + ResourcePackageInspectResult inspectResult = new ResourcePackageInspectResult(); + inspectResult.setPayloadArchiveBytes(payloadArchive( + """ + {"schemaVersion":"2.0","resources":[ + {"code":"TMS_CONFIG","kind":"FILE","restorePath":"/etc", + "files":[{"entryPath":"files/TMS_CONFIG/application.yml","restorePath":"/etc/application.yml"}]} + ]} + """, + "files/TMS_CONFIG/application.yml", + "server.port=8080" + )); + Mockito.when(packageService.inspectPackage(Mockito.any(Path.class))).thenReturn(inspectResult); + + ResourceBackupProperties properties = new ResourceBackupProperties(); + properties.setAllowedRestoreRoots(List.of(tempDir.toString())); + ResourcePayloadExtractor extractor = new ResourcePayloadExtractor(packageService, new ObjectMapper(), properties); + + IllegalStateException ex = Assertions.assertThrows( + IllegalStateException.class, + () -> extractor.extract(tempDir.resolve("backup.tmsbak"), tempDir.resolve("restore")) + ); + + Assertions.assertTrue(ex.getMessage().contains("restore path is not allowed")); + } + + private static byte[] payloadArchive(String manifestJson, String extraEntry, String extraContent) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { + zipOutputStream.putNextEntry(new ZipEntry("payload-manifest.json")); + zipOutputStream.write(manifestJson.getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + zipOutputStream.putNextEntry(new ZipEntry(extraEntry)); + zipOutputStream.write(extraContent.getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + } + return outputStream.toByteArray(); + } +} diff --git a/src/test/java/com/cisd/tms/modules/backup/repository/ResourceTaskRepositoryTest.java b/src/test/java/com/cisd/tms/modules/backup/repository/ResourceTaskRepositoryTest.java index da14e8e..bfca9cb 100644 --- a/src/test/java/com/cisd/tms/modules/backup/repository/ResourceTaskRepositoryTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/repository/ResourceTaskRepositoryTest.java @@ -57,8 +57,8 @@ class ResourceTaskRepositoryTest { entity.setBackupId("RBACKUP-001"); entity.setStatus("READY"); entity.setPackageFileId("FILE-001"); - entity.setStagingDir("/home/tms/tmp/resource-restore/RRST-001/staging"); - entity.setRunnerStatePath("/home/tms/tmp/resource-restore/RRST-001/restore-state.json"); + entity.setStagingDir("/home/tms/tmp/resource-restore/RRST-001/payload"); + entity.setRunnerStatePath("/home/tms/tmp/resource-restore/RRST-001/status.json"); entity.setSourceProductType("DIRECT"); entity.setTargetProductType("DIRECT"); entity.setConsistencyReportJson("{\"ok\":true}"); @@ -74,8 +74,8 @@ class ResourceTaskRepositoryTest { Assertions.assertEquals("RRST-001", found.getTaskId()); Assertions.assertEquals("RPRE-001", found.getPrecheckId()); Assertions.assertEquals("FILE-001", found.getPackageFileId()); - Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/staging", found.getStagingDir()); - Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/restore-state.json", found.getRunnerStatePath()); + Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/payload", found.getStagingDir()); + Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/status.json", found.getRunnerStatePath()); Assertions.assertEquals("DIRECT", found.getSourceProductType()); Assertions.assertEquals("DIRECT", found.getTargetProductType()); Assertions.assertEquals("{\"ok\":true}", found.getConsistencyReportJson()); @@ -90,9 +90,9 @@ class ResourceTaskRepositoryTest { entity.setTaskId("RRST-001"); entity.setTaskType("RESOURCE_RESTORE"); entity.setStepNo(1); - entity.setStepCode("STOP_SERVICES"); + entity.setStepCode("SHELL_APPLY"); entity.setStatus("SUCCESS"); - entity.setLogPath("/home/tms/tmp/resource-restore/RRST-001/steps/01-STOP_SERVICES.log"); + entity.setLogPath("/home/tms/tmp/resource-restore/RRST-001/restore.log"); entity.setExitCode(0); repository.save(entity); @@ -104,8 +104,8 @@ class ResourceTaskRepositoryTest { Assertions.assertEquals(1, steps.size()); Assertions.assertEquals("RESOURCE_RESTORE", steps.get(0).getTaskType()); - Assertions.assertEquals("STOP_SERVICES", steps.get(0).getStepCode()); - Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/steps/01-STOP_SERVICES.log", steps.get(0).getLogPath()); + Assertions.assertEquals("SHELL_APPLY", steps.get(0).getStepCode()); + Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/restore.log", steps.get(0).getLogPath()); Assertions.assertEquals(0, steps.get(0).getExitCode()); } diff --git a/src/test/java/com/cisd/tms/modules/backup/repository/ResourceTaskSchemaContractTest.java b/src/test/java/com/cisd/tms/modules/backup/repository/ResourceTaskSchemaContractTest.java index c7bb674..da18b2d 100644 --- a/src/test/java/com/cisd/tms/modules/backup/repository/ResourceTaskSchemaContractTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/repository/ResourceTaskSchemaContractTest.java @@ -62,8 +62,7 @@ class ResourceTaskSchemaContractTest { "PRECHECKING", "PRECHECK_FAILED", "READY", - "HANDOFF", - "RUNNING", + "APPLYING", "VERIFYING", "SUCCESS", "FAILED", @@ -77,9 +76,8 @@ class ResourceTaskSchemaContractTest { assertEnumConstants( "com.cisd.tms.modules.backup.enums.ResourceControlPhase", "TMS_PRECHECK", - "HANDOFF_READY", - "RUNNER_OWNED", - "RECONCILE_PENDING", + "JAVA_EXTRACT", + "SHELL_APPLY", "DONE" ); assertEnumConstants( diff --git a/src/test/java/com/cisd/tms/modules/backup/restore/ApplyResourceBackupScriptContractTest.java b/src/test/java/com/cisd/tms/modules/backup/restore/ApplyResourceBackupScriptContractTest.java new file mode 100644 index 0000000..1f18313 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/backup/restore/ApplyResourceBackupScriptContractTest.java @@ -0,0 +1,154 @@ +package com.cisd.tms.modules.backup.restore; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class ApplyResourceBackupScriptContractTest { + + @TempDir + Path tempDir; + + @Test + void scriptShouldContainRequiredPhasesAndNoSecretsEcho() throws Exception { + String script = Files.readString(Path.of("scripts/resource-restore/apply-resource-backup.sh")); + + Assertions.assertTrue(script.contains("FILE")); + Assertions.assertTrue(script.contains("DATABASE")); + Assertions.assertTrue(script.contains("MQ_REPLAY")); + Assertions.assertTrue(script.contains("HEALTH_CHECK")); + Assertions.assertFalse(script.contains("channelPassword")); + } + + @Test + void shouldRestoreFilesFromGeneratedPlan() throws Exception { + Path workDir = tempDir.resolve("work"); + Path payloadDir = workDir.resolve("payload"); + Path sourceFile = payloadDir.resolve("resources/TMS_CONFIG/application.yml"); + Path targetFile = tempDir.resolve("target/application.yml"); + Files.createDirectories(sourceFile.getParent()); + Files.writeString(payloadDir.resolve("payload-manifest.json"), "{\"resources\":[]}"); + Files.writeString(sourceFile, "server.port=8080"); + Files.writeString(workDir.resolve("restore-files.tsv"), + "resources/TMS_CONFIG/application.yml\t" + targetFile + System.lineSeparator()); + + runScript(workDir, Map.of()); + + Assertions.assertEquals("server.port=8080", Files.readString(targetFile)); + Assertions.assertTrue(Files.readString(workDir.resolve("status.json")).contains("\"status\":\"SUCCESS\"")); + } + + @Test + void shouldRunDatabaseAndMqReplayScriptsFromGeneratedPlans() throws Exception { + Path workDir = tempDir.resolve("work"); + Path payloadDir = workDir.resolve("payload"); + Files.createDirectories(payloadDir.resolve("db")); + Files.createDirectories(payloadDir.resolve("mq")); + Files.writeString(payloadDir.resolve("payload-manifest.json"), "{\"resources\":[]}"); + Files.writeString(payloadDir.resolve("db/TMS.sql"), "select 1;"); + Files.writeString(payloadDir.resolve("mq/mq-restore-context.json"), "{}"); + Files.writeString(workDir.resolve("restore-databases.tsv"), "db/TMS.sql\tTMS" + System.lineSeparator()); + Files.writeString(workDir.resolve("restore-mq.tsv"), "mq/mq-restore-context.json" + System.lineSeparator()); + + Path commandLog = tempDir.resolve("commands.log"); + Path dbScript = fakeScript("db-restore.sh", commandLog); + Path mqScript = fakeScript("mq-replay.sh", commandLog); + + runScript(workDir, Map.of( + "DB_RESTORE_SCRIPT_PATH", dbScript.toString(), + "MQ_REPLAY_SCRIPT_PATH", mqScript.toString(), + "MYSQL_PATH", "mysql-test", + "DB_HOST", "127.0.0.1", + "DB_PORT", "4000", + "DB_USER", "root", + "DB_PASSWORD", "secret" + )); + + String log = Files.readString(commandLog); + Assertions.assertTrue(log.contains("--db-name TMS")); + Assertions.assertTrue(log.contains("--sql-file " + payloadDir.resolve("db/TMS.sql"))); + Assertions.assertTrue(log.contains("--context-file " + payloadDir.resolve("mq/mq-restore-context.json"))); + } + + @Test + void shouldLoadRestoreEnvFileFromWorkDir() throws Exception { + Path workDir = tempDir.resolve("work"); + Path payloadDir = workDir.resolve("payload"); + Files.createDirectories(payloadDir.resolve("db")); + Files.writeString(payloadDir.resolve("payload-manifest.json"), "{\"resources\":[]}"); + Files.writeString(payloadDir.resolve("db/TMS.sql"), "select 1;"); + Files.writeString(workDir.resolve("restore-databases.tsv"), "db/TMS.sql\tTMS" + System.lineSeparator()); + + Path commandLog = tempDir.resolve("commands.log"); + Path dbScript = fakeScript("db-restore.sh", commandLog); + Files.writeString(workDir.resolve("restore-env.sh"), """ + export DB_RESTORE_SCRIPT_PATH='%s' + export MYSQL_PATH='mysql-from-env' + export DB_HOST='10.0.0.1' + export DB_PORT='4000' + export DB_USER='root' + export DB_PASSWORD='secret' + """.formatted(dbScript)); + + runScript(workDir, Map.of()); + + String log = Files.readString(commandLog); + Assertions.assertTrue(log.contains("--mysql-path mysql-from-env")); + Assertions.assertTrue(log.contains("--host 10.0.0.1")); + } + + @Test + void shouldRunHealthCheckWhenConfigured() throws Exception { + Path workDir = tempDir.resolve("work"); + Path payloadDir = workDir.resolve("payload"); + Files.createDirectories(payloadDir); + Files.writeString(payloadDir.resolve("payload-manifest.json"), "{\"resources\":[]}"); + Path commandLog = tempDir.resolve("curl.log"); + Path binDir = tempDir.resolve("bin"); + Files.createDirectories(binDir); + Path curl = binDir.resolve("curl"); + Files.writeString(curl, """ + #!/usr/bin/env bash + printf '%%s\\n' "$*" >> "%s" + printf '{"status":"UP"}' + """.formatted(commandLog)); + curl.toFile().setExecutable(true); + + runScript(workDir, Map.of( + "HEALTH_CHECK_URL", "http://127.0.0.1:8080/actuator/health", + "HEALTH_CHECK_TIMEOUT_SECONDS", "3", + "PATH", binDir + ":" + System.getenv("PATH") + )); + + String log = Files.readString(commandLog); + Assertions.assertTrue(log.contains("--max-time 3")); + Assertions.assertTrue(log.contains("http://127.0.0.1:8080/actuator/health")); + } + + private static void runScript(Path workDir, Map environment) throws Exception { + ProcessBuilder builder = new ProcessBuilder( + "bash", + Path.of("scripts/resource-restore/apply-resource-backup.sh").toAbsolutePath().normalize().toString(), + "--work-dir", + workDir.toString() + ); + builder.directory(Path.of(".").toAbsolutePath().normalize().toFile()); + builder.environment().putAll(environment); + Process process = builder.start(); + int exitCode = process.waitFor(); + Assertions.assertEquals(0, exitCode, new String(process.getErrorStream().readAllBytes())); + } + + private Path fakeScript(String name, Path commandLog) throws Exception { + Path script = tempDir.resolve(name); + Files.writeString(script, """ + #!/usr/bin/env bash + printf '%%s %%s\\n' "$0" "$*" >> "%s" + """.formatted(commandLog)); + script.toFile().setExecutable(true); + return script; + } +} diff --git a/src/test/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncherTest.java b/src/test/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncherTest.java new file mode 100644 index 0000000..32d3d9a --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncherTest.java @@ -0,0 +1,67 @@ +package com.cisd.tms.modules.backup.restore; + +import com.cisd.tms.modules.backup.config.ResourceBackupProperties; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class LightweightRestoreApplierLauncherTest { + + @TempDir + Path tempDir; + + @Test + void shouldLaunchApplyScriptWithWorkDir() throws Exception { + ResourceBackupProperties properties = new ResourceBackupProperties(); + properties.setRestoreApplyScriptPath("/home/tms/bin/resource-restore/apply-resource-backup.sh"); + properties.setRestoreDbScriptPath("/home/tms/bin/resource-restore/restore-db.sh"); + properties.setMqReplayScriptPath("/home/tms/bin/resource-restore/replay-mq.sh"); + RecordingInvoker invoker = new RecordingInvoker(); + LightweightRestoreApplierLauncher launcher = new LightweightRestoreApplierLauncher( + properties, + invoker, + "jdbc:mysql://db-host:4000/tms?useUnicode=true", + "root", + "p'ass" + ); + + Path workDir = tempDir.resolve("RRST-001"); + launcher.launch(workDir); + + Assertions.assertEquals(List.of( + "/bin/bash", + "/home/tms/bin/resource-restore/apply-resource-backup.sh", + "--work-dir", + workDir.toAbsolutePath().normalize().toString() + ), invoker.command); + Assertions.assertEquals(workDir.toAbsolutePath().normalize().resolve("restore-launch.log"), invoker.logFile); + Assertions.assertEquals(workDir.toAbsolutePath().normalize(), invoker.workingDirectory); + + String env = Files.readString(workDir.toAbsolutePath().normalize().resolve("restore-env.sh")); + Assertions.assertTrue(env.contains("export DB_RESTORE_SCRIPT_PATH='/home/tms/bin/resource-restore/restore-db.sh'")); + Assertions.assertTrue(env.contains("export MQ_REPLAY_SCRIPT_PATH='/home/tms/bin/resource-restore/replay-mq.sh'")); + Assertions.assertTrue(env.contains("export MYSQL_PATH='mysql'")); + Assertions.assertTrue(env.contains("export DB_HOST='db-host'")); + Assertions.assertTrue(env.contains("export DB_PORT='4000'")); + Assertions.assertTrue(env.contains("export DB_USER='root'")); + Assertions.assertTrue(env.contains("export DB_PASSWORD='p'\\''ass'")); + } + + private static final class RecordingInvoker implements RestoreCommandInvoker { + + private List command; + private Path logFile; + private Path workingDirectory; + + @Override + public int run(List command, Path logFile, Path workingDirectory) { + this.command = command; + this.logFile = logFile; + this.workingDirectory = workingDirectory; + return 0; + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/backup/restore/MqReplayScriptContractTest.java b/src/test/java/com/cisd/tms/modules/backup/restore/MqReplayScriptContractTest.java new file mode 100644 index 0000000..2726bca --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/backup/restore/MqReplayScriptContractTest.java @@ -0,0 +1,74 @@ +package com.cisd.tms.modules.backup.restore; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class MqReplayScriptContractTest { + + @TempDir + Path tempDir; + + @Test + void shouldReplayRabbitMqUserAndPermissionsFromContext() throws Exception { + Path context = tempDir.resolve("mq-restore-context.json"); + Files.writeString(context, """ + {"schemaVersion":"1.0","productType":"INDIRECT","mqType":"RABBITMQ_TLQ","orgCode":"BKCHCNBJXXX", + "mq":{"channelUsername":"tms","channelPassword":"Secret123!"}} + """); + Path commandLog = tempDir.resolve("commands.log"); + Path rabbitmqctl = fakeRabbitmqctl(commandLog); + + ProcessBuilder builder = new ProcessBuilder( + "bash", + Path.of("scripts/resource-restore/replay-mq.sh").toAbsolutePath().normalize().toString(), + "--context-file", + context.toString(), + "--work-dir", + tempDir.resolve("work").toString() + ); + builder.environment().put("RABBITMQ_CTL_COMMAND", rabbitmqctl.toString()); + Process process = builder.start(); + + Assertions.assertEquals(0, process.waitFor(), new String(process.getErrorStream().readAllBytes())); + String log = Files.readString(commandLog); + Assertions.assertTrue(log.contains("add_vhost /RQ")); + Assertions.assertTrue(log.contains("add_user tms Secret123!")); + Assertions.assertTrue(log.contains("set_permissions -p /RQ tms .* .* .*")); + } + + @Test + void shouldDelegateToCustomReplayCommandWhenConfigured() throws Exception { + Path context = tempDir.resolve("mq-restore-context.json"); + Files.writeString(context, "{}"); + Path commandLog = tempDir.resolve("commands.log"); + Path custom = fakeRabbitmqctl(commandLog); + + ProcessBuilder builder = new ProcessBuilder( + "bash", + Path.of("scripts/resource-restore/replay-mq.sh").toAbsolutePath().normalize().toString(), + "--context-file", + context.toString(), + "--work-dir", + tempDir.resolve("work").toString() + ); + builder.environment().put("MQ_REPLAY_COMMAND", custom.toString()); + Process process = builder.start(); + + Assertions.assertEquals(0, process.waitFor(), new String(process.getErrorStream().readAllBytes())); + Assertions.assertTrue(Files.readString(commandLog).contains("--context-file " + context)); + } + + private Path fakeRabbitmqctl(Path commandLog) throws Exception { + Path script = tempDir.resolve("rabbitmqctl"); + Files.writeString(script, """ + #!/usr/bin/env bash + printf '%%s\\n' "$*" >> "%s" + exit 0 + """.formatted(commandLog)); + script.toFile().setExecutable(true); + return script; + } +} diff --git a/src/test/java/com/cisd/tms/modules/backup/runner/RunnerCommandInvokerTest.java b/src/test/java/com/cisd/tms/modules/backup/restore/RestoreCommandInvokerTest.java similarity index 54% rename from src/test/java/com/cisd/tms/modules/backup/runner/RunnerCommandInvokerTest.java rename to src/test/java/com/cisd/tms/modules/backup/restore/RestoreCommandInvokerTest.java index a79984d..ca630d1 100644 --- a/src/test/java/com/cisd/tms/modules/backup/runner/RunnerCommandInvokerTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/restore/RestoreCommandInvokerTest.java @@ -1,4 +1,4 @@ -package com.cisd.tms.modules.backup.runner; +package com.cisd.tms.modules.backup.restore; import java.nio.file.Files; import java.nio.file.Path; @@ -7,23 +7,23 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -class RunnerCommandInvokerTest { +class RestoreCommandInvokerTest { @TempDir Path tempDir; @Test void shouldExecuteCommandAndAppendLog() throws Exception { - ProcessBuilderRunnerCommandInvoker invoker = new ProcessBuilderRunnerCommandInvoker(); - Path logFile = tempDir.resolve("steps/01-STOP_SERVICES.log"); + ProcessBuilderRestoreCommandInvoker invoker = new ProcessBuilderRestoreCommandInvoker(); + Path logFile = tempDir.resolve("restore.log"); int exitCode = invoker.run( - List.of("/bin/zsh", "-lc", "printf 'hello-runner'"), + List.of("/bin/zsh", "-lc", "printf 'hello-restore'"), logFile, tempDir ); Assertions.assertEquals(0, exitCode); - Assertions.assertEquals("hello-runner", Files.readString(logFile)); + Assertions.assertEquals("hello-restore", Files.readString(logFile)); } } diff --git a/src/test/java/com/cisd/tms/modules/backup/restore/RestoreStatusFileReaderTest.java b/src/test/java/com/cisd/tms/modules/backup/restore/RestoreStatusFileReaderTest.java new file mode 100644 index 0000000..5a44711 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/backup/restore/RestoreStatusFileReaderTest.java @@ -0,0 +1,33 @@ +package com.cisd.tms.modules.backup.restore; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class RestoreStatusFileReaderTest { + + @TempDir + Path tempDir; + + @Test + void shouldReadStatusJsonAndLog() throws Exception { + Path workDir = tempDir.resolve("RRST-001"); + Files.createDirectories(workDir); + Files.writeString(workDir.resolve("status.json"), + "{\"status\":\"RUNNING\",\"phase\":\"MQ_REPLAY\",\"message\":\"replaying mq\",\"updatedAt\":\"2026-04-29T10:00:00+08:00\"}"); + Files.writeString(workDir.resolve("restore.log"), "line1\nline2\n"); + + RestoreStatusFileReader reader = new RestoreStatusFileReader(new ObjectMapper()); + + RestoreStatusFileReader.RestoreStatusSnapshot snapshot = reader.read(workDir); + + Assertions.assertEquals("RUNNING", snapshot.status()); + Assertions.assertEquals("MQ_REPLAY", snapshot.phase()); + Assertions.assertEquals("replaying mq", snapshot.message()); + Assertions.assertEquals("2026-04-29T10:00:00+08:00", snapshot.updatedAt()); + Assertions.assertTrue(snapshot.log().contains("line1")); + } +} diff --git a/src/test/java/com/cisd/tms/modules/backup/runner/ConsistencyVerifyRunnerStepExecutorTest.java b/src/test/java/com/cisd/tms/modules/backup/runner/ConsistencyVerifyRunnerStepExecutorTest.java deleted file mode 100644 index f6d38e5..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/runner/ConsistencyVerifyRunnerStepExecutorTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -class ConsistencyVerifyRunnerStepExecutorTest { - - @TempDir - Path tempDir; - - @Test - void shouldVerifyRequiredStepOutputsAndHealthEndpoint() throws Exception { - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setHealthCheckUrl("http://127.0.0.1:8080/actuator/health"); - HttpClient httpClient = Mockito.mock(HttpClient.class); - @SuppressWarnings("unchecked") - HttpResponse response = Mockito.mock(HttpResponse.class); - Mockito.when(response.statusCode()).thenReturn(200); - Mockito.when(response.body()).thenReturn("{\"status\":\"UP\"}"); - Mockito.when(httpClient.send(Mockito.any(HttpRequest.class), Mockito.>any())) - .thenReturn(response); - ConsistencyVerifyRunnerStepExecutor executor = new ConsistencyVerifyRunnerStepExecutor( - properties, - new ObjectMapper(), - httpClient - ); - - Path taskDir = tempDir.resolve("task"); - Files.createDirectories(taskDir.resolve("step-output")); - Files.writeString(taskDir.resolve("step-output/02-RESTORE_FILES.json"), "{\"result\":\"SUCCESS\"}"); - Files.writeString(taskDir.resolve("step-output/03-RESTORE_DATABASES.json"), "{\"result\":\"SUCCESS\"}"); - Files.writeString(taskDir.resolve("step-output/04-START_SERVICES.json"), "{\"result\":\"SUCCESS\"}"); - - RunnerStepExecutionResult result = executor.execute(new RunnerStepContext( - "RRST-TASK-20260418-000004", - "CONSISTENCY_VERIFY", - 5, - taskDir, - taskDir.resolve("handoff.json"), - taskDir.resolve("restore-state.json") - )); - - Assertions.assertEquals("SUCCESS", result.getResult()); - Assertions.assertTrue(result.getSummary().contains("health endpoint is UP")); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - Mockito.verify(httpClient).send(requestCaptor.capture(), Mockito.>any()); - Assertions.assertEquals("http://127.0.0.1:8080/actuator/health", requestCaptor.getValue().uri().toString()); - } - - @Test - void shouldFailWhenRequiredStepOutputIsMissing() { - ResourceBackupProperties properties = new ResourceBackupProperties(); - ConsistencyVerifyRunnerStepExecutor executor = new ConsistencyVerifyRunnerStepExecutor( - properties, - new ObjectMapper(), - Mockito.mock(HttpClient.class) - ); - Path taskDir = tempDir.resolve("task-missing"); - - IllegalStateException exception = Assertions.assertThrows( - IllegalStateException.class, - () -> executor.execute(new RunnerStepContext( - "RRST-TASK-20260418-000005", - "CONSISTENCY_VERIFY", - 5, - taskDir, - taskDir.resolve("handoff.json"), - taskDir.resolve("restore-state.json") - )) - ); - - Assertions.assertTrue(exception.getMessage().contains("step output")); - } - - @Test - void shouldFailWhenHealthEndpointIsNotUp() throws Exception { - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setHealthCheckUrl("http://127.0.0.1:8080/actuator/health"); - HttpClient httpClient = Mockito.mock(HttpClient.class); - @SuppressWarnings("unchecked") - HttpResponse response = Mockito.mock(HttpResponse.class); - Mockito.when(response.statusCode()).thenReturn(503); - Mockito.when(response.body()).thenReturn("{\"status\":\"DOWN\"}"); - Mockito.when(httpClient.send(Mockito.any(HttpRequest.class), Mockito.>any())) - .thenReturn(response); - ConsistencyVerifyRunnerStepExecutor executor = new ConsistencyVerifyRunnerStepExecutor( - properties, - new ObjectMapper(), - httpClient - ); - - Path taskDir = tempDir.resolve("task-down"); - Files.createDirectories(taskDir.resolve("step-output")); - Files.writeString(taskDir.resolve("step-output/02-RESTORE_FILES.json"), "{\"result\":\"SUCCESS\"}"); - Files.writeString(taskDir.resolve("step-output/03-RESTORE_DATABASES.json"), "{\"result\":\"SUCCESS\"}"); - Files.writeString(taskDir.resolve("step-output/04-START_SERVICES.json"), "{\"result\":\"SUCCESS\"}"); - - IllegalStateException exception = Assertions.assertThrows( - IllegalStateException.class, - () -> executor.execute(new RunnerStepContext( - "RRST-TASK-20260418-000006", - "CONSISTENCY_VERIFY", - 5, - taskDir, - taskDir.resolve("handoff.json"), - taskDir.resolve("restore-state.json") - )) - ); - - Assertions.assertTrue(exception.getMessage().contains("failed to verify health endpoint")); - } -} diff --git a/src/test/java/com/cisd/tms/modules/backup/runner/DatabaseRestoreRunnerStepExecutorTest.java b/src/test/java/com/cisd/tms/modules/backup/runner/DatabaseRestoreRunnerStepExecutorTest.java deleted file mode 100644 index fbdcfdb..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/runner/DatabaseRestoreRunnerStepExecutorTest.java +++ /dev/null @@ -1,196 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -class DatabaseRestoreRunnerStepExecutorTest { - - @TempDir - Path tempDir; - - @Test - void shouldExecuteRestoreScriptForTmsAndCmepSql() throws Exception { - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setRestoreDbScriptPath("/tmp/restore-db.sh"); - properties.setMysqlPath("/usr/bin/mysql"); - properties.setTmsDatabaseName("TMS"); - properties.setCmepDatabaseName("CMEP"); - RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class); - Mockito.when(invoker.run(Mockito.anyList(), Mockito.any(), Mockito.any())).thenReturn(0); - - DatabaseRestoreRunnerStepExecutor executor = new DatabaseRestoreRunnerStepExecutor( - properties, - invoker, - "jdbc:mysql://127.0.0.1:4000/TMS?useSSL=false", - "root", - "secret" - ); - Path sqlFile = tempDir.resolve("task/staging/payload/db/TMS.sql"); - Path cmepSqlFile = tempDir.resolve("task/staging/payload/db/CMEP.sql"); - Files.createDirectories(sqlFile.getParent()); - Files.writeString(sqlFile, "CREATE TABLE t_demo(id bigint);"); - Files.writeString(cmepSqlFile, "CREATE TABLE c_demo(id bigint);"); - - RunnerStepExecutionResult result = executor.execute(new RunnerStepContext( - "RRST-TASK-20260418-000001", - "RESTORE_DATABASES", - 4, - tempDir.resolve("task"), - tempDir.resolve("task/handoff.json"), - tempDir.resolve("task/restore-state.json") - )); - - Assertions.assertEquals("SUCCESS", result.getResult()); - Assertions.assertTrue(result.getSummary().contains("TMS")); - Assertions.assertTrue(result.getSummary().contains("CMEP")); - - @SuppressWarnings("unchecked") - ArgumentCaptor> commandCaptor = ArgumentCaptor.forClass(List.class); - Mockito.verify(invoker, Mockito.times(2)).run(commandCaptor.capture(), Mockito.any(), Mockito.eq(tempDir.resolve("task"))); - List> commands = commandCaptor.getAllValues(); - Assertions.assertEquals( - List.of( - "/bin/bash", - "/tmp/restore-db.sh", - "--mysql-path", - "/usr/bin/mysql", - "--host", - "127.0.0.1", - "--port", - "4000", - "--username", - "root", - "--password", - "secret", - "--db-name", - "TMS", - "--sql-file", - sqlFile.toString() - ), - commands.get(0) - ); - Assertions.assertEquals( - List.of( - "/bin/bash", - "/tmp/restore-db.sh", - "--mysql-path", - "/usr/bin/mysql", - "--host", - "127.0.0.1", - "--port", - "4000", - "--username", - "root", - "--password", - "secret", - "--db-name", - "CMEP", - "--sql-file", - cmepSqlFile.toString() - ), - commands.get(1) - ); - } - - @Test - void shouldFailWhenSqlFileIsMissing() { - ResourceBackupProperties properties = new ResourceBackupProperties(); - RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class); - DatabaseRestoreRunnerStepExecutor executor = new DatabaseRestoreRunnerStepExecutor( - properties, - invoker, - "jdbc:mysql://127.0.0.1:4000/TMS", - "root", - "secret" - ); - - IllegalStateException exception = Assertions.assertThrows( - IllegalStateException.class, - () -> executor.execute(new RunnerStepContext( - "RRST-TASK-20260418-000002", - "RESTORE_DATABASES", - 4, - tempDir.resolve("task"), - tempDir.resolve("task/handoff.json"), - tempDir.resolve("task/restore-state.json") - )) - ); - - Assertions.assertTrue(exception.getMessage().contains("sql file missing")); - Mockito.verifyNoInteractions(invoker); - } - - @Test - void shouldSkipCmepWhenSqlFileIsAbsent() throws Exception { - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setRestoreDbScriptPath("/tmp/restore-db.sh"); - properties.setMysqlPath("/usr/bin/mysql"); - properties.setTmsDatabaseName("TMS"); - properties.setCmepDatabaseName("CMEP"); - RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class); - Mockito.when(invoker.run(Mockito.anyList(), Mockito.any(), Mockito.any())).thenReturn(0); - DatabaseRestoreRunnerStepExecutor executor = new DatabaseRestoreRunnerStepExecutor( - properties, - invoker, - "jdbc:mysql://127.0.0.1:4000/TMS", - "root", - "secret" - ); - Path sqlFile = tempDir.resolve("task/staging/payload/db/TMS.sql"); - Files.createDirectories(sqlFile.getParent()); - Files.writeString(sqlFile, "CREATE TABLE t_demo(id bigint);"); - - RunnerStepExecutionResult result = executor.execute(new RunnerStepContext( - "RRST-TASK-20260418-000004", - "RESTORE_DATABASES", - 4, - tempDir.resolve("task"), - tempDir.resolve("task/handoff.json"), - tempDir.resolve("task/restore-state.json") - )); - - Assertions.assertEquals("SUCCESS", result.getResult()); - Assertions.assertTrue(result.getSummary().contains("TMS")); - Assertions.assertFalse(result.getSummary().contains("CMEP")); - Mockito.verify(invoker, Mockito.times(1)).run(Mockito.anyList(), Mockito.any(), Mockito.eq(tempDir.resolve("task"))); - } - - @Test - void shouldFailWhenRestoreScriptReturnsNonZeroExitCode() throws Exception { - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setRestoreDbScriptPath("/tmp/restore-db.sh"); - RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class); - Mockito.when(invoker.run(Mockito.anyList(), Mockito.any(), Mockito.any())).thenReturn(7); - DatabaseRestoreRunnerStepExecutor executor = new DatabaseRestoreRunnerStepExecutor( - properties, - invoker, - "jdbc:mysql://127.0.0.1:4000/TMS", - "root", - "secret" - ); - Path sqlFile = tempDir.resolve("task/staging/payload/db/TMS.sql"); - Files.createDirectories(sqlFile.getParent()); - Files.writeString(sqlFile, "CREATE TABLE t_demo(id bigint);"); - - IllegalStateException exception = Assertions.assertThrows( - IllegalStateException.class, - () -> executor.execute(new RunnerStepContext( - "RRST-TASK-20260418-000003", - "RESTORE_DATABASES", - 4, - tempDir.resolve("task"), - tempDir.resolve("task/handoff.json"), - tempDir.resolve("task/restore-state.json") - )) - ); - - Assertions.assertTrue(exception.getMessage().contains("exitCode=7")); - } -} diff --git a/src/test/java/com/cisd/tms/modules/backup/runner/PayloadArchiveRunnerStepExecutorTest.java b/src/test/java/com/cisd/tms/modules/backup/runner/PayloadArchiveRunnerStepExecutorTest.java deleted file mode 100644 index 9527cca..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/runner/PayloadArchiveRunnerStepExecutorTest.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; -import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest; -import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest; -import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoServiceImpl; -import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest; -import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem; -import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.cisd.tms.modules.backup.packagex.DatabaseBackupCollector; -import com.cisd.tms.modules.backup.packagex.ResourcePackageService; -import com.cisd.tms.modules.backup.packagex.ResourcePackageServiceImpl; -import com.cisd.tms.modules.backup.support.file.HandoffFileStore; -import com.cisd.tms.modules.init.entity.InitTaskEntity; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.Mockito; - -class PayloadArchiveRunnerStepExecutorTest { - - @TempDir - Path tempDir; - - @Test - void shouldExtractPayloadArchiveIntoStagingDirectoryAndRestoreCriticalFiles() throws Exception { - ObjectMapper objectMapper = new ObjectMapper(); - ResourcePackageService packageService = new ResourcePackageServiceImpl( - backupProperties(), - new ResourceEnvelopeCryptoServiceImpl(mockCrypto(), backupProperties()), - mockDatabaseBackupCollector(), - objectMapper - ); - Path sourceFile = writeFile("source-config/application.yml", "server.port=8088"); - Path cpConfig = writeFile("runtime/cpconfig.cfg", "cp=old"); - Path taskDir = tempDir.resolve("restore-task"); - Files.createDirectories(taskDir); - - String packagePath = packageService.createPackage( - "RBKP-TASK-20260414-000101", - "RBKP-20260414-000101", - sampleInitTask(), - List.of( - resource("TMS_CONFIG", sourceFile.getParent().toString()), - resource("CP_CONFIG", cpConfig.toString()), - databaseResource("TMS_DB_DUMP", "TMS"), - databaseResource("CMEP_DB_DUMP", "CMEP") - ), - new CreateResourceBackupRequest() - ).getPackagePath(); - - Files.delete(sourceFile); - Files.writeString(cpConfig, "cp=newer-runtime-content"); - - ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity(); - entity.setTaskId("RRST-TASK-20260414-000001"); - entity.setBackupId("RBKP-20260414-000101"); - entity.setPackageFileId("FILE-20260414-000001"); - entity.setPackagePath(packagePath); - entity.setStagingDir(taskDir.resolve("staging").toString()); - entity.setRunnerStatePath(taskDir.resolve("restore-state.json").toString()); - entity.setRestorePlanJson("[\"RESTORE_LOCK\",\"RESTORE_FILES\",\"CONSISTENCY_VERIFY\"]"); - new HandoffFileStore(objectMapper).write(taskDir, entity); - - PayloadArchiveRunnerStepExecutor executor = new PayloadArchiveRunnerStepExecutor( - new HandoffFileStore(objectMapper), - packageService, - objectMapper - ); - - RunnerStepExecutionResult result = executor.execute(new RunnerStepContext( - entity.getTaskId(), - "RESTORE_FILES", - 2, - taskDir, - taskDir.resolve("handoff.json"), - taskDir.resolve("restore-state.json") - )); - - Assertions.assertEquals("SUCCESS", result.getResult()); - Assertions.assertTrue(result.getSummary().contains("restoredFileCount=2")); - Assertions.assertTrue(Files.exists(taskDir.resolve("staging/payload/payload-manifest.json"))); - Assertions.assertTrue(Files.exists(taskDir.resolve("staging/payload/db/TMS.sql"))); - Assertions.assertTrue(Files.exists(taskDir.resolve("staging/payload/db/CMEP.sql"))); - Assertions.assertEquals("server.port=8088", Files.readString(sourceFile)); - Assertions.assertEquals("cp=old", Files.readString(cpConfig)); - } - - private ResourceBackupProperties backupProperties() { - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setOutputDir(tempDir.resolve("backup-output").toString()); - return properties; - } - - private Path writeFile(String relativePath, String content) throws Exception { - Path file = tempDir.resolve(relativePath); - Files.createDirectories(file.getParent()); - Files.writeString(file, content); - return file; - } - - private static PcieCryptoService mockCrypto() { - PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); - Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class))) - .thenAnswer(invocation -> { - EccInternalEncryptRequest request = invocation.getArgument(0); - return backup(request.getData()); - }); - Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class))) - .thenAnswer(invocation -> { - EccInternalDecryptRequest request = invocation.getArgument(0); - return backup(request.getCipherBlob()); - }); - Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any())) - .thenReturn(backup(new byte[]{0x21, 0x22, 0x23})); - Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any()); - return pcieCryptoService; - } - - private static BackupDataResult backup(byte[] data) { - BackupDataResult result = new BackupDataResult(); - result.setData(data); - result.setLength(data.length); - return result; - } - - private static InitTaskEntity sampleInitTask() { - InitTaskEntity entity = new InitTaskEntity(); - entity.setTaskId("INIT-20260408-000001"); - entity.setTaskType("INIT"); - entity.setStatus("SUCCESS"); - entity.setProductType("ENTERPRISE"); - entity.setMqType("RABBITMQ"); - entity.setOrgCode("AAAABBBBXXX"); - entity.setDeployMode("SINGLE"); - return entity; - } - - private static DiscoveredResourceItem resource(String code, String path) { - DiscoveredResourceItem item = new DiscoveredResourceItem(); - item.setResourceType("FILE"); - item.setResourceCode(code); - item.setResolvedPath(path); - item.setRequired(true); - return item; - } - - private static DiscoveredResourceItem databaseResource(String code, String path) { - DiscoveredResourceItem item = new DiscoveredResourceItem(); - item.setResourceType("DB"); - item.setResourceCode(code); - item.setResolvedPath(path); - item.setRequired(true); - return item; - } - - private static DatabaseBackupCollector mockDatabaseBackupCollector() { - DatabaseBackupCollector collector = Mockito.mock(DatabaseBackupCollector.class); - DatabaseBackupCollector.CollectedDatabaseDump tmsDump = new DatabaseBackupCollector.CollectedDatabaseDump(); - tmsDump.setDatabaseName("TMS"); - tmsDump.setFileName("TMS.sql"); - tmsDump.setContent("CREATE TABLE t_restore(id bigint);".getBytes(java.nio.charset.StandardCharsets.UTF_8)); - DatabaseBackupCollector.CollectedDatabaseDump cmepDump = new DatabaseBackupCollector.CollectedDatabaseDump(); - cmepDump.setDatabaseName("CMEP"); - cmepDump.setFileName("CMEP.sql"); - cmepDump.setContent("CREATE TABLE c_restore(id bigint);".getBytes(java.nio.charset.StandardCharsets.UTF_8)); - Mockito.when(collector.collectTmsDatabaseDump()).thenReturn(tmsDump); - Mockito.when(collector.collectCmepDatabaseDump()).thenReturn(cmepDump); - return collector; - } -} diff --git a/src/test/java/com/cisd/tms/modules/backup/runner/ProcessResourceRestoreRunnerLauncherTest.java b/src/test/java/com/cisd/tms/modules/backup/runner/ProcessResourceRestoreRunnerLauncherTest.java deleted file mode 100644 index 73d095c..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/runner/ProcessResourceRestoreRunnerLauncherTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import java.nio.file.Path; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -class ProcessResourceRestoreRunnerLauncherTest { - - @TempDir - Path tempDir; - - @Test - void shouldSkipLaunchWhenAutoLaunchDisabled() { - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setRunnerAutoLaunchEnabled(false); - RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class); - - ProcessResourceRestoreRunnerLauncher launcher = new ProcessResourceRestoreRunnerLauncher(properties, invoker); - launcher.launch(tempDir.resolve("handoff.json")); - - Mockito.verifyNoInteractions(invoker); - } - - @Test - void shouldInvokeExternalLaunchScriptWhenAutoLaunchEnabled() { - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setRunnerAutoLaunchEnabled(true); - properties.setRunnerLaunchScriptPath("/tmp/start-runner.sh"); - RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class); - Mockito.when(invoker.run(Mockito.anyList(), Mockito.any(), Mockito.any())).thenReturn(0); - - ProcessResourceRestoreRunnerLauncher launcher = new ProcessResourceRestoreRunnerLauncher(properties, invoker); - Path handoffFile = tempDir.resolve("task/handoff.json"); - launcher.launch(handoffFile); - - @SuppressWarnings("unchecked") - ArgumentCaptor> commandCaptor = ArgumentCaptor.forClass(List.class); - ArgumentCaptor logCaptor = ArgumentCaptor.forClass(Path.class); - ArgumentCaptor workDirCaptor = ArgumentCaptor.forClass(Path.class); - Mockito.verify(invoker).run(commandCaptor.capture(), logCaptor.capture(), workDirCaptor.capture()); - - Assertions.assertEquals( - List.of("/bin/bash", "/tmp/start-runner.sh", "--handoff-file", handoffFile.toAbsolutePath().normalize().toString()), - commandCaptor.getValue() - ); - Assertions.assertEquals(handoffFile.toAbsolutePath().normalize().getParent(), workDirCaptor.getValue()); - Assertions.assertEquals(handoffFile.toAbsolutePath().normalize().getParent().resolve("runner-launch.log"), logCaptor.getValue()); - } - - @Test - void shouldFailWhenLaunchScriptReturnsNonZeroExitCode() { - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setRunnerAutoLaunchEnabled(true); - properties.setRunnerLaunchScriptPath("/tmp/start-runner.sh"); - RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class); - Mockito.when(invoker.run(Mockito.anyList(), Mockito.any(), Mockito.any())).thenReturn(9); - - ProcessResourceRestoreRunnerLauncher launcher = new ProcessResourceRestoreRunnerLauncher(properties, invoker); - - IllegalStateException exception = Assertions.assertThrows( - IllegalStateException.class, - () -> launcher.launch(tempDir.resolve("task/handoff.json")) - ); - Assertions.assertTrue(exception.getMessage().contains("exitCode=9")); - } -} diff --git a/src/test/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerTest.java b/src/test/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerTest.java deleted file mode 100644 index 293f830..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/runner/ResourceRestoreRunnerTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.cisd.tms.modules.backup.support.file.HandoffFileStore; -import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class ResourceRestoreRunnerTest { - - @TempDir - Path tempDir; - - @Test - void shouldTakeOwnershipFromHandoffReadyState() throws Exception { - ObjectMapper objectMapper = new ObjectMapper(); - HandoffFileStore handoffFileStore = new HandoffFileStore(objectMapper); - ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper); - RecordingRunnerStepExecutor stepExecutor = new RecordingRunnerStepExecutor(); - ResourceRestoreRunner runner = new ResourceRestoreRunner( - handoffFileStore, - stateFileStore, - objectMapper, - List.of(stepExecutor) - ); - - ResourceRestoreTaskEntity entity = sampleTask(); - handoffFileStore.write(tempDir, entity); - stateFileStore.initialize(tempDir, entity); - stateFileStore.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1); - - runner.run(tempDir.resolve("handoff.json")); - - JsonNode state = objectMapper.readTree(Files.readString(tempDir.resolve("restore-state.json"))); - Assertions.assertEquals("SUCCESS", state.path("status").asText()); - Assertions.assertEquals("RECONCILE_PENDING", state.path("controlPhase").asText()); - Assertions.assertEquals("RUNNER_FILE", state.path("stateSource").asText()); - Assertions.assertEquals("START_SERVICES", state.path("currentStepCode").asText()); - Assertions.assertEquals(3, state.path("currentStepNo").asInt()); - Assertions.assertFalse(state.path("runnerInstanceId").asText().isBlank()); - Assertions.assertTrue(state.path("runnerPid").asLong() > 0); - Assertions.assertFalse(state.path("lastHeartbeatAt").asText().isBlank()); - Assertions.assertEquals(List.of("STOP_SERVICES", "START_SERVICES"), stepExecutor.executedSteps); - JsonNode stepOutput = objectMapper.readTree(Files.readString(tempDir.resolve("step-output/02-STOP_SERVICES.json"))); - Assertions.assertEquals("STOP_SERVICES", stepOutput.path("stepCode").asText()); - Assertions.assertEquals("SUCCESS", stepOutput.path("result").asText()); - Assertions.assertEquals("executed STOP_SERVICES", stepOutput.path("summary").asText()); - } - - @Test - void shouldRejectDuplicateOwnershipWhenStateAlreadyTaken() { - ObjectMapper objectMapper = new ObjectMapper(); - HandoffFileStore handoffFileStore = new HandoffFileStore(objectMapper); - ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper); - RecordingRunnerStepExecutor stepExecutor = new RecordingRunnerStepExecutor(); - ResourceRestoreRunner runner = new ResourceRestoreRunner( - handoffFileStore, - stateFileStore, - objectMapper, - List.of(stepExecutor) - ); - - ResourceRestoreTaskEntity entity = sampleTask(); - handoffFileStore.write(tempDir, entity); - stateFileStore.initialize(tempDir, entity); - stateFileStore.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1); - - runner.run(tempDir.resolve("handoff.json")); - - IllegalStateException exception = Assertions.assertThrows( - IllegalStateException.class, - () -> runner.run(tempDir.resolve("handoff.json")) - ); - Assertions.assertTrue(exception.getMessage().contains("handoff ready")); - } - - @Test - void shouldPersistFailureWhenStepExecutorMissing() throws Exception { - ObjectMapper objectMapper = new ObjectMapper(); - HandoffFileStore handoffFileStore = new HandoffFileStore(objectMapper); - ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper); - ResourceRestoreRunner runner = new ResourceRestoreRunner( - handoffFileStore, - stateFileStore, - objectMapper, - List.of() - ); - - ResourceRestoreTaskEntity entity = sampleTask(); - handoffFileStore.write(tempDir, entity); - stateFileStore.initialize(tempDir, entity); - stateFileStore.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1); - - IllegalStateException exception = Assertions.assertThrows( - IllegalStateException.class, - () -> runner.run(tempDir.resolve("handoff.json")) - ); - Assertions.assertTrue(exception.getMessage().contains("no runner step executor")); - - JsonNode state = objectMapper.readTree(Files.readString(tempDir.resolve("restore-state.json"))); - Assertions.assertEquals("FAILED", state.path("status").asText()); - Assertions.assertEquals("RECONCILE_PENDING", state.path("controlPhase").asText()); - Assertions.assertEquals("STOP_SERVICES", state.path("currentStepCode").asText()); - Assertions.assertEquals(2, state.path("currentStepNo").asInt()); - Assertions.assertEquals("RUNNER_EXECUTION_FAILED", state.path("errorCode").asText()); - Assertions.assertTrue(state.path("errorMessage").asText().contains("no runner step executor")); - JsonNode stepOutput = objectMapper.readTree(Files.readString(tempDir.resolve("step-output/02-STOP_SERVICES.json"))); - Assertions.assertEquals("FAILED", stepOutput.path("result").asText()); - Assertions.assertTrue(stepOutput.path("summary").asText().contains("no runner step executor")); - } - - private ResourceRestoreTaskEntity sampleTask() { - ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity(); - entity.setTaskId("RRST-TASK-20260413-000001"); - entity.setPrecheckId("RPRE-20260413-000001"); - entity.setBackupId("RBKP-20260413-153000-000001"); - entity.setPackageFileId("FILE-20260413-000001"); - entity.setPackagePath("/tmp/backup.tmsbak"); - entity.setStagingDir(tempDir.resolve("staging").toString()); - entity.setRunnerStatePath(tempDir.resolve("restore-state.json").toString()); - entity.setRestorePlanJson("[\"RESTORE_LOCK\",\"STOP_SERVICES\",\"START_SERVICES\"]"); - entity.setStatus("HANDOFF"); - return entity; - } - - private static class RecordingRunnerStepExecutor implements RunnerStepExecutor { - - private final List executedSteps = new ArrayList<>(); - - @Override - public boolean supports(String stepCode) { - return "STOP_SERVICES".equals(stepCode) || "START_SERVICES".equals(stepCode); - } - - @Override - public RunnerStepExecutionResult execute(RunnerStepContext context) { - executedSteps.add(context.stepCode()); - return RunnerStepExecutionResult.success("executed " + context.stepCode()); - } - } -} diff --git a/src/test/java/com/cisd/tms/modules/backup/runner/RestoreStateReconcilerTest.java b/src/test/java/com/cisd/tms/modules/backup/runner/RestoreStateReconcilerTest.java deleted file mode 100644 index 55071dd..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/runner/RestoreStateReconcilerTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository; -import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -class RestoreStateReconcilerTest { - - @TempDir - Path tempDir; - - @Test - void shouldOnlyReconcileTasksInReconcilePendingPhase() throws Exception { - ObjectMapper objectMapper = new ObjectMapper(); - ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper); - ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); - - ResourceRestoreTaskEntity running = task("RRST-TASK-001", tempDir.resolve("running/restore-state.json")); - Files.createDirectories(runningStateDir(running)); - Files.writeString(Path.of(running.getRunnerStatePath()), """ - {"taskId":"RRST-TASK-001","status":"RUNNING","controlPhase":"RUNNER_OWNED","stateSource":"RUNNER_FILE"} - """); - ResourceRestoreTaskEntity pending = task("RRST-TASK-002", tempDir.resolve("pending/restore-state.json")); - Files.createDirectories(runningStateDir(pending)); - Files.writeString(Path.of(pending.getRunnerStatePath()), """ - {"taskId":"RRST-TASK-002","status":"SUCCESS","controlPhase":"RECONCILE_PENDING","stateSource":"RUNNER_FILE","errorCode":"","errorMessage":""} - """); - Mockito.when(repository.findAll()).thenReturn(List.of(running, pending)); - - RestoreStateReconciler reconciler = new RestoreStateReconciler(repository, stateFileStore, LocalDateTime::now); - reconciler.reconcileOnce(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(ResourceRestoreTaskEntity.class); - Mockito.verify(repository, Mockito.times(1)).update(captor.capture()); - Assertions.assertEquals("RRST-TASK-002", captor.getValue().getTaskId()); - - String stateContent = Files.readString(Path.of(pending.getRunnerStatePath())); - Assertions.assertTrue(stateContent.contains("\"controlPhase\":\"DONE\"")); - } - - private ResourceRestoreTaskEntity task(String taskId, Path statePath) { - ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity(); - entity.setTaskId(taskId); - entity.setStatus("HANDOFF"); - entity.setRunnerStatePath(statePath.toString()); - return entity; - } - - private Path runningStateDir(ResourceRestoreTaskEntity entity) { - return Path.of(entity.getRunnerStatePath()).getParent(); - } -} diff --git a/src/test/java/com/cisd/tms/modules/backup/runner/RunnerStallDetectorTest.java b/src/test/java/com/cisd/tms/modules/backup/runner/RunnerStallDetectorTest.java deleted file mode 100644 index e220803..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/runner/RunnerStallDetectorTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository; -import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.Mockito; - -class RunnerStallDetectorTest { - - @TempDir - Path tempDir; - - @Test - void shouldMarkRunnerOwnedTaskAsFailedWhenHeartbeatExpired() throws Exception { - ObjectMapper objectMapper = new ObjectMapper(); - ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper); - ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setRunnerHeartbeatTimeoutSeconds(60); - - ResourceRestoreTaskEntity stale = task("RRST-TASK-003", tempDir.resolve("stale/restore-state.json")); - Files.createDirectories(Path.of(stale.getRunnerStatePath()).getParent()); - Files.writeString(Path.of(stale.getRunnerStatePath()), """ - {"taskId":"RRST-TASK-003","status":"RUNNING","controlPhase":"RUNNER_OWNED","stateSource":"RUNNER_FILE","runnerInstanceId":"RRUN-001","lastHeartbeatAt":"2026-04-14T08:00:00","currentStepCode":"STOP_SERVICES","currentStepNo":2,"orphaned":false} - """); - Mockito.when(repository.findAll()).thenReturn(List.of(stale)); - - RunnerStallDetector detector = new RunnerStallDetector(repository, stateFileStore, properties, () -> LocalDateTime.parse("2026-04-14T08:05:01")); - detector.detectOnce(); - - String content = Files.readString(Path.of(stale.getRunnerStatePath())); - Assertions.assertTrue(content.contains("\"status\":\"FAILED\"")); - Assertions.assertTrue(content.contains("\"controlPhase\":\"RECONCILE_PENDING\"")); - Assertions.assertTrue(content.contains("\"orphaned\":true")); - Assertions.assertTrue(content.contains("RUNNER_STALLED")); - } - - private ResourceRestoreTaskEntity task(String taskId, Path statePath) { - ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity(); - entity.setTaskId(taskId); - entity.setRunnerStatePath(statePath.toString()); - return entity; - } -} diff --git a/src/test/java/com/cisd/tms/modules/backup/runner/ServiceCommandRunnerStepExecutorTest.java b/src/test/java/com/cisd/tms/modules/backup/runner/ServiceCommandRunnerStepExecutorTest.java deleted file mode 100644 index 38d44cc..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/runner/ServiceCommandRunnerStepExecutorTest.java +++ /dev/null @@ -1,145 +0,0 @@ -package com.cisd.tms.modules.backup.runner; - -import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -import com.cisd.tms.modules.init.config.InitExecutorProperties; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class ServiceCommandRunnerStepExecutorTest { - - @TempDir - Path tempDir; - - @Test - void shouldInvokeStopCommandsInExpectedOrder() { - RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker(); - ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor( - backupProperties(), - initProperties(), - invoker - ); - - RunnerStepExecutionResult result = executor.execute(new RunnerStepContext( - "RRST-TASK-20260414-000001", - "STOP_SERVICES", - 2, - tempDir, - tempDir.resolve("handoff.json"), - tempDir.resolve("restore-state.json") - )); - - Assertions.assertEquals("SUCCESS", result.getResult()); - Assertions.assertEquals("executed 3 command(s)", result.getSummary()); - Assertions.assertEquals(List.of( - List.of("bash", "/mock/scripts/tms.sh", "stop"), - List.of("/bin/zsh", "-lc", "/mock/bin/stop_standard_apps.sh"), - List.of("/bin/zsh", "-lc", "/mock/bin/stop_standard_rabbitmq.sh") - ), invoker.commands); - } - - @Test - void shouldInvokeStartCommandsInExpectedOrder() { - RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker(); - ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor( - backupProperties(), - initProperties(), - invoker - ); - - RunnerStepExecutionResult result = executor.execute(new RunnerStepContext( - "RRST-TASK-20260414-000001", - "START_SERVICES", - 3, - tempDir, - tempDir.resolve("handoff.json"), - tempDir.resolve("restore-state.json") - )); - - Assertions.assertEquals("SUCCESS", result.getResult()); - Assertions.assertEquals("executed 2 command(s)", result.getSummary()); - Assertions.assertEquals(List.of( - List.of("/bin/zsh", "-lc", "/mock/bin/start_standard_apps.sh"), - List.of("bash", "/mock/scripts/tms.sh", "start") - ), invoker.commands); - } - - @Test - void shouldRejectUnsupportedStep() { - RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker(); - ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor( - backupProperties(), - initProperties(), - invoker - ); - - IllegalArgumentException exception = Assertions.assertThrows( - IllegalArgumentException.class, - () -> executor.execute(new RunnerStepContext( - "RRST-TASK-20260414-000001", - "RESTORE_DATABASES", - 4, - tempDir, - tempDir.resolve("handoff.json"), - tempDir.resolve("restore-state.json") - )) - ); - Assertions.assertTrue(exception.getMessage().contains("unsupported runner step")); - } - - @Test - void shouldSurfaceCommandFailure() { - RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker(); - invoker.exitCodes.add(1); - ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor( - backupProperties(), - initProperties(), - invoker - ); - - IllegalStateException exception = Assertions.assertThrows( - IllegalStateException.class, - () -> executor.execute(new RunnerStepContext( - "RRST-TASK-20260414-000001", - "STOP_SERVICES", - 2, - tempDir, - tempDir.resolve("handoff.json"), - tempDir.resolve("restore-state.json") - )) - ); - Assertions.assertTrue(exception.getMessage().contains("exitCode=1")); - } - - private ResourceBackupProperties backupProperties() { - ResourceBackupProperties properties = new ResourceBackupProperties(); - properties.setTmsScriptPath("/mock/scripts/tms.sh"); - return properties; - } - - private InitExecutorProperties initProperties() { - InitExecutorProperties properties = new InitExecutorProperties(); - properties.setStandardAppStartCommand("/mock/bin/start_standard_apps.sh"); - properties.setStandardAppStopCommand("/mock/bin/stop_standard_apps.sh"); - properties.setStandardRabbitStopCommand("/mock/bin/stop_standard_rabbitmq.sh"); - return properties; - } - - private static class RecordingRunnerCommandInvoker implements RunnerCommandInvoker { - - private final List> commands = new ArrayList<>(); - private final List exitCodes = new ArrayList<>(); - - @Override - public int run(List command, Path logFile, Path workingDirectory) { - commands.add(List.copyOf(command)); - if (exitCodes.isEmpty()) { - return 0; - } - return exitCodes.remove(0); - } - } -} diff --git a/src/test/java/com/cisd/tms/modules/backup/service/ResourcePrecheckLightweightServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/service/ResourcePrecheckLightweightServiceTest.java new file mode 100644 index 0000000..fb004f1 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/backup/service/ResourcePrecheckLightweightServiceTest.java @@ -0,0 +1,81 @@ +package com.cisd.tms.modules.backup.service; + +import com.cisd.tms.common.config.properties.CisdPresetProperties; +import com.cisd.tms.modules.backup.config.ResourceBackupProperties; +import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest; +import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse; +import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult; +import com.cisd.tms.modules.backup.packagex.ResourcePackageService; +import com.cisd.tms.modules.backup.service.impl.ResourcePrecheckServiceImpl; +import com.cisd.tms.modules.backup.support.InMemoryResourcePrecheckStateStore; +import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator; +import com.cisd.tms.modules.file.entity.FileRecordEntity; +import com.cisd.tms.modules.file.repository.FileRecordRepository; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +class ResourcePrecheckLightweightServiceTest { + + @TempDir + Path tempDir; + + @Test + void shouldExposeLightweightRestorePlanWithoutDecryptingPayload() { + FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class); + ResourcePackageService packageService = Mockito.mock(ResourcePackageService.class); + ResourceBackupProperties backupProperties = new ResourceBackupProperties(); + backupProperties.setStorageEncKeyIndex(7); + backupProperties.setAuthenticitySignKeyIndex(9); + CisdPresetProperties presetProperties = new CisdPresetProperties(); + presetProperties.setProductType("ENTERPRISE"); + InMemoryResourcePrecheckStateStore store = new InMemoryResourcePrecheckStateStore(); + Path packagePath = tempDir.resolve("resource.tmsbak"); + + FileRecordEntity fileRecord = new FileRecordEntity(); + fileRecord.setFileId("FILE-20260429-000001"); + fileRecord.setStoragePath(packagePath.toString()); + Mockito.when(fileRecordRepository.findByFileId("FILE-20260429-000001")) + .thenReturn(Optional.of(fileRecord)); + Mockito.when(packageService.inspectPackageHeader(packagePath)).thenReturn(inspectResult()); + + ResourcePrecheckService service = new ResourcePrecheckServiceImpl( + fileRecordRepository, + packageService, + store, + new ResourceTaskIdGenerator(), + presetProperties, + backupProperties + ); + ResourceRestorePrecheckRequest request = new ResourceRestorePrecheckRequest(); + request.setFileId("FILE-20260429-000001"); + + ResourceRestorePrecheckResponse response = service.precheck(request); + + Assertions.assertEquals(List.of("JAVA_EXTRACT", "SHELL_APPLY"), response.getRestorePlan()); + Assertions.assertFalse(response.getPayloadDecrypted()); + Assertions.assertTrue(response.getSignatureValid()); + Assertions.assertTrue(store.find(response.getPrecheckId()).isPresent()); + } + + private static ResourcePackageInspectResult inspectResult() { + Map manifest = new LinkedHashMap<>(); + manifest.put("backupId", "RBKP-20260429-000001"); + manifest.put("productType", "ENTERPRISE"); + manifest.put("mqType", "RABBITMQ"); + manifest.put("orgCode", "AAAABBBBXXX"); + manifest.put("backupTime", "2026-04-29T10:00:00"); + manifest.put("resourceCount", 3); + manifest.put("storageEncKeyFingerprint", "USER_KEY_ENC:7"); + manifest.put("authenticityKeyFingerprint", "USER_KEY_SIGN:9"); + ResourcePackageInspectResult result = new ResourcePackageInspectResult(); + result.setManifest(manifest); + return result; + } +} diff --git a/src/test/java/com/cisd/tms/modules/backup/service/ResourceRestoreHandoffServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/service/ResourceRestoreHandoffServiceTest.java index 7bfeb2d..f861152 100644 --- a/src/test/java/com/cisd/tms/modules/backup/service/ResourceRestoreHandoffServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/service/ResourceRestoreHandoffServiceTest.java @@ -1,24 +1,24 @@ package com.cisd.tms.modules.backup.service; import com.cisd.tms.modules.backup.config.ResourceBackupProperties; +import com.cisd.tms.common.exception.BizException; import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest; import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse; import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse; import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.cisd.tms.modules.backup.runner.ResourceRestoreRunnerLauncher; +import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractor; import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository; +import com.cisd.tms.modules.backup.restore.LightweightRestoreApplierLauncher; import com.cisd.tms.modules.backup.service.impl.ResourceRestoreServiceImpl; import com.cisd.tms.modules.backup.support.ResourcePrecheckContext; import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator; import com.cisd.tms.modules.backup.support.file.FileSystemResourcePrecheckStateStore; -import com.cisd.tms.modules.backup.support.file.HandoffFileStore; -import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; import com.cisd.tms.modules.file.entity.FileRecordEntity; import com.cisd.tms.modules.file.repository.FileRecordRepository; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.file.Files; import java.nio.file.Path; +import java.time.LocalDateTime; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -31,15 +31,15 @@ class ResourceRestoreHandoffServiceTest { Path tempDir; @Test - void shouldCreateRestoreTaskWriteHandoffFilesAndLaunchRunner() throws Exception { + void shouldCreateRestoreTaskExtractPayloadAndLaunchLightweightApplier() throws Exception { ResourceBackupProperties properties = new ResourceBackupProperties(); properties.setPrecheckStoreDir(tempDir.resolve("prechecks").toString()); properties.setRestoreTaskRootDir(tempDir.resolve("resource-restore").toString()); FileSystemResourcePrecheckStateStore store = new FileSystemResourcePrecheckStateStore(properties, new ObjectMapper()); ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class); - ResourceRestoreRunnerLauncher runnerLauncher = Mockito.mock(ResourceRestoreRunnerLauncher.class); - ObjectMapper objectMapper = new ObjectMapper(); + ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class); + LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class); Path packagePath = tempDir.resolve("download").resolve("RBKP-20260413-153000-000001.tmsbak"); Files.createDirectories(packagePath.getParent()); Files.writeString(packagePath, "package-bytes"); @@ -52,7 +52,8 @@ class ResourceRestoreHandoffServiceTest { precheckResponse.setKeysetMatched(true); precheckResponse.setSignatureValid(true); precheckResponse.setCompatible(true); - precheckResponse.setRestorePlan(java.util.List.of("RESTORE_LOCK", "STOP_SERVICES", "RESTORE_DATABASES", "START_SERVICES")); + precheckResponse.setExpiresAt(LocalDateTime.now().plusMinutes(10).toString()); + precheckResponse.setRestorePlan(java.util.List.of("JAVA_EXTRACT", "SHELL_APPLY")); store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse)); FileRecordEntity fileRecordEntity = new FileRecordEntity(); fileRecordEntity.setFileId("FILE-20260413-000001"); @@ -65,10 +66,9 @@ class ResourceRestoreHandoffServiceTest { fileRecordRepository, store, new ResourceTaskIdGenerator(), - new HandoffFileStore(objectMapper), - new ResourceRestoreStateFileStore(objectMapper), properties, - runnerLauncher + payloadExtractor, + applierLauncher ); CreateResourceRestoreRequest request = new CreateResourceRestoreRequest(); @@ -79,25 +79,99 @@ class ResourceRestoreHandoffServiceTest { CreateResourceRestoreResponse response = service.createRestoreTask(request); Path taskDir = tempDir.resolve("resource-restore").resolve(response.getTaskId()); - Path handoffFile = taskDir.resolve("handoff.json"); - Path stateFile = taskDir.resolve("restore-state.json"); - Assertions.assertTrue(Files.exists(handoffFile)); - Assertions.assertTrue(Files.exists(stateFile)); - - JsonNode handoff = objectMapper.readTree(Files.readString(handoffFile)); - Assertions.assertEquals(response.getTaskId(), handoff.path("taskId").asText()); - Assertions.assertEquals(packagePath.toString(), handoff.path("packagePath").asText()); - - JsonNode state = objectMapper.readTree(Files.readString(stateFile)); - Assertions.assertEquals("HANDOFF_READY", state.path("controlPhase").asText()); - Assertions.assertEquals("HANDOFF", state.path("status").asText()); ArgumentCaptor captor = ArgumentCaptor.forClass(ResourceRestoreTaskEntity.class); Mockito.verify(repository).save(captor.capture()); ResourceRestoreTaskEntity saved = captor.getValue(); Assertions.assertEquals(packagePath.toString(), saved.getPackagePath()); - Assertions.assertEquals("HANDOFF", saved.getStatus()); + Assertions.assertEquals("APPLYING", saved.getStatus()); + Assertions.assertEquals(taskDir.resolve("payload").toString(), saved.getStagingDir()); + Assertions.assertEquals(taskDir.resolve("status.json").toString(), saved.getRunnerStatePath()); + Assertions.assertEquals("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]", saved.getRestorePlanJson()); - Mockito.verify(runnerLauncher).launch(handoffFile); + Mockito.verify(payloadExtractor).extract(packagePath, taskDir); + Mockito.verify(applierLauncher).launch(taskDir); + } + + @Test + void shouldRejectIncompatiblePrecheckWhenCreatingRestoreTask() { + ResourceBackupProperties properties = new ResourceBackupProperties(); + properties.setPrecheckStoreDir(tempDir.resolve("prechecks").toString()); + properties.setRestoreTaskRootDir(tempDir.resolve("resource-restore").toString()); + FileSystemResourcePrecheckStateStore store = new FileSystemResourcePrecheckStateStore(properties, new ObjectMapper()); + ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); + FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class); + ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class); + LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class); + ResourceRestorePrecheckResponse precheckResponse = validPrecheckResponse(); + precheckResponse.setCompatible(false); + store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse)); + ResourceRestoreService service = new ResourceRestoreServiceImpl( + repository, + fileRecordRepository, + store, + new ResourceTaskIdGenerator(), + properties, + payloadExtractor, + applierLauncher + ); + + BizException exception = Assertions.assertThrows(BizException.class, + () -> service.createRestoreTask(validCreateRequest())); + + Assertions.assertTrue(exception.getMessage().contains("not compatible")); + Mockito.verifyNoInteractions(repository, fileRecordRepository, payloadExtractor, applierLauncher); + } + + @Test + void shouldRejectExpiredPrecheckWhenCreatingRestoreTask() { + ResourceBackupProperties properties = new ResourceBackupProperties(); + properties.setPrecheckStoreDir(tempDir.resolve("prechecks").toString()); + properties.setRestoreTaskRootDir(tempDir.resolve("resource-restore").toString()); + FileSystemResourcePrecheckStateStore store = new FileSystemResourcePrecheckStateStore(properties, new ObjectMapper()); + ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); + FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class); + ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class); + LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class); + ResourceRestorePrecheckResponse precheckResponse = validPrecheckResponse(); + precheckResponse.setExpiresAt(LocalDateTime.now().minusMinutes(1).toString()); + store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse)); + ResourceRestoreService service = new ResourceRestoreServiceImpl( + repository, + fileRecordRepository, + store, + new ResourceTaskIdGenerator(), + properties, + payloadExtractor, + applierLauncher + ); + + BizException exception = Assertions.assertThrows(BizException.class, + () -> service.createRestoreTask(validCreateRequest())); + + Assertions.assertTrue(exception.getMessage().contains("expired")); + Mockito.verifyNoInteractions(repository, fileRecordRepository, payloadExtractor, applierLauncher); + } + + private static ResourceRestorePrecheckResponse validPrecheckResponse() { + ResourceRestorePrecheckResponse precheckResponse = new ResourceRestorePrecheckResponse(); + precheckResponse.setPrecheckId("RPRE-20260413-000001"); + precheckResponse.setBackupId("RBKP-20260413-153000-000001"); + precheckResponse.setSourceProductType("ENTERPRISE"); + precheckResponse.setTargetProductType("ENTERPRISE"); + precheckResponse.setKeysetMatched(true); + precheckResponse.setSignatureValid(true); + precheckResponse.setCompatible(true); + precheckResponse.setExpiresAt(LocalDateTime.now().plusMinutes(10).toString()); + precheckResponse.setRestorePlan(java.util.List.of("JAVA_EXTRACT", "SHELL_APPLY")); + return precheckResponse; + } + + private static CreateResourceRestoreRequest validCreateRequest() { + CreateResourceRestoreRequest request = new CreateResourceRestoreRequest(); + request.setPrecheckId("RPRE-20260413-000001"); + request.setConfirmBackupId("RBKP-20260413-153000-000001"); + request.setConfirmRiskAccepted(true); + return request; } } diff --git a/src/test/java/com/cisd/tms/modules/backup/service/ResourceRestoreSupportServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/service/ResourceRestoreSupportServiceTest.java deleted file mode 100644 index 4bd3608..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/service/ResourceRestoreSupportServiceTest.java +++ /dev/null @@ -1,241 +0,0 @@ -//package com.cisd.tms.modules.backup.service; -// -//import com.cisd.tms.common.config.properties.CisdPresetProperties; -//import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; -//import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest; -//import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; -//import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -//import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoServiceImpl; -//import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest; -//import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest; -//import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse; -//import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse; -//import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem; -//import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse; -//import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -//import com.cisd.tms.modules.backup.packagex.ResourcePackageResult; -//import com.cisd.tms.modules.backup.packagex.ResourcePackageService; -//import com.cisd.tms.modules.backup.packagex.ResourcePackageServiceImpl; -//import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository; -//import com.cisd.tms.modules.backup.service.impl.ResourcePrecheckServiceImpl; -//import com.cisd.tms.modules.backup.service.impl.ResourceRestoreServiceImpl; -//import com.cisd.tms.modules.backup.service.impl.ResourceTaskQueryServiceImpl; -//import com.cisd.tms.modules.backup.support.InMemoryResourcePrecheckStateStore; -//import com.cisd.tms.modules.backup.support.ResourcePrecheckContext; -//import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator; -//import com.cisd.tms.modules.backup.support.file.HandoffFileStore; -//import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; -//import com.cisd.tms.modules.file.entity.FileRecordEntity; -//import com.cisd.tms.modules.file.repository.FileRecordRepository; -//import com.cisd.tms.modules.init.entity.InitTaskEntity; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import java.nio.file.Files; -//import java.nio.file.Path; -//import java.util.Optional; -//import org.junit.jupiter.api.Assertions; -//import org.junit.jupiter.api.Test; -//import org.junit.jupiter.api.io.TempDir; -//import org.mockito.ArgumentCaptor; -//import org.mockito.Mockito; -// -//class ResourceRestoreSupportServiceTest { -// -// @TempDir -// Path tempDir; -// -// @Test -// void shouldParseRealBackupPackageByFileIdAndCachePrecheckContext() { -// InMemoryResourcePrecheckStateStore store = new InMemoryResourcePrecheckStateStore(); -// FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class); -// CisdPresetProperties presetProperties = new CisdPresetProperties(); -// presetProperties.setProductType("ENTERPRISE"); -// ResourceBackupProperties backupProperties = new ResourceBackupProperties(); -// backupProperties.setOutputDir(tempDir.toString()); -// Path payloadFile = writePayloadFile("runtime/application.yml", "server.port=9090"); -// PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); -// Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class))) -// .thenAnswer(invocation -> { -// EccInternalEncryptRequest request = invocation.getArgument(0); -// return backup(request.getData()); -// }); -// Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any())) -// .thenReturn(backup(new byte[]{0x21, 0x22, 0x23})); -// Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any())) -// .thenAnswer(invocation -> { -// com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest request = invocation.getArgument(0); -// return backup(request.getCipherBlob()); -// }); -// -// ResourcePackageService packageService = new ResourcePackageServiceImpl( -// backupProperties, -// new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, backupProperties), -// new ObjectMapper() -// ); -// ResourcePackageResult packageResult = packageService.createPackage( -// "RBKP-TASK-20260413-000001", -// "RBKP-20260413-153000-000001", -// sampleInitTask(), -// java.util.List.of(resource("TMS_CONFIG", payloadFile.toString())), -// new com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest() -// ); -// -// FileRecordEntity fileRecordEntity = new FileRecordEntity(); -// fileRecordEntity.setFileId("FILE-20260413-000001"); -// fileRecordEntity.setStoragePath(packageResult.getPackagePath()); -// Mockito.when(fileRecordRepository.findByFileId("FILE-20260413-000001")).thenReturn(Optional.of(fileRecordEntity)); -// -// ResourcePrecheckService service = new ResourcePrecheckServiceImpl( -// fileRecordRepository, -// packageService, -// store, -// new ResourceTaskIdGenerator(), -// presetProperties, -// backupProperties -// ); -// -// ResourceRestorePrecheckRequest request = new ResourceRestorePrecheckRequest(); -// request.setFileId("FILE-20260413-000001"); -// -// ResourceRestorePrecheckResponse response = service.precheck(request); -// -// Assertions.assertTrue(response.getPrecheckId().startsWith("RPRE-")); -// Assertions.assertEquals("RBKP-20260413-153000-000001", response.getBackupId()); -// Assertions.assertEquals("ENTERPRISE", response.getSourceProductType()); -// Assertions.assertEquals("RABBITMQ", response.getSourceMqType()); -// Assertions.assertEquals("AAAABBBBXXX", response.getSourceOrgCode()); -// Assertions.assertEquals("ENTERPRISE", response.getTargetProductType()); -// Assertions.assertTrue(response.getKeysetMatched()); -// Assertions.assertTrue(response.getSignatureValid()); -// Assertions.assertTrue(response.getCompatible()); -// Assertions.assertFalse(response.getPayloadDecrypted()); -// Assertions.assertEquals("RESTORE_LOCK", response.getRestorePlan().get(0)); -// -// ResourcePrecheckContext context = store.find(response.getPrecheckId()).orElseThrow(); -// Assertions.assertEquals("FILE-20260413-000001", context.getFileId()); -// Assertions.assertEquals(response.getBackupId(), context.getResponse().getBackupId()); -// } -// -// @Test -// void shouldCreateAndQueryRestoreTaskFromCachedPrecheck() { -// InMemoryResourcePrecheckStateStore store = new InMemoryResourcePrecheckStateStore(); -// ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); -// FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class); -// Path packagePath = tempDir.resolve("download").resolve("RBKP-20260413-153000-000001.tmsbak"); -// try { -// Files.createDirectories(packagePath.getParent()); -// Files.writeString(packagePath, "backup-package"); -// } catch (Exception ex) { -// throw new IllegalStateException(ex); -// } -// -// ResourceRestorePrecheckResponse precheckResponse = new ResourceRestorePrecheckResponse(); -// precheckResponse.setPrecheckId("RPRE-20260413-000001"); -// precheckResponse.setBackupId("RBKP-20260413-153000-000001"); -// precheckResponse.setSourceProductType("ENTERPRISE"); -// precheckResponse.setTargetProductType("ENTERPRISE"); -// precheckResponse.setKeysetMatched(true); -// precheckResponse.setSignatureValid(true); -// precheckResponse.setCompatible(true); -// store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse)); -// FileRecordEntity fileRecordEntity = new FileRecordEntity(); -// fileRecordEntity.setFileId("FILE-20260413-000001"); -// fileRecordEntity.setStoragePath(packagePath.toString()); -// Mockito.when(fileRecordRepository.findByFileId("FILE-20260413-000001")) -// .thenReturn(Optional.of(fileRecordEntity)); -// -// ResourceRestoreService restoreService = new ResourceRestoreServiceImpl( -// repository, -// fileRecordRepository, -// store, -// new ResourceTaskIdGenerator(), -// new HandoffFileStore(new ObjectMapper()), -// new ResourceRestoreStateFileStore(new ObjectMapper()), -// restoreProperties() -// ); -// -// CreateResourceRestoreRequest request = new CreateResourceRestoreRequest(); -// request.setPrecheckId("RPRE-20260413-000001"); -// request.setConfirmBackupId("RBKP-20260413-153000-000001"); -// request.setConfirmRiskAccepted(true); -// -// CreateResourceRestoreResponse response = restoreService.createRestoreTask(request); -// -// Assertions.assertTrue(response.getTaskId().startsWith("RRST-TASK-")); -// Assertions.assertEquals("HANDOFF", response.getStatus()); -// -// ArgumentCaptor captor = ArgumentCaptor.forClass(ResourceRestoreTaskEntity.class); -// Mockito.verify(repository).save(captor.capture()); -// ResourceRestoreTaskEntity saved = captor.getValue(); -// Assertions.assertEquals(response.getTaskId(), saved.getTaskId()); -// Assertions.assertEquals("RPRE-20260413-000001", saved.getPrecheckId()); -// Assertions.assertEquals("RBKP-20260413-153000-000001", saved.getBackupId()); -// Assertions.assertEquals("FILE-20260413-000001", saved.getPackageFileId()); -// Assertions.assertEquals(packagePath.toString(), saved.getPackagePath()); -// Assertions.assertEquals("ENTERPRISE", saved.getSourceProductType()); -// Assertions.assertEquals("ENTERPRISE", saved.getTargetProductType()); -// Assertions.assertTrue(saved.getStagingDir().contains(response.getTaskId())); -// Assertions.assertTrue(saved.getRunnerStatePath().contains(response.getTaskId())); -// -// Mockito.when(repository.findByTaskId(response.getTaskId())).thenReturn(Optional.of(saved)); -// ResourceTaskQueryService queryService = new ResourceTaskQueryServiceImpl( -// repository, -// new ResourceRestoreStateFileStore(new ObjectMapper()), -// new ObjectMapper() -// ); -// -// ResourceRestoreTaskDetailResponse detail = queryService.getRestoreTask(response.getTaskId()); -// -// Assertions.assertEquals(response.getTaskId(), detail.getTaskId()); -// Assertions.assertEquals("HANDOFF", detail.getStatus()); -// Assertions.assertEquals("DB", detail.getStateSource()); -// Assertions.assertEquals("HANDOFF_READY", detail.getControlPhase()); -// Assertions.assertEquals("FILE-20260413-000001", detail.getPackageFileId()); -// Assertions.assertEquals("RESTORE_LOCK", detail.getCurrentStepCode()); -// Assertions.assertEquals(1, detail.getCurrentStepNo()); -// } -// -// private static InitTaskEntity sampleInitTask() { -// InitTaskEntity entity = new InitTaskEntity(); -// entity.setTaskId("INIT-20260408-000001"); -// entity.setTaskType("INIT"); -// entity.setStatus("SUCCESS"); -// entity.setProductType("ENTERPRISE"); -// entity.setMqType("RABBITMQ"); -// entity.setOrgCode("AAAABBBBXXX"); -// entity.setDeployMode("SINGLE"); -// return entity; -// } -// -// private static DiscoveredResourceItem resource(String code, String path) { -// DiscoveredResourceItem item = new DiscoveredResourceItem(); -// item.setResourceType("FILE"); -// item.setResourceCode(code); -// item.setResolvedPath(path); -// item.setRequired(true); -// return item; -// } -// -// private Path writePayloadFile(String relativePath, String content) { -// try { -// Path file = tempDir.resolve(relativePath); -// Files.createDirectories(file.getParent()); -// Files.writeString(file, content); -// return file; -// } catch (Exception ex) { -// throw new IllegalStateException(ex); -// } -// } -// -// private static BackupDataResult backup(byte[] data) { -// BackupDataResult result = new BackupDataResult(); -// result.setData(data); -// result.setLength(data.length); -// return result; -// } -// -// private ResourceBackupProperties restoreProperties() { -// ResourceBackupProperties properties = new ResourceBackupProperties(); -// properties.setRestoreTaskRootDir(tempDir.resolve("resource-restore").toString()); -// return properties; -// } -//} diff --git a/src/test/java/com/cisd/tms/modules/backup/service/ResourceTaskQueryServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/service/ResourceTaskQueryServiceTest.java index 00941d1..28c662f 100644 --- a/src/test/java/com/cisd/tms/modules/backup/service/ResourceTaskQueryServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/service/ResourceTaskQueryServiceTest.java @@ -1,158 +1,111 @@ -//package com.cisd.tms.modules.backup.service; -// -//import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse; -//import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse; -//import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -//import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository; -//import com.cisd.tms.modules.backup.service.impl.ResourceTaskQueryServiceImpl; -//import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import java.nio.file.Files; -//import java.nio.file.Path; -//import java.time.LocalDateTime; -//import java.util.List; -//import java.util.Optional; -//import org.junit.jupiter.api.Assertions; -//import org.junit.jupiter.api.Test; -//import org.junit.jupiter.api.io.TempDir; -//import org.mockito.Mockito; -// -//class ResourceTaskQueryServiceTest { -// -// @TempDir -// Path tempDir; -// -// @Test -// void shouldPreferRunnerStateFileWhenPresent() throws Exception { -// Path taskDir = tempDir.resolve("RRST-TASK-20260413-000001"); -// Files.createDirectories(taskDir); -// Path stateFile = taskDir.resolve("restore-state.json"); -// Files.writeString(stateFile, """ -// { -// "taskId":"RRST-TASK-20260413-000001", -// "status":"RUNNING", -// "controlPhase":"RUNNER_OWNED", -// "stateSource":"RUNNER_FILE", -// "runnerInstanceId":"RRUN-20260413-000001", -// "runnerPid":1234, -// "ownershipToken":"TOKEN-001", -// "lastHeartbeatAt":"2026-04-13T17:10:00", -// "orphaned":false, -// "cleanupWarnings":["cleanup skipped"] -// } -// """); -// -// ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity(); -// entity.setTaskId("RRST-TASK-20260413-000001"); -// entity.setPrecheckId("RPRE-20260413-000001"); -// entity.setBackupId("RBKP-20260413-153000-000001"); -// entity.setStatus("PENDING"); -// entity.setSourceProductType("ENTERPRISE"); -// entity.setTargetProductType("ENTERPRISE"); -// entity.setPackageFileId("FILE-20260413-000001"); -// entity.setRunnerStatePath(stateFile.toString()); -// entity.setCreateBy("SYSTEM"); -// entity.setCreateTime(LocalDateTime.parse("2026-04-13T17:00:00")); -// -// ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); -// Mockito.when(repository.findByTaskId("RRST-TASK-20260413-000001")).thenReturn(Optional.of(entity)); -// -// ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl( -// repository, -// new ResourceRestoreStateFileStore(new ObjectMapper()), -// new ObjectMapper() -// ); -// -// var detail = service.getRestoreTask("RRST-TASK-20260413-000001"); -// -// Assertions.assertEquals("RUNNING", detail.getStatus()); -// Assertions.assertEquals("RUNNER_OWNED", detail.getControlPhase()); -// Assertions.assertEquals("RUNNER_FILE", detail.getStateSource()); -// Assertions.assertEquals("2026-04-13T17:10:00", detail.getLastHeartbeatAt()); -// Assertions.assertEquals(Boolean.FALSE, detail.getOrphaned()); -// Assertions.assertEquals(1, detail.getCleanupWarnings().size()); -// Assertions.assertEquals("cleanup skipped", detail.getCleanupWarnings().get(0)); -// } -// -// @Test -// void shouldFallbackToDatabaseStateWhenRunnerStateFileMissing() { -// ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity(); -// entity.setTaskId("RRST-TASK-20260413-000002"); -// entity.setPrecheckId("RPRE-20260413-000002"); -// entity.setBackupId("RBKP-20260413-153500-000002"); -// entity.setStatus("PENDING"); -// entity.setSourceProductType("ENTERPRISE"); -// entity.setTargetProductType("ENTERPRISE"); -// entity.setPackageFileId("FILE-20260413-000002"); -// entity.setRunnerStatePath(tempDir.resolve("missing.json").toString()); -// -// ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); -// Mockito.when(repository.findByTaskId("RRST-TASK-20260413-000002")).thenReturn(Optional.of(entity)); -// -// ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl( -// repository, -// new ResourceRestoreStateFileStore(new ObjectMapper()), -// new ObjectMapper() -// ); -// -// var detail = service.getRestoreTask("RRST-TASK-20260413-000002"); -// -// Assertions.assertEquals("PENDING", detail.getStatus()); -// Assertions.assertEquals("PRECHECKED", detail.getControlPhase()); -// Assertions.assertEquals("DB", detail.getStateSource()); -// } -// -// @Test -// void shouldExposeRestoreStepsAndLogsFromRunnerArtifacts() throws Exception { -// Path taskDir = tempDir.resolve("RRST-TASK-20260413-000003"); -// Files.createDirectories(taskDir.resolve("step-output")); -// Files.createDirectories(taskDir.resolve("steps")); -// Path stateFile = taskDir.resolve("restore-state.json"); -// Files.writeString(stateFile, """ -// { -// "taskId":"RRST-TASK-20260413-000003", -// "status":"RUNNING", -// "controlPhase":"RUNNER_OWNED", -// "stateSource":"RUNNER_FILE", -// "currentStepCode":"START_SERVICES", -// "currentStepNo":3 -// } -// """); -// Files.writeString(taskDir.resolve("step-output/02-STOP_SERVICES.json"), """ -// { -// "stepCode":"STOP_SERVICES", -// "result":"SUCCESS", -// "summary":"stopped services" -// } -// """); -// Files.writeString(taskDir.resolve("steps/02-STOP_SERVICES.log"), "stop log line"); -// -// ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity(); -// entity.setTaskId("RRST-TASK-20260413-000003"); -// entity.setStatus("HANDOFF"); -// entity.setRestorePlanJson("[\"RESTORE_LOCK\",\"STOP_SERVICES\",\"START_SERVICES\"]"); -// entity.setRunnerStatePath(stateFile.toString()); -// -// ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); -// Mockito.when(repository.findByTaskId("RRST-TASK-20260413-000003")).thenReturn(Optional.of(entity)); -// -// ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl( -// repository, -// new ResourceRestoreStateFileStore(new ObjectMapper()), -// new ObjectMapper() -// ); -// -// List steps = service.getRestoreTaskSteps("RRST-TASK-20260413-000003"); -// Assertions.assertEquals(3, steps.size()); -// Assertions.assertEquals("PENDING", steps.get(0).getStatus()); -// Assertions.assertEquals("SUCCESS", steps.get(1).getStatus()); -// Assertions.assertEquals("stopped services", steps.get(1).getMessage()); -// Assertions.assertEquals(Boolean.TRUE, steps.get(1).getLogAvailable()); -// Assertions.assertEquals("RUNNING", steps.get(2).getStatus()); -// -// ResourceTaskStepLogResponse log = service.getRestoreTaskStepLog("RRST-TASK-20260413-000003", 2); -// Assertions.assertEquals("STOP_SERVICES", log.getStepCode()); -// Assertions.assertEquals("stop log line", log.getContent()); -// Assertions.assertFalse(log.getTruncated()); -// } -//} +package com.cisd.tms.modules.backup.service; + +import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse; +import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse; +import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; +import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository; +import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader; +import com.cisd.tms.modules.backup.service.impl.ResourceTaskQueryServiceImpl; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +class ResourceTaskQueryServiceTest { + + @TempDir + Path tempDir; + + @Test + void shouldPreferLightweightStatusFileWhenPresent() throws Exception { + Path taskDir = tempDir.resolve("RRST-TASK-20260429-000001"); + Files.createDirectories(taskDir); + Files.writeString(taskDir.resolve("status.json"), """ + { + "status":"RUNNING", + "phase":"MQ_REPLAY", + "message":"replaying mq", + "updatedAt":"2026-04-29T10:00:00+0800" + } + """); + Files.writeString(taskDir.resolve("restore.log"), "line1\nline2\n"); + + ResourceRestoreTaskEntity entity = restoreTask(taskDir); + ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); + Mockito.when(repository.findByTaskId("RRST-TASK-20260429-000001")).thenReturn(Optional.of(entity)); + + ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl( + repository, + new ObjectMapper(), + new RestoreStatusFileReader(new ObjectMapper()) + ); + + var detail = service.getRestoreTask("RRST-TASK-20260429-000001"); + + Assertions.assertEquals("RUNNING", detail.getStatus()); + Assertions.assertEquals("MQ_REPLAY", detail.getControlPhase()); + Assertions.assertEquals("SHELL_STATUS", detail.getStateSource()); + Assertions.assertEquals("MQ_REPLAY", detail.getCurrentStepCode()); + Assertions.assertEquals("2026-04-29T10:00:00+0800", detail.getLastHeartbeatAt()); + Assertions.assertEquals("replaying mq", detail.getErrorMessage()); + } + + @Test + void shouldExposeLightweightStepsAndRestoreLog() throws Exception { + Path taskDir = tempDir.resolve("RRST-TASK-20260429-000001"); + Files.createDirectories(taskDir); + Files.writeString(taskDir.resolve("status.json"), """ + { + "status":"RUNNING", + "phase":"MQ_REPLAY", + "message":"replaying mq", + "updatedAt":"2026-04-29T10:00:00+0800" + } + """); + Files.writeString(taskDir.resolve("restore.log"), "line1\nline2\n"); + + ResourceRestoreTaskEntity entity = restoreTask(taskDir); + ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class); + Mockito.when(repository.findByTaskId("RRST-TASK-20260429-000001")).thenReturn(Optional.of(entity)); + + ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl( + repository, + new ObjectMapper(), + new RestoreStatusFileReader(new ObjectMapper()) + ); + + List steps = service.getRestoreTaskSteps("RRST-TASK-20260429-000001"); + Assertions.assertEquals(2, steps.size()); + Assertions.assertEquals("SUCCESS", steps.get(0).getStatus()); + Assertions.assertEquals("RUNNING", steps.get(1).getStatus()); + Assertions.assertEquals("replaying mq", steps.get(1).getMessage()); + Assertions.assertEquals(Boolean.TRUE, steps.get(1).getLogAvailable()); + + ResourceTaskStepLogResponse log = service.getRestoreTaskStepLog("RRST-TASK-20260429-000001", 2); + Assertions.assertEquals("SHELL_APPLY", log.getStepCode()); + Assertions.assertEquals("line1\nline2\n", log.getContent()); + Assertions.assertFalse(log.getTruncated()); + } + + private static ResourceRestoreTaskEntity restoreTask(Path taskDir) { + ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity(); + entity.setTaskId("RRST-TASK-20260429-000001"); + entity.setPrecheckId("RPRE-20260429-000001"); + entity.setBackupId("RBKP-20260429-153000-000001"); + entity.setStatus("APPLYING"); + entity.setSourceProductType("ENTERPRISE"); + entity.setTargetProductType("ENTERPRISE"); + entity.setPackageFileId("FILE-20260429-000001"); + entity.setRunnerStatePath(taskDir.resolve("status.json").toString()); + entity.setRestorePlanJson("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]"); + entity.setCreateBy("SYSTEM"); + entity.setCreateTime(LocalDateTime.parse("2026-04-29T09:59:00")); + return entity; + } +} diff --git a/src/test/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateFileStoreTest.java b/src/test/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateFileStoreTest.java deleted file mode 100644 index 54b75b7..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/support/file/ResourceRestoreStateFileStoreTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.cisd.tms.modules.backup.support.file; - -import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class ResourceRestoreStateFileStoreTest { - - @TempDir - Path tempDir; - - @Test - void shouldPromoteStateToHandoffReady() throws Exception { - ObjectMapper objectMapper = new ObjectMapper(); - ResourceRestoreStateFileStore store = new ResourceRestoreStateFileStore(objectMapper); - ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity(); - entity.setTaskId("RRST-TASK-20260413-000001"); - entity.setStatus("PENDING"); - - store.initialize(tempDir, entity); - store.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1); - - JsonNode state = objectMapper.readTree(Files.readString(tempDir.resolve("restore-state.json"))); - Assertions.assertEquals("HANDOFF", state.path("status").asText()); - Assertions.assertEquals("HANDOFF_READY", state.path("controlPhase").asText()); - Assertions.assertEquals("DB", state.path("stateSource").asText()); - Assertions.assertEquals("RESTORE_LOCK", state.path("currentStepCode").asText()); - Assertions.assertEquals(1, state.path("currentStepNo").asInt()); - } -} diff --git a/src/test/java/com/cisd/tms/modules/cert/service/EntityServiceTest.java b/src/test/java/com/cisd/tms/modules/cert/service/EntityServiceTest.java index 7caded4..f814d65 100644 --- a/src/test/java/com/cisd/tms/modules/cert/service/EntityServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/cert/service/EntityServiceTest.java @@ -1,12 +1,18 @@ package com.cisd.tms.modules.cert.service; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.cisd.tms.integration.crypto.pcie.jna.EccRefPublicKey; import com.cisd.tms.integration.crypto.pcie.jna.EccSignature; import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; +import com.cisd.tms.integration.crypto.pcie.model.GenerateKeyPairRequest; import com.cisd.tms.integration.crypto.pcie.model.UserKeySignRequest; import com.cisd.tms.integration.crypto.pcie.model.UserKeySm2SignRequest; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.cert.dto.EntityCreateRequest; import com.cisd.tms.modules.cert.dto.EntityGenP10Request; +import com.cisd.tms.modules.cert.dto.EntityItemResponse; +import com.cisd.tms.modules.cert.dto.EntityListRequest; +import com.cisd.tms.modules.cert.dto.EntityUpdateRequest; import com.cisd.tms.modules.cert.dto.Subject; import com.cisd.tms.modules.cert.entity.KeyEntity; import com.cisd.tms.modules.cert.repository.CertificateRepository; @@ -26,14 +32,177 @@ import java.security.Security; import java.security.interfaces.ECPublicKey; import java.security.spec.ECGenParameterSpec; import java.util.Base64; +import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; class EntityServiceTest { + @Test + void shouldListEntityWithUiFieldsAndStatusName() { + KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class); + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService service = new EntityService( + keyEntityRepository, + certificateRepository, + Mockito.mock(PcieCryptoService.class), + new ObjectMapper() + ); + KeyEntity entity = new KeyEntity() + .setOrgCode("00000001") + .setOrgName("中国工商银行杭州分行") + .setAlgoType("SM2") + .setKeyLen(256) + .setKeyIdx(2) + .setStatus("CERT_IMPORTED") + .setAllowIps("[]"); + entity.setId(10L); + Page page = new Page<>(1, 10, 1); + page.setRecords(List.of(entity)); + EntityListRequest request = new EntityListRequest(); + request.setOrgCode("00000001"); + request.setAlgoType("SM2"); + request.setStatus("CERT_IMPORTED"); + Mockito.when(keyEntityRepository.page(request)).thenReturn(page); + + Page result = service.list(request); + + assertEquals(1, result.getRecords().size()); + EntityItemResponse item = result.getRecords().get(0); + assertEquals("00000001", item.getOrgCode()); + assertEquals("中国工商银行杭州分行", item.getOrgName()); + assertEquals("SM2", item.getAlgoType()); + assertEquals("CERT_IMPORTED", item.getStatus()); + assertEquals("已导入证书", item.getStatusName()); + } + + @Test + void shouldCreateEntityWithOrgFieldsAndCreatedStatus() { + KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class); + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + EntityService service = new EntityService(keyEntityRepository, certificateRepository, pcieCryptoService, new ObjectMapper()); + Mockito.when(keyEntityRepository.findUsedKeyIdx()).thenReturn(List.of()); + Mockito.when(keyEntityRepository.save(any())).thenAnswer(invocation -> { + KeyEntity saved = invocation.getArgument(0); + saved.setId(100L); + return saved; + }); + Mockito.when(pcieCryptoService.backupUserKey(Mockito.eq(2), Mockito.anyInt(), Mockito.anyInt())) + .thenReturn(binaryResult(new byte[] {0x01, 0x02})); + Mockito.when(pcieCryptoService.exportSignPublicKeyEcc(2)) + .thenReturn(binaryResult(new byte[] {0x03, 0x04})); + + EntityCreateRequest request = new EntityCreateRequest(); + request.setOrgCode("00000001"); + request.setOrgName("中国工商银行杭州分行"); + request.setAlgoType("SM2"); + request.setAllowIps(List.of("127.0.0.1")); + + service.create(request); + + ArgumentCaptor generateCaptor = ArgumentCaptor.forClass(GenerateKeyPairRequest.class); + Mockito.verify(pcieCryptoService).generateKeyPairEcc(generateCaptor.capture()); + assertEquals(256, generateCaptor.getValue().getKeyBits()); + assertEquals(2, generateCaptor.getValue().getKeyIndex()); + + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(KeyEntity.class); + Mockito.verify(keyEntityRepository).save(entityCaptor.capture()); + KeyEntity saved = entityCaptor.getValue(); + assertEquals("00000001", saved.getOrgCode()); + assertEquals("中国工商银行杭州分行", saved.getOrgName()); + assertEquals("SM2", saved.getAlgoType()); + assertEquals("CREATED", saved.getStatus()); + } + + @Test + void shouldRejectUnsupportedEntityAlgoType() { + EntityService service = new EntityService( + Mockito.mock(KeyEntityRepository.class), + Mockito.mock(CertificateRepository.class), + Mockito.mock(PcieCryptoService.class), + new ObjectMapper() + ); + EntityCreateRequest request = new EntityCreateRequest(); + request.setOrgCode("00000001"); + request.setOrgName("中国工商银行杭州分行"); + request.setAlgoType("DSA"); + + assertThrows(RuntimeException.class, () -> service.create(request)); + } + + @Test + void shouldCreateRsaEntityWhenAlgoTypeIsRsa() { + KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class); + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + EntityService service = new EntityService(keyEntityRepository, certificateRepository, pcieCryptoService, new ObjectMapper()); + Mockito.when(keyEntityRepository.findUsedKeyIdx()).thenReturn(List.of()); + Mockito.when(keyEntityRepository.save(any())).thenAnswer(invocation -> { + KeyEntity saved = invocation.getArgument(0); + saved.setId(101L); + return saved; + }); + Mockito.when(pcieCryptoService.backupUserKey(Mockito.eq(2), Mockito.anyInt(), Mockito.anyInt())) + .thenReturn(binaryResult(new byte[] {0x05, 0x06})); + Mockito.when(pcieCryptoService.exportSignPublicKeyRsa(2)) + .thenReturn(binaryResult(new byte[] {0x07, 0x08})); + + EntityCreateRequest request = new EntityCreateRequest(); + request.setOrgCode("00000002"); + request.setOrgName("中国工商银行上海分行"); + request.setAlgoType("RSA"); + + service.create(request); + + ArgumentCaptor generateCaptor = ArgumentCaptor.forClass(GenerateKeyPairRequest.class); + Mockito.verify(pcieCryptoService).generateKeyPairRsa(generateCaptor.capture()); + assertEquals(2048, generateCaptor.getValue().getKeyBits()); + assertEquals(2, generateCaptor.getValue().getKeyIndex()); + + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(KeyEntity.class); + Mockito.verify(keyEntityRepository).save(entityCaptor.capture()); + assertEquals("RSA", entityCaptor.getValue().getAlgoType()); + assertEquals(2048, entityCaptor.getValue().getKeyLen()); + assertEquals("CREATED", entityCaptor.getValue().getStatus()); + } + + @Test + void shouldUpdateEntityOrgFields() { + KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class); + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService service = new EntityService( + keyEntityRepository, + certificateRepository, + Mockito.mock(PcieCryptoService.class), + new ObjectMapper() + ); + KeyEntity entity = new KeyEntity(); + entity.setId(10L); + entity.setOrgCode("00000001"); + entity.setOrgName("旧机构"); + Mockito.when(keyEntityRepository.findById(10L)).thenReturn(Optional.of(entity)); + Mockito.when(keyEntityRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + EntityUpdateRequest request = new EntityUpdateRequest(); + request.setId(10L); + request.setOrgCode("00000002"); + request.setOrgName("中国工商银行上海分行"); + request.setAllowIps(List.of("10.0.0.1")); + + service.update(request); + + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(KeyEntity.class); + Mockito.verify(keyEntityRepository).save(entityCaptor.capture()); + assertEquals("00000002", entityCaptor.getValue().getOrgCode()); + assertEquals("中国工商银行上海分行", entityCaptor.getValue().getOrgName()); + assertEquals("[\"10.0.0.1\"]", entityCaptor.getValue().getAllowIps()); + } + @Test void shouldGenerateP10WithStandardSm2Sm3Signer() throws Exception { KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class); @@ -52,6 +221,11 @@ class EntityServiceTest { Mockito.verify(pcieCryptoService, Mockito.never()).userKeySignWithSm3(any(UserKeySignRequest.class)); assertEquals(5, sm2Captor.getValue().getKeyIndex()); assertEquals("1234567812345678", new String(sm2Captor.getValue().getUserId())); + + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(KeyEntity.class); + Mockito.verify(keyEntityRepository).save(entityCaptor.capture()); + assertEquals("SINGLE", entityCaptor.getValue().getCertMode()); + assertEquals("P10_GENERATED", entityCaptor.getValue().getStatus()); } @Test @@ -82,6 +256,7 @@ class EntityServiceTest { subject.setCountry("CN"); EntityGenP10Request request = new EntityGenP10Request(); request.setId(10L); + request.setCertMode("SINGLE"); request.setSubject(subject); return request; } @@ -90,10 +265,19 @@ class EntityServiceTest { KeyEntity entity = new KeyEntity(); entity.setId(id); entity.setKeyIdx(keyIdx); + entity.setAlgoType("SM2"); + entity.setStatus("CREATED"); entity.setPublicKey(Base64.getEncoder().encodeToString(gm0018EccRefPublicKey(generateSm2PublicKey()))); return entity; } + private BackupDataResult binaryResult(byte[] data) { + BackupDataResult result = new BackupDataResult(); + result.setLength(data.length); + result.setData(data); + return result; + } + private BackupDataResult signatureResult() { EccSignature signature = new EccSignature(); signature.r[EccRefPublicKey.MAX_LEN - 1] = 0x01;