mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-05 06:05:39 +00:00
431 lines
12 KiB
Go
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)
|
|
}
|