Skip to content

Commit 8e88c5a

Browse files
authored
Merge branch 'main' into feature/search_acl
2 parents 7ca2470 + fbe4974 commit 8e88c5a

File tree

97 files changed

+6323
-177
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+6323
-177
lines changed

.github/workflows/e2e-playwright.yml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: "E2E: Playwright Manual run"
2+
on:
3+
workflow_dispatch:
4+
sha:
5+
required: true
6+
type: string
7+
8+
permissions:
9+
contents: read
10+
checks: write
11+
statuses: write
12+
13+
jobs:
14+
build-and-test:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # infered from @v4
19+
with:
20+
token: ${{ github.token }}
21+
ref: ${{ inputs.sha }}
22+
- name: Set up JDK
23+
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # infered from @v4
24+
with:
25+
java-version: '21'
26+
distribution: 'zulu'
27+
cache: 'gradle'
28+
29+
- name: Set up Node.js
30+
uses: actions/setup-node@v3
31+
with:
32+
node-version: 18
33+
34+
- name: Install NPM dependencies
35+
working-directory: ./e2e-playwright
36+
run: npm install
37+
38+
- name: Cache Playwright browser binaries
39+
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf
40+
id: playwright-cache
41+
with:
42+
path: ~/.cache/ms-playwright
43+
key: ${{ runner.os }}-playwright-${{ hashFiles('./e2e-playwright/package-lock.json') }}
44+
45+
- name: Cache apt binaries
46+
uses: awalsh128/cache-apt-pkgs-action@5902b33ae29014e6ca012c5d8025d4346556bd40
47+
with:
48+
packages: fonts-freefont-ttf fonts-ipafont-gothic fonts-tlwg-loma-otf fonts-unifont fonts-wqy-zenhei glib-networking glib-networking-common glib-networking-services gsettings-desktop-schemas gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-base gstreamer1.0-plugins-good libaa1 libabsl20220623t64 libass9 libasyncns0 libavc1394-0 libavcodec60 libavfilter9 libavformat60 libavif16 libavtp0 libavutil58 libblas3 libbluray2 libbs2b0 libcaca0 libcairo-script-interpreter2 libcdparanoia0 libchromaprint1 libcjson1 libcodec2-1.2 libdav1d7 libdc1394-25 libdca0 libdecor-0-0 libdirectfb-1.7-7t64 libdv4t64 libdvdnav4 libdvdread8t64 libegl-mesa0 libegl1 libevent-2.1-7t64 libfaad2 libflac12t64 libflite1 libfluidsynth3 libfreeaptx0 libgav1-1 libgles2 libgme0 libgraphene-1.0-0 libgsm1 libgssdp-1.6-0 libgstreamer-gl1.0-0 libgstreamer-plugins-bad1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer-plugins-good1.0-0 libgtk-4-1 libgtk-4-common libgupnp-1.6-0 libgupnp-igd-1.6-0 libharfbuzz-icu0 libhwy1t64 libhyphen0 libiec61883-0 libimath-3-1-29t64 libinstpatch-1.0-2 libjack-jackd2-0 libjxl0.7 liblapack3 liblc3-1 libldacbt-enc2 liblilv-0-0 liblrdf0 libltc11 libmanette-0.2-0 libmbedcrypto7t64 libmfx1 libmjpegutils-2.1-0t64 libmodplug1 libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0t64 libmpg123-0t64 libmplex2-2.1-0t64 libmysofa1 libneon27t64 libnice10 libopenal-data libopenal1 libopenexr-3-1-30 libopenh264-7 libopenmpt0t64 libopenni2-0 libopus0 liborc-0.4-0t64 libpipewire-0.3-0t64 libplacebo338 libpocketsphinx3 libpostproc57 libproxy1v5 libpulse0 libqrencode4 libraptor2-0 librav1e0 libraw1394-11 librist4 librsvg2-2 librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libsecret-1-0 libsecret-common libserd-0-0 libshine3 libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0 libsoup-3.0-common libsoxr0 libspa-0.2-modules libspandsp2t64 libspeex1 libsphinxbase3t64 libsratom-0-0 libsrt1.5-gnutls libsrtp2-1 libssh-gcrypt-4 libsvtav1enc1d1 libswresample4 libswscale7 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0 libunibreak5 libv4l-0t64 libv4lconvert0t64 libva-drm2 libva-x11-2 libva2 libvdpau1 libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbisenc2 libvpl2 libvpx9 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libwoff1 libx264-164 libx265-199 libxcb-xkb1 libxkbcommon-x11-0 libxvidcore4 libyuv0 libzbar0t64 libzimg2 libzix-0-0 libzvbi-common libzvbi0t64 libzxing3 ocl-icd-libopencl1 session-migration timgm6mb-soundfont xfonts-cyrillic xfonts-encodings xfonts-scalable xfonts-utils
49+
50+
- name: Install Playwright browsers
51+
working-directory: ./e2e-playwright
52+
run: npx playwright install --with-deps
53+
54+
- name: Build with Gradle
55+
id: build_app
56+
run: |
57+
./gradlew build \
58+
-x :e2e-tests:check \
59+
-x :e2e-tests:build \
60+
-x test \
61+
-Pbuild-docker-images=true \
62+
-Pinclude-frontend=true \
63+
-Pversion=latest
64+
65+
- name: Run docker-compose
66+
run: docker compose -f ./documentation/compose/e2e-tests.yaml up -d
67+
68+
- name: Run tests with ENV=prod
69+
working-directory: ./e2e-playwright
70+
run: ENV=prod npm run test
71+
72+
- name: Upload report
73+
if: failure()
74+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # infered from @v4
75+
with:
76+
name: playwright-results
77+
path: ./e2e-playwright/test-results/
78+
retention-days: 7
79+
80+

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ node_modules/
4949
out/
5050
node_modules/
5151
.gradle
52+
@rerun.txt
53+
test-results

api/build.gradle

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ dependencies {
6262
implementation libs.netty.common
6363
implementation libs.netty.handler
6464

65+
implementation libs.modelcontextprotocol.spring.webflux
66+
implementation libs.victools.jsonschema.generator
6567

6668
// Google Managed Service for Kafka IAM support
6769
implementation (libs.google.managed.kafka.login.handler) {
@@ -112,6 +114,9 @@ sourceSets {
112114
java {
113115
srcDirs += generateGrammarSource.outputDirectory
114116
}
117+
resources {
118+
srcDirs += project(":frontend").layout.buildDirectory.dir("vite")
119+
}
115120
}
116121
}
117122

@@ -149,15 +154,7 @@ if (buildDockerImages) {
149154
}
150155

151156
if (includeFrontend) {
152-
tasks.named("build") {
153-
dependsOn(":frontend:buildFrontend")
154-
}
155-
156157
tasks.named("processResources") {
157158
dependsOn(":frontend:buildFrontend")
158-
from(project(":frontend").layout.buildDirectory.dir("vite")) {
159-
into("static")
160-
}
161-
into("$buildDir/resources/main")
162159
}
163160
}

api/src/main/java/io/kafbat/ui/config/CustomWebFilter.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ public class CustomWebFilter implements WebFilter {
1717

1818
final String path = exchange.getRequest().getPath().pathWithinApplication().value();
1919

20+
ServerWebExchange filterExchange = exchange;
21+
2022
if (path.startsWith("/ui") || path.isEmpty() || path.equals("/")) {
21-
return chain.filter(
22-
exchange.mutate().request(
23-
exchange.getRequest().mutate()
24-
.path(basePath + "/index.html")
25-
.contextPath(basePath)
26-
.build()
27-
).build()
28-
);
23+
filterExchange = exchange.mutate().request(
24+
exchange.getRequest().mutate()
25+
.path(basePath + "/index.html")
26+
.contextPath(basePath)
27+
.build()
28+
).build();
2929
}
3030

31-
return chain.filter(exchange);
31+
return chain.filter(filterExchange).contextWrite(ctx -> ctx.put(ServerWebExchange.class, exchange));
3232
}
3333
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.kafbat.ui.config;
2+
3+
import com.github.victools.jsonschema.generator.OptionPreset;
4+
import com.github.victools.jsonschema.generator.SchemaGenerator;
5+
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
6+
import com.github.victools.jsonschema.generator.SchemaVersion;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
@Configuration
11+
public class JsonSchemaConfig {
12+
@Bean
13+
public SchemaGenerator schemaGenerator() {
14+
SchemaGeneratorConfigBuilder configBuilder =
15+
new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON);
16+
return new SchemaGenerator(configBuilder.build());
17+
}
18+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.kafbat.ui.config;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import io.kafbat.ui.service.mcp.McpSpecificationGenerator;
5+
import io.kafbat.ui.service.mcp.McpTool;
6+
import io.modelcontextprotocol.server.McpAsyncServer;
7+
import io.modelcontextprotocol.server.McpServer;
8+
import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptSpecification;
9+
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
10+
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
11+
import io.modelcontextprotocol.spec.McpSchema;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
16+
import org.springframework.context.annotation.Bean;
17+
import org.springframework.context.annotation.Configuration;
18+
import org.springframework.web.reactive.function.server.RouterFunction;
19+
20+
@Configuration
21+
@RequiredArgsConstructor
22+
@ConditionalOnProperty(value = "mcp.enabled", havingValue = "true")
23+
public class McpConfig {
24+
25+
private final List<McpTool> mcpTools;
26+
private final McpSpecificationGenerator mcpSpecificationGenerator;
27+
28+
// SSE transport
29+
@Bean
30+
public WebFluxSseServerTransportProvider sseServerTransport(ObjectMapper mapper) {
31+
return new WebFluxSseServerTransportProvider(mapper, "/mcp/message", "/mcp/sse");
32+
}
33+
34+
// Router function for SSE transport used by Spring WebFlux to start an HTTP
35+
// server.
36+
37+
@Bean
38+
public RouterFunction<?> mcpRouterFunction(WebFluxSseServerTransportProvider transport) {
39+
return transport.getRouterFunction();
40+
}
41+
42+
@Bean
43+
public McpAsyncServer mcpServer(WebFluxSseServerTransportProvider transport) {
44+
45+
// Configure server capabilities with resource support
46+
var capabilities = McpSchema.ServerCapabilities.builder()
47+
.resources(false, true)
48+
.tools(true) // Tool support with list changes notifications
49+
.prompts(false) // Prompt support with list changes notifications
50+
.logging() // Logging support
51+
.build();
52+
53+
// Create the server with both tool and resource capabilities
54+
return McpServer.async(transport)
55+
.serverInfo("Kafka UI MCP", "0.0.1")
56+
.capabilities(capabilities)
57+
.tools(tools())
58+
.build();
59+
}
60+
61+
private List<AsyncToolSpecification> tools() {
62+
List<AsyncToolSpecification> tools = new ArrayList<>();
63+
for (McpTool mcpTool : mcpTools) {
64+
tools.addAll(mcpSpecificationGenerator.convertTool(mcpTool));
65+
}
66+
return tools;
67+
}
68+
}

api/src/main/java/io/kafbat/ui/controller/AclsController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.kafbat.ui.model.rbac.AccessContext;
1212
import io.kafbat.ui.model.rbac.permission.AclAction;
1313
import io.kafbat.ui.service.acl.AclsService;
14+
import io.kafbat.ui.service.mcp.McpTool;
1415
import java.util.Optional;
1516
import lombok.RequiredArgsConstructor;
1617
import org.apache.kafka.common.resource.PatternType;
@@ -24,7 +25,7 @@
2425

2526
@RestController
2627
@RequiredArgsConstructor
27-
public class AclsController extends AbstractController implements AclsApi {
28+
public class AclsController extends AbstractController implements AclsApi, McpTool {
2829

2930
private final AclsService aclsService;
3031

api/src/main/java/io/kafbat/ui/controller/BrokersController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.kafbat.ui.model.rbac.AccessContext;
1212
import io.kafbat.ui.model.rbac.permission.ClusterConfigAction;
1313
import io.kafbat.ui.service.BrokerService;
14+
import io.kafbat.ui.service.mcp.McpTool;
1415
import java.util.List;
1516
import java.util.Map;
1617
import javax.annotation.Nullable;
@@ -25,7 +26,7 @@
2526
@RestController
2627
@RequiredArgsConstructor
2728
@Slf4j
28-
public class BrokersController extends AbstractController implements BrokersApi {
29+
public class BrokersController extends AbstractController implements BrokersApi, McpTool {
2930
private static final String BROKER_ID = "brokerId";
3031

3132
private final BrokerService brokerService;

api/src/main/java/io/kafbat/ui/controller/ClientQuotasController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.kafbat.ui.model.ClientQuotasDTO;
77
import io.kafbat.ui.model.rbac.AccessContext;
88
import io.kafbat.ui.model.rbac.permission.ClientQuotaAction;
9+
import io.kafbat.ui.service.mcp.McpTool;
910
import io.kafbat.ui.service.quota.ClientQuotaRecord;
1011
import io.kafbat.ui.service.quota.ClientQuotaService;
1112
import java.math.BigDecimal;
@@ -22,7 +23,7 @@
2223

2324
@RestController
2425
@RequiredArgsConstructor
25-
public class ClientQuotasController extends AbstractController implements ClientQuotasApi {
26+
public class ClientQuotasController extends AbstractController implements ClientQuotasApi, McpTool {
2627

2728
private static final Comparator<ClientQuotaRecord> QUOTA_RECORDS_COMPARATOR =
2829
Comparator.nullsLast(Comparator.comparing(ClientQuotaRecord::user))

api/src/main/java/io/kafbat/ui/controller/ClustersController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.kafbat.ui.model.ClusterStatsDTO;
77
import io.kafbat.ui.model.rbac.AccessContext;
88
import io.kafbat.ui.service.ClusterService;
9+
import io.kafbat.ui.service.mcp.McpTool;
910
import lombok.RequiredArgsConstructor;
1011
import lombok.extern.slf4j.Slf4j;
1112
import org.springframework.http.ResponseEntity;
@@ -17,7 +18,7 @@
1718
@RestController
1819
@RequiredArgsConstructor
1920
@Slf4j
20-
public class ClustersController extends AbstractController implements ClustersApi {
21+
public class ClustersController extends AbstractController implements ClustersApi, McpTool {
2122
private final ClusterService clusterService;
2223

2324
@Override

api/src/main/java/io/kafbat/ui/controller/ConsumerGroupsController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.kafbat.ui.model.rbac.permission.TopicAction;
2020
import io.kafbat.ui.service.ConsumerGroupService;
2121
import io.kafbat.ui.service.OffsetsResetService;
22+
import io.kafbat.ui.service.mcp.McpTool;
2223
import java.util.Map;
2324
import java.util.Optional;
2425
import java.util.function.Supplier;
@@ -35,7 +36,7 @@
3536
@RestController
3637
@RequiredArgsConstructor
3738
@Slf4j
38-
public class ConsumerGroupsController extends AbstractController implements ConsumerGroupsApi {
39+
public class ConsumerGroupsController extends AbstractController implements ConsumerGroupsApi, McpTool {
3940

4041
private final ConsumerGroupService consumerGroupService;
4142
private final OffsetsResetService offsetsResetService;

api/src/main/java/io/kafbat/ui/controller/KafkaConnectController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.kafbat.ui.model.rbac.AccessContext;
2121
import io.kafbat.ui.model.rbac.permission.ConnectAction;
2222
import io.kafbat.ui.service.KafkaConnectService;
23+
import io.kafbat.ui.service.mcp.McpTool;
2324
import java.util.Comparator;
2425
import java.util.Map;
2526
import java.util.Set;
@@ -35,7 +36,7 @@
3536
@RestController
3637
@RequiredArgsConstructor
3738
@Slf4j
38-
public class KafkaConnectController extends AbstractController implements KafkaConnectApi {
39+
public class KafkaConnectController extends AbstractController implements KafkaConnectApi, McpTool {
3940
private static final Set<ConnectorActionDTO> RESTART_ACTIONS
4041
= Set.of(RESTART, RESTART_FAILED_TASKS, RESTART_ALL_TASKS);
4142
private static final String CONNECTOR_NAME = "connectorName";

api/src/main/java/io/kafbat/ui/controller/KsqlController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.kafbat.ui.model.rbac.AccessContext;
1111
import io.kafbat.ui.model.rbac.permission.KsqlAction;
1212
import io.kafbat.ui.service.ksql.KsqlServiceV2;
13+
import io.kafbat.ui.service.mcp.McpTool;
1314
import java.util.List;
1415
import java.util.Map;
1516
import java.util.Optional;
@@ -24,7 +25,7 @@
2425
@RestController
2526
@RequiredArgsConstructor
2627
@Slf4j
27-
public class KsqlController extends AbstractController implements KsqlApi {
28+
public class KsqlController extends AbstractController implements KsqlApi, McpTool {
2829

2930
private final KsqlServiceV2 ksqlServiceV2;
3031

api/src/main/java/io/kafbat/ui/controller/MessagesController.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
import io.kafbat.ui.serde.api.Serde;
2727
import io.kafbat.ui.service.DeserializationService;
2828
import io.kafbat.ui.service.MessagesService;
29+
import io.kafbat.ui.service.mcp.McpTool;
2930
import java.util.List;
3031
import java.util.Optional;
3132
import javax.validation.Valid;
3233
import lombok.RequiredArgsConstructor;
3334
import lombok.extern.slf4j.Slf4j;
35+
import org.springframework.http.HttpStatus;
3436
import org.springframework.http.ResponseEntity;
3537
import org.springframework.web.bind.annotation.RestController;
3638
import org.springframework.web.server.ServerWebExchange;
@@ -41,7 +43,7 @@
4143
@RestController
4244
@RequiredArgsConstructor
4345
@Slf4j
44-
public class MessagesController extends AbstractController implements MessagesApi {
46+
public class MessagesController extends AbstractController implements MessagesApi, McpTool {
4547

4648
private final MessagesService messagesService;
4749
private final DeserializationService deserializationService;
@@ -148,8 +150,8 @@ public Mono<ResponseEntity<Void>> sendTopicMessages(
148150

149151
return validateAccess(context).then(
150152
createTopicMessage.flatMap(msg ->
151-
messagesService.sendMessage(getCluster(clusterName), topicName, msg).then()
152-
).map(ResponseEntity::ok)
153+
messagesService.sendMessage(getCluster(clusterName), topicName, msg)
154+
).map(m -> new ResponseEntity<Void>(HttpStatus.OK))
153155
).doOnEach(sig -> audit(context, sig));
154156
}
155157

0 commit comments

Comments
 (0)