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 e45a1e5da docs: align the write contracts and deploy docs with what
the server does (#5438)
e45a1e5da is described below
commit e45a1e5da5bb47608aa8751a99c82a52c9812a24
Author: 烤化の初雪 <[email protected]>
AuthorDate: Fri Oct 9 17:47:48 2026 +0800
docs: align the write contracts and deploy docs with what the server does
(#5438)
Documentation and the contract tests that pin it, same author.
- `docs/api-spec.md` §5.5 / §5.8 / §6.6 / §6.7 / §6.8 request and response
tables now match the DTOs
the server validates, including the required `instanceId` on topic
creation. (#5438)
- The five unimplemented NameServer write operations are marked as such:
each validates the cluster
and address and then throws a fixed `501`, so the documented success
branch is unreachable. §4.4
(restart Broker) is the same family and is now marked the same way.
(#5435)
- `deploy/README.md` gains the idempotent `docker network create` step for
the local compose
quickstart, since `docker-compose.yml` declares `rocketmq_net` as
external. (#5431)
- `CONTRIBUTING.md` / `README.md` / `README_zh.md` no longer claim the
backend port is reachable after
a full-stack start: 6789 serves the frontend and proxies `/api`, while
the backend container listens
on 8888 inside the compose network only. (#5439)
New `GroupWriteRequestValidationTest` (4) and `DeleteTopicDTOTest` (3) pin
the corrected tables;
7 tests green, 0 checkstyle violations. Merged on top of today's spec
changes - the DLQ `Body Base64`
column (#5205) and the `/api/clients` section (#5453) are untouched.
Folded in #5431, #5435 and #5439.
---
CONTRIBUTING.md | 6 +-
README.md | 2 +-
README_zh.md | 2 +-
deploy/README.md | 5 +-
docs/api-spec.md | 62 +++++++++++----
.../group/GroupWriteRequestValidationTest.java | 90 ++++++++++++++++++++++
.../studio/instance/topic/DeleteTopicDTOTest.java | 59 ++++++++++++++
7 files changed, 205 insertions(+), 21 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 531055c61..4b2e2ffce 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -37,8 +37,10 @@ Recommended background reading:
docker compose -f deploy/docker-compose.yml up -d --build
```
-Studio is then available at <http://127.0.0.1:6789> (frontend) and
<http://127.0.0.1:8888>
-(backend). See [`deploy/README.md`](deploy/README.md) for the configuration
options.
+Studio is then available at <http://127.0.0.1:6789> (frontend, which also
proxies `/api` to the
+backend). The backend container listens on 8888 but is only reachable inside
the compose network,
+so that URL answers only when you run the backend directly (see below). See
+[`deploy/README.md`](deploy/README.md) for the configuration options.
### Run the backend only
diff --git a/README.md b/README.md
index 0a4576e03..1b020eed0 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ default deployment. Import `upgrade-demo-instance.sql` first
and then `upgrade-d
target the current numeric-ID schema and are idempotent. They are sample-data
loaders, not upgrade migrations,
and should never be imported into a production database.
-**Studio ports:** Frontend 6789 (Nginx), Backend 8888 (Spring Boot)
+**Studio ports:** Frontend 6789 (Nginx, also proxies `/api`), Backend 8888
(Spring Boot, container-internal — published only when the backend runs outside
compose)
To enable login protection for a shared environment, copy
`deploy/.env.example` to
`deploy/.env`, set `STUDIO_AUTH_LOGIN_REQUIRED=true`, and configure
diff --git a/README_zh.md b/README_zh.md
index a6827f93e..46560ee41 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -41,7 +41,7 @@ docker compose -f deploy/docker-compose.yml up -d --build
RocketMQ 集群。要管理真实资源,请注册一个指向你自己 RocketMQ 的实例,或启动下方的
「内置 RocketMQ 集群(可选)」。
-**Studio 服务端口:** 前端 6789(Nginx)、后端 8888(Spring Boot)
+**Studio 服务端口:** 前端 6789(Nginx,同时代理 `/api`)、后端 8888(Spring Boot,仅容器内可达;后端在
compose 之外直接运行时才对外暴露)
共享环境可复制 `deploy/.env.example` 为 `deploy/.env`,设置
`STUDIO_AUTH_LOGIN_REQUIRED=true`,并配置 `STUDIO_AUTH_ADMIN_USERNAME` /
diff --git a/deploy/README.md b/deploy/README.md
index f080791eb..978a8de77 100644
--- a/deploy/README.md
+++ b/deploy/README.md
@@ -30,10 +30,13 @@ MAVEN_IMAGE=maven:3.9.9-eclipse-temurin-21
## 本地 Docker Compose
-复制示例配置后启动:
+`docker-compose.yml` 中的 `mysql` 与 `rocketmq-server` 加入共享网络 `rocketmq_net`(声明为
+`external`),因此首次启动前需要先创建该网络,否则 Compose 会以
+`network rocketmq_net declared as external, but could not be found` 终止:
```bash
cp deploy/.env.example deploy/.env
+docker network inspect rocketmq_net >/dev/null 2>&1 || docker network create
rocketmq_net
cd deploy && docker compose up -d --build
```
diff --git a/docs/api-spec.md b/docs/api-spec.md
index a53e229ff..289ad8b4f 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -533,7 +533,16 @@ POST /api/clusters/:clusterId/brokers/:name/restart
| `clusterId` | `string` | 集群 ID |
| `name` | `string` | Broker 名称 |
-**Response `data`:** `{ success: boolean, message: string }`
+**Response `data`:** `{ message: string }`
+
+接口先校验集群存在、且该集群下登记了这个 Broker,再把动作交给集群 Provider。当前 Provider 尚未实现 Broker
重启,校验通过后固定抛出 `501`,因此上面的成功分支不可达,调用方不应依赖任何 `success` 字段。
+
+**错误响应:**
+
+| HTTP 状态 | 场景 |
+|-----------|------|
+| `404` | 集群不存在,或该集群下没有这个 Broker |
+| `501` | 当前集群 Provider 未实现 Broker 重启 |
### 4.5 创建 NameServer
@@ -541,6 +550,8 @@ POST /api/clusters/:clusterId/brokers/:name/restart
POST /api/nameservers/create
```
+> **未实现**:与 §4.11 的重启 Proxy 相同,本接口在完成集群与地址校验后固定抛出
`501`,因此下表中的成功响应不会出现。NameServer 注册表的查询与增删改由 `GET /api/nameservers` 与 `POST
/api/nameservers/registry/{create,update,delete}` 提供,本 spec 尚未收录。
+
**Request Body:**
| 字段 | 类型 | 必填 | 说明 |
@@ -556,6 +567,8 @@ POST /api/nameservers/create
POST /api/nameservers/update
```
+> **未实现**:与 §4.11 的重启 Proxy 相同,本接口在完成集群与地址校验后固定抛出
`501`,因此下表中的成功响应不会出现。NameServer 注册表的查询与增删改由 `GET /api/nameservers` 与 `POST
/api/nameservers/registry/{create,update,delete}` 提供,本 spec 尚未收录。
+
**Request Body:**
| 字段 | 类型 | 必填 | 说明 |
@@ -572,6 +585,8 @@ POST /api/nameservers/update
POST /api/nameservers/restart
```
+> **未实现**:与 §4.11 的重启 Proxy 相同,本接口在完成集群与地址校验后固定抛出
`501`,因此下表中的成功响应不会出现。NameServer 注册表的查询与增删改由 `GET /api/nameservers` 与 `POST
/api/nameservers/registry/{create,update,delete}` 提供,本 spec 尚未收录。
+
**Request Body:**
| 字段 | 类型 | 必填 | 说明 |
@@ -586,6 +601,8 @@ POST /api/nameservers/restart
POST /api/nameservers/upgrade
```
+> **未实现**:与 §4.11 的重启 Proxy 相同,本接口在完成集群与地址校验后固定抛出
`501`,因此下表中的成功响应不会出现。NameServer 注册表的查询与增删改由 `GET /api/nameservers` 与 `POST
/api/nameservers/registry/{create,update,delete}` 提供,本 spec 尚未收录。
+
**Request Body:**
| 字段 | 类型 | 必填 | 说明 |
@@ -600,6 +617,8 @@ POST /api/nameservers/upgrade
POST /api/nameservers/delete
```
+> **未实现**:与 §4.11 的重启 Proxy 相同,本接口在完成集群与地址校验后固定抛出
`501`,因此下表中的成功响应不会出现。NameServer 注册表的查询与增删改由 `GET /api/nameservers` 与 `POST
/api/nameservers/registry/{create,update,delete}` 提供,本 spec 尚未收录。
+
**Request Body:**
| 字段 | 类型 | 必填 | 说明 |
@@ -939,12 +958,13 @@ POST /api/topics/create
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
+| `instanceId` | `string` | 是 | 所属实例 ID,缺失返回 `400 instanceId is required` |
| `name` | `string` | 是 | Topic 名称 |
| `namespace` | `string` | 否 | 命名空间 |
| `type` | `string` | 是 | 类型 |
-| `clusterId` | `string` | 是 | 所属集群 |
-| `writeQueues` | `number` | 是 | 写队列数 |
-| `readQueues` | `number` | 是 | 读队列数 |
+| `clusterId` | `string` | 否 | 所属集群;Apache 写路径按实例与所有权记录解析目标集群,不读取该字段 |
+| `writeQueues` | `number` | 否 | 写队列数,须 ≥ 0;缺省或非正数时取 8 |
+| `readQueues` | `number` | 否 | 读队列数,须 ≥ 0;缺省或非正数时取 8 |
| `perm` | `string` | 否 | 权限,默认 `RW` |
| `remark` | `string` | 否 | 备注 |
@@ -960,12 +980,13 @@ POST /api/topics/update
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
+| `instanceId` | `string` | 是 | 所属实例 ID,缺失返回 `400 instanceId is required` |
| `name` | `string` | 是 | Topic 名称(不可修改) |
| `namespace` | `string` | 否 | 命名空间 |
| `type` | `string` | 是 | 类型 |
-| `clusterId` | `string` | 是 | 所属集群 |
-| `writeQueues` | `number` | 是 | 写队列数 |
-| `readQueues` | `number` | 是 | 读队列数 |
+| `clusterId` | `string` | 否 | 所属集群;Apache 写路径按实例与所有权记录解析目标集群,不读取该字段 |
+| `writeQueues` | `number` | 否 | 写队列数,须 ≥ 0;缺省或非正数时保留现值 |
+| `readQueues` | `number` | 否 | 读队列数,须 ≥ 0;缺省或非正数时保留现值 |
| `perm` | `string` | 否 | 权限,默认 `RW` |
| `remark` | `string` | 否 | 备注 |
@@ -982,6 +1003,7 @@ POST /api/topics/delete
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | `string` | 是 | Topic 名称 |
+| `instanceId` | `string` | 是 | 实例 ID(`DeleteTopicDTO` 校验非空) |
**Response `data`:** `null`
@@ -1027,6 +1049,7 @@ POST /api/topics/send
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
+| `instanceId` | `string` | 是 | 实例 ID(消息写入该实例) |
| `topic` | `string` | 是 | Topic 名称 |
| `tag` | `string` | 否 | 消息 Tag |
| `key` | `string` | 否 | 消息 Key(用于消息查询) |
@@ -1165,13 +1188,16 @@ POST /api/groups/create
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
+| `instanceId` | `string` | 是 | 所属实例 ID,缺失返回 `400 instanceId is required` |
| `name` | `string` | 是 | 消费组名称 |
| `namespace` | `string` | 否 | 命名空间 |
| `clusterId` | `string` | 是 | 所属集群 |
| `subscriptionMode` | `string` | 是 | `Push` / `Pop` |
| `consumeType` | `string` | 是 | `CLUSTERING` / `BROADCASTING` |
-| `subscribedTopics` | `string[]` | 否 | 订阅 Topic |
-| `retryMaxTimes` | `number` | 否 | 最大重试次数 |
+| `subscriptionDataType` | `string` | 否 | 订阅数据类型: `NORMAL` / `FIFO` / `DELAY`
/ `TRANSACTION` |
+| `deliveryOrderType` | `string` | 否 | 顺序类型(FIFO 时): `PARTITON_ORDER` /
`MESSAGES_ORDER` |
+| `retryMaxTimes` | `number` | 否 | 最大重试次数,须 ≥ 0 |
+| `delaySeconds` | `number` | 否 | 延迟秒数,须 ≥ 0 |
**Response `data`:** `ConsumerGroup`
@@ -1186,6 +1212,7 @@ POST /api/groups/delete
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | `string` | 是 | 消费组名称 |
+| `instanceId` | `string` | 是 | 实例 ID(`DeleteConsumerGroupDTO` 校验非空) |
**Response `data`:** `null`
@@ -1200,8 +1227,9 @@ POST /api/groups/reset-offset
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | `string` | 是 | 消费组名称 |
-| `timestamp` | `string` | 是 | 重置到指定时间 (ISO 8601) |
-| `topic` | `string` | 否 | 指定 Topic,不传则全部重置 |
+| `instanceId` | `string` | 是 | 实例 ID |
+| `timestamp` | `number` | 是 | 重置到的时间(Unix 毫秒时间戳,必须为正数) |
+| `topic` | `string` | 是 | 要重置的 Topic;本接口没有「全部重置」分支,必须显式指定 |
**Response `data`:** `null`
@@ -1211,11 +1239,12 @@ POST /api/groups/reset-offset
POST /api/groups/import
```
-**Request Body:** `multipart/form-data`
+**Request Body:** `application/json`
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| `file` | `File` | JSON 配置文件 |
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `instanceId` | `string` | 是 | 目标实例 ID |
+| `groups` | `CreateConsumerGroupDTO[]` | 是 | 待导入的消费组,单次最多 100 条 |
**Response `data`:**
@@ -1223,7 +1252,8 @@ POST /api/groups/import
|------|------|------|
| `imported` | `number` | 成功导入数量 |
| `failed` | `number` | 失败数量 |
-| `errors` | `string[]` | 错误信息列表 |
+| `groups` | `ConsumerGroupVO[]` | 成功导入的消费组 |
+| `failures` | `{ index, name, message }[]` | 每条失败的原始下标、消费组名称与原因 |
### 6.9 导出消费组配置
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/group/GroupWriteRequestValidationTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/GroupWriteRequestValidationTest.java
new file mode 100644
index 000000000..61d121892
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/GroupWriteRequestValidationTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.instance.group;
+
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Pins the request contract api-spec 6.6-6.8 documents: the owning instance
is mandatory, a reset
+ * needs a topic and a positive epoch-millis timestamp, and an import is a
bounded JSON list.
+ */
+class GroupWriteRequestValidationTest {
+
+ private final Validator validator =
Validation.buildDefaultValidatorFactory().getValidator();
+
+ @Test
+ void deleteShouldRequireTheOwningInstanceTest() {
+ DeleteConsumerGroupDTO request = new DeleteConsumerGroupDTO();
+ request.setName("cg-orders");
+
+ assertThat(validator.validate(request))
+ .extracting(violation -> violation.getMessage())
+ .containsExactly("instanceId is required");
+ }
+
+ @Test
+ void
resetOffsetShouldRequireTheInstanceTheTopicAndAPositiveTimestampTest() {
+ ResetConsumerOffsetDTO request = new ResetConsumerOffsetDTO();
+ request.setName("cg-orders");
+ request.setTimestamp(0L);
+
+ assertThat(validator.validate(request))
+ .extracting(violation -> violation.getMessage())
+ .containsExactlyInAnyOrder("instanceId is required", "topic is
required",
+ "timestamp must be positive");
+ }
+
+ @Test
+ void resetOffsetShouldAcceptAnEpochMillisTimestampTest() {
+ ResetConsumerOffsetDTO request = new ResetConsumerOffsetDTO();
+ request.setInstanceId("instance-a");
+ request.setName("cg-orders");
+ request.setTopic("orders");
+ request.setTimestamp(1_780_000_000_000L);
+
+ assertThat(validator.validate(request)).isEmpty();
+ }
+
+ @Test
+ void importShouldRequireTheInstanceAndANonEmptyBoundedListTest() {
+ ImportConsumerGroupsDTO empty = new ImportConsumerGroupsDTO();
+ assertThat(validator.validate(empty))
+ .extracting(violation -> violation.getMessage())
+ .containsExactlyInAnyOrder("instanceId is required", "groups
is required");
+
+ List<CreateConsumerGroupDTO> tooMany = new ArrayList<>();
+ for (int index = 0; index < 101; index++) {
+ CreateConsumerGroupDTO group = new CreateConsumerGroupDTO();
+ group.setName("cg-" + index);
+ tooMany.add(group);
+ }
+ ImportConsumerGroupsDTO oversized = new ImportConsumerGroupsDTO();
+ oversized.setInstanceId("instance-a");
+ oversized.setGroups(tooMany);
+
+ assertThat(validator.validate(oversized))
+ .extracting(violation -> violation.getMessage())
+ .containsExactly("At most 100 consumer groups are allowed per
import");
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTOTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTOTest.java
new file mode 100644
index 000000000..19d9575ae
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTOTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.instance.topic;
+
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DeleteTopicDTOTest {
+
+ private final Validator validator =
Validation.buildDefaultValidatorFactory().getValidator();
+
+ @Test
+ void shouldRequireTheOwningInstanceTest() {
+ // The console sends both fields and api-spec 5.5 documents instanceId
as required, because
+ // deleting topic names is ambiguous across the instances that share a
cluster.
+ DeleteTopicDTO request = new DeleteTopicDTO();
+ request.setName("orders");
+
+ assertThat(validator.validate(request))
+ .extracting(violation -> violation.getMessage())
+ .containsExactly("instanceId is required");
+ }
+
+ @Test
+ void shouldRequireTheTopicNameTest() {
+ DeleteTopicDTO request = new DeleteTopicDTO();
+ request.setInstanceId("instance-a");
+
+ assertThat(validator.validate(request))
+ .extracting(violation -> violation.getMessage())
+ .containsExactly("name is required");
+ }
+
+ @Test
+ void shouldAcceptBothFieldsTest() {
+ DeleteTopicDTO request = new DeleteTopicDTO();
+ request.setInstanceId("instance-a");
+ request.setName("orders");
+
+ assertThat(validator.validate(request)).isEmpty();
+ }
+}