MCP Server Kubernetes
by Flux159
import { expect, test, describe, beforeEach, afterEach } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { HelmResponseSchema } from "../src/models/helm-models.js";
import * as fs from "fs";
async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Helper function to wait for cluster readiness
async function waitForClusterReadiness(
client: Client,
namespace: string
): Promise<void> {
let attempts = 0;
const maxAttempts = 15;
const waitTime = 3000;
while (attempts < maxAttempts) {
try {
// First check if namespace exists
await client.request(
{
method: "tools/call",
params: {
name: "list_namespaces",
arguments: {},
},
},
HelmResponseSchema
);
// Then check if we can list services
await client.request(
{
method: "tools/call",
params: {
name: "list_services",
arguments: {
namespace: namespace,
},
},
},
HelmResponseSchema
);
return;
} catch (e) {
attempts++;
if (attempts === maxAttempts) {
throw new Error(
`Cluster not ready after ${maxAttempts} attempts. Last error: ${e.message}`
);
}
await sleep(waitTime);
}
}
}
describe("helm operations", () => {
let transport: StdioClientTransport;
let client: Client;
const testReleaseName = "test-nginx";
const testNamespace = "default-helm";
beforeEach(async () => {
try {
transport = new StdioClientTransport({
command: "bun",
args: ["src/index.ts"],
stderr: "pipe",
});
client = new Client(
{
name: "test-client",
version: "1.0.0",
},
{
capabilities: {},
}
);
await client.connect(transport);
await sleep(1000);
} catch (e) {
console.error("Error in beforeEach:", e);
throw e;
}
});
afterEach(async () => {
try {
// Cleanup: Uninstall the test release if it exists
await client
.request(
{
method: "tools/call",
params: {
name: "uninstall_helm_chart",
arguments: {
name: testReleaseName,
namespace: testNamespace,
},
},
},
HelmResponseSchema
)
.catch(() => {}); // Ignore errors if release doesn't exist
await transport.close();
await sleep(1000);
// Cleanup generated values files
if (fs.existsSync("test-nginx-values.yaml")) {
fs.unlinkSync("test-nginx-values.yaml");
}
} catch (e) {
console.error("Error during cleanup:", e);
}
});
test("helm chart lifecycle", async () => {
// Create namespace if it doesn't exist
try {
await client.request(
{
method: "tools/call",
params: {
name: "create_namespace",
arguments: {
name: testNamespace,
},
},
},
HelmResponseSchema
);
// Wait for namespace to be ready
await sleep(2000);
} catch (e) {
// Ignore error if namespace already exists
}
// Ensure cluster is ready before starting
await waitForClusterReadiness(client, testNamespace);
// First ensure any existing release is cleaned up
try {
await client.request(
{
method: "tools/call",
params: {
name: "uninstall_helm_chart",
arguments: {
name: testReleaseName,
namespace: testNamespace,
},
},
},
HelmResponseSchema
);
// Wait for cleanup
await sleep(5000);
} catch (e) {
// Ignore errors if release doesn't exist
}
// Verify no existing deployment
const initialCheckResult = await client.request(
{
method: "tools/call",
params: {
name: "list_deployments",
arguments: {
namespace: testNamespace,
},
},
},
HelmResponseSchema
);
const initialDeploymentsCheck = JSON.parse(
initialCheckResult.content[0].text
);
expect(
initialDeploymentsCheck.deployments.every(
(d: any) => !d.name.startsWith(testReleaseName)
)
).toBe(true);
// Step 1: Install the chart
const installResult = await client.request(
{
method: "tools/call",
params: {
name: "install_helm_chart",
arguments: {
name: testReleaseName,
chart: "bitnami/nginx",
repo: "https://charts.bitnami.com/bitnami",
namespace: testNamespace,
values: {
service: {
type: "ClusterIP",
},
resources: {
limits: {
cpu: "100m",
memory: "128Mi",
},
requests: {
cpu: "50m",
memory: "64Mi",
},
},
},
},
},
},
HelmResponseSchema
);
expect(installResult.content[0].type).toBe("text");
const installResponse = JSON.parse(installResult.content[0].text);
expect(installResponse.status).toBe("installed");
// Wait for initial deployment to be ready
await sleep(10000);
// Verify initial deployment
const initialDeploymentResult = await client.request(
{
method: "tools/call",
params: {
name: "list_deployments",
arguments: {
namespace: testNamespace,
},
},
},
HelmResponseSchema
);
const initialDeploymentsAfterInstall = JSON.parse(
initialDeploymentResult.content[0].text
);
expect(
initialDeploymentsAfterInstall.deployments.some((d: any) =>
d.name.startsWith(testReleaseName)
)
).toBe(true);
// Step 2: Upgrade the chart
await waitForClusterReadiness(client, testNamespace);
const upgradeResult = await client.request(
{
method: "tools/call",
params: {
name: "upgrade_helm_chart",
arguments: {
name: testReleaseName,
chart: "bitnami/nginx",
repo: "https://charts.bitnami.com/bitnami",
namespace: testNamespace,
values: {
replicaCount: 2,
service: {
type: "ClusterIP",
},
},
},
},
},
HelmResponseSchema
);
expect(upgradeResult.content[0].type).toBe("text");
const upgradeResponse = JSON.parse(upgradeResult.content[0].text);
expect(upgradeResponse.status).toBe("upgraded");
// Wait for upgrade to take effect
await sleep(15000);
// Verify the deployment was updated
const deploymentResult = await client.request(
{
method: "tools/call",
params: {
name: "list_deployments",
arguments: {
namespace: testNamespace,
},
},
},
HelmResponseSchema
);
const deployments = JSON.parse(deploymentResult.content[0].text);
const nginxDeployment = deployments.deployments.find((d: any) =>
d.name.startsWith(testReleaseName)
);
expect(nginxDeployment).toBeDefined();
expect(nginxDeployment.replicas).toBe(2);
// Step 3: Uninstall the chart
await waitForClusterReadiness(client, testNamespace);
const uninstallResult = await client.request(
{
method: "tools/call",
params: {
name: "uninstall_helm_chart",
arguments: {
name: testReleaseName,
namespace: testNamespace,
},
},
},
HelmResponseSchema
);
expect(uninstallResult.content[0].type).toBe("text");
const uninstallResponse = JSON.parse(uninstallResult.content[0].text);
expect(uninstallResponse.status).toBe("uninstalled");
// Wait for resources to be cleaned up
await sleep(10000);
// Verify the deployment is gone
const finalDeploymentResult = await client.request(
{
method: "tools/call",
params: {
name: "list_deployments",
arguments: {
namespace: testNamespace,
},
},
},
HelmResponseSchema
);
const finalDeployments = JSON.parse(finalDeploymentResult.content[0].text);
expect(
finalDeployments.deployments.every(
(d: any) => !d.name.startsWith(testReleaseName)
)
).toBe(true);
}, 120000); // Increase timeout to 120s for the entire lifecycle test
});