Skip to main content
Glama

mcp-server-kubernetes

by Flux159
cronjob.test.ts10.8 kB
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 { z } from "zod"; // Define the response type for easier use in tests type KubectlResponse = { content: Array<{ type: "text"; text: string; }>; }; /** * Utility function to create a promise that resolves after specified milliseconds */ async function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Generates a random identifier for resource naming */ function generateRandomId(): string { return Math.random().toString(36).substring(2, 10); } /** * Test suite for CronJob related operations using kubectl commands * Tests CronJob creation, listing, describing, and associated Job operations */ describe("kubernetes cronjob operations with kubectl commands", () => { let transport: StdioClientTransport; let client: Client; let testNamespace: string; const NAMESPACE_PREFIX = "test-cronjob-ns"; /** * Set up before each test: * - Creates a new StdioClientTransport instance * - Initializes and connects the MCP client * - Creates a test namespace for isolation */ beforeEach(async () => { try { // Create transport and client 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); // Wait longer for connection to be established await sleep(5000); // Create a unique test namespace for test isolation testNamespace = `${NAMESPACE_PREFIX}-${generateRandomId()}`; console.log(`Creating test namespace: ${testNamespace}`); await client.request( { method: "tools/call", params: { name: "kubectl_create", arguments: { resourceType: "namespace", name: testNamespace, }, }, }, // @ts-ignore - Ignoring type error for now z.any() ); // Wait longer for namespace to be fully created await sleep(5000); } catch (e) { console.error("Error in beforeEach:", e); throw e; } }); /** * Clean up after each test: * - Delete test namespace and resources * - Close transport connection */ afterEach(async () => { try { // Clean up namespace using kubectl_delete console.log(`Cleaning up test namespace: ${testNamespace}`); await client.request( { method: "tools/call", params: { name: "kubectl_delete", arguments: { resourceType: "namespace", name: testNamespace, }, }, }, // @ts-ignore - Ignoring type error for now z.any() ); // Close client connection await transport.close(); await sleep(5000); } catch (e) { console.error("Error during cleanup:", e); } }); /** * Test case: Verify CronJob listing functionality */ test("list cronjobs in namespace", async () => { // List CronJobs using kubectl_get const listResult = await client.request( { method: "tools/call", params: { name: "kubectl_get", arguments: { resourceType: "cronjobs", namespace: testNamespace, output: "json" }, }, }, // @ts-ignore - Ignoring type error for now z.any() ) as KubectlResponse; expect(listResult.content[0].type).toBe("text"); const cronJobs = JSON.parse(listResult.content[0].text); expect(cronJobs).toBeDefined(); // Check the structure of the response if (cronJobs.items) { expect(Array.isArray(cronJobs.items)).toBe(true); } else if (Array.isArray(cronJobs)) { // Direct array response expect(Array.isArray(cronJobs)).toBe(true); } else { // Unexpected format, log for debugging console.log("Unexpected CronJobs response format:", JSON.stringify(cronJobs).substring(0, 300)); } }, 60000); // 60 second timeout /** * Test case: Comprehensive CronJob lifecycle * Tests creating, describing, and managing a CronJob */ test( "cronjob lifecycle management with kubectl commands", async () => { const cronJobName = `test-cronjob-${generateRandomId()}`; // Step 1: Create a new CronJob console.log(`Creating CronJob: ${cronJobName}`); // Create CronJob manifest const cronJobManifest = ` apiVersion: batch/v1 kind: CronJob metadata: name: ${cronJobName} namespace: ${testNamespace} labels: mcp-managed: "true" app: ${cronJobName} spec: schedule: "*/5 * * * *" suspend: true jobTemplate: spec: template: spec: containers: - name: ${cronJobName} image: busybox command: ["/bin/sh", "-c", "echo Hello from CronJob $(date)"] restartPolicy: OnFailure `; const createResult = await client.request( { method: "tools/call", params: { name: "kubectl_create", arguments: { manifest: cronJobManifest, namespace: testNamespace }, }, }, // @ts-ignore - Ignoring type error for now z.any() ) as KubectlResponse; // Verify creation response expect(createResult.content[0].type).toBe("text"); expect(createResult.content[0].text).toContain("CronJob"); expect(createResult.content[0].text).toContain(cronJobName); // Wait for CronJob to be fully created await sleep(5000); // Step 2: Verify CronJob appears in list const listResult = await client.request( { method: "tools/call", params: { name: "kubectl_get", arguments: { resourceType: "cronjobs", namespace: testNamespace, output: "json" }, }, }, // @ts-ignore - Ignoring type error for now z.any() ) as KubectlResponse; //print the response console.log("List CronJobs response:", listResult.content[0].text); expect(listResult.content[0].type).toBe("text"); const cronJobs = JSON.parse(listResult.content[0].text); expect(cronJobs).toBeDefined(); // Find our CronJob in the list - handle different response formats let createdCronJob; if (cronJobs.items && Array.isArray(cronJobs.items)) { // Standard K8s API response createdCronJob = cronJobs.items.find((cj: any) => { if (cj.metadata && cj.metadata.name) { return cj.metadata.name === cronJobName; } else if (cj.name) { return cj.name === cronJobName; } return false; }); } else if (Array.isArray(cronJobs)) { // Direct array response createdCronJob = cronJobs.find((cj: any) => { if (typeof cj === 'string') { return cj === cronJobName; } else if (cj.metadata && cj.metadata.name) { return cj.metadata.name === cronJobName; } else if (cj.name) { return cj.name === cronJobName; } return false; }); } expect(createdCronJob).toBeDefined(); // Based on the actual response, we can only verify that the CronJob was created // The simplified API response doesn't include schedule or suspend values // Let's use kubectl_describe to get more detailed information // Step 3: Describe the CronJob const describeResult = await client.request( { method: "tools/call", params: { name: "kubectl_describe", arguments: { resourceType: "cronjob", name: cronJobName, namespace: testNamespace, }, }, }, // @ts-ignore - Ignoring type error for now z.any() ) as KubectlResponse; expect(describeResult.content[0].type).toBe("text"); const describeOutput = describeResult.content[0].text; console.log("Describe CronJob output excerpt:", describeOutput.substring(0, 300)); // Verify the schedule and suspended state from the describe output expect(describeOutput).toContain(cronJobName); expect(describeOutput).toContain("*/5 * * * *"); // Schedule should appear in the describe output expect(describeOutput).toContain("Suspend:"); // Check that suspend property exists expect(describeOutput).toMatch(/Suspend:.*True/i); // Check for suspend status (case-insensitive) expect(describeOutput).toContain("busybox"); // Step 4: List Jobs (should be empty since CronJob is suspended) const listJobsResult = await client.request( { method: "tools/call", params: { name: "kubectl_get", arguments: { resourceType: "jobs", namespace: testNamespace, labelSelector: `app=${cronJobName}`, output: "json" }, }, }, // @ts-ignore - Ignoring type error for now z.any() ) as KubectlResponse; expect(listJobsResult.content[0].type).toBe("text"); const jobs = JSON.parse(listJobsResult.content[0].text); expect(jobs.items).toBeDefined(); expect(Array.isArray(jobs.items)).toBe(true); // Should be empty since we suspended the CronJob expect(jobs.items.length).toBe(0); // Step 5: Delete the CronJob const deleteCronJobResult = await client.request( { method: "tools/call", params: { name: "kubectl_delete", arguments: { resourceType: "cronjob", name: cronJobName, namespace: testNamespace, }, }, }, // @ts-ignore - Ignoring type error for now z.any() ) as KubectlResponse; expect(deleteCronJobResult.content[0].type).toBe("text"); expect(deleteCronJobResult.content[0].text).toContain(`"${cronJobName}" deleted`); // We should rely on the cleanup in afterEach to remove all resources }, { timeout: 120000 } // 120 second timeout for increased reliability ); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Flux159/mcp-server-kubernetes'

If you have feedback or need assistance with the MCP directory API, please join our Discord server