Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev

This commit is contained in:
xiaojunnuo
2026-06-08 16:48:39 +08:00
98 changed files with 1110 additions and 405 deletions
+9
View File
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.41.1](https://github.com/certd/certd/compare/v1.41.0...v1.41.1) (2026-06-05)
### Performance Improvements
* 流水线、监控站点支持导出 ([99fd308](https://github.com/certd/certd/commit/99fd3083f259cdb96fd656f04858dd708d1251c7))
* 优化列表页面请求两次的问题 ([5546af5](https://github.com/certd/certd/commit/5546af518e92c765513787ccaf8e856be789bcf9))
* 优化邀请注册流程 ([7a71e45](https://github.com/certd/certd/commit/7a71e45799d782d0691606fb42b4236f1d3009b0))
* **settings:** 新增NO_PROXY代理排除配置 ([c0df8be](https://github.com/certd/certd/commit/c0df8be83237e323c2c9a5bd02507430a86a00cc))
# [1.41.0](https://github.com/certd/certd/compare/v1.40.5...v1.41.0) (2026-06-04)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.41.0",
"version": "1.41.1",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -106,8 +106,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.41.0",
"@certd/pipeline": "^1.41.0",
"@certd/lib-iframe": "^1.41.1",
"@certd/pipeline": "^1.41.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -82,6 +82,7 @@ export default {
pipelineContent: "Pipeline Content",
scheduledTaskCount: "Scheduled Task Count",
deployTaskCount: "Deployment Task Count",
certDomains: "Certificate Domains",
remainingValidity: "Remaining Validity",
effectiveTime: "Effective time",
expiryTime: "Expiry Time",
@@ -41,6 +41,7 @@ export default {
pi: {
validTime: "Piepline Valid Time",
validTimeHelper: "Not filled in means permanent validity",
permanentValid: "Permanent",
},
types: {
certApply: "Cert Apply",
@@ -111,6 +111,9 @@ export default {
httpsProxyPlaceholder: "http://192.168.1.2:18010/",
saveThenTestTitle: "Save first, then click test",
httpsProxyHelper: "Usually both proxies are the same, save first then test",
noProxy: "Proxy Bypass",
noProxyPlaceholder: "localhost,127.0.0.1,.example.com,192.168.*",
noProxyHelper: "Configure NO_PROXY. Separate entries with commas, spaces, or line breaks; matched requests bypass the proxy. \nExample: localhost,127.0.0.1,.example.com,192.168.*",
dualStackNetwork: "Dual Stack Network",
ipv4Priority: "IPv4 Priority",
ipv6Priority: "IPv6 Priority",
@@ -86,6 +86,7 @@ export default {
pipelineContent: "流水线内容",
scheduledTaskCount: "定时任务数",
deployTaskCount: "部署任务数",
certDomains: "证书域名",
remainingValidity: "到期剩余",
effectiveTime: "生效时间",
expiryTime: "过期时间",
@@ -41,6 +41,7 @@ export default {
pi: {
validTime: "流水线有效期",
validTimeHelper: "不填则为永久有效",
permanentValid: "永久有效",
},
types: {
certApply: "证书申请",
@@ -108,6 +108,9 @@ export default {
httpsProxyPlaceholder: "http://192.168.1.2:18010/",
saveThenTestTitle: "保存后,再点击测试",
httpsProxyHelper: "一般这两个代理填一样的,保存后再测试",
noProxy: "代理排除",
noProxyPlaceholder: "localhost,127.0.0.1,.example.com,192.168.*",
noProxyHelper: "配置NO_PROXY,多个地址可用英文逗号、空格或换行分隔,命中的请求将不走代理\n例如:localhost,127.0.0.1,.example.com,192.168.*",
dualStackNetwork: "双栈网络",
ipv4Priority: "IPV4优先",
ipv6Priority: "IPV6优先",
@@ -82,6 +82,7 @@ export const certdResources = [
isMenu: true,
icon: "ion:duplicate-outline",
auth: true,
keepAlive: true,
},
},
{
@@ -282,6 +283,7 @@ export const certdResources = [
meta: {
icon: "ion:barcode-outline",
auth: true,
keepAlive: true,
isMenu: true,
},
},
@@ -350,6 +352,7 @@ export const certdResources = [
},
icon: "ion:gift-outline",
auth: true,
keepAlive: true,
},
},
{
@@ -112,7 +112,7 @@ export const sysResources = [
},
{
title: "certd.sysResources.headerMenus",
name: "HeaderMenus",
name: "SettingsHeaderMenus",
path: "/sys/settings/header-menus",
component: "/sys/settings/header-menus/index.vue",
meta: {
@@ -128,7 +128,7 @@ export const sysResources = [
},
{
title: "certd.sysResources.sysAccess",
name: "SysAccess",
name: "SysAccessManager",
path: "/sys/access",
component: "/sys/access/index.vue",
meta: {
@@ -311,7 +311,7 @@ export const sysResources = [
},
icon: "ion:bag-check",
permission: "sys:settings:edit",
keepAlive: true,
keepAlive: false,
auth: true,
},
},
@@ -105,6 +105,7 @@ export type InviteSetting = {
export type SysPrivateSetting = {
httpProxy?: string;
httpsProxy?: string;
noProxy?: string;
commonHeaders?: string;
reverseProxies?: any;
dnsResultOrder?: string;
@@ -0,0 +1,24 @@
import { onActivated, onMounted } from "vue";
/**
* 可靠的页面刷新钩子:
* - 如果组件实际被 KeepAlive 缓存命中,由 onActivated 触发 init
* - 如果没有被缓存,由 onMounted 兜底触发,避免不刷新也不触发 onActivated。
*/
export function useMounted(init: () => void | Promise<void>) {
let activated = false;
onActivated(() => {
activated = true;
init();
});
onMounted(() => {
// 让 onActivated 有机会先执行;组件未被 KeepAlive 缓存时 onActivated 不会触发,由这里兜底。
setTimeout(() => {
if (!activated) {
init();
}
});
});
}
@@ -11,7 +11,8 @@
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { defineComponent } from "vue";
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createAccessApi } from "/@/views/certd/access/api";
@@ -23,14 +24,7 @@ export default defineComponent({
const { t } = useI18n();
const api = createAccessApi("user");
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api, permission: { isProjectPermission: true } } });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
return {
crudBinding,
@@ -11,7 +11,8 @@
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { defineComponent } from "vue";
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createAddonApi } from "./api";
@@ -26,14 +27,7 @@ export default defineComponent({
createCrudOptions,
context: { api, permission: { isProjectPermission: true } },
});
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
return {
crudBinding,
@@ -11,7 +11,8 @@
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { defineComponent } from "vue";
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
@@ -24,14 +25,7 @@ export default defineComponent({
permission: { isProjectPermission: true },
},
});
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
return {
crudBinding,
@@ -109,7 +109,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
},
rowHandle: {
fixed: "right",
width: 120,
width: 200,
buttons: {
edit: {
click: ({ row }) => openForm(row),
@@ -11,9 +11,9 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
defineOptions({
name: "CertApplyTemplate",
@@ -25,12 +25,5 @@ const { crudBinding, crudRef, crudExpose } = useFs({
permission: { isProjectPermission: true },
},
});
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -25,7 +25,7 @@ const context: any = {
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -62,7 +62,7 @@ const handleBatchDelete = () => {
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -62,7 +62,7 @@ const handleBatchDelete = () => {
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -14,13 +14,13 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "/src/locales";
import createCrudOptions from "./crud";
import { useCrudPermission } from "/@/plugin/permission";
import { useMounted } from "/@/use/use-mounted";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -52,13 +52,6 @@ const handleBatchDelete = () => {
message.error(t("certd.pleaseSelectRecords"));
}
};
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
<style lang="less"></style>
@@ -11,9 +11,9 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -22,12 +22,5 @@ defineOptions({
name: "CertStore",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { permission: { isProjectPermission: true } } });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -22,8 +22,8 @@
<script lang="ts" setup>
import { useFs } from "@fast-crud/fast-crud";
import { onActivated, onMounted } from "vue";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
@@ -37,12 +37,5 @@ const context: any = {
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
const handleBatchDelete = context.handleBatchDelete;
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -27,8 +27,8 @@
<script lang="ts" setup>
import { useFs } from "@fast-crud/fast-crud";
import { onActivated, onMounted } from "vue";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
@@ -42,12 +42,5 @@ const context: any = {
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
const handleBatchDelete = context.handleBatchDelete;
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -5,9 +5,9 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted, ref, Ref } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
defineOptions({
name: "SiteIpCertMonitor",
@@ -21,12 +21,5 @@ const { crudBinding, crudRef, crudExpose } = useFs({
props,
},
});
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -11,7 +11,8 @@
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { defineComponent } from "vue";
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createNotificationApi } from "./api";
@@ -23,14 +24,7 @@ export default defineComponent({
const api = createNotificationApi();
notificationProvide(api);
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api, permission: { isProjectPermission: true } } });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
return {
crudBinding,
@@ -11,21 +11,14 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
import { OPEN_API_DOC } from "/@/views/certd/open/openkey/api";
defineOptions({
name: "OpenKey",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { permission: { isProjectPermission: true } } });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -1,4 +1,4 @@
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, useUi } from "@fast-crud/fast-crud";
import { AddReq, ColumnProps, CreateCrudOptionsProps, CreateCrudOptionsRet, DataFormatterContext, DelReq, dict, EditReq, UserPageQuery, UserPageRes, useUi } from "@fast-crud/fast-crud";
import { Modal, notification } from "ant-design-vue";
import dayjs from "dayjs";
import { computed, ref } from "vue";
@@ -75,6 +75,17 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
const projectStore = useProjectStore();
const { myProjectDict } = useDicts();
const DEFAULT_WILL_EXPIRE_DAYS = settingStore.sysPublic.defaultWillExpireDays || settingStore.sysPublic.defaultCertRenewDays || 15;
const pipelineTypeDictData = [
{ value: "cert", label: t("certd.types.certApply") },
{ value: "cert_upload", label: t("certd.types.certUpload") },
{ value: "custom", label: t("certd.types.custom") },
{ value: "template", label: t("certd.types.template") },
{ value: "cert_auto", label: t("certd.types.certApply") },
];
const disabledDictData = [
{ value: false, label: t("certd.fields.enabledLabel") },
{ value: true, label: t("certd.fields.disabledLabel") },
];
function onDialogOpen(opt: any) {
const searchForm = crudExpose.getSearchValidatedFormData();
@@ -84,6 +95,79 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
};
}
function getRecordValue(row: any, key: string) {
return key.split(".").reduce((target, item) => target?.[item], row);
}
function findDictLabel(data: any[], value: any) {
return data.find(item => item.value === value)?.label ?? value;
}
function formatValidTime(value: any) {
if (!value || value <= 0) {
return t("certd.pi.permanentValid");
}
if (value < Date.now()) {
return t("certd.hasExpired");
}
return dayjs(value).format("YYYY-MM-DD");
}
function formatRemainingValidity(lastVars: any) {
const expiresTime = lastVars?.certExpiresTime;
if (!expiresTime) {
return "-";
}
const leftDays = dayjs(expiresTime).diff(dayjs(), "day");
if (leftDays < 0) {
return t("certd.hasExpired");
}
return `${leftDays}${t("certd.days")}`;
}
function formatListValue(value: any) {
if (Array.isArray(value)) {
return value.join(",");
}
return value ?? "";
}
function exportColumnFilter(col: ColumnProps) {
if (!col.key || ["_index", "_selection", "rowHandle"].includes(col.key)) {
return false;
}
if (col.key === "lastVars.certDomains") {
return true;
}
return col.show !== false;
}
function exportDataFormatter(opts: DataFormatterContext) {
const { row, originalRow, col, exportCol } = opts;
const key = col.key;
const value = getRecordValue(originalRow, key);
if (key === "validTime") {
row[key] = formatValidTime(value);
} else if (key === "lastVars") {
row[key] = formatRemainingValidity(value);
} else if (key === "lastVars.certDomains") {
row[key] = formatListValue(value);
} else if (key === "status") {
row[key] = statusUtil.get(value)?.label ?? value;
} else if (key === "disabled") {
row[key] = findDictLabel(disabledDictData, value);
} else if (key === "type") {
row[key] = findDictLabel(pipelineTypeDictData, value);
} else if (key.includes("Time") && value) {
row[key] = dayjs(value).format("YYYY-MM-DD HH:mm:ss");
}
if (col.width) {
exportCol.width = col.width / 10;
}
}
return {
crudOptions: {
request: {
@@ -178,6 +262,18 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
confirmMessage: t("certd.table.confirmDeleteMessage"),
},
},
toolbar: {
buttons: {
export: {
show: true,
},
},
export: {
dataFrom: "search",
columnFilter: exportColumnFilter,
dataFormatter: exportDataFormatter,
},
},
tabs: {
name: "groupId",
show: true,
@@ -419,6 +515,19 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
width: 150,
},
},
"lastVars.certDomains": {
title: t("certd.fields.certDomains"),
type: "text",
form: {
show: false,
},
column: {
width: 260,
show: false,
ellipsis: true,
showTitle: true,
},
},
"lastVars.certEffectiveTime": {
title: t("certd.fields.effectiveTime"),
search: {
@@ -503,10 +612,7 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
},
},
dict: dict({
data: [
{ value: false, label: t("certd.fields.enabledLabel") },
{ value: true, label: t("certd.fields.disabledLabel") },
],
data: disabledDictData,
}),
form: {
value: false,
@@ -563,13 +669,7 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
col: { span: 2 },
},
dict: dict({
data: [
{ value: "cert", label: t("certd.types.certApply") },
{ value: "cert_upload", label: t("certd.types.certUpload") },
{ value: "custom", label: t("certd.types.custom") },
{ value: "template", label: t("certd.types.template") },
{ value: "cert_auto", label: t("certd.types.certApply") },
],
data: pipelineTypeDictData,
}),
form: {
show: false,
@@ -650,7 +750,7 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
align: "center",
cellRender({ value }) {
if (!value || value <= 0) {
return "-";
return t("certd.pi.permanentValid");
}
if (value < Date.now()) {
return t("certd.hasExpired");
@@ -11,7 +11,8 @@
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { defineComponent } from "vue";
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
@@ -24,14 +25,7 @@ export default defineComponent({
permission: { isProjectPermission: true },
},
});
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
return {
crudBinding,
@@ -129,7 +129,7 @@ const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
@@ -37,7 +37,7 @@ import { useCrudPermission } from "/@/plugin/permission";
const { t } = useI18n();
defineOptions({
name: "CnameRecord",
name: "SubDomain",
});
const context: any = {
permission: {
@@ -68,7 +68,7 @@ const handleBatchDelete = () => {
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -14,9 +14,9 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
import { useI18n } from "/src/locales";
defineOptions({
name: "PipelineTemplate",
@@ -28,11 +28,5 @@ const { crudBinding, crudRef, crudExpose } = useFs({
},
});
const { t } = useI18n();
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -28,6 +28,7 @@
<script lang="ts" setup>
import { onActivated, onMounted, Ref, ref } from "vue";
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
@@ -117,13 +118,12 @@ onMounted(async () => {
return;
}
await loadProjectDetail();
await crudExpose.doRefresh();
if (migrate === "true") {
openTransferDialog();
}
});
onActivated(async () => {
useMounted(async () => {
await crudExpose.doRefresh();
});
</script>
@@ -53,7 +53,7 @@ const handleBatchDelete = () => {
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -15,14 +15,14 @@
</template>
<script lang="ts" setup>
import { computed, onActivated, onMounted, ref } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import { computed, ref } from "vue";
import createCrudOptions from "./crud";
import { mySuiteApi, SuiteDetail } from "/@/views/certd/suite/mine/api";
import SuiteCard from "/@/views/framework/home/dashboard/suite-card.vue";
import { useMounted } from "/@/use/use-mounted";
defineOptions({
name: "MySuites",
name: "MySuite",
});
const detail = ref<SuiteDetail>({});
const currentSuite = computed(() => {
@@ -39,11 +39,8 @@ async function loadSuiteDetail() {
}
// 页面打开后获取列表数据
onMounted(async () => {
useMounted(async () => {
await loadSuiteDetail();
await crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
</script>
@@ -19,7 +19,7 @@ const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -11,7 +11,8 @@
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { defineComponent } from "vue";
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "../../certd/access/crud";
import { createAccessApi } from "/@/views/certd/access/api";
@@ -21,15 +22,7 @@ export default defineComponent({
setup() {
const api = createAccessApi("sys");
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api } });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
return {
crudBinding,
@@ -22,7 +22,7 @@ import { useFs, useUi } from "@fast-crud/fast-crud";
import { useI18n } from "/src/locales";
export default defineComponent({
name: "AuthorityManager",
name: "PermissionManager",
components: { FsPermissionTree },
setup() {
// 此处传入permission进行通用按钮权限设置,会通过commonOptions去设置actionbar和rowHandle的按钮的show属性
@@ -32,7 +32,7 @@ export default defineComponent({
// 页面打开后获取列表数据
onMounted(async () => {
await crudExpose.doRefresh();
// await crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -11,14 +11,15 @@
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted, ref } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import * as permissionApi from "../permission/api";
import * as api from "./api";
import { message } from "ant-design-vue";
import { defineComponent, ref } from "vue";
import * as permissionApi from "../permission/api";
import FsPermissionTree from "../permission/fs-permission-tree.vue";
import * as api from "./api";
import createCrudOptions from "./crud";
import { UseCrudPermissionCompProps, UseCrudPermissionExtraProps } from "/@/plugin/permission";
import { useMounted } from "/@/use/use-mounted";
import { useI18n } from "/src/locales";
function useAuthz() {
@@ -104,15 +105,7 @@ export default defineComponent({
// 更多关于按钮权限的源代码设置,请参考 ./src/plugin/fast-crud/index.js 75-77行)
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { authz, permission } });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
return {
crudBinding,
crudRef,
@@ -8,9 +8,10 @@
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onActivated } from "vue";
import { useCrud, useExpose, useFs } from "@fast-crud/fast-crud";
import { useFs } from "@fast-crud/fast-crud";
import { defineComponent } from "vue";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
export default defineComponent({
name: "UserManager",
setup() {
@@ -18,14 +19,7 @@ export default defineComponent({
// 此处传入权限前缀进行通用按钮权限设置,会通过commonOptions去设置actionbar和rowHandle的按钮的show属性
// 更多关于按钮权限的源代码设置,请参考 ./src/plugin/fast-crud/index.js 75-77行)
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { permission: "sys:auth:user" } });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
return {
crudBinding,
@@ -32,7 +32,7 @@ import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
name: "CnameProvider",
name: "CnameSetting",
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
@@ -56,7 +56,7 @@ const handleBatchDelete = () => {
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -56,7 +56,7 @@ const handleBatchDelete = () => {
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -62,7 +62,7 @@ const handleBatchDelete = () => {
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -53,7 +53,7 @@ const handleBatchDelete = () => {
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -15,7 +15,7 @@
<script lang="ts" setup>
import { useFs } from "@fast-crud/fast-crud";
import { onActivated, onMounted } from "vue";
import { useMounted } from "/@/use/use-mounted";
import createCrudOptions from "./crud";
defineOptions({
@@ -25,11 +25,5 @@ defineOptions({
const context: any = {};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
const handleBatchDelete = context.handleBatchDelete;
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -1,5 +1,5 @@
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import { CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { ColumnProps, CreateCrudOptionsProps, CreateCrudOptionsRet, DataFormatterContext, DelReq, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { message, Modal } from "ant-design-vue";
import dayjs from "dayjs";
import { ref } from "vue";
@@ -18,6 +18,77 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
};
const selectedRowKeys = ref<number[]>([]);
const pipelineTypeDictData = [
{ value: "cert", label: "证书申请" },
{ value: "cert_upload", label: "证书上传" },
{ value: "custom", label: "自定义" },
{ value: "template", label: "模板" },
{ value: "cert_auto", label: "证书申请" },
];
const disabledDictData = [
{ label: "启用", value: false, color: "green" },
{ label: "禁用", value: true, color: "red" },
];
function findDictLabel(data: any[], value: any) {
return data.find(item => item.value === value)?.label ?? value;
}
function formatValidTime(value: any) {
if (!value || value <= 0) {
return "永久有效";
}
if (value < Date.now()) {
return "已过期";
}
return dayjs(value).format("YYYY-MM-DD");
}
function getRecordValue(row: any, key: string) {
return key.split(".").reduce((target, item) => target?.[item], row);
}
function formatListValue(value: any) {
if (Array.isArray(value)) {
return value.join(",");
}
return value ?? "";
}
function exportColumnFilter(col: ColumnProps) {
if (!col.key || ["_index", "_selection", "rowHandle"].includes(col.key)) {
return false;
}
if (col.key === "lastVars.certDomains") {
return true;
}
return col.show !== false;
}
function exportDataFormatter(opts: DataFormatterContext) {
const { row, originalRow, col, exportCol } = opts;
const key = col.key;
const value = getRecordValue(originalRow, key);
if (key === "validTime") {
row[key] = formatValidTime(value);
} else if (key === "lastVars.certDomains") {
row[key] = formatListValue(value);
} else if (key === "status") {
row[key] = statusUtil.get(value)?.label ?? value;
} else if (key === "disabled") {
row[key] = findDictLabel(disabledDictData, value);
} else if (key === "type") {
row[key] = findDictLabel(pipelineTypeDictData, value);
} else if (key.includes("Time") && value) {
row[key] = dayjs(value).format("YYYY-MM-DD HH:mm:ss");
}
if (col.width) {
exportCol.width = col.width / 10;
}
}
const handleBatchDelete = () => {
if (!selectedRowKeys.value?.length) {
message.error("请先选择要删除的记录");
@@ -54,6 +125,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
export: {
dataFrom: "search",
columnFilter: exportColumnFilter,
dataFormatter: exportDataFormatter,
},
},
pagination: {
@@ -185,13 +258,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
dict: dict({
data: [
{ value: "cert", label: "证书申请" },
{ value: "cert_upload", label: "证书上传" },
{ value: "custom", label: "自定义" },
{ value: "template", label: "模板" },
{ value: "cert_auto", label: "证书申请" },
],
data: pipelineTypeDictData,
}),
column: {
width: 110,
@@ -236,10 +303,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
dict: dict({
data: [
{ label: "启用", value: false, color: "green" },
{ label: "禁用", value: true, color: "red" },
],
data: disabledDictData,
}),
column: {
width: 90,
@@ -273,6 +337,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: false,
},
},
"lastVars.certDomains": {
title: "证书域名",
type: "text",
column: {
width: 260,
show: false,
ellipsis: true,
},
form: {
show: false,
},
},
lastHistoryTime: {
title: "最后执行时间",
type: "datetime",
@@ -306,7 +382,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
align: "center",
cellRender({ value }) {
if (!value || value <= 0) {
return "-";
return "永久有效";
}
if (value < Date.now()) {
return <span style={{ color: "red" }}></span>;
@@ -14,8 +14,8 @@
</template>
<script lang="ts" setup>
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import { onActivated, onMounted } from "vue";
import createCrudOptions from "./crud";
defineOptions({
@@ -25,11 +25,5 @@ defineOptions({
const context: any = {};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
const handleBatchDelete = context.handleBatchDelete;
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -21,7 +21,7 @@ const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions
const settingStore = useSettingStore();
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -14,6 +14,11 @@
<div class="helper">{{ t("certd.httpsProxyHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.noProxy')" :name="['private', 'noProxy']">
<a-textarea v-model:value="formState.private.noProxy" :placeholder="t('certd.noProxyPlaceholder')" rows="3" />
<div class="helper">{{ t("certd.noProxyHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.environmentVars')" :name="['private', 'environmentVars']">
<a-textarea v-model:value="formState.private.environmentVars" :placeholder="environmentVarsExample" rows="4" />
<div class="helper">{{ t("certd.sys.setting.environmentVarsHelper") }}</div>
@@ -123,4 +123,12 @@ const loginLogoCropperOptions = ref({
border-radius: 0 !important;
margin-top: 0 !important;
}
.page-sys-site {
.sys-settings-form {
width: 900px;
max-width: 100%;
padding: 20px;
}
}
</style>
@@ -16,13 +16,13 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
defineOptions({
name: "ProductActivationCodeManager",
name: "SysProductActivationCode",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
onMounted(() => {
crudExpose.doRefresh();
// crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -46,12 +46,13 @@
</template>
<script lang="ts" setup>
import { computed, onActivated, onMounted } from "vue";
import { Modal, notification } from "ant-design-vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud-level";
import { Modal, notification } from "ant-design-vue";
import { computed } from "vue";
import * as api from "./api";
import createCrudOptions from "./crud-level";
import { util } from "/@/utils";
import { useMounted } from "/@/use/use-mounted";
defineOptions({ name: "SysInviteLevel" });
@@ -92,13 +93,7 @@ function confirmRemove(opts: any) {
},
});
}
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
<style lang="less">
@@ -8,18 +8,12 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud-user-level";
defineOptions({ name: "SysInviteUserLevel" });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -8,18 +8,12 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud-withdraw";
defineOptions({ name: "SysInviteWithdraw" });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
@@ -11,21 +11,15 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
defineOptions({
name: "TradeManager",
name: "OrderManager",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
<style lang="less"></style>
@@ -11,7 +11,7 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useMounted } from "/@/use/use-mounted";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
@@ -19,12 +19,5 @@ defineOptions({
name: "UserSuites",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
useMounted(() => crudExpose.doRefresh());
</script>
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.41.1](https://github.com/certd/certd/compare/v1.41.0...v1.41.1) (2026-06-05)
### Performance Improvements
* 流水线、监控站点支持导出 ([99fd308](https://github.com/certd/certd/commit/99fd3083f259cdb96fd656f04858dd708d1251c7))
* 优化邀请注册流程 ([7a71e45](https://github.com/certd/certd/commit/7a71e45799d782d0691606fb42b4236f1d3009b0))
* **volcengine-vke:** 火山VKE集群证书支持两种类型的证书保密字典 ([77b8024](https://github.com/certd/certd/commit/77b802445322d576d54d194f7c505da49e0e824c))
# [1.41.0](https://github.com/certd/certd/compare/v1.40.5...v1.41.0) (2026-06-04)
### Bug Fixes
@@ -7,6 +7,7 @@ title: 火山引擎-替换VKE证书
icon: svg:icon-volcengine
group: volcengine
desc: 替换火山引擎VKE集群中的TLS Secret证书
needPlus: true
input:
cert:
title: 域名证书
@@ -15,6 +16,7 @@ input:
name: output-selector
from:
- ':cert:'
- VolcengineUploadToCertCenter
required: true
order: 0
certDomains:
@@ -118,11 +120,11 @@ input:
component:
name: a-select
options:
- label: 按Ingress替换
value: ingress
- label: 按Secret替换
value: secret
value: ingress
- label: 按Ingress替换
value: ingress
value: secret
required: true
order: 0
ingressName:
@@ -140,23 +142,37 @@ input:
secretName:
title: Secret名称
required: true
helper: 存储TLS证书的Secret名称,可填写多个
helper: 选择要替换的Secret,可多选
component:
name: a-select
name: remote-select
vModel: value
mode: tags
open: false
type: plugin
action: onGetSecretList
search: false
pager: false
single: false
watches:
- certDomains
- accessId
- regionId
- clusterId
- kubeconfigType
- namespace
mergeScript: |2-
return {
show: ctx.compute(({form}) => form.targetType === 'secret'),
required: ctx.compute(({form}) => form.targetType === 'secret')
required: ctx.compute(({form}) => form.targetType === 'secret'),
component: {
form: ctx.compute(({form}) => form)
}
}
order: 0
createOnNotFound:
title: Secret自动创建
helper: 如果Secret不存在,则创建kubernetes.io/tls类型Secret
helper: 如果Secret不存在,则创建Opaque类型Secret
value: false
component:
name: a-switch
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.41.0",
"version": "1.41.1",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -54,20 +54,20 @@
"@aws-sdk/client-sts": "^3.990.0",
"@azure/arm-dns": "^5.1.0",
"@azure/identity": "^4.13.1",
"@certd/acme-client": "^1.41.0",
"@certd/basic": "^1.41.0",
"@certd/commercial-core": "^1.41.0",
"@certd/acme-client": "^1.41.1",
"@certd/basic": "^1.41.1",
"@certd/commercial-core": "^1.41.1",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.41.0",
"@certd/lib-huawei": "^1.41.0",
"@certd/lib-k8s": "^1.41.0",
"@certd/lib-server": "^1.41.0",
"@certd/midway-flyway-js": "^1.41.0",
"@certd/pipeline": "^1.41.0",
"@certd/plugin-cert": "^1.41.0",
"@certd/plugin-lib": "^1.41.0",
"@certd/plugin-plus": "^1.41.0",
"@certd/plus-core": "^1.41.0",
"@certd/jdcloud": "^1.41.1",
"@certd/lib-huawei": "^1.41.1",
"@certd/lib-k8s": "^1.41.1",
"@certd/lib-server": "^1.41.1",
"@certd/midway-flyway-js": "^1.41.1",
"@certd/pipeline": "^1.41.1",
"@certd/plugin-cert": "^1.41.1",
"@certd/plugin-lib": "^1.41.1",
"@certd/plugin-plus": "^1.41.1",
"@certd/plus-core": "^1.41.1",
"@google-cloud/dns": "^5.3.1",
"@google-cloud/publicca": "^1.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "3.1.185",
@@ -30,16 +30,23 @@ export class AwsClient {
},
});
const cert = certInfo.crt.split("-----END CERTIFICATE-----")[0] + "-----END CERTIFICATE-----";
// Split the full PEM chain: first block is the leaf cert, the rest is the intermediate chain
const pemBlocks = certInfo.crt.split(/(?<=-----END CERTIFICATE-----)/);
const cert = pemBlocks[0].trim();
const chain = pemBlocks
.slice(1)
.join("")
.trim();
// 构建上传参数
const data = await acmClient.send(
new ImportCertificateCommand({
Certificate: Buffer.from(cert),
PrivateKey: Buffer.from(certInfo.key),
// CertificateChain: certificateChain, // 可选
CertificateChain: chain ? Buffer.from(chain) : undefined,
})
);
console.log("Upload successful:", data);
this.logger.info(`Upload successful: ${data.CertificateArn}`);
// 返回证书 ARNAmazon Resource Name
return data.CertificateArn;
}
@@ -102,6 +102,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
this._result.pipelineVars.certEffectiveTime = dayjs(certReader.detail.notBefore).valueOf();
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
this._result.pipelineVars.certDomains = certReader.getAllDomains();
if (!this._result.pipelinePrivateVars) {
this._result.pipelinePrivateVars = {};
}
@@ -3,6 +3,7 @@
import assert from "node:assert/strict";
import { VolcengineDeployToVKE } from "./plugin-deploy-to-vke.js";
import { CertInfo } from "@certd/plugin-cert";
describe("VolcengineDeployToVKE", () => {
it("uses a single-select cluster field", () => {
@@ -86,4 +87,65 @@ describe("VolcengineDeployToVKE", () => {
/当前命名空间可用Ingress:app-web,api-web/
);
});
it("creates Secret with cert_center format for new non-tls secrets", async () => {
const plugin = new VolcengineDeployToVKE();
plugin.namespace = "default";
plugin.targetType = "secret";
plugin.secretName = "test-tls";
plugin.createOnNotFound = true;
plugin.logger = { info: () => undefined } as any;
plugin.appendTimeSuffix = (s: string) => s + "-test";
let secretBody: any;
await (plugin as any).patchCertSecret({
certId: "cert-abc123",
k8sClient: {
patchSecret: async (opts: any) => {
secretBody = opts.body;
return {};
},
client: {
readNamespacedSecret: async () => {
throw Object.assign(new Error("Not Found"), { response: { body: { code: 404 } } });
},
},
},
secretNames: ["test-tls"],
});
assert.equal(secretBody.type, "Opaque");
assert.equal(secretBody.data["cert_id"], Buffer.from("cert-abc123").toString("base64"));
assert.equal(secretBody.data["cert_source"], Buffer.from("cert_center").toString("base64"));
});
it("uses tls.crt/tls.key format for kubernetes.io/tls secrets", async () => {
const plugin = new VolcengineDeployToVKE();
plugin.namespace = "default";
plugin.targetType = "secret";
plugin.secretName = "test-tls";
plugin.logger = { info: () => undefined } as any;
plugin.appendTimeSuffix = (s: string) => s + "-test";
plugin.cert = { crt: "MY_CRT", key: "MY_KEY" } as CertInfo;
let secretBody: any;
await (plugin as any).patchCertSecret({
certId: "cert-abc123",
k8sClient: {
patchSecret: async (opts: any) => {
secretBody = opts.body;
return {};
},
client: {
readNamespacedSecret: async () => ({
body: { type: "kubernetes.io/tls" },
}),
},
},
secretNames: ["test-tls"],
});
assert.equal(secretBody.data["tls.crt"], Buffer.from("MY_CRT").toString("base64"));
assert.equal(secretBody.data["tls.key"], Buffer.from("MY_KEY").toString("base64"));
});
});
@@ -1,9 +1,10 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { utils } from "@certd/basic";
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { AbstractPlusTaskPlugin } from "@certd/plugin-plus";
import { VolcengineAccess } from "../access.js";
import { VolcengineClient } from "../ve-client.js";
import { utils } from "@certd/basic";
const regionOptions = [
{ label: "北京", value: "cn-beijing" },
@@ -20,23 +21,24 @@ const regionOptions = [
icon: "svg:icon-volcengine",
group: pluginGroups.volcengine.key,
desc: "替换火山引擎VKE集群中的TLS Secret证书",
needPlus:true,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class VolcengineDeployToVKE extends AbstractTaskPlugin {
export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin {
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames],
from: [...CertApplyPluginNames, "VolcengineUploadToCertCenter"],
},
required: true,
})
cert!: CertInfo;
cert!: CertInfo | string;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@@ -107,11 +109,11 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
component: {
name: "a-select",
options: [
{ label: "按Ingress替换", value: "ingress" },
{ label: "按Secret替换", value: "secret" },
{ label: "按Ingress替换", value: "ingress" },
],
},
value: "ingress",
value: "secret",
required: true,
})
targetType!: "ingress" | "secret";
@@ -132,17 +134,25 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
@TaskInput({
title: "Secret名称",
required: true,
helper: "存储TLS证书的Secret名称,可填写多个",
helper: "选择要替换的Secret,可多选",
component: {
name: "a-select",
name: "remote-select",
vModel: "value",
mode: "tags",
open: false,
type: "plugin",
action: "onGetSecretList",
search: false,
pager: false,
single: false,
watches: ["certDomains", "accessId", "regionId", "clusterId", "kubeconfigType", "namespace"],
},
mergeScript: `
return {
show: ctx.compute(({form}) => form.targetType === 'secret'),
required: ctx.compute(({form}) => form.targetType === 'secret')
required: ctx.compute(({form}) => form.targetType === 'secret'),
component: {
form: ctx.compute(({form}) => form)
}
}
`,
})
@@ -150,7 +160,7 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
@TaskInput({
title: "Secret自动创建",
helper: "如果Secret不存在,则创建kubernetes.io/tls类型Secret",
helper: "如果Secret不存在,则创建Opaque类型Secret",
value: false,
component: {
name: "a-switch",
@@ -181,6 +191,23 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
this.logger.info("开始替换火山引擎VKE证书");
const access = await this.getAccess<VolcengineAccess>(this.accessId);
const vkeService = await this.getVkeService(access);
// 上传证书到证书中心
let certId: string;
if (typeof this.cert !== "string") {
const certInfo = this.cert as CertInfo;
this.logger.info("开始上传证书到证书中心");
const certService = await this.getCertService(access);
certId = await certService.ImportCertificate({
certName: this.appendTimeSuffix("certd"),
cert: certInfo,
});
this.logger.info("上传证书到证书中心成功:" + certId);
} else {
certId = this.cert;
this.logger.info("使用已有证书中心ID:" + certId);
}
const kubeconfigId = await this.createKubeconfig(vkeService);
try {
@@ -191,7 +218,7 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
skipTLSVerify: this.skipTLSVerify,
});
const secretNames = await this.getTargetSecretNames(k8sClient);
await this.patchCertSecret({ cert: this.cert, k8sClient, secretNames });
await this.patchCertSecret({ certId, k8sClient, secretNames });
} catch (e) {
if (e.response?.body) {
throw new Error(this.formatK8sError(e.response.body));
@@ -205,6 +232,15 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
this.logger.info("VKE证书替换完成");
}
private async getCertService(access: VolcengineAccess) {
const client = new VolcengineClient({
logger: this.logger,
access,
http: this.http,
});
return await client.getCertCenterService();
}
private async getVkeService(access: VolcengineAccess) {
const client = new VolcengineClient({
logger: this.logger,
@@ -293,6 +329,11 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
}
private formatK8sError(body: any) {
if (body?.code === 422 && body?.message?.includes("field is immutable")) {
const secretName = body.details?.name || "未知";
return `Secret类型不可变:Secret ${secretName} 已是kubernetes.io/tls类型,type字段不可修改。\n请删除该Secret后重试,或选择正确的Secret。\n原始错误:${JSON.stringify(body)}`;
}
if (body?.code !== 403 || body?.reason !== "Forbidden") {
return JSON.stringify(body);
}
@@ -334,34 +375,71 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
return secretNames;
}
private async patchCertSecret(options: { cert: CertInfo; k8sClient: any; secretNames: string[] }) {
const { cert, k8sClient, secretNames } = options;
private async patchCertSecret(options: { certId: string; k8sClient: any; secretNames: string[] }) {
const { certId, k8sClient, secretNames } = options;
if (!secretNames || secretNames.length === 0) {
throw new Error("Secret名称不能为空");
}
const body: any = {
data: {
"tls.crt": Buffer.from(cert.crt).toString("base64"),
"tls.key": Buffer.from(cert.key).toString("base64"),
},
metadata: {
labels: {
certd: this.appendTimeSuffix("certd"),
},
},
};
for (const secretName of secretNames) {
let useTlsFormat = false;
try {
const res = await k8sClient.client.readNamespacedSecret(secretName, this.namespace);
useTlsFormat = res.body?.type === "kubernetes.io/tls";
} catch (e) {
// Secret 不存在,将走创建逻辑
}
let body: any;
if (useTlsFormat) {
let crt: string;
let key: string;
if (typeof this.cert === "string") {
const access = await this.getAccess<VolcengineAccess>(this.accessId);
const certService = await this.getCertService(access);
const detail = await certService.GetCertificateDetail(this.cert);
crt = detail.CertificateChain || "";
key = detail.PrivateKey || "";
this.logger.info("从证书中心获取证书详情成功");
} else {
crt = this.cert.crt;
key = this.cert.key;
}
body = {
data: {
"tls.crt": Buffer.from(crt).toString("base64"),
"tls.key": Buffer.from(key).toString("base64"),
},
metadata: {
labels: {
certd: this.appendTimeSuffix("certd"),
},
},
};
} else {
body = {
type: "Opaque",
data: {
cert_id: Buffer.from(certId).toString("base64"),
cert_source: Buffer.from("cert_center").toString("base64"),
},
metadata: {
labels: {
certd: this.appendTimeSuffix("certd"),
},
},
};
}
body.metadata.name = secretName;
this.logger.info(`开始更新VKE Secret:${secretName}`);
this.logger.info("开始更新VKE Secret:" + secretName);
await k8sClient.patchSecret({
namespace: this.namespace,
secretName,
body,
createOnNotFound: this.createOnNotFound,
});
this.logger.info(`VKE Secret已更新:${secretName}`);
this.logger.info("VKE Secret已更新:" + secretName);
}
if (this.targetType === "ingress" && this.ingressName) {
@@ -389,6 +467,35 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
value: item.Id,
}));
}
async onGetSecretList() {
if (!this.accessId) {
throw new Error("请选择Access授权");
}
if (!this.clusterId) {
throw new Error("请选择VKE集群");
}
const access = await this.getAccess<VolcengineAccess>(this.accessId);
const vkeService = await this.getVkeService(access);
const kubeconfigId = await this.createKubeconfig(vkeService);
try {
const kubeconfig = await this.getKubeconfig(vkeService, kubeconfigId);
const k8sClient = new this.K8sClient({
kubeConfigStr: kubeconfig,
logger: this.logger,
skipTLSVerify: this.skipTLSVerify,
});
const res = await k8sClient.getSecrets({ namespace: this.namespace || "default" });
const list = res.body?.items || res.items || [];
return list.map((item: any) => ({
label: item.metadata.name,
value: item.metadata.name,
}));
} finally {
await this.deleteKubeconfig(vkeService, kubeconfigId);
}
}
}
new VolcengineDeployToVKE();
new VolcengineDeployToVKE();
@@ -1,4 +1,4 @@
import { VolcengineAccess } from "./access.js";
import { VolcengineAccess } from "./access.js";
import { HttpClient, ILogger } from "@certd/basic";
export type VolcengineOpts = {