Files
kportal/internal/k8s/resolver_extended_test.go
T

431 lines
12 KiB
Go

package k8s
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
// =============================================================================
// ResourceResolver Extended Tests
// =============================================================================
func TestResourceResolver_ResolvePodPrefix_CacheHit(t *testing.T) {
baseTime := time.Now()
pool := setupTestPool(t, "test-context",
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app-xyz789",
Namespace: "default",
CreationTimestamp: metav1.Time{Time: baseTime},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
)
r := NewResourceResolver(pool)
// First call - hits API
result1, err := r.Resolve(t.Context(), "test-context", "default", "pod/my-app", "")
require.NoError(t, err)
assert.Equal(t, "pod/my-app-xyz789", result1)
// Second call - should use cache (instant)
start := time.Now()
result2, err := r.Resolve(t.Context(), "test-context", "default", "pod/my-app", "")
require.NoError(t, err)
assert.Equal(t, result1, result2)
// Should be very fast since it's cached
assert.Less(t, time.Since(start), 10*time.Millisecond)
}
func TestResourceResolver_ResolvePodSelector_CacheHit(t *testing.T) {
pool := setupTestPool(t, "test-context",
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "app-pod",
Namespace: "default",
Labels: map[string]string{"app": "myapp"},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
)
r := NewResourceResolver(pool)
// First call - hits API
result1, err := r.Resolve(t.Context(), "test-context", "default", "pod", "app=myapp")
require.NoError(t, err)
assert.Equal(t, "pod/app-pod", result1)
// Second call - should use cache
result2, err := r.Resolve(t.Context(), "test-context", "default", "pod", "app=myapp")
require.NoError(t, err)
assert.Equal(t, result1, result2)
}
func TestResourceResolver_ResolvePodPrefix_ExcludesNonRunning(t *testing.T) {
baseTime := time.Now()
pool := setupTestPool(t, "test-context",
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app-pending",
Namespace: "default",
CreationTimestamp: metav1.Time{Time: baseTime},
},
Status: corev1.PodStatus{Phase: corev1.PodPending},
},
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app-succeeded",
Namespace: "default",
CreationTimestamp: metav1.Time{Time: baseTime},
},
Status: corev1.PodStatus{Phase: corev1.PodSucceeded},
},
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app-failed",
Namespace: "default",
CreationTimestamp: metav1.Time{Time: baseTime},
},
Status: corev1.PodStatus{Phase: corev1.PodFailed},
},
)
r := NewResourceResolver(pool)
_, err := r.Resolve(t.Context(), "test-context", "default", "pod/my-app", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "no running pods found matching prefix")
}
func TestResourceResolver_ResolvePodSelector_ExcludesNonRunning(t *testing.T) {
pool := setupTestPool(t, "test-context",
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "app-pod-pending",
Namespace: "default",
Labels: map[string]string{"app": "myapp"},
},
Status: corev1.PodStatus{Phase: corev1.PodPending},
},
)
r := NewResourceResolver(pool)
_, err := r.Resolve(t.Context(), "test-context", "default", "pod", "app=myapp")
assert.Error(t, err)
assert.Contains(t, err.Error(), "no running pods found matching selector")
}
func TestResourceResolver_getFromCache_NotFound(t *testing.T) {
pool, _ := NewClientPool()
r := NewResourceResolver(pool)
result := r.getFromCache("non-existent-key")
assert.Empty(t, result)
}
func TestResourceResolver_getFromCache_ExpiredEntry(t *testing.T) {
pool, _ := NewClientPool()
r := NewResourceResolver(pool)
r.SetCacheTTL(1 * time.Millisecond)
// Put entry in cache
r.putInCache("test-key", "test-value")
// Verify it's there
result := r.getFromCache("test-key")
assert.Equal(t, "test-value", result)
// Wait for expiry
time.Sleep(10 * time.Millisecond)
// Should be expired and cleaned up
result = r.getFromCache("test-key")
assert.Empty(t, result)
// Verify entry was deleted
r.cacheMu.RLock()
_, exists := r.cache["test-key"]
r.cacheMu.RUnlock()
assert.False(t, exists)
}
func TestResourceResolver_InvalidateCache_NoEntries(t *testing.T) {
pool, _ := NewClientPool()
r := NewResourceResolver(pool)
// Should not panic on empty cache
r.InvalidateCache("test-context", "default", "pod/app")
assert.NotNil(t, r.cache)
}
func TestResourceResolver_Resolve_GetClientError(t *testing.T) {
// Create pool without test client - should fail when trying to get client
pool, _ := NewClientPool()
r := NewResourceResolver(pool)
_, err := r.Resolve(t.Context(), "non-existent-context", "default", "pod/test", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to get client")
}
func TestResourceResolver_ResolvePodPrefix_MultipleMatchesReturnsNewest(t *testing.T) {
baseTime := time.Now()
pool := setupTestPool(t, "test-context",
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app-oldest",
Namespace: "default",
CreationTimestamp: metav1.Time{Time: baseTime.Add(-2 * time.Hour)},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app-middle",
Namespace: "default",
CreationTimestamp: metav1.Time{Time: baseTime.Add(-1 * time.Hour)},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app-newest",
Namespace: "default",
CreationTimestamp: metav1.Time{Time: baseTime},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
)
r := NewResourceResolver(pool)
result, err := r.Resolve(t.Context(), "test-context", "default", "pod/my-app", "")
require.NoError(t, err)
assert.Equal(t, "pod/my-app-newest", result)
}
func TestResourceResolver_ResolvePodSelector_FirstRunning(t *testing.T) {
pool := setupTestPool(t, "test-context",
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "app-pod-1",
Namespace: "default",
Labels: map[string]string{"app": "myapp"},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "app-pod-2",
Namespace: "default",
Labels: map[string]string{"app": "myapp"},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
)
r := NewResourceResolver(pool)
result, err := r.Resolve(t.Context(), "test-context", "default", "pod", "app=myapp")
require.NoError(t, err)
// Should return the first running pod found
assert.Equal(t, "pod/app-pod-1", result)
}
// =============================================================================
// Discovery Extended Tests
// =============================================================================
func TestDiscovery_ListPods_FilteringAndSorting(t *testing.T) {
baseTime := time.Now()
pool := setupTestPool(t, "test-context",
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "newer-running-pod",
Namespace: "default",
CreationTimestamp: metav1.Time{Time: baseTime},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Ports: []corev1.ContainerPort{
{ContainerPort: 8080, Protocol: corev1.ProtocolTCP},
},
},
},
},
},
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "older-pending-pod",
Namespace: "default",
CreationTimestamp: metav1.Time{Time: baseTime.Add(-time.Hour)},
},
Status: corev1.PodStatus{Phase: corev1.PodPending},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "main"},
},
},
},
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "older-running-pod",
Namespace: "default",
CreationTimestamp: metav1.Time{Time: baseTime.Add(-2 * time.Hour)},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "main"},
},
},
},
// Pods in other namespaces should not appear
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "other-namespace-pod",
Namespace: "kube-system",
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
)
d := NewDiscovery(pool)
pods, err := d.ListPods(t.Context(), "test-context", "default")
require.NoError(t, err)
assert.Len(t, pods, 3) // 2 running + 1 pending
// Should be sorted by creation time (newest first)
assert.Equal(t, "newer-running-pod", pods[0].Name)
assert.Equal(t, "older-pending-pod", pods[1].Name)
assert.Equal(t, "older-running-pod", pods[2].Name)
// Check protocol is set correctly
assert.Equal(t, "TCP", pods[0].Containers[0].Ports[0].Protocol)
}
func TestDiscovery_ListPodsWithSelector_OnlyRunning(t *testing.T) {
pool := setupTestPool(t, "test-context",
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "running-pod",
Namespace: "default",
Labels: map[string]string{"app": "myapp"},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
},
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pending-pod",
Namespace: "default",
Labels: map[string]string{"app": "myapp"},
},
Status: corev1.PodStatus{Phase: corev1.PodPending},
},
)
d := NewDiscovery(pool)
pods, err := d.ListPodsWithSelector(t.Context(), "test-context", "default", "app=myapp")
require.NoError(t, err)
// Only running pods should be returned for selector-based queries
assert.Len(t, pods, 1)
assert.Equal(t, "running-pod", pods[0].Name)
}
func TestDiscovery_ListServices_WithNamedPortResolution(t *testing.T) {
pool := setupTestPool(t, "test-context",
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "backend-pod",
Namespace: "default",
Labels: map[string]string{"app": "backend"},
},
Status: corev1.PodStatus{Phase: corev1.PodRunning},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Ports: []corev1.ContainerPort{
{Name: "http", ContainerPort: 8080},
{Name: "grpc", ContainerPort: 50051},
},
},
},
},
},
&corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "backend-svc",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
Selector: map[string]string{"app": "backend"},
Ports: []corev1.ServicePort{
{Name: "http", Port: 80, TargetPort: intstr.FromString("http")},
{Name: "grpc", Port: 50051, TargetPort: intstr.FromString("grpc")},
},
},
},
)
d := NewDiscovery(pool)
services, err := d.ListServices(t.Context(), "test-context", "default")
require.NoError(t, err)
assert.Len(t, services, 1)
// Named ports should be resolved
assert.Len(t, services[0].Ports, 2)
assert.Equal(t, int32(80), services[0].Ports[0].Port)
assert.Equal(t, int32(8080), services[0].Ports[0].TargetPort) // Resolved from pod
assert.Equal(t, int32(50051), services[0].Ports[1].Port)
assert.Equal(t, int32(50051), services[0].Ports[1].TargetPort) // Resolved from pod
}
func TestDiscovery_ListServices_NoBackingPods(t *testing.T) {
pool := setupTestPool(t, "test-context",
&corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "backend-svc",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
Selector: map[string]string{"app": "nonexistent"},
Ports: []corev1.ServicePort{
{Name: "http", Port: 80, TargetPort: intstr.FromString("http")},
},
},
},
)
d := NewDiscovery(pool)
services, err := d.ListServices(t.Context(), "test-context", "default")
require.NoError(t, err)
assert.Len(t, services, 1)
// When no backing pods, falls back to service port
assert.Equal(t, int32(80), services[0].Ports[0].TargetPort)
}