mirror of
https://github.com/certd/certd.git
synced 2026-10-25 18:13:00 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b99e78f245 | ||
|
|
289028859c | ||
|
|
e2da200ddb | ||
|
|
7d285840f9 |
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.45.5](https://github.com/certd/certd/compare/v1.45.4...v1.45.5) (2026-10-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 非企业模式下禁止API密钥指定项目ID并校验权限【安全加强】 ([f50651e](https://github.com/certd/certd/commit/f50651ef91813b529b8498ddefe0f66b8b7946da))
|
||||
* **authority:** 修复权限校验时角色值错误的问题 ([5326d7d](https://github.com/certd/certd/commit/5326d7dff3967974d70090b45f8580a155c1a032))
|
||||
|
||||
## [1.45.4](https://github.com/certd/certd/compare/v1.45.3...v1.45.4) (2026-10-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docker:** 将构建和运行镜像的 Node.js 固定到 22.23.4 安全版本 ([#817](https://github.com/certd/certd/issues/817)) ([113c96b](https://github.com/certd/certd/commit/113c96b866e2ec5c85e37e42150445ed8b0eefbe))
|
||||
* **pipeline:** 修复定时触发器cron归一化截取位置错误导致日期字段被吃掉的问题 ([#813](https://github.com/certd/certd/issues/813)) ([7ac9101](https://github.com/certd/certd/commit/7ac9101266f2821fb13ff68ea6d36969c9485675))
|
||||
|
||||
## [1.45.3](https://github.com/certd/certd/compare/v1.45.2...v1.45.3) (2026-10-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -77,5 +77,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
@@ -51,5 +51,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
@@ -31,5 +31,5 @@
|
||||
"prettier": "3.3.3",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
@@ -63,5 +63,5 @@
|
||||
"fetch"
|
||||
]
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
@@ -69,5 +69,5 @@
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
@@ -52,5 +52,5 @@
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "9f18453d2b67993ce384d04203baa4e9c0229301"
|
||||
"gitHead": "5e3958f67a1249b372ebebf45540a655a12a4824"
|
||||
}
|
||||
|
||||
+544
@@ -0,0 +1,544 @@
|
||||
import "reflect-metadata";
|
||||
import assert from "node:assert/strict";
|
||||
import esmock from "esmock";
|
||||
import { AbstractTaskPlugin, ConcurrencyStrategy, Executor, pluginRegistry, ResultType, RunStrategy } from "@certd/pipeline";
|
||||
import { utils } from "@certd/basic";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const pluginName = "TencentDeleteExpiringCert";
|
||||
const originalNow = Date.now;
|
||||
const originalSleep = utils.sleep;
|
||||
let originalRegistration: any;
|
||||
|
||||
async function createFixture(statuses: Array<number | string | undefined> = [1]) {
|
||||
const certificates = statuses.map((status, index) => ({
|
||||
CertificateId: `cert-${index}`,
|
||||
CertEndTime: "2000-01-01 00:00:00",
|
||||
Alias: "测试证书",
|
||||
Domain: "example.invalid",
|
||||
}));
|
||||
const taskIds = certificates.map((cert, index) => ({ TaskId: `task-${index}` }));
|
||||
const results = statuses.map((status, index) => ({ TaskId: `task-${index}`, CertId: `cert-${index}`, Status: status }));
|
||||
const state: any = {
|
||||
now: originalNow(),
|
||||
certificates,
|
||||
deleteResponse: { CertTaskIds: taskIds },
|
||||
responses: [{ DeleteTaskResult: results }],
|
||||
pollCount: 0,
|
||||
failAction: "",
|
||||
requestError: new Error("模拟接口异常"),
|
||||
};
|
||||
const logs: string[] = [];
|
||||
const calls: Array<{ action: string; params: any }> = [];
|
||||
const sleeps: number[] = [];
|
||||
const controller = new AbortController();
|
||||
const recordLog = (...args: any[]) => {
|
||||
const parts = args.map(String);
|
||||
const line = parts.join(" ");
|
||||
logs.push(line);
|
||||
};
|
||||
const logger = { info: recordLog, error: recordLog, warn: recordLog, debug() {} };
|
||||
const sleep = async (ms: number) => {
|
||||
sleeps.push(ms);
|
||||
state.now += ms;
|
||||
};
|
||||
|
||||
class MockSslClient {
|
||||
async DescribeCertificates(params: any) {
|
||||
const copiedParams = structuredClone(params);
|
||||
calls.push({ action: "DescribeCertificates", params: copiedParams });
|
||||
if (state.failAction === "DescribeCertificates") {
|
||||
throw state.requestError;
|
||||
}
|
||||
return { Certificates: state.certificates };
|
||||
}
|
||||
async doRequest(action: string, params: any) {
|
||||
const copiedParams = structuredClone(params);
|
||||
calls.push({ action, params: copiedParams });
|
||||
if (state.failAction === action) {
|
||||
throw state.requestError;
|
||||
}
|
||||
if (action === "DeleteCertificates") {
|
||||
return state.deleteResponse;
|
||||
}
|
||||
assert.equal(action, "DescribeDeleteCertificatesTaskResult");
|
||||
state.now += 1000;
|
||||
const index = Math.min(state.pollCount, state.responses.length - 1);
|
||||
state.pollCount++;
|
||||
assert.ok(state.pollCount <= 100, "轮询必须有界");
|
||||
return structuredClone(state.responses[index]);
|
||||
}
|
||||
}
|
||||
|
||||
const loaded = await esmock("./index.js", {
|
||||
"@certd/plugin-plus": { AbstractPlusTaskPlugin: AbstractTaskPlugin },
|
||||
"../../../plugin-lib/tencent/index.js": { TencentSslClient: MockSslClient },
|
||||
});
|
||||
const Plugin = loaded.TencentDeleteExpiringCert;
|
||||
Plugin.prototype.getAccess = async () => ({});
|
||||
Date.now = () => state.now;
|
||||
utils.sleep = sleep;
|
||||
const plugin = new Plugin();
|
||||
Object.assign(plugin, { accessId: "mock-access", maxCount: 100, expiringDays: 18, checkTimeout: 1, logger });
|
||||
plugin.ctx = { logger, utils: { sleep }, signal: controller.signal };
|
||||
return { plugin, Plugin, state, logs, calls, sleeps, signal: controller, sleep };
|
||||
}
|
||||
|
||||
async function runPipelineFixture() {
|
||||
const sent: any[] = [];
|
||||
const finished: any[] = [];
|
||||
const writes: Array<{ key: string; value: string }> = [];
|
||||
const pipeline: any = {
|
||||
id: 1,
|
||||
title: "腾讯云清理回归",
|
||||
userId: 1,
|
||||
stages: [
|
||||
{
|
||||
id: "stage-1",
|
||||
title: "部署",
|
||||
concurrency: ConcurrencyStrategy.Serial,
|
||||
tasks: [
|
||||
{
|
||||
id: "task-1",
|
||||
title: "清理",
|
||||
steps: [
|
||||
{
|
||||
id: "step-1",
|
||||
title: "删除旧证书",
|
||||
type: pluginName,
|
||||
strategy: { runStrategy: RunStrategy.AlwaysRun },
|
||||
input: { accessId: "mock-access", maxCount: 100, expiringDays: 18, checkTimeout: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
notifications: [
|
||||
{ id: "error-notice", title: "失败通知", type: "other", when: ["error"], notificationId: 1 },
|
||||
{ id: "success-notice", title: "成功通知", type: "other", when: ["success"], notificationId: 2 },
|
||||
],
|
||||
};
|
||||
const executor = new Executor({
|
||||
pipeline,
|
||||
storage: {
|
||||
get: async () => null,
|
||||
set: async (_scope, _namespace, _version, key, value) => {
|
||||
writes.push({ key, value });
|
||||
},
|
||||
remove: async () => {},
|
||||
},
|
||||
onChanged: async () => {},
|
||||
onFinished: async history => {
|
||||
const snapshot = structuredClone(history.pipeline);
|
||||
finished.push(snapshot);
|
||||
},
|
||||
accessService: {} as any,
|
||||
emailService: {} as any,
|
||||
notificationService: {
|
||||
send: async request => {
|
||||
sent.push(request);
|
||||
},
|
||||
} as any,
|
||||
cnameProxyService: {} as any,
|
||||
pluginConfigService: { getPluginConfig: async () => ({ sysSetting: { input: {} } }) } as any,
|
||||
urlService: { getPipelineDetailUrl: async () => "https://example.invalid/pipeline/1" } as any,
|
||||
user: { id: 1, role: "user" },
|
||||
serviceGetter: {} as any,
|
||||
fileRootDir: tmpdir(),
|
||||
});
|
||||
const result = await executor.run(1, "manual");
|
||||
return { executor, result, sent, finished, writes };
|
||||
}
|
||||
|
||||
describe("TencentDeleteExpiringCert", () => {
|
||||
beforeEach(() => {
|
||||
const storage = pluginRegistry.getStorage();
|
||||
originalRegistration = storage[pluginName];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date.now = originalNow;
|
||||
utils.sleep = originalSleep;
|
||||
if (originalRegistration) {
|
||||
pluginRegistry.register(pluginName, originalRegistration);
|
||||
} else {
|
||||
pluginRegistry.unRegister(pluginName);
|
||||
}
|
||||
});
|
||||
|
||||
for (const [status, reason] of [
|
||||
[2, /失败/],
|
||||
[3, /SSL_QCSLinkedRoleInReplaceLoadCertificate/],
|
||||
[4, /未解绑/],
|
||||
[5, /查询关联.*超时/],
|
||||
[99, /未知状态.*99/],
|
||||
[undefined, /未知状态.*undefined/],
|
||||
["1", /未知状态.*1/],
|
||||
] as Array<[number | string | undefined, RegExp]>) {
|
||||
it(`状态 ${String(status)} 必须传播失败`, async () => {
|
||||
const fixture = await createFixture([status]);
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.match(error.message, reason);
|
||||
assert.match(error.message, /task-0/);
|
||||
assert.match(error.message, /cert-0/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it("全部成功保持成功且删除参数不变", async () => {
|
||||
const fixture = await createFixture([1, 1]);
|
||||
await fixture.plugin.execute();
|
||||
const request = fixture.calls.find(call => call.action === "DeleteCertificates");
|
||||
assert.deepEqual(request?.params, { CertificateIds: ["cert-0", "cert-1"], IsSync: true });
|
||||
});
|
||||
|
||||
it("跨轮询保留失败而不是被后续成功覆盖", async () => {
|
||||
const fixture = await createFixture([4, 1]);
|
||||
fixture.state.responses = [
|
||||
{
|
||||
DeleteTaskResult: [
|
||||
{ TaskId: "task-0", CertId: "cert-0", Status: 4 },
|
||||
{ TaskId: "task-1", CertId: "cert-1", Status: 0 },
|
||||
],
|
||||
},
|
||||
{ DeleteTaskResult: [{ TaskId: "task-1", CertId: "cert-1", Status: 1 }] },
|
||||
];
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.match(error.message, /成功[::]\s*1/);
|
||||
assert.match(error.message, /失败[::]\s*1/);
|
||||
assert.match(error.message, /task-0/);
|
||||
assert.match(error.message, /未解绑/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
assert.equal(fixture.state.pollCount, 2);
|
||||
});
|
||||
|
||||
for (const response of [{ DeleteTaskResult: [{ TaskId: "task-0", CertId: "cert-0", Status: 0 }] }, { DeleteTaskResult: [] }, {}]) {
|
||||
it("未完成或无有效查询结果必须超时失败", async () => {
|
||||
const fixture = await createFixture([0]);
|
||||
fixture.state.responses = [response];
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.match(error.message, /超时/);
|
||||
assert.match(error.message, /task-0/);
|
||||
assert.match(error.message, /未完成[::]\s*1/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it("混合成功、失败和进行中时超时摘要保留全部结果", async () => {
|
||||
const fixture = await createFixture([1, 4, 0]);
|
||||
fixture.state.responses.push({ DeleteTaskResult: [{ TaskId: "task-2", CertId: "cert-2", Status: 0 }] });
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
for (const pattern of [/超时/, /成功[::]\s*1/, /失败[::]\s*1/, /未完成[::]\s*1/, /task-1/, /未解绑/, /task-2/]) {
|
||||
assert.match(error.message, pattern);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("同批次部分成功不能掩盖失败", async () => {
|
||||
const fixture = await createFixture([1, 4]);
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.match(error.message, /成功[::]\s*1/);
|
||||
assert.match(error.message, /失败[::]\s*1/);
|
||||
assert.match(error.message, /task-1/);
|
||||
assert.match(error.message, /未解绑/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
assert.equal(fixture.state.pollCount, 1);
|
||||
});
|
||||
|
||||
for (const status of [2, 3, 4, 5, 99]) {
|
||||
it(`状态 ${status} 通过真实执行链产生失败历史及失败通知`, async () => {
|
||||
await createFixture([status]);
|
||||
const run = await runPipelineFixture();
|
||||
assert.equal(run.result, ResultType.error);
|
||||
const pipeline = run.executor.pipeline;
|
||||
const stage = pipeline.stages[0];
|
||||
const task = stage.tasks[0];
|
||||
const step = task.steps[0];
|
||||
for (const runnable of [step, task, stage, pipeline]) {
|
||||
assert.equal(runnable.status?.status, ResultType.error);
|
||||
assert.equal(runnable.status?.result, ResultType.error);
|
||||
}
|
||||
assert.equal(run.finished.length, 1);
|
||||
assert.equal(run.finished[0].status.status, ResultType.error);
|
||||
const saved = run.writes.find(write => write.key === "lastRuntime");
|
||||
assert.ok(saved);
|
||||
const storedValue = JSON.parse(saved.value);
|
||||
const history = storedValue.value;
|
||||
assert.equal(history.pipeline.status.status, ResultType.error);
|
||||
const sentIds = run.sent.map(request => request.id);
|
||||
assert.deepEqual(sentIds, [1]);
|
||||
assert.match(run.sent[0].body.content, /task-0/);
|
||||
});
|
||||
}
|
||||
|
||||
it("成功对照通过真实执行链只发送成功通知", async () => {
|
||||
await createFixture([1]);
|
||||
const run = await runPipelineFixture();
|
||||
assert.equal(run.result, ResultType.success);
|
||||
const pipeline = run.executor.pipeline;
|
||||
const stage = pipeline.stages[0];
|
||||
const task = stage.tasks[0];
|
||||
for (const runnable of [task.steps[0], task, stage, pipeline]) {
|
||||
assert.equal(runnable.status?.status, ResultType.success);
|
||||
}
|
||||
const sentIds = run.sent.map(request => request.id);
|
||||
assert.deepEqual(sentIds, [2]);
|
||||
});
|
||||
|
||||
for (const response of [undefined, null, {}, { CertTaskIds: [] }, { CertTaskIds: {} }, { CertTaskIds: [{}] }, { CertTaskIds: [{ TaskId: " " }] }, { CertTaskIds: [{ TaskId: "task-0" }, {}] }]) {
|
||||
it("提交结果不完整时不允许误判成功或依赖偶然TypeError", async () => {
|
||||
const fixture = await createFixture([1]);
|
||||
fixture.state.deleteResponse = response;
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.notEqual(error.name, "TypeError");
|
||||
assert.match(error.message, /删除任务提交结果异常/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
const queries = fixture.calls.filter(call => call.action === "DescribeDeleteCertificatesTaskResult");
|
||||
assert.equal(queries.length, 0);
|
||||
});
|
||||
}
|
||||
|
||||
it("重复、无关和缺少TaskId的结果不污染计数", async () => {
|
||||
const fixture = await createFixture([1, 4]);
|
||||
fixture.state.responses = [
|
||||
{
|
||||
DeleteTaskResult: [
|
||||
{ TaskId: "task-0", CertId: "cert-0", Status: 1 },
|
||||
{ TaskId: "task-0", CertId: "cert-0", Status: 1 },
|
||||
{ TaskId: "other-task", CertId: "other-cert", Status: 4 },
|
||||
{ Status: 4 },
|
||||
{ TaskId: "task-1", CertId: "cert-1", Status: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
DeleteTaskResult: [
|
||||
{ TaskId: "task-0", CertId: "cert-0", Status: 1 },
|
||||
{ TaskId: "task-1", CertId: "cert-1", Status: 4 },
|
||||
],
|
||||
},
|
||||
];
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.match(error.message, /成功[::]\s*1/);
|
||||
assert.match(error.message, /失败[::]\s*1/);
|
||||
assert.doesNotMatch(error.message, /other-task/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
assert.equal(fixture.state.pollCount, 2);
|
||||
const queries = fixture.calls.filter(call => call.action === "DescribeDeleteCertificatesTaskResult");
|
||||
assert.deepEqual(queries[1].params.TaskIds, ["task-1"]);
|
||||
});
|
||||
|
||||
it("重复的已提交任务ID仅处理一次", async () => {
|
||||
const fixture = await createFixture([1]);
|
||||
fixture.state.deleteResponse = { CertTaskIds: [{ TaskId: "task-0" }, { TaskId: "task-0" }] };
|
||||
await fixture.plugin.execute();
|
||||
const query = fixture.calls.find(call => call.action === "DescribeDeleteCertificatesTaskResult");
|
||||
assert.deepEqual(query?.params.TaskIds, ["task-0"]);
|
||||
});
|
||||
|
||||
for (const response of [{ DeleteTaskResult: [] }, {}]) {
|
||||
it("无有效查询结果时等待而不是忙等", async () => {
|
||||
const fixture = await createFixture([0]);
|
||||
fixture.state.responses = [response];
|
||||
await assert.rejects(() => fixture.plugin.execute(), /超时/);
|
||||
assert.ok(fixture.sleeps.length > 0);
|
||||
const allWaitsUseExistingInterval = fixture.sleeps.every(ms => ms === 10000);
|
||||
assert.ok(allWaitsUseExistingInterval);
|
||||
assert.ok(fixture.state.pollCount < 20);
|
||||
});
|
||||
}
|
||||
|
||||
for (const noCandidates of ["empty", "not-expiring"]) {
|
||||
it(`没有符合条件的证书时正常结束:${noCandidates}`, async () => {
|
||||
const fixture = await createFixture([1]);
|
||||
if (noCandidates === "empty") {
|
||||
fixture.state.certificates = [];
|
||||
} else {
|
||||
fixture.state.certificates[0].CertEndTime = "2999-01-01 00:00:00";
|
||||
}
|
||||
await fixture.plugin.execute();
|
||||
const deleteWasRequested = fixture.calls.some(call => call.action === "DeleteCertificates");
|
||||
assert.equal(deleteWasRequested, false);
|
||||
});
|
||||
}
|
||||
|
||||
for (const action of ["DescribeCertificates", "DeleteCertificates", "DescribeDeleteCertificatesTaskResult"]) {
|
||||
it(`接口异常原样传播:${action}`, async () => {
|
||||
const fixture = await createFixture([1]);
|
||||
fixture.state.failAction = action;
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
error => error === fixture.state.requestError
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it("取消信号不被转换为成功或普通超时", async () => {
|
||||
const fixture = await createFixture([0]);
|
||||
fixture.signal.abort();
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
// CancelError 未设置 this.name,依赖侧 .name 仍为 Error;用构造名确认取消语义
|
||||
assert.equal(error.constructor.name, "CancelError");
|
||||
assert.match(error.message, /取消/);
|
||||
assert.doesNotMatch(error.message, /超时/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("超时通过真实执行链产生失败历史及失败通知", async () => {
|
||||
const fixture = await createFixture([0]);
|
||||
const run = await runPipelineFixture();
|
||||
assert.equal(run.result, ResultType.error);
|
||||
const pipeline = run.executor.pipeline;
|
||||
const stage = pipeline.stages[0];
|
||||
const task = stage.tasks[0];
|
||||
for (const runnable of [task.steps[0], task, stage, pipeline]) {
|
||||
assert.equal(runnable.status?.status, ResultType.error);
|
||||
}
|
||||
assert.equal(run.finished[0].status.status, ResultType.error);
|
||||
const sentIds = run.sent.map(request => request.id);
|
||||
assert.deepEqual(sentIds, [1]);
|
||||
assert.match(run.sent[0].body.content, /超时/);
|
||||
assert.ok(fixture.sleeps.length > 0);
|
||||
});
|
||||
|
||||
it("提交响应Fail与任务成功并存时整批失败并保留失败证书明细", async () => {
|
||||
const fixture = await createFixture([1, 1]);
|
||||
fixture.state.deleteResponse = {
|
||||
Fail: [{ CertId: "cert-0", Msg: "证书仍被资源绑定" }],
|
||||
CertTaskIds: [{ CertId: "cert-1", TaskId: "task-1" }],
|
||||
};
|
||||
fixture.state.responses = [{ DeleteTaskResult: [{ TaskId: "task-1", CertId: "cert-1", Status: 1 }] }];
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.match(error.message, /删除证书任务失败/);
|
||||
assert.match(error.message, /成功[::]\s*1/);
|
||||
assert.match(error.message, /失败[::]\s*1/);
|
||||
assert.match(error.message, /cert-0/);
|
||||
assert.match(error.message, /证书仍被资源绑定/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("CertTaskIds为空且Fail非空时抛删除证书任务失败并含Fail明细", async () => {
|
||||
const fixture = await createFixture([1]);
|
||||
fixture.state.deleteResponse = {
|
||||
Fail: [{ CertId: "cert-0", Msg: "证书不存在或已删除" }],
|
||||
CertTaskIds: [],
|
||||
};
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.match(error.message, /删除证书任务失败/);
|
||||
assert.doesNotMatch(error.message, /删除任务提交结果异常/);
|
||||
assert.match(error.message, /cert-0/);
|
||||
assert.match(error.message, /证书不存在或已删除/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
const queries = fixture.calls.filter(call => call.action === "DescribeDeleteCertificatesTaskResult");
|
||||
assert.equal(queries.length, 0);
|
||||
});
|
||||
|
||||
it("提交响应Fail通过真实执行链产生失败历史及失败通知", async () => {
|
||||
const fixture = await createFixture([1, 1]);
|
||||
fixture.state.deleteResponse = {
|
||||
Fail: [{ CertId: "cert-0", Msg: "证书仍被资源绑定" }],
|
||||
CertTaskIds: [{ CertId: "cert-1", TaskId: "task-1" }],
|
||||
};
|
||||
fixture.state.responses = [{ DeleteTaskResult: [{ TaskId: "task-1", CertId: "cert-1", Status: 1 }] }];
|
||||
const run = await runPipelineFixture();
|
||||
assert.equal(run.result, ResultType.error);
|
||||
const pipeline = run.executor.pipeline;
|
||||
const stage = pipeline.stages[0];
|
||||
const task = stage.tasks[0];
|
||||
for (const runnable of [task.steps[0], task, stage, pipeline]) {
|
||||
assert.equal(runnable.status?.status, ResultType.error);
|
||||
}
|
||||
const sentIds = run.sent.map(request => request.id);
|
||||
assert.deepEqual(sentIds, [1]);
|
||||
assert.match(run.sent[0].body.content, /cert-0/);
|
||||
assert.match(run.sent[0].body.content, /证书仍被资源绑定/);
|
||||
});
|
||||
|
||||
it("提交含CertId映射且查询持续为空时超时摘要保留CertId", async () => {
|
||||
const fixture = await createFixture([0]);
|
||||
fixture.state.deleteResponse = { CertTaskIds: [{ CertId: "cert-0", TaskId: "task-0" }] };
|
||||
fixture.state.responses = [{ DeleteTaskResult: [] }];
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.match(error.message, /超时/);
|
||||
assert.match(error.message, /task-0/);
|
||||
assert.match(error.message, /cert-0/);
|
||||
assert.match(error.message, /未完成[::]\s*1/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("轮询结果丢失CertId时失败摘要保留此前已知CertId", async () => {
|
||||
const fixture = await createFixture([0]);
|
||||
fixture.state.deleteResponse = { CertTaskIds: [{ TaskId: "task-0" }] };
|
||||
fixture.state.responses = [{ DeleteTaskResult: [{ TaskId: "task-0", CertId: "cert-0", Status: 0 }] }, { DeleteTaskResult: [{ TaskId: "task-0", Status: 4 }] }];
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.match(error.message, /删除证书任务失败/);
|
||||
assert.match(error.message, /task-0/);
|
||||
assert.match(error.message, /cert-0/);
|
||||
assert.match(error.message, /未解绑/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("提交映射有CertId且轮询全程不带CertId时摘要仍含CertId", async () => {
|
||||
const fixture = await createFixture([0]);
|
||||
fixture.state.deleteResponse = { CertTaskIds: [{ CertId: "cert-0", TaskId: "task-0" }] };
|
||||
fixture.state.responses = [{ DeleteTaskResult: [{ TaskId: "task-0", Status: 4 }] }];
|
||||
await assert.rejects(
|
||||
() => fixture.plugin.execute(),
|
||||
(error: Error) => {
|
||||
assert.match(error.message, /删除证书任务失败/);
|
||||
assert.match(error.message, /task-0/);
|
||||
assert.match(error.message, /cert-0/);
|
||||
assert.match(error.message, /未解绑/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
+122
-33
@@ -127,15 +127,8 @@ export class TencentDeleteExpiringCert extends AbstractPlusTaskPlugin {
|
||||
IsSync: true,
|
||||
});
|
||||
this.logger.info("删除任务已提交: ", JSON.stringify(deleteRes));
|
||||
const ids = deleteRes?.CertTaskIds;
|
||||
if (!ids && !ids.length) {
|
||||
this.logger.error("没有找到任务ID");
|
||||
return;
|
||||
}
|
||||
const taskIds = ids.map((id: any) => id.TaskId);
|
||||
const startTime = Date.now();
|
||||
const results = {};
|
||||
|
||||
const results: Record<string, { certId?: string; status: unknown; reason: string }> = {};
|
||||
const statusCount = {
|
||||
success: 0,
|
||||
failed: 0,
|
||||
@@ -143,6 +136,67 @@ export class TencentDeleteExpiringCert extends AbstractPlusTaskPlugin {
|
||||
unbind: 0,
|
||||
timeout: 0,
|
||||
};
|
||||
|
||||
const failList = deleteRes?.Fail;
|
||||
if (failList != null && !Array.isArray(failList)) {
|
||||
throw new Error("删除证书任务失败;提交阶段失败明细格式异常");
|
||||
}
|
||||
if (Array.isArray(failList)) {
|
||||
for (let i = 0; i < failList.length; i++) {
|
||||
const failItem = failList[i];
|
||||
let failCertId = failItem?.CertId;
|
||||
if (typeof failCertId !== "string" || failCertId.trim().length === 0) {
|
||||
failCertId = `unknown-${i}`;
|
||||
}
|
||||
let failReason = failItem?.Msg;
|
||||
if (typeof failReason !== "string" || failReason.trim().length === 0) {
|
||||
failReason = "未知原因";
|
||||
}
|
||||
const failKey = `submit-fail:${failCertId}`;
|
||||
results[failKey] = {
|
||||
certId: failCertId,
|
||||
status: "submit-fail",
|
||||
reason: failReason,
|
||||
};
|
||||
statusCount.failed++;
|
||||
this.logger.error(`提交阶段删除失败 证书${failCertId}: ${failReason}`);
|
||||
}
|
||||
}
|
||||
const submitFailCount = Array.isArray(failList) ? failList.length : 0;
|
||||
|
||||
const ids = deleteRes?.CertTaskIds;
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
if (submitFailCount > 0) {
|
||||
const failSummary = `任务总数:0,成功:0,失败:${statusCount.failed},未完成:0,任务结果:${JSON.stringify(results)},未完成任务:`;
|
||||
throw new Error(`删除证书任务失败;${failSummary}`);
|
||||
}
|
||||
throw new Error("删除任务提交结果异常:没有有效任务ID");
|
||||
}
|
||||
const taskIds: string[] = [];
|
||||
const certIdByTaskId: Record<string, string> = {};
|
||||
for (const entry of ids) {
|
||||
const taskId = entry?.TaskId;
|
||||
if (typeof taskId !== "string" || taskId.trim().length === 0) {
|
||||
throw new Error("删除任务提交结果异常:存在无效任务ID");
|
||||
}
|
||||
if (!taskIds.includes(taskId)) {
|
||||
taskIds.push(taskId);
|
||||
}
|
||||
const submitCertId = entry?.CertId;
|
||||
if (typeof submitCertId === "string" && submitCertId.trim().length > 0) {
|
||||
certIdByTaskId[taskId] = submitCertId;
|
||||
if (!results[taskId]) {
|
||||
results[taskId] = {
|
||||
certId: submitCertId,
|
||||
status: undefined,
|
||||
reason: "待查询",
|
||||
};
|
||||
} else if (!results[taskId].certId) {
|
||||
results[taskId].certId = submitCertId;
|
||||
}
|
||||
}
|
||||
}
|
||||
const startTime = Date.now();
|
||||
const total = taskIds.length;
|
||||
|
||||
while (Date.now() < startTime + this.checkTimeout * 60 * 1000) {
|
||||
@@ -150,44 +204,61 @@ export class TencentDeleteExpiringCert extends AbstractPlusTaskPlugin {
|
||||
const taskResultRes = await sslClient.doRequest("DescribeDeleteCertificatesTaskResult", {
|
||||
TaskIds: taskIds,
|
||||
});
|
||||
const result = taskResultRes.DeleteTaskResult;
|
||||
if (!result || result.length === 0) {
|
||||
const result = taskResultRes?.DeleteTaskResult;
|
||||
if (!Array.isArray(result) || result.length === 0) {
|
||||
this.logger.info("暂未获取到有效的任务结果");
|
||||
await this.ctx.utils.sleep(10000);
|
||||
continue;
|
||||
}
|
||||
for (const item of result) {
|
||||
//遍历结果
|
||||
const status = item.Status;
|
||||
if (status !== 0) {
|
||||
remove(taskIds, id => id === item.TaskId);
|
||||
const taskId = item?.TaskId;
|
||||
if (!taskIds.includes(taskId)) {
|
||||
continue;
|
||||
}
|
||||
// Status : 0表示任务进行中、 1表示任务成功、 2表示任务失败、3表示未授权服务角色导致任务失败、4表示有未解绑的云资源导致任务失败、5表示查询关联云资源超时导致任务失败
|
||||
const status = item.Status;
|
||||
let reason = "";
|
||||
if (status === 0) {
|
||||
this.logger.info(`任务${item.TaskId}<${item.CertId}>: 进行中`);
|
||||
reason = "进行中";
|
||||
} else if (status === 1) {
|
||||
this.logger.info(`任务${item.TaskId}<${item.CertId}>: 成功`);
|
||||
results[item.TaskId] = "成功";
|
||||
reason = "成功";
|
||||
statusCount.success++;
|
||||
} else if (status === 2) {
|
||||
this.logger.error(`任务${item.TaskId}<${item.CertId}>: 失败`);
|
||||
results[item.TaskId] = "失败";
|
||||
reason = "失败";
|
||||
statusCount.failed++;
|
||||
} else if (status === 3) {
|
||||
this.logger.error(`任务${item.TaskId}<${item.CertId}>: 未授权服务角色导致任务失败`);
|
||||
results[item.TaskId] = "未授权服务角色导致任务失败";
|
||||
reason = "未授权服务角色导致任务失败";
|
||||
statusCount.unauthorized++;
|
||||
} else if (status === 4) {
|
||||
this.logger.error(`任务${item.TaskId}<${item.CertId}>: 有未解绑的云资源导致任务失败`);
|
||||
results[item.TaskId] = "有未解绑的云资源导致任务失败";
|
||||
reason = "有未解绑的云资源导致任务失败";
|
||||
statusCount.unbind++;
|
||||
} else if (status === 5) {
|
||||
this.logger.error(`任务${item.TaskId}<${item.CertId}>: 查询关联云资源超时导致任务失败`);
|
||||
results[item.TaskId] = "查询关联云资源超时导致任务失败";
|
||||
reason = "查询关联云资源超时导致任务失败";
|
||||
statusCount.timeout++;
|
||||
} else {
|
||||
this.logger.info(`任务${item.TaskId}<${item.CertId}>: 未知状态:${status}`);
|
||||
reason = `未知状态:${String(status)}`;
|
||||
statusCount.failed++;
|
||||
}
|
||||
let certId = item.CertId;
|
||||
const hasPollCertId = typeof certId === "string" && certId.trim().length > 0;
|
||||
if (!hasPollCertId) {
|
||||
const previousCertId = results[taskId]?.certId;
|
||||
if (previousCertId) {
|
||||
certId = previousCertId;
|
||||
} else {
|
||||
certId = certIdByTaskId[taskId];
|
||||
}
|
||||
}
|
||||
results[taskId] = { certId, status, reason };
|
||||
const message = `任务${taskId}<${certId}>: ${reason}`;
|
||||
if (status === 0 || status === 1) {
|
||||
this.logger.info(message);
|
||||
} else {
|
||||
this.logger.error(message);
|
||||
}
|
||||
if (status !== 0) {
|
||||
remove(taskIds, id => id === taskId);
|
||||
}
|
||||
}
|
||||
this.logger.info(
|
||||
// eslint-disable-next-line max-len
|
||||
@@ -195,16 +266,34 @@ export class TencentDeleteExpiringCert extends AbstractPlusTaskPlugin {
|
||||
);
|
||||
if (taskIds.length === 0) {
|
||||
this.logger.info("任务已全部完成");
|
||||
|
||||
if (statusCount.unauthorized > 0) {
|
||||
throw new Error("有未授权服务角色导致任务失败,需给Access授权服务角色SSL_QCSLinkedRoleInReplaceLoadCertificate");
|
||||
}
|
||||
|
||||
return;
|
||||
break;
|
||||
}
|
||||
await this.ctx.utils.sleep(10000);
|
||||
}
|
||||
this.logger.error("检查任务结果超时", JSON.stringify(results));
|
||||
|
||||
const failedCount = statusCount.failed + statusCount.unauthorized + statusCount.unbind + statusCount.timeout;
|
||||
const resultDetails = JSON.stringify(results);
|
||||
const pendingParts: string[] = [];
|
||||
for (const pendingTaskId of taskIds) {
|
||||
const pendingCertId = results[pendingTaskId]?.certId ?? certIdByTaskId[pendingTaskId];
|
||||
if (pendingCertId) {
|
||||
pendingParts.push(`${pendingTaskId}<${pendingCertId}>`);
|
||||
} else {
|
||||
pendingParts.push(pendingTaskId);
|
||||
}
|
||||
}
|
||||
const pendingTaskDetails = pendingParts.join(",");
|
||||
let summary = `任务总数:${total},成功:${statusCount.success},失败:${failedCount},未完成:${taskIds.length},任务结果:${resultDetails},未完成任务:${pendingTaskDetails}`;
|
||||
if (statusCount.unauthorized > 0) {
|
||||
summary += ";有未授权服务角色导致任务失败,需给Access授权服务角色SSL_QCSLinkedRoleInReplaceLoadCertificate";
|
||||
}
|
||||
if (taskIds.length > 0) {
|
||||
throw new Error(`检查任务结果超时;${summary}`);
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
throw new Error(`删除证书任务失败;${summary}`);
|
||||
}
|
||||
this.logger.info(`删除证书任务全部成功;${summary}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
10:35
|
||||
15:43
|
||||
|
||||
@@ -1 +1 @@
|
||||
10:38
|
||||
15:37
|
||||
|
||||
Reference in New Issue
Block a user