mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-14 07:51:00 +00:00
Port forward to the named service ports.
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.
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user