Skip to content

Commit

Permalink
feat(kubernetes): don't serialize metadata.managedFields
Browse files Browse the repository at this point in the history
  • Loading branch information
manusa committed Feb 7, 2025
1 parent 1f3533c commit 8b2a8e0
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.quarkus.mcp.servers.kubernetes;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.fabric8.kubernetes.api.model.ManagedFieldsEntry;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.quarkus.kubernetes.client.KubernetesClientObjectMapperCustomizer;
import jakarta.inject.Singleton;

import java.util.List;

/**
* Default Kubernetes Client serialization customizer.
* <p>
* The main purpose is to customize the serialization of Kubernetes resources to minimize the amount of data sent to the LLM.
* <p>
* LLMs are limited in the amount of input and output tokens they can process.
* This is especially important for smaller models.
* In addition, inference providers may also impose rate limits, usually involving the number of tokens per minute.
* <p>
* Any data that's not needed for the LLM to make a decision should be removed to reduce the amount of input tokens.
* Currently, we remove the following fields:
* <ul>
* <li>managedFields: Only useful for server-side apply, completely useless for LLMs (they contain redundant information already present in spec)</li>
* </ul>
*/
@Singleton
public class ObjectMapperCustomizer implements KubernetesClientObjectMapperCustomizer {

@Override
public void customize(ObjectMapper objectMapper) {
objectMapper.addMixIn(ObjectMeta.class, ObjectMetaMixin.class);
}

@SuppressWarnings("unused")
public static abstract class ObjectMetaMixin extends ObjectMeta {

@JsonIgnore
private List<ManagedFieldsEntry> managedFields;

@JsonIgnore
public abstract List<ManagedFieldsEntry> getManagedFields();

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
import io.fabric8.kubernetes.api.model.Pod;
Expand All @@ -22,12 +20,10 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;

import static io.quarkus.mcp.servers.kubernetes.MCPTestUtils.initMcpClient;
import static org.assertj.core.api.Assertions.assertThat;

public class MCPServerKubernetesIT {
Expand All @@ -37,38 +33,12 @@ public class MCPServerKubernetesIT {
private static McpClient client;

@BeforeAll
static void initMcpClient() throws Exception {
static void setUp() throws Exception {
mockServer = new KubernetesMockServer(new Context(new ObjectMapper()),
new MockWebServer(), new HashMap<>(), new KubernetesCrudDispatcher(), true);
mockServer.init();
kubernetesClient = mockServer.createClient();
final var kubeConfigArgs = List.of(
"-Dquarkus.kubernetes-client.api-server-url=" + kubernetesClient.getConfiguration().getMasterUrl(),
"-Dquarkus.kubernetes-client.trust-certs=true",
"-Dquarkus.kubernetes-client.namespace=test"
);
final List<String> command = new ArrayList<>();
if (Objects.equals(System.getProperty("quarkus.native.enabled"), "true")) {
command.add(System.getProperty("native.image.path"));
command.addAll(kubeConfigArgs);
} else {
command.add(ProcessHandle.current().info().command().orElseThrow());
command.addAll(kubeConfigArgs);
command.add("-jar");
command.add(System.getProperty("java.jar.path"));
}
final var transport = new StdioMcpTransport.Builder().command(command).logEvents(true).build();
client = new DefaultMcpClient.Builder()
.clientName("test-mcp-client-kubernetes")
.toolExecutionTimeout(Duration.ofSeconds(10))
.transport(transport)
.build();
// TODO: Remove once LangChain4J is fixed (1.0.0-alpha2)
// https://github.com/langchain4j/langchain4j/pull/2360
// https://github.com/langchain4j/langchain4j/issues/2341#issuecomment-2564081377
final var execute = StdioMcpTransport.class.getDeclaredMethod("execute", String.class, Long.class);
execute.setAccessible(true);
execute.invoke(transport, "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}", 1000L);
client = initMcpClient(kubernetesClient.getConfiguration().getMasterUrl());
}

@AfterAll
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.quarkus.mcp.servers.kubernetes;

import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class MCPTestUtils {
private MCPTestUtils() {
}

public static McpClient initMcpClient(String masterUrl) throws ReflectiveOperationException {
final var kubeConfigArgs = List.of(
"-Dquarkus.kubernetes-client.api-server-url=" + masterUrl,
"-Dquarkus.kubernetes-client.trust-certs=true",
"-Dquarkus.kubernetes-client.namespace=test"
);
final List<String> command = new ArrayList<>();
if (Objects.equals(System.getProperty("quarkus.native.enabled"), "true")) {
command.add(System.getProperty("native.image.path"));
command.addAll(kubeConfigArgs);
} else {
command.add(ProcessHandle.current().info().command().orElseThrow());
command.addAll(kubeConfigArgs);
command.add("-jar");
command.add(System.getProperty("java.jar.path"));
}
final var transport = new StdioMcpTransport.Builder().command(command).logEvents(true).build();
final var client = new DefaultMcpClient.Builder()
.clientName("test-mcp-client-kubernetes")
.toolExecutionTimeout(Duration.ofSeconds(10))
.transport(transport)
.build();
// TODO: Remove once LangChain4J is fixed (1.0.0-alpha2)
// https://github.com/langchain4j/langchain4j/pull/2360
// https://github.com/langchain4j/langchain4j/issues/2341#issuecomment-2564081377
final var execute = StdioMcpTransport.class.getDeclaredMethod("execute", String.class, Long.class);
execute.setAccessible(true);
execute.invoke(transport, "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}", 1000L);
return client;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.quarkus.mcp.servers.kubernetes;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.mcp.client.McpClient;
import io.fabric8.kubernetes.api.model.NamespaceListBuilder;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesMixedDispatcher;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import io.fabric8.mockwebserver.Context;
import io.fabric8.mockwebserver.MockWebServer;
import io.fabric8.mockwebserver.ServerRequest;
import io.fabric8.mockwebserver.ServerResponse;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;
import java.util.Queue;

import static io.quarkus.mcp.servers.kubernetes.MCPTestUtils.initMcpClient;
import static org.assertj.core.api.Assertions.assertThat;

public class ObjectMapperCustomizerIT {

private static KubernetesMockServer mockServer;
private static KubernetesClient kubernetesClient;
private static McpClient client;

@BeforeAll
static void setUp() throws Exception {
final Map<ServerRequest, Queue<ServerResponse>> responses = new HashMap<>();
mockServer = new KubernetesMockServer(new Context(new ObjectMapper()),
new MockWebServer(), responses, new KubernetesMixedDispatcher(responses), true);
mockServer.init();
kubernetesClient = mockServer.createClient();
client = initMcpClient(kubernetesClient.getConfiguration().getMasterUrl());
}

@AfterAll
static void closeMcpClient() {
kubernetesClient.close();
mockServer.destroy();
}

@BeforeEach
void resetMockServer() {
mockServer.reset();
}

@Test
void serializedObjectsDontContainManagedFields() {
mockServer.expect().get()
.withPath("/api/v1/namespaces")
.andReturn(200, new NamespaceListBuilder()
.addNewItem().withMetadata(new ObjectMetaBuilder()
.withName("a-namespace-to-list")
.addNewManagedField().withManager("the-manager").endManagedField()
.build()).endItem()
.build())
.once();
assertThat(client.executeTool(ToolExecutionRequest.builder().name("namespaces_list").arguments("{}").build()))
.isNotBlank()
.isEqualTo("[{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"name\":\"a-namespace-to-list\"}}]");
}
}

0 comments on commit 8b2a8e0

Please sign in to comment.