mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-11 00:09:31 +00:00
b7a32e4aab
When adding a service via the wizard, resolve the service's targetPort to the actual pod container port instead of using the service port directly. Problem: Service port 80 → Pod port 8000, but kportal was trying to forward to port 80 on the pod. Solution: Look up the pod's actual containerPort when the service uses a named targetPort (like http), and use that for port-forwarding.
309 lines
7.2 KiB
Go
309 lines
7.2 KiB
Go
package k8s
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
)
|
|
|
|
func TestResolveTargetPort(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
servicePort corev1.ServicePort
|
|
service *corev1.Service
|
|
pods []corev1.Pod
|
|
expectedPort int32
|
|
description string
|
|
}{
|
|
{
|
|
name: "numeric targetPort",
|
|
servicePort: corev1.ServicePort{
|
|
Port: 80,
|
|
TargetPort: intstr.FromInt(8000),
|
|
},
|
|
service: &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-svc",
|
|
Namespace: "default",
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Selector: map[string]string{"app": "test"},
|
|
},
|
|
},
|
|
pods: nil, // No pods needed for numeric targetPort
|
|
expectedPort: 8000,
|
|
description: "should use numeric targetPort directly",
|
|
},
|
|
{
|
|
name: "named targetPort resolved from pod",
|
|
servicePort: corev1.ServicePort{
|
|
Name: "http",
|
|
Port: 80,
|
|
TargetPort: intstr.FromString("http"),
|
|
},
|
|
service: &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-svc",
|
|
Namespace: "default",
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Selector: map[string]string{"app": "test"},
|
|
},
|
|
},
|
|
pods: []corev1.Pod{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-pod",
|
|
Namespace: "default",
|
|
Labels: map[string]string{"app": "test"},
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "main",
|
|
Ports: []corev1.ContainerPort{
|
|
{Name: "http", ContainerPort: 8000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedPort: 8000,
|
|
description: "should resolve named port from pod container",
|
|
},
|
|
{
|
|
name: "targetPort not set - defaults to service port",
|
|
servicePort: corev1.ServicePort{
|
|
Port: 80,
|
|
TargetPort: intstr.FromInt(0), // Not set
|
|
},
|
|
service: &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-svc",
|
|
Namespace: "default",
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Selector: map[string]string{"app": "test"},
|
|
},
|
|
},
|
|
pods: nil,
|
|
expectedPort: 80,
|
|
description: "should fall back to service port when targetPort is not set",
|
|
},
|
|
{
|
|
name: "named targetPort with no matching pod",
|
|
servicePort: corev1.ServicePort{
|
|
Name: "http",
|
|
Port: 80,
|
|
TargetPort: intstr.FromString("http"),
|
|
},
|
|
service: &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-svc",
|
|
Namespace: "default",
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Selector: map[string]string{"app": "test"},
|
|
},
|
|
},
|
|
pods: nil, // No pods available
|
|
expectedPort: 80,
|
|
description: "should fall back to service port when no pods found",
|
|
},
|
|
{
|
|
name: "service without selector",
|
|
servicePort: corev1.ServicePort{
|
|
Name: "http",
|
|
Port: 80,
|
|
TargetPort: intstr.FromString("http"),
|
|
},
|
|
service: &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-svc",
|
|
Namespace: "default",
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Selector: nil, // No selector
|
|
},
|
|
},
|
|
pods: nil,
|
|
expectedPort: 80,
|
|
description: "should fall back to service port when service has no selector",
|
|
},
|
|
{
|
|
name: "named targetPort not found in pod containers",
|
|
servicePort: corev1.ServicePort{
|
|
Name: "http",
|
|
Port: 80,
|
|
TargetPort: intstr.FromString("nonexistent"),
|
|
},
|
|
service: &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-svc",
|
|
Namespace: "default",
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Selector: map[string]string{"app": "test"},
|
|
},
|
|
},
|
|
pods: []corev1.Pod{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-pod",
|
|
Namespace: "default",
|
|
Labels: map[string]string{"app": "test"},
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "main",
|
|
Ports: []corev1.ContainerPort{
|
|
{Name: "http", ContainerPort: 8000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedPort: 80,
|
|
description: "should fall back to service port when named port not found in pod",
|
|
},
|
|
{
|
|
name: "multiple containers with named port in second container",
|
|
servicePort: corev1.ServicePort{
|
|
Name: "metrics",
|
|
Port: 9090,
|
|
TargetPort: intstr.FromString("metrics"),
|
|
},
|
|
service: &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-svc",
|
|
Namespace: "default",
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Selector: map[string]string{"app": "test"},
|
|
},
|
|
},
|
|
pods: []corev1.Pod{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-pod",
|
|
Namespace: "default",
|
|
Labels: map[string]string{"app": "test"},
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "main",
|
|
Ports: []corev1.ContainerPort{
|
|
{Name: "http", ContainerPort: 8000},
|
|
},
|
|
},
|
|
{
|
|
Name: "sidecar",
|
|
Ports: []corev1.ContainerPort{
|
|
{Name: "metrics", ContainerPort: 9100},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedPort: 9100,
|
|
description: "should find named port in any container",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create fake client with pods
|
|
var objects []runtime.Object
|
|
for i := range tt.pods {
|
|
objects = append(objects, &tt.pods[i])
|
|
}
|
|
fakeClient := fake.NewSimpleClientset(objects...)
|
|
|
|
// Create discovery instance (we only need it to call resolveTargetPort)
|
|
d := &Discovery{}
|
|
|
|
// Call resolveTargetPort
|
|
result := d.resolveTargetPort(
|
|
context.Background(),
|
|
fakeClient,
|
|
"default",
|
|
tt.service,
|
|
&tt.servicePort,
|
|
)
|
|
|
|
assert.Equal(t, tt.expectedPort, result, tt.description)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPortInfoTargetPort(t *testing.T) {
|
|
// Test that PortInfo correctly stores TargetPort
|
|
portInfo := PortInfo{
|
|
Name: "http",
|
|
Port: 80,
|
|
TargetPort: 8000,
|
|
Protocol: "TCP",
|
|
}
|
|
|
|
assert.Equal(t, int32(80), portInfo.Port)
|
|
assert.Equal(t, int32(8000), portInfo.TargetPort)
|
|
assert.Equal(t, "http", portInfo.Name)
|
|
assert.Equal(t, "TCP", portInfo.Protocol)
|
|
}
|
|
|
|
func TestGetUniquePorts(t *testing.T) {
|
|
// Test GetUniquePorts still works with the new PortInfo struct
|
|
pods := []PodInfo{
|
|
{
|
|
Name: "pod1",
|
|
Containers: []ContainerInfo{
|
|
{
|
|
Name: "main",
|
|
Ports: []PortInfo{
|
|
{Name: "http", Port: 8080},
|
|
{Name: "metrics", Port: 9090},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "pod2",
|
|
Containers: []ContainerInfo{
|
|
{
|
|
Name: "main",
|
|
Ports: []PortInfo{
|
|
{Name: "http", Port: 8080}, // Duplicate
|
|
{Name: "grpc", Port: 50051},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ports := GetUniquePorts(pods)
|
|
|
|
// Should have 3 unique ports
|
|
assert.Len(t, ports, 3)
|
|
|
|
// Should be sorted by port number
|
|
assert.Equal(t, int32(8080), ports[0].Port)
|
|
assert.Equal(t, int32(9090), ports[1].Port)
|
|
assert.Equal(t, int32(50051), ports[2].Port)
|
|
|
|
// Names should be preserved
|
|
assert.Equal(t, "http", ports[0].Name)
|
|
assert.Equal(t, "metrics", ports[1].Name)
|
|
assert.Equal(t, "grpc", ports[2].Name)
|
|
}
|