mirror of
https://github.com/certd/certd.git
synced 2026-07-26 05:37:36 +08:00
Compare commits
6 Commits
9d2937dd4b
...
b8a64a6b5b
| Author | SHA1 | Date | |
|---|---|---|---|
| b8a64a6b5b | |||
| 25ad1e6f86 | |||
| 6b6f1604e9 | |||
| 63be1c1cbd | |||
| b75c625ddc | |||
| 7083e7aff7 |
@@ -32,6 +32,7 @@ export class SysPublicSettings extends BaseSettings {
|
||||
customFooter?: string;
|
||||
robots?: boolean = true;
|
||||
aiChatEnabled = true;
|
||||
homePageEnabled = true;
|
||||
|
||||
|
||||
//验证码是否开启
|
||||
@@ -271,4 +272,3 @@ export class SysSafeSetting extends BaseSettings {
|
||||
autoHiddenTimes: 5,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export default {
|
||||
|
||||
showRunStrategy: "Show RunStrategy",
|
||||
showRunStrategyHelper: "Allow modify the run strategy of the task",
|
||||
homePageEnabled: "Enable Home Page",
|
||||
|
||||
captchaEnabled: "Enable Login Captcha",
|
||||
captchaHelper: "Whether to enable captcha verification for login",
|
||||
|
||||
@@ -18,6 +18,7 @@ export default {
|
||||
|
||||
showRunStrategy: "显示运行策略选择",
|
||||
showRunStrategyHelper: "任务设置中是否允许选择运行策略",
|
||||
homePageEnabled: "启用首页",
|
||||
|
||||
captchaEnabled: "启用登录验证码",
|
||||
captchaHelper: "登录时是否启用验证码",
|
||||
|
||||
@@ -47,6 +47,13 @@ export function setupCommonGuard(router: Router) {
|
||||
const settingStore = useSettingStore();
|
||||
await settingStore.initOnce();
|
||||
|
||||
if (to.path === "/" && settingStore.sysPublic?.homePageEnabled === false) {
|
||||
return {
|
||||
path: DEFAULT_HOME_PATH,
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
|
||||
to.meta.loaded = loadedPaths.has(to.path);
|
||||
|
||||
// 页面加载进度条
|
||||
|
||||
@@ -48,6 +48,7 @@ export type SysPublicSetting = {
|
||||
customFooter?: string;
|
||||
robots?: boolean;
|
||||
aiChatEnabled?: boolean;
|
||||
homePageEnabled?: boolean;
|
||||
|
||||
showRunStrategy?: boolean;
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ export const useSettingStore = defineStore({
|
||||
registerEnabled: false,
|
||||
managerOtherUserPipeline: false,
|
||||
icpNo: env.ICP_NO || "",
|
||||
homePageEnabled: true,
|
||||
},
|
||||
installInfo: {
|
||||
siteId: "",
|
||||
|
||||
@@ -110,6 +110,14 @@ export async function BatchUpdateNotificaiton(pipelineIds: number[], notificatio
|
||||
});
|
||||
}
|
||||
|
||||
export async function BatchUpdateCertApplyOptions(pipelineIds: number[], options: any): Promise<void> {
|
||||
return await request({
|
||||
url: apiPrefix + "/batchUpdateCertApplyOptions",
|
||||
method: "post",
|
||||
data: { ids: pipelineIds, options },
|
||||
});
|
||||
}
|
||||
|
||||
export async function BatchUpdateProject(pipelineIds: number[], toProjectId: number): Promise<void> {
|
||||
return await request({
|
||||
url: apiPrefix + "/batchTransfer",
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<fs-button icon="ph:certificate" class="need-plus" type="link" text="修改证书申请参数" @click="openFormDialog"></fs-button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFormWrapper } from "@fast-crud/fast-crud";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import * as api from "../api";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import { useReference } from "/@/use/use-refrence";
|
||||
|
||||
const props = defineProps<{
|
||||
selectedRowKeys: any[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: any;
|
||||
}>();
|
||||
|
||||
const batchUpdateFields = ["renewDays", "privateKeyType"];
|
||||
|
||||
function hasFormValue(form: any, field: string) {
|
||||
return form[field] != null && form[field] !== "";
|
||||
}
|
||||
|
||||
function buildBatchUpdateOptions(form: any) {
|
||||
const options: any = {};
|
||||
for (const field of batchUpdateFields) {
|
||||
if (hasFormValue(form, field)) {
|
||||
options[field] = form[field];
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function batchUpdateRequest(form: any) {
|
||||
const options = buildBatchUpdateOptions(form);
|
||||
await api.BatchUpdateCertApplyOptions(props.selectedRowKeys, options);
|
||||
emit("change");
|
||||
}
|
||||
|
||||
const { openCrudFormDialog } = useFormWrapper();
|
||||
const settingStore = useSettingStore();
|
||||
const pluginStore = usePluginStore();
|
||||
|
||||
function createInputColumn(inputDefine: any) {
|
||||
const form = cloneDeep(inputDefine);
|
||||
useReference(form);
|
||||
delete form.value;
|
||||
delete form.rules;
|
||||
form.required = false;
|
||||
if (form.component) {
|
||||
form.component.allowClear = true;
|
||||
}
|
||||
return {
|
||||
title: inputDefine.title,
|
||||
form,
|
||||
};
|
||||
}
|
||||
|
||||
function createColumns(inputDefines: any) {
|
||||
const columns: any = {};
|
||||
for (const field of batchUpdateFields) {
|
||||
columns[field] = createInputColumn(inputDefines[field]);
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
function hasAnyBatchUpdateValue(form: any) {
|
||||
return batchUpdateFields.some(field => hasFormValue(form, field));
|
||||
}
|
||||
|
||||
async function openFormDialog() {
|
||||
settingStore.checkPlus();
|
||||
const certApplyPlugin: any = await pluginStore.getPluginDefine("CertApply");
|
||||
const certApplyInput = certApplyPlugin?.input || {};
|
||||
|
||||
const crudOptions: any = {
|
||||
columns: createColumns(certApplyInput),
|
||||
form: {
|
||||
mode: "edit",
|
||||
//@ts-ignore
|
||||
async doSubmit({ form }) {
|
||||
if (!hasAnyBatchUpdateValue(form)) {
|
||||
throw new Error("请至少选择一个要修改的参数");
|
||||
}
|
||||
await batchUpdateRequest(form);
|
||||
},
|
||||
col: {
|
||||
span: 22,
|
||||
},
|
||||
labelCol: {
|
||||
style: {
|
||||
width: "120px",
|
||||
},
|
||||
},
|
||||
wrapper: {
|
||||
title: "批量修改证书申请参数",
|
||||
width: 620,
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
await openCrudFormDialog({ crudOptions });
|
||||
}
|
||||
</script>
|
||||
@@ -35,11 +35,12 @@
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<div v-if="selectedRowKeys.length > 0" class="batch-actions">
|
||||
<div class="batch-actions-inner">
|
||||
<span>{{ t("certd.selectedCount", { count: selectedRowKeys.length }) }}</span>
|
||||
<div class="batch-actions-inner overflow-x-auto">
|
||||
<span class="mr-2 inline-flex whitespace-nowrap">{{ t("certd.selectedCount", { count: selectedRowKeys.length }) }}</span>
|
||||
<fs-button v-if="hasActionPermission('write')" icon="ion:trash-outline" class="color-red" type="link" :text="t('certd.batchDelete')" @click="batchDelete"></fs-button>
|
||||
<batch-rerun :selected-row-keys="selectedRowKeys" @change="batchFinished"></batch-rerun>
|
||||
<change-group v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-group>
|
||||
<change-cert-apply-options v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-cert-apply-options>
|
||||
<change-notification v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-notification>
|
||||
<change-trigger v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-trigger>
|
||||
<change-project v-if="hasActionPermission('write') && settingStore.isEnterprise" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-project>
|
||||
@@ -57,6 +58,7 @@ import { computed, onActivated, onMounted, provide, ref } from "vue";
|
||||
import { dict, useFs } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud";
|
||||
import ChangeGroup from "./components/change-group.vue";
|
||||
import ChangeCertApplyOptions from "./components/change-cert-apply-options.vue";
|
||||
import ChangeTrigger from "./components/change-trigger.vue";
|
||||
import ChangeProject from "./components/change-project.vue";
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
<a-form-item :label="t('certd.allowCrawlers')" :name="['public', 'robots']">
|
||||
<a-switch v-model:checked="formState.public.robots" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.sys.setting.homePageEnabled')" :name="['public', 'homePageEnabled']">
|
||||
<a-switch v-model:checked="formState.public.homePageEnabled" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('certd.enableCommonCnameService')" :name="['private', 'commonCnameEnabled']">
|
||||
<a-switch v-model:checked="formState.private.commonCnameEnabled" />
|
||||
@@ -62,6 +65,7 @@ const formState = reactive<Partial<SysSettings>>({
|
||||
public: {
|
||||
icpNo: "",
|
||||
mpsNo: "",
|
||||
homePageEnabled: true,
|
||||
},
|
||||
private: {},
|
||||
});
|
||||
|
||||
@@ -349,6 +349,14 @@ export class PipelineController extends CrudController<PipelineService> {
|
||||
return this.ok({});
|
||||
}
|
||||
|
||||
@Post('/batchUpdateCertApplyOptions', { description: Constants.per.authOnly, summary: "批量更新证书申请任务配置" })
|
||||
async batchUpdateCertApplyOptions(@Body('ids') ids: number[], @Body('options') options: any) {
|
||||
await this.checkPermissionCall(async ({userId,projectId})=>{
|
||||
await this.service.batchUpdateCertApplyOptions(ids, options, userId,projectId);
|
||||
})
|
||||
return this.ok({});
|
||||
}
|
||||
|
||||
@Post('/batchRerun', { description: Constants.per.authOnly, summary: "批量重新运行流水线" })
|
||||
async batchRerun(@Body('ids') ids: number[], @Body('force') force: boolean) {
|
||||
await this.checkPermissionCall(async ({userId,projectId})=>{
|
||||
|
||||
@@ -33,6 +33,32 @@ describe("TldClient", () => {
|
||||
assert.equal(result.expirationDate, 1795104000000);
|
||||
});
|
||||
|
||||
it("delegates rdap.ss fallback to RdapSsClient", async () => {
|
||||
const client = new TldClient() as any;
|
||||
const queriedDomains: string[] = [];
|
||||
|
||||
client.init = async () => {};
|
||||
client.getDomainExpirationByRdap = async () => {
|
||||
throw new Error("rdap failed");
|
||||
};
|
||||
client.getDomainExpirationByWhoiser = async () => {
|
||||
throw new Error("whoiser failed");
|
||||
};
|
||||
client.rdapSsClient = {
|
||||
getDomainInfo: async (domain: string) => {
|
||||
queriedDomains.push(domain);
|
||||
return { expirationDate: 1795104000000, registrationDate: 995040000000 };
|
||||
},
|
||||
};
|
||||
|
||||
const result = await client.getDomainExpirationDate("google.com.hk");
|
||||
|
||||
assert.deepEqual(queriedDomains, ["google.com.hk"]);
|
||||
assert.deepEqual(result, { expirationDate: 1795104000000, registrationDate: 995040000000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("RdapSsClient", () => {
|
||||
it("queries rdap.ss and parses HK whois date fields", async () => {
|
||||
const originalRequest = http.request;
|
||||
let requestedConfig: any;
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { updateCertApplyStepInputs } from "./pipeline-batch-update.js";
|
||||
|
||||
describe("pipeline batch update", () => {
|
||||
it("updates only cert apply step inputs", () => {
|
||||
const pipeline: any = {
|
||||
stages: [
|
||||
{
|
||||
tasks: [
|
||||
{
|
||||
steps: [
|
||||
{
|
||||
type: "CertApply",
|
||||
input: {
|
||||
renewDays: 20,
|
||||
privateKeyType: "rsa_2048",
|
||||
domains: ["example.com"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "DeployToHost",
|
||||
input: {
|
||||
renewDays: 1,
|
||||
privateKeyType: "rsa_1024",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const count = updateCertApplyStepInputs(pipeline, {
|
||||
renewDays: 10,
|
||||
privateKeyType: "ec_256",
|
||||
});
|
||||
|
||||
assert.equal(count, 1);
|
||||
assert.deepEqual(pipeline.stages[0].tasks[0].steps[0].input, {
|
||||
renewDays: 10,
|
||||
privateKeyType: "ec_256",
|
||||
domains: ["example.com"],
|
||||
});
|
||||
assert.deepEqual(pipeline.stages[0].tasks[0].steps[1].input, {
|
||||
renewDays: 1,
|
||||
privateKeyType: "rsa_1024",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not overwrite fields omitted from the patch", () => {
|
||||
const pipeline: any = {
|
||||
stages: [
|
||||
{
|
||||
tasks: [
|
||||
{
|
||||
steps: [
|
||||
{
|
||||
type: "CertApply",
|
||||
input: {
|
||||
renewDays: 20,
|
||||
privateKeyType: "ec_256",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
updateCertApplyStepInputs(pipeline, {
|
||||
renewDays: 15,
|
||||
});
|
||||
|
||||
assert.deepEqual(pipeline.stages[0].tasks[0].steps[0].input, {
|
||||
renewDays: 15,
|
||||
privateKeyType: "ec_256",
|
||||
});
|
||||
});
|
||||
|
||||
it("updates uploaded cert pipelines only for fields defined by the plugin", () => {
|
||||
const pipeline: any = {
|
||||
stages: [
|
||||
{
|
||||
tasks: [
|
||||
{
|
||||
steps: [
|
||||
{
|
||||
type: "CertApplyUpload",
|
||||
input: {
|
||||
renewDays: 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "CertApply",
|
||||
input: {
|
||||
renewDays: 20,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const inputDefines: Record<string, Record<string, unknown>> = {
|
||||
CertApplyUpload: {
|
||||
renewDays: {},
|
||||
},
|
||||
CertApply: {
|
||||
renewDays: {},
|
||||
privateKeyType: {},
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(updateCertApplyStepInputs(pipeline, {}, stepType => inputDefines[stepType]), 0);
|
||||
assert.equal(
|
||||
updateCertApplyStepInputs(
|
||||
pipeline,
|
||||
{
|
||||
renewDays: 12,
|
||||
privateKeyType: "ec_256",
|
||||
},
|
||||
stepType => inputDefines[stepType]
|
||||
),
|
||||
2
|
||||
);
|
||||
assert.deepEqual(pipeline.stages[0].tasks[0].steps[0].input, {
|
||||
renewDays: 12,
|
||||
});
|
||||
assert.deepEqual(pipeline.stages[0].tasks[0].steps[1].input, {
|
||||
renewDays: 12,
|
||||
privateKeyType: "ec_256",
|
||||
});
|
||||
});
|
||||
|
||||
it("skips lego cert apply steps", () => {
|
||||
const pipeline: any = {
|
||||
stages: [
|
||||
{
|
||||
tasks: [
|
||||
{
|
||||
steps: [
|
||||
{
|
||||
type: "CertApplyLego",
|
||||
input: {
|
||||
renewDays: 20,
|
||||
privateKeyType: "ec256",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert.equal(updateCertApplyStepInputs(pipeline, { renewDays: 12, privateKeyType: "ec_256" }), 0);
|
||||
assert.deepEqual(pipeline.stages[0].tasks[0].steps[0].input, {
|
||||
renewDays: 20,
|
||||
privateKeyType: "ec256",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
export type CertApplyStepInputPatch = {
|
||||
renewDays?: number;
|
||||
privateKeyType?: string;
|
||||
};
|
||||
|
||||
export type GetStepInputDefine = (stepType: string) => Record<string, unknown> | undefined;
|
||||
|
||||
function isCertApplyStep(step: any) {
|
||||
return typeof step?.type === "string" && step.type !== "CertApplyLego" && step.type.startsWith("CertApply");
|
||||
}
|
||||
|
||||
function hasPatchValue(patch: CertApplyStepInputPatch, key: keyof CertApplyStepInputPatch) {
|
||||
return Object.prototype.hasOwnProperty.call(patch, key) && patch[key] !== undefined;
|
||||
}
|
||||
|
||||
function hasInputDefine(inputDefine: Record<string, unknown> | undefined, key: keyof CertApplyStepInputPatch) {
|
||||
return inputDefine == null || Object.prototype.hasOwnProperty.call(inputDefine, key);
|
||||
}
|
||||
|
||||
function applyPatchFields(target: Record<string, unknown>, patch: CertApplyStepInputPatch, inputDefine: Record<string, unknown> | undefined, fields: (keyof CertApplyStepInputPatch)[]) {
|
||||
let changed = false;
|
||||
for (const field of fields) {
|
||||
if (!hasPatchValue(patch, field) || !hasInputDefine(inputDefine, field)) {
|
||||
continue;
|
||||
}
|
||||
target[field] = patch[field];
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
export function updateCertApplyStepInputs(pipeline: any, patch: CertApplyStepInputPatch, getStepInputDefine?: GetStepInputDefine) {
|
||||
const fields: (keyof CertApplyStepInputPatch)[] = ["renewDays", "privateKeyType"];
|
||||
let count = 0;
|
||||
for (const stage of pipeline?.stages || []) {
|
||||
for (const task of stage?.tasks || []) {
|
||||
for (const step of task?.steps || []) {
|
||||
if (!isCertApplyStep(step)) {
|
||||
continue;
|
||||
}
|
||||
const inputDefine = getStepInputDefine?.(step.type);
|
||||
if (step.input == null) {
|
||||
step.input = {};
|
||||
}
|
||||
if (!applyPatchFields(step.input, patch, inputDefine, fields)) {
|
||||
continue;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -52,6 +52,7 @@ import { set } from "lodash-es";
|
||||
import { executorQueue } from "@certd/lib-server";
|
||||
import parser from "cron-parser";
|
||||
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
|
||||
import { CertApplyStepInputPatch, updateCertApplyStepInputs } from "./pipeline-batch-update.js";
|
||||
const runningTasks: Map<string | number, Executor> = new Map();
|
||||
|
||||
|
||||
@@ -1211,6 +1212,37 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
}
|
||||
}
|
||||
|
||||
async batchUpdateCertApplyOptions(ids: number[], options: CertApplyStepInputPatch, userId: any, projectId?: number) {
|
||||
if (!isPlus()) {
|
||||
throw new NeedVIPException("此功能需要升级Certd专业版");
|
||||
}
|
||||
const query: any = {}
|
||||
if (userId && userId > 0) {
|
||||
query.userId = userId;
|
||||
}
|
||||
if (projectId) {
|
||||
query.projectId = projectId;
|
||||
}
|
||||
const list = await this.find({
|
||||
where: {
|
||||
id: In(ids),
|
||||
...query
|
||||
}
|
||||
});
|
||||
|
||||
for (const item of list) {
|
||||
const pipeline = JSON.parse(item.content);
|
||||
const updatedCount = updateCertApplyStepInputs(pipeline, options, stepType => {
|
||||
const pluginDefine: any = pluginRegistry.getDefine(stepType);
|
||||
return pluginDefine?.input;
|
||||
});
|
||||
if (updatedCount === 0) {
|
||||
continue;
|
||||
}
|
||||
await this.doUpdatePipelineJson(item, pipeline);
|
||||
}
|
||||
}
|
||||
|
||||
async batchRerun(ids: number[], force: boolean, userId: any, projectId?: number) {
|
||||
if (!isPlus()) {
|
||||
throw new NeedVIPException("此功能需要升级Certd专业版");
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/// <reference types="mocha" />
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { AliyunAccess } from "./aliyun-access.js";
|
||||
|
||||
function createAccess(result: Record<string, unknown>) {
|
||||
const access = new AliyunAccess();
|
||||
access.ctx = {
|
||||
logger: {
|
||||
log() {},
|
||||
},
|
||||
} as any;
|
||||
access.getStsClient = async () =>
|
||||
({
|
||||
getCallerIdentity: async () => result,
|
||||
}) as any;
|
||||
return access;
|
||||
}
|
||||
|
||||
describe("AliyunAccess", () => {
|
||||
it("rejects STS error responses when testing access keys", async () => {
|
||||
const access = createAccess({
|
||||
Code: "InvalidAccessKeyId.NotFound",
|
||||
Message: "Specified access key is not found.",
|
||||
RequestId: "request-id",
|
||||
});
|
||||
|
||||
await assert.rejects(() => access.onTestRequest(), /InvalidAccessKeyId\.NotFound/);
|
||||
});
|
||||
|
||||
it("returns ok for valid STS identity responses", async () => {
|
||||
const access = createAccess({
|
||||
AccountId: "123456789",
|
||||
Arn: "acs:ram::123456789:user/test",
|
||||
UserId: "test-user",
|
||||
});
|
||||
|
||||
assert.equal(await access.onTestRequest(), "ok");
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,7 @@ export class AliyunAccess extends BaseAccess {
|
||||
|
||||
async onTestRequest() {
|
||||
await this.getCallerIdentity();
|
||||
return "ok"
|
||||
return "ok";
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,11 @@ export class AliyunAccess extends BaseAccess {
|
||||
const sts = await this.getStsClient();
|
||||
// 调用 GetCallerIdentity 接口
|
||||
const result = await sts.getCallerIdentity();
|
||||
if (result.Code || !result.AccountId) {
|
||||
const message = result.Message || "阿里云密钥校验失败";
|
||||
const code = result.Code ? `[${result.Code}] ` : "";
|
||||
throw new Error(`${code}${message}`);
|
||||
}
|
||||
|
||||
this.ctx.logger.log("✅ 密钥有效!");
|
||||
this.ctx.logger.log(` 账户ID: ${result.AccountId}`);
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/// <reference types="mocha" />
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { VolcengineDeployToVKE } from "./plugin-deploy-to-vke.js";
|
||||
|
||||
describe("VolcengineDeployToVKE", () => {
|
||||
it("uses a single-select cluster field", () => {
|
||||
const clusterInput = (VolcengineDeployToVKE as any).define.input.clusterId;
|
||||
|
||||
assert.equal(clusterInput.component.multi, false);
|
||||
assert.equal(clusterInput.component.mode, "default");
|
||||
});
|
||||
|
||||
it("sends the configured string ClusterId", async () => {
|
||||
const plugin = new VolcengineDeployToVKE();
|
||||
plugin.clusterId = "cc1234567890123456789";
|
||||
plugin.kubeconfigType = "Public";
|
||||
plugin.logger = { info: () => undefined } as any;
|
||||
|
||||
let requestBody: any;
|
||||
const kubeconfigId = await (plugin as any).createKubeconfig({
|
||||
request: async (req: any) => {
|
||||
requestBody = req.body;
|
||||
return { Result: { Id: "kc-123" } };
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(kubeconfigId, "kc-123");
|
||||
assert.equal(requestBody.ClusterId, "cc1234567890123456789");
|
||||
});
|
||||
|
||||
it("decodes the base64 kubeconfig returned by VKE", async () => {
|
||||
const plugin = new VolcengineDeployToVKE();
|
||||
plugin.clusterId = "cc1234567890123456789";
|
||||
plugin.kubeconfigType = "Public";
|
||||
|
||||
const kubeconfig = [
|
||||
"apiVersion: v1",
|
||||
"clusters:",
|
||||
"- cluster:",
|
||||
" server: https://example.com",
|
||||
" name: vke",
|
||||
"contexts: []",
|
||||
"current-context: vke"
|
||||
].join("\n");
|
||||
|
||||
const result = await (plugin as any).getKubeconfig(
|
||||
{
|
||||
request: async () => ({
|
||||
Result: {
|
||||
Items: [{ Id: "kc-123", Kubeconfig: Buffer.from(kubeconfig).toString("base64") }]
|
||||
}
|
||||
})
|
||||
},
|
||||
"kc-123"
|
||||
);
|
||||
|
||||
assert.equal(result, kubeconfig);
|
||||
});
|
||||
|
||||
it("formats Kubernetes forbidden errors with VKE RBAC guidance", () => {
|
||||
const plugin = new VolcengineDeployToVKE();
|
||||
plugin.clusterId = "cc1234567890123456789";
|
||||
plugin.namespace = "default";
|
||||
|
||||
const message = (plugin as any).formatK8sError({
|
||||
status: "Failure",
|
||||
message:
|
||||
'secrets "aaaa" is forbidden: User "2100656669-kd7ubde6lsvqbdgsa40t0" cannot get resource "secrets" in API group "" in the namespace "default"',
|
||||
reason: "Forbidden",
|
||||
details: { name: "aaaa", kind: "secrets" },
|
||||
code: 403
|
||||
});
|
||||
|
||||
assert.match(message, /VKE集群RBAC权限不足/);
|
||||
assert.match(message, /用户ID:2100656669/);
|
||||
assert.match(message, /命名空间:default/);
|
||||
assert.match(message, /secrets get\/create\/update\/patch/);
|
||||
});
|
||||
|
||||
it("explains missing ingress names with available choices", async () => {
|
||||
const plugin = new VolcengineDeployToVKE();
|
||||
plugin.targetType = "ingress";
|
||||
plugin.namespace = "default";
|
||||
plugin.ingressName = "ingress-nginx-controller";
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
(plugin as any).getTargetSecretNames({
|
||||
getIngressList: async () => ({
|
||||
items: [{ metadata: { name: "app-web" } }, { metadata: { name: "api-web" } }]
|
||||
})
|
||||
}),
|
||||
/当前命名空间可用Ingress:app-web,api-web/
|
||||
);
|
||||
});
|
||||
});
|
||||
+46
-10
@@ -70,6 +70,7 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
helper: "选择要替换证书的VKE集群,也可以手动输入集群ID",
|
||||
action: VolcengineDeployToVKE.prototype.onGetClusterList.name,
|
||||
watches: ["accessId", "regionId"],
|
||||
multi: false,
|
||||
required: true
|
||||
})
|
||||
)
|
||||
@@ -82,14 +83,13 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
name: "a-select",
|
||||
options: [
|
||||
{ label: "公网", value: "Public" },
|
||||
{ label: "私网", value: "Private" },
|
||||
{ label: "集群内", value: "TargetCluster" }
|
||||
{ label: "私网", value: "Private" }
|
||||
]
|
||||
},
|
||||
value: "Public",
|
||||
required: true
|
||||
})
|
||||
kubeconfigType!: "Public" | "Private" | "TargetCluster";
|
||||
kubeconfigType!: "Public" | "Private";
|
||||
|
||||
@TaskInput({
|
||||
title: "命名空间",
|
||||
@@ -194,7 +194,7 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
await this.patchCertSecret({ cert: this.cert, k8sClient, secretNames });
|
||||
} catch (e) {
|
||||
if (e.response?.body) {
|
||||
throw new Error(JSON.stringify(e.response.body));
|
||||
throw new Error(this.formatK8sError(e.response.body));
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
@@ -215,11 +215,12 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
private async createKubeconfig(vkeService: any) {
|
||||
const clusterId = this.getClusterId();
|
||||
const res = await vkeService.request({
|
||||
action: "CreateKubeconfig",
|
||||
method: "POST",
|
||||
body: {
|
||||
ClusterId: this.clusterId,
|
||||
ClusterId: clusterId,
|
||||
Type: this.kubeconfigType,
|
||||
ValidDuration: 3600
|
||||
}
|
||||
@@ -233,12 +234,13 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
private async getKubeconfig(vkeService: any, kubeconfigId: string) {
|
||||
const clusterId = this.getClusterId();
|
||||
const res = await vkeService.request({
|
||||
action: "ListKubeconfigs",
|
||||
method: "POST",
|
||||
body: {
|
||||
Filter: {
|
||||
ClusterIds: [this.clusterId],
|
||||
ClusterIds: [clusterId],
|
||||
Ids: [kubeconfigId],
|
||||
Types: [this.kubeconfigType]
|
||||
},
|
||||
@@ -252,19 +254,20 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
if (!kubeconfig) {
|
||||
throw new Error(`获取VKE Kubeconfig失败:${JSON.stringify(res)}`);
|
||||
}
|
||||
return kubeconfig;
|
||||
return this.decodeKubeconfig(kubeconfig);
|
||||
}
|
||||
|
||||
private async deleteKubeconfig(vkeService: any, kubeconfigId?: string) {
|
||||
if (!kubeconfigId) {
|
||||
return;
|
||||
}
|
||||
const clusterId = this.getClusterId();
|
||||
try {
|
||||
await vkeService.request({
|
||||
action: "DeleteKubeconfigs",
|
||||
method: "POST",
|
||||
body: {
|
||||
ClusterId: this.clusterId,
|
||||
ClusterId: clusterId,
|
||||
Ids: [kubeconfigId]
|
||||
}
|
||||
});
|
||||
@@ -274,6 +277,35 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private getClusterId() {
|
||||
if (!this.clusterId) {
|
||||
throw new Error("VKE集群ID不能为空");
|
||||
}
|
||||
return this.clusterId;
|
||||
}
|
||||
|
||||
private decodeKubeconfig(kubeconfig: string) {
|
||||
const decoded = Buffer.from(kubeconfig, "base64").toString("utf8");
|
||||
if (!decoded.includes("apiVersion:") || !decoded.includes("clusters:")) {
|
||||
throw new Error("解析VKE Kubeconfig失败:接口返回的Kubeconfig不是有效的BASE64编码");
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
private formatK8sError(body: any) {
|
||||
if (body?.code !== 403 || body?.reason !== "Forbidden") {
|
||||
return JSON.stringify(body);
|
||||
}
|
||||
|
||||
const message = body.message || "";
|
||||
const user = message.match(/User "([^"]+)"/)?.[1];
|
||||
const granteeId = user?.split("-")[0];
|
||||
const resource = message.match(/resource "([^"]+)"/)?.[1] || body.details?.kind || "目标资源";
|
||||
const namespace = message.match(/namespace "([^"]+)"/)?.[1] || this.namespace;
|
||||
const userText = granteeId ? `,用户ID:${granteeId}` : "";
|
||||
return `VKE集群RBAC权限不足:当前火山引擎授权${userText}在集群:${this.getClusterId()} 的命名空间:${namespace} 没有操作 ${resource} 的权限。请在火山引擎 VKE 集群权限管理中,为该用户授予此命名空间的管理员权限,或授予包含 secrets get/create/update/patch 的自定义角色。原始错误:${JSON.stringify(body)}`;
|
||||
}
|
||||
|
||||
private async getTargetSecretNames(k8sClient: any) {
|
||||
if (this.targetType === "secret") {
|
||||
if (typeof this.secretName === "string") {
|
||||
@@ -287,11 +319,15 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
||||
});
|
||||
const ingress = ingressList.items.find((item: any) => item.metadata.name === this.ingressName);
|
||||
if (!ingress) {
|
||||
throw new Error(`Ingress不存在:${this.ingressName}`);
|
||||
const ingressNames = ingressList.items.map((item: any) => item.metadata.name).filter(Boolean);
|
||||
const availableText = ingressNames.length > 0 ? ingressNames.join(",") : "无";
|
||||
throw new Error(
|
||||
`Ingress不存在:${this.ingressName}(命名空间:${this.namespace})。当前命名空间可用Ingress:${availableText}。请填写业务Ingress资源名称,不是 ingress-nginx-controller 这类控制器Service/Deployment名称;如果只想更新指定Secret,请将替换方式改为“按Secret替换”。`
|
||||
);
|
||||
}
|
||||
const secretNames = ingress.spec?.tls?.map((tls: any) => tls.secretName).filter(Boolean) || [];
|
||||
if (secretNames.length === 0) {
|
||||
throw new Error(`Ingress:${this.ingressName} 未找到TLS Secret`);
|
||||
throw new Error(`Ingress:${this.ingressName}(命名空间:${this.namespace})未找到spec.tls[].secretName,请确认该Ingress已配置TLS,或改用“按Secret替换”。`);
|
||||
}
|
||||
return secretNames;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user