167 lines
5.3 KiB
TypeScript
167 lines
5.3 KiB
TypeScript
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
import {
|
||
|
|
generatePodSpec,
|
||
|
|
generateDeploymentSpec,
|
||
|
|
generateNamespaceSpec,
|
||
|
|
formatMemory,
|
||
|
|
formatCpu,
|
||
|
|
sanitizeName,
|
||
|
|
} from '../src/services/k8s/manifest-generator.js';
|
||
|
|
import type { ContainerSpec } from '../src/services/orchestrator.js';
|
||
|
|
|
||
|
|
const baseSpec: ContainerSpec = {
|
||
|
|
image: 'mcpctl/test-server:latest',
|
||
|
|
name: 'test-server',
|
||
|
|
};
|
||
|
|
|
||
|
|
describe('formatMemory', () => {
|
||
|
|
it('formats bytes to Gi', () => {
|
||
|
|
expect(formatMemory(1024 * 1024 * 1024)).toBe('1Gi');
|
||
|
|
expect(formatMemory(2 * 1024 * 1024 * 1024)).toBe('2Gi');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('formats bytes to Mi', () => {
|
||
|
|
expect(formatMemory(512 * 1024 * 1024)).toBe('512Mi');
|
||
|
|
expect(formatMemory(256 * 1024 * 1024)).toBe('256Mi');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('formats bytes to Ki', () => {
|
||
|
|
expect(formatMemory(64 * 1024)).toBe('64Ki');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('formats small values as plain bytes', () => {
|
||
|
|
expect(formatMemory(500)).toBe('500');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('formatCpu', () => {
|
||
|
|
it('converts nanoCPUs to millicores', () => {
|
||
|
|
expect(formatCpu(500_000_000)).toBe('500m');
|
||
|
|
expect(formatCpu(1_000_000_000)).toBe('1000m');
|
||
|
|
expect(formatCpu(250_000_000)).toBe('250m');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('sanitizeName', () => {
|
||
|
|
it('lowercases and replaces invalid chars', () => {
|
||
|
|
expect(sanitizeName('My Server')).toBe('my-server');
|
||
|
|
expect(sanitizeName('test_server.v2')).toBe('test-server-v2');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('strips leading/trailing hyphens', () => {
|
||
|
|
expect(sanitizeName('-hello-')).toBe('hello');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('truncates to 63 chars', () => {
|
||
|
|
const long = 'a'.repeat(100);
|
||
|
|
expect(sanitizeName(long).length).toBeLessThanOrEqual(63);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('generatePodSpec', () => {
|
||
|
|
it('generates valid pod manifest', () => {
|
||
|
|
const pod = generatePodSpec(baseSpec, 'default');
|
||
|
|
|
||
|
|
expect(pod.apiVersion).toBe('v1');
|
||
|
|
expect(pod.kind).toBe('Pod');
|
||
|
|
expect(pod.metadata.name).toBe('test-server');
|
||
|
|
expect(pod.metadata.namespace).toBe('default');
|
||
|
|
expect(pod.metadata.labels['mcpctl.managed']).toBe('true');
|
||
|
|
expect(pod.spec.containers).toHaveLength(1);
|
||
|
|
expect(pod.spec.containers[0]!.image).toBe('mcpctl/test-server:latest');
|
||
|
|
expect(pod.spec.restartPolicy).toBe('Always');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('applies default resource limits', () => {
|
||
|
|
const pod = generatePodSpec(baseSpec, 'default');
|
||
|
|
const container = pod.spec.containers[0]!;
|
||
|
|
expect(container.resources.limits.memory).toBe('512Mi');
|
||
|
|
expect(container.resources.limits.cpu).toBe('500m');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('applies custom resource limits', () => {
|
||
|
|
const spec: ContainerSpec = {
|
||
|
|
...baseSpec,
|
||
|
|
memoryLimit: 1024 * 1024 * 1024,
|
||
|
|
nanoCpus: 1_000_000_000,
|
||
|
|
};
|
||
|
|
const pod = generatePodSpec(spec, 'default');
|
||
|
|
const container = pod.spec.containers[0]!;
|
||
|
|
expect(container.resources.limits.memory).toBe('1Gi');
|
||
|
|
expect(container.resources.limits.cpu).toBe('1000m');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('includes env vars when specified', () => {
|
||
|
|
const spec: ContainerSpec = {
|
||
|
|
...baseSpec,
|
||
|
|
env: { API_KEY: 'secret', PORT: '3000' },
|
||
|
|
};
|
||
|
|
const pod = generatePodSpec(spec, 'test-ns');
|
||
|
|
const container = pod.spec.containers[0]!;
|
||
|
|
expect(container.env).toEqual([
|
||
|
|
{ name: 'API_KEY', value: 'secret' },
|
||
|
|
{ name: 'PORT', value: '3000' },
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('includes port when specified', () => {
|
||
|
|
const spec: ContainerSpec = { ...baseSpec, containerPort: 8080 };
|
||
|
|
const pod = generatePodSpec(spec, 'default');
|
||
|
|
const container = pod.spec.containers[0]!;
|
||
|
|
expect(container.ports).toEqual([{ containerPort: 8080 }]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('omits env and ports when not specified', () => {
|
||
|
|
const pod = generatePodSpec(baseSpec, 'default');
|
||
|
|
const container = pod.spec.containers[0]!;
|
||
|
|
expect(container.env).toBeUndefined();
|
||
|
|
expect(container.ports).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('sets security context', () => {
|
||
|
|
const pod = generatePodSpec(baseSpec, 'default');
|
||
|
|
const sc = pod.spec.containers[0]!.securityContext;
|
||
|
|
expect(sc.runAsNonRoot).toBe(true);
|
||
|
|
expect(sc.readOnlyRootFilesystem).toBe(true);
|
||
|
|
expect(sc.allowPrivilegeEscalation).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('propagates custom labels', () => {
|
||
|
|
const spec: ContainerSpec = {
|
||
|
|
...baseSpec,
|
||
|
|
labels: { team: 'infra', version: 'v1' },
|
||
|
|
};
|
||
|
|
const pod = generatePodSpec(spec, 'default');
|
||
|
|
expect(pod.metadata.labels['team']).toBe('infra');
|
||
|
|
expect(pod.metadata.labels['version']).toBe('v1');
|
||
|
|
expect(pod.metadata.labels['mcpctl.managed']).toBe('true');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('generateDeploymentSpec', () => {
|
||
|
|
it('generates valid deployment manifest', () => {
|
||
|
|
const dep = generateDeploymentSpec(baseSpec, 'prod', 3);
|
||
|
|
|
||
|
|
expect(dep.apiVersion).toBe('apps/v1');
|
||
|
|
expect(dep.kind).toBe('Deployment');
|
||
|
|
expect(dep.metadata.namespace).toBe('prod');
|
||
|
|
expect(dep.spec.replicas).toBe(3);
|
||
|
|
expect(dep.spec.selector.matchLabels['mcpctl.managed']).toBe('true');
|
||
|
|
expect(dep.spec.template.spec.containers).toHaveLength(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('defaults to 1 replica', () => {
|
||
|
|
const dep = generateDeploymentSpec(baseSpec, 'default');
|
||
|
|
expect(dep.spec.replicas).toBe(1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('generateNamespaceSpec', () => {
|
||
|
|
it('generates namespace manifest', () => {
|
||
|
|
const ns = generateNamespaceSpec('mcpctl-prod');
|
||
|
|
expect(ns.apiVersion).toBe('v1');
|
||
|
|
expect(ns.kind).toBe('Namespace');
|
||
|
|
expect(ns.metadata.name).toBe('mcpctl-prod');
|
||
|
|
});
|
||
|
|
});
|