feat: initial kube-vip Headlamp plugin
Headlamp plugin providing visibility into kube-vip virtual IP and load balancer deployments. Features: - Overview dashboard with deployment status, VIP mode, leader election - Services page with LoadBalancer VIP assignments and detail panels - Nodes page showing kube-vip pod status and leader designation - Configuration page with DaemonSet config, IP pools, leases - Service detail section injected into native Headlamp Service views Read-only plugin — no cluster write operations. Uses standard K8s resources (no CRDs): Services, Nodes, Pods, DaemonSets, Leases, ConfigMaps with kube-vip.io/* annotations. 74 tests across 7 test files. All tsc/lint/format/test checks pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock(
|
||||
'@kinvolk/headlamp-plugin/lib/CommonComponents',
|
||||
async () => await import('./__mocks__/commonComponents')
|
||||
);
|
||||
|
||||
vi.mock('../api/KubeVipDataContext');
|
||||
|
||||
import { useKubeVipContext } from '../api/KubeVipDataContext';
|
||||
import { defaultContext, makeSampleService } from '../test-helpers';
|
||||
import ServiceDetailSection from './ServiceDetailSection';
|
||||
|
||||
function mockContext(overrides?: Parameters<typeof defaultContext>[0]) {
|
||||
vi.mocked(useKubeVipContext).mockReturnValue(defaultContext(overrides));
|
||||
}
|
||||
|
||||
describe('ServiceDetailSection', () => {
|
||||
it('returns null when loading', () => {
|
||||
mockContext({ loading: true });
|
||||
const { container } = render(
|
||||
<ServiceDetailSection
|
||||
resource={{
|
||||
metadata: { name: 'svc', namespace: 'default' },
|
||||
spec: { type: 'LoadBalancer' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('returns null for non-LoadBalancer services', () => {
|
||||
mockContext();
|
||||
const { container } = render(
|
||||
<ServiceDetailSection
|
||||
resource={{ metadata: { name: 'svc', namespace: 'default' }, spec: { type: 'ClusterIP' } }}
|
||||
/>
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('returns null when service is not in filtered list', () => {
|
||||
mockContext({ loadBalancerServices: [] });
|
||||
const { container } = render(
|
||||
<ServiceDetailSection
|
||||
resource={{
|
||||
metadata: { name: 'unknown', namespace: 'default' },
|
||||
spec: { type: 'LoadBalancer' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('renders kube-vip details for matching LoadBalancer service', () => {
|
||||
const svc = makeSampleService();
|
||||
mockContext({ loadBalancerServices: [svc] });
|
||||
render(
|
||||
<ServiceDetailSection
|
||||
resource={{
|
||||
metadata: { name: 'my-service', namespace: 'default' },
|
||||
spec: { type: 'LoadBalancer' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('kube-vip Details')).toBeInTheDocument();
|
||||
expect(screen.getByText('192.168.1.200')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows VIP host when available', () => {
|
||||
const svc = makeSampleService();
|
||||
mockContext({ loadBalancerServices: [svc] });
|
||||
render(
|
||||
<ServiceDetailSection
|
||||
resource={{
|
||||
metadata: { name: 'my-service', namespace: 'default' },
|
||||
spec: { type: 'LoadBalancer' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
// "node-1" appears in both the VIP Host row and the vipHost annotation
|
||||
expect(screen.getAllByText('node-1').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('VIP Host Node')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows egress label when enabled', () => {
|
||||
const svc = makeSampleService({
|
||||
metadata: {
|
||||
name: 'egress-svc',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'kube-vip.io/loadbalancerIPs': '10.0.0.1',
|
||||
'kube-vip.io/egress': 'true',
|
||||
'kube-vip.io/vipHost': 'node-1',
|
||||
},
|
||||
},
|
||||
});
|
||||
mockContext({ loadBalancerServices: [svc] });
|
||||
render(
|
||||
<ServiceDetailSection
|
||||
resource={{
|
||||
metadata: { name: 'egress-svc', namespace: 'default' },
|
||||
spec: { type: 'LoadBalancer' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Egress')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows ignored warning when service is ignored', () => {
|
||||
const svc = makeSampleService({
|
||||
metadata: {
|
||||
name: 'ignored-svc',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'kube-vip.io/ignore': 'true',
|
||||
'kube-vip.io/loadbalancerIPs': '10.0.0.1',
|
||||
},
|
||||
},
|
||||
});
|
||||
mockContext({ loadBalancerServices: [svc] });
|
||||
render(
|
||||
<ServiceDetailSection
|
||||
resource={{
|
||||
metadata: { name: 'ignored-svc', namespace: 'default' },
|
||||
spec: { type: 'LoadBalancer' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/ignoring this service/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user