This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 208cdfe8 fix(k8s): make certificate management observational (#1536)
208cdfe8 is described below
commit 208cdfe89a13309cb55e664461503bf3ef43a21a
Author: aias00 <[email protected]>
AuthorDate: Tue Aug 11 20:22:07 2026 +0800
fix(k8s): make certificate management observational (#1536)
Signed-off-by: liuhy <[email protected]>
---
.../pages/cluster/__tests__/K8sCertsPage.test.tsx | 34 +---
web/src/pages/cluster/certs.tsx | 216 +--------------------
2 files changed, 10 insertions(+), 240 deletions(-)
diff --git a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
index aa9aa43f..f0fa2f2d 100644
--- a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
@@ -16,19 +16,15 @@
*/
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { App, Modal } from 'antd';
+import { App } from 'antd';
import type { K8sCertInfo } from '../../../api/cluster';
-import { listK8sCerts, renewK8sCert } from '../../../services/clusterService';
+import { listK8sCerts } from '../../../services/clusterService';
import K8sCertsPage from '../certs';
vi.mock('../../../services/clusterService', () => ({
- createK8sCert: vi.fn(),
- deleteK8sCert: vi.fn(),
listK8sCerts: vi.fn(),
- renewK8sCert: vi.fn(),
- updateK8sCert: vi.fn(),
}));
const certs: K8sCertInfo[] = [
@@ -146,27 +142,15 @@ describe('K8sCertsPage', () => {
expect(screen.queryByText('rocketmq-prod-tls')).not.toBeInTheDocument();
});
- it('tracks simultaneous certificate renewals independently', async () => {
- vi.mocked(renewK8sCert).mockImplementation(() => new Promise(() => {}));
- const confirmSpy = vi.spyOn(Modal, 'confirm');
+ it('does not expose local metadata mutations as Kubernetes certificate
operations', async () => {
renderPage();
await screen.findByText('rocketmq-prod-tls');
- const renewButtons = screen.getAllByRole('button', { name: /续期/ });
- fireEvent.click(renewButtons[0]);
- fireEvent.click(renewButtons[1]);
- expect(confirmSpy).toHaveBeenCalledTimes(2);
-
- void confirmSpy.mock.calls[0][0].onOk?.(() => {});
- void confirmSpy.mock.calls[1][0].onOk?.(() => {});
-
- await waitFor(() => {
- expect(renewK8sCert).toHaveBeenCalledWith('cert-prod');
- expect(renewK8sCert).toHaveBeenCalledWith('cert-staging');
- expect(renewButtons[0]).toHaveClass('ant-btn-loading');
- expect(renewButtons[1]).toHaveClass('ant-btn-loading');
- });
- confirmSpy.mockRestore();
+
+ expect(screen.queryByRole('button', { name: '添加证书'
})).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: '编辑'
})).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: '续期'
})).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: '删除'
})).not.toBeInTheDocument();
});
it('trims certificate search text before filtering', async () => {
diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx
index 9a971891..c06e8dba 100644
--- a/web/src/pages/cluster/certs.tsx
+++ b/web/src/pages/cluster/certs.tsx
@@ -19,11 +19,8 @@ import { useEffect, useState } from 'react';
import {
Table,
Tag,
- Button,
Input,
Select,
- Modal,
- Form,
Flex,
Space,
Typography,
@@ -32,16 +29,9 @@ import {
message,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
-import { EditOutlined, DeleteOutlined, PlusOutlined, SyncOutlined } from
'@ant-design/icons';
import PageHeader from '../../components/PageHeader';
import type { K8sCertInfo } from '../../api/cluster';
-import {
- createK8sCert,
- deleteK8sCert,
- listK8sCerts,
- renewK8sCert,
- updateK8sCert,
-} from '../../services/clusterService';
+import { listK8sCerts } from '../../services/clusterService';
const { Text } = Typography;
@@ -57,14 +47,9 @@ const getErrorMessage = (error: unknown): string =>
const K8sCertsPage = () => {
const [certs, setCerts] = useState<K8sCertInfo[]>([]);
const [loading, setLoading] = useState(true);
- const [submitting, setSubmitting] = useState(false);
- const [renewingIds, setRenewingIds] = useState<Set<string>>(() => new Set());
const [certSearch, setCertSearch] = useState('');
const [certTypeFilter, setCertTypeFilter] = useState<string>('');
const [certNamespaceFilter, setCertNamespaceFilter] = useState<string>('');
- const [editModalOpen, setEditModalOpen] = useState(false);
- const [editingCert, setEditingCert] = useState<K8sCertInfo | null>(null);
- const [editForm] = Form.useForm();
useEffect(() => {
let active = true;
@@ -83,54 +68,6 @@ const K8sCertsPage = () => {
};
}, []);
- const openCreateModal = () => {
- setEditingCert(null);
- editForm.resetFields();
- editForm.setFieldsValue({ type: 'TLS', namespace: 'default' });
- setEditModalOpen(true);
- };
-
- const closeEditModal = () => {
- setEditModalOpen(false);
- setEditingCert(null);
- editForm.resetFields();
- };
-
- const saveCert = async () => {
- const values = await editForm.validateFields();
- const data = {
- name: values.name,
- namespace: values.namespace,
- cluster: values.cluster,
- type: values.type,
- issuer: values.issuer,
- san: values.san
- ? String(values.san)
- .split(',')
- .map((value) => value.trim())
- .filter(Boolean)
- : [],
- };
-
- setSubmitting(true);
- try {
- if (editingCert) {
- const updated = await updateK8sCert({ id: editingCert.id, ...data });
- setCerts((prev) => prev.map((cert) => (cert.id === updated.id ?
updated : cert)));
- message.success(`证书「${updated.name}」已更新`);
- } else {
- const created = await createK8sCert(data);
- setCerts((prev) => [...prev, created]);
- message.success(`证书「${created.name}」已创建`);
- }
- closeEditModal();
- } catch (error) {
- message.error(getErrorMessage(error));
- } finally {
- setSubmitting(false);
- }
- };
-
const normalizedCertSearch = certSearch.trim().toLowerCase();
const namespaceOptions = Array.from(new Set(certs.map((cert) =>
cert.namespace)))
.sort((a, b) => a.localeCompare(b))
@@ -146,24 +83,6 @@ const K8sCertsPage = () => {
return matchSearch && matchType && matchNamespace;
});
- const renewCert = async (cert: K8sCertInfo) => {
- setRenewingIds((current) => new Set(current).add(cert.id));
- try {
- const renewed = await renewK8sCert(cert.id);
- setCerts((prev) => prev.map((item) => (item.id === renewed.id ? renewed
: item)));
- message.success(`证书「${renewed.name}」已续期`);
- } catch (error) {
- message.error(getErrorMessage(error));
- throw error;
- } finally {
- setRenewingIds((current) => {
- const next = new Set(current);
- next.delete(cert.id);
- return next;
- });
- }
- };
-
const certColumns: ColumnsType<K8sCertInfo> = [
{
title: 'K8s 集群名称',
@@ -274,76 +193,6 @@ const K8sCertsPage = () => {
return <Tag color={cfg.color}>{cfg.label}</Tag>;
},
},
- {
- title: '操作',
- key: 'action',
- width: 270,
- render: (_: unknown, record: K8sCertInfo) => (
- <Flex gap={6}>
- <Button
- size="small"
- icon={<EditOutlined />}
- style={{ borderColor: '#1677ff', color: '#1677ff' }}
- onClick={() => {
- setEditingCert(record);
- editForm.setFieldsValue({
- name: record.name,
- type: record.type,
- issuer: record.issuer,
- namespace: record.namespace,
- cluster: record.cluster,
- san: record.san?.join(', ') ?? '',
- });
- setEditModalOpen(true);
- }}
- >
- 编辑
- </Button>
- <Button
- size="small"
- icon={<SyncOutlined />}
- loading={renewingIds.has(record.id)}
- onClick={() => {
- Modal.confirm({
- title: '确认续期',
- content: `确定要为证书 "${record.name}" 续期一年吗?`,
- okText: '续期',
- cancelText: '取消',
- onOk: () => renewCert(record),
- });
- }}
- >
- 续期
- </Button>
- <Button
- size="small"
- icon={<DeleteOutlined />}
- style={{ borderColor: '#ff4d4f', color: '#ff4d4f' }}
- onClick={() => {
- Modal.confirm({
- title: '确认删除',
- content: `确定要删除证书 "${record.name}" 吗?`,
- okText: '确认',
- cancelText: '取消',
- okButtonProps: { danger: true },
- onOk: async () => {
- try {
- await deleteK8sCert(record.id);
- setCerts((prev) => prev.filter((c) => c.id !== record.id));
- message.success(`证书已删除: ${record.name}`);
- } catch (error) {
- message.error(getErrorMessage(error));
- throw error;
- }
- },
- });
- }}
- >
- 删除
- </Button>
- </Flex>
- ),
- },
];
return (
@@ -351,11 +200,6 @@ const K8sCertsPage = () => {
<PageHeader
title="K8s 证书管理"
subtitle={`共 ${filteredCerts.length} 个证书`}
- extra={
- <Button type="primary" icon={<PlusOutlined />}
onClick={openCreateModal}>
- 添加证书
- </Button>
- }
/>
<Alert
data-testid="k8s-cert-local-metadata-notice"
@@ -406,64 +250,6 @@ const K8sCertsPage = () => {
/>
</Card>
- {/* Edit Cert Modal */}
- <Modal
- title={editingCert ? `编辑证书 — ${editingCert.name}` : '添加证书'}
- open={editModalOpen}
- onCancel={closeEditModal}
- onOk={saveCert}
- confirmLoading={submitting}
- okText="保存"
- cancelText="取消"
- width={520}
- >
- <Form form={editForm} layout="vertical" style={{ marginTop: 16 }}>
- <Form.Item
- label="证书名称"
- name="name"
- rules={[{ required: true, message: '请输入证书名称' }]}
- >
- <Input placeholder="例:rocketmq-tls"
disabled={Boolean(editingCert)} />
- </Form.Item>
- <Form.Item
- label="K8s 集群名称"
- name="cluster"
- rules={[{ required: true, message: '请输入集群名称' }]}
- >
- <Input placeholder="例:prod-cluster" />
- </Form.Item>
- <Form.Item
- label="类型"
- name="type"
- rules={[{ required: true, message: '请选择证书类型' }]}
- >
- <Select
- options={[
- { value: 'TLS', label: 'TLS' },
- { value: 'mTLS', label: 'mTLS' },
- { value: 'ServiceAccount', label: 'ServiceAccount' },
- ]}
- />
- </Form.Item>
- <Form.Item
- label="签发者"
- name="issuer"
- rules={[{ required: true, message: '请输入签发者' }]}
- >
- <Input placeholder="例:kubernetes-ca" />
- </Form.Item>
- <Form.Item
- label="命名空间"
- name="namespace"
- rules={[{ required: true, message: '请输入命名空间' }]}
- >
- <Input placeholder="例:kube-system" />
- </Form.Item>
- <Form.Item label="SAN" name="san" tooltip="多个域名或 IP 使用英文逗号分隔">
- <Input placeholder="例:broker.example.com, *.rocketmq.example.com"
/>
- </Form.Item>
- </Form>
- </Modal>
</div>
);
};