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:
DevContainer User
2026-03-04 00:23:08 +00:00
commit 3b9d007e8b
37 changed files with 22722 additions and 0 deletions
@@ -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();
});
});