package kubernetes import ( "context" "errors" "testing" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client/fake" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider" ) func newTestProvider(objs ...runtime.Object) *Provider { scheme := runtime.NewScheme() if err := corev1.AddToScheme(scheme); err != nil { panic(err) } c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build() return newWithClient(c, provider.ProviderConfig{Name: "kubernetes"}) } func testPod(namespace, name, uid string, phase corev1.PodPhase, ip string) *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, Labels: map[string]string{ provider.LabelManaged: provider.LabelManagedYes, provider.LabelUID: uid, }, }, Status: corev1.PodStatus{Phase: phase, PodIP: ip}, } } func TestProvider_Create_buildsPodAndReturnsProviderID(t *testing.T) { t.Parallel() p := newTestProvider() id, err := p.Create(context.Background(), provider.CreateRequest{ Name: "proxy-abc", UID: "uid-1", Namespace: "crawl", Port: 3128, }) if err != nil { t.Fatalf("Create() error = %v", err) } if id != "crawl/proxy-abc" { t.Errorf("Create() id = %q, want %q", id, "crawl/proxy-abc") } var pod corev1.Pod key := types.NamespacedName{Namespace: "crawl", Name: "proxy-abc"} if err := p.client.Get(context.Background(), key, &pod); err != nil { t.Fatalf("expected the Pod to exist: %v", err) } if pod.Labels[provider.LabelUID] != "uid-1" { t.Errorf("pod UID label = %q, want %q", pod.Labels[provider.LabelUID], "uid-1") } } func TestProvider_Create_idempotent(t *testing.T) { t.Parallel() p := newTestProvider() ctx := context.Background() req := provider.CreateRequest{Name: "proxy-dup", UID: "uid-2", Namespace: "crawl", Port: 3128} id1, err := p.Create(ctx, req) if err != nil { t.Fatalf("Create() #1 error = %v", err) } id2, err := p.Create(ctx, req) if err != nil { t.Fatalf("Create() #2 error = %v, want nil (AlreadyExists must be absorbed)", err) } if id1 != id2 { t.Errorf("Create() not idempotent: %q != %q", id1, id2) } } func TestProvider_Get_stateMapping(t *testing.T) { t.Parallel() tests := []struct { name string phase corev1.PodPhase ip string wantState provider.InstanceState wantIP string }{ {"pending", corev1.PodPending, "", provider.StateProvisioning, ""}, {"running with IP", corev1.PodRunning, "10.0.0.5", provider.StateRunning, "10.0.0.5"}, {"running without IP yet", corev1.PodRunning, "", provider.StateProvisioning, ""}, {"succeeded", corev1.PodSucceeded, "10.0.0.5", provider.StateTerminated, ""}, {"failed", corev1.PodFailed, "10.0.0.5", provider.StateTerminated, ""}, {"unknown", corev1.PodUnknown, "10.0.0.5", provider.StateTerminated, ""}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() pod := testPod("crawl", "proxy-"+tc.name, "uid", tc.phase, tc.ip) p := newTestProvider(pod) inst, err := p.Get(context.Background(), "crawl/"+pod.Name) if err != nil { t.Fatalf("Get() error = %v", err) } if inst.State != tc.wantState { t.Errorf("State = %v, want %v", inst.State, tc.wantState) } if inst.IP != tc.wantIP { t.Errorf("IP = %q, want %q", inst.IP, tc.wantIP) } if inst.UID != "uid" { t.Errorf("UID = %q, want %q", inst.UID, "uid") } }) } } func TestProvider_Get_notFound(t *testing.T) { t.Parallel() p := newTestProvider() _, err := p.Get(context.Background(), "crawl/does-not-exist") if !errors.Is(err, provider.ErrNotFound) { t.Errorf("Get() error = %v, want ErrNotFound", err) } } func TestProvider_Get_malformedProviderID(t *testing.T) { t.Parallel() p := newTestProvider() _, err := p.Get(context.Background(), "no-namespace-here") if err == nil { t.Fatal("Get() error = nil, want error for a providerID with no namespace") } } func TestProvider_Delete_idempotent(t *testing.T) { t.Parallel() pod := testPod("crawl", "proxy-del", "uid", corev1.PodRunning, "10.0.0.5") p := newTestProvider(pod) ctx := context.Background() if err := p.Delete(ctx, "crawl/proxy-del"); err != nil { t.Fatalf("Delete() error = %v", err) } if err := p.Delete(ctx, "crawl/proxy-del"); err != nil { t.Errorf("Delete() (repeat) error = %v, want nil", err) } if err := p.Delete(ctx, "crawl/never-existed"); err != nil { t.Errorf("Delete() on unknown ID error = %v, want nil", err) } if _, err := p.Get(ctx, "crawl/proxy-del"); !errors.Is(err, provider.ErrNotFound) { t.Errorf("Get() after Delete() error = %v, want ErrNotFound", err) } } func TestProvider_Delete_malformedProviderID(t *testing.T) { t.Parallel() p := newTestProvider() if err := p.Delete(context.Background(), "no-namespace-here"); err == nil { t.Fatal("Delete() error = nil, want error for a providerID with no namespace") } } func TestProvider_ListByTag_filtersByLabelAcrossNamespaces(t *testing.T) { t.Parallel() managed1 := testPod("crawl", "proxy-a", "uid-a", corev1.PodRunning, "10.0.0.1") managed2 := testPod("other-ns", "proxy-b", "uid-b", corev1.PodPending, "") unmanaged := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "not-ours", Namespace: "crawl"}} p := newTestProvider(managed1, managed2, unmanaged) instances, err := p.ListByTag(context.Background()) if err != nil { t.Fatalf("ListByTag() error = %v", err) } if len(instances) != 2 { t.Fatalf("len(instances) = %d, want 2 (unmanaged Pod must be excluded)", len(instances)) } byID := make(map[string]provider.Instance, len(instances)) for _, inst := range instances { byID[inst.ID] = inst } a, ok := byID["crawl/proxy-a"] if !ok { t.Fatal(`instances missing "crawl/proxy-a"`) } if a.State != provider.StateRunning || a.IP != "10.0.0.1" { t.Errorf("crawl/proxy-a = %+v, want Running/10.0.0.1", a) } if _, ok := byID["other-ns/proxy-b"]; !ok { t.Fatal(`instances missing "other-ns/proxy-b" (ListByTag must not be namespace-scoped)`) } } func TestNewWithClient_image(t *testing.T) { t.Parallel() scheme := runtime.NewScheme() _ = corev1.AddToScheme(scheme) c := fake.NewClientBuilder().WithScheme(scheme).Build() def := newWithClient(c, provider.ProviderConfig{Name: "k8s"}) if def.image != defaultImage { t.Errorf("default image = %q, want %q", def.image, defaultImage) } custom := newWithClient(c, provider.ProviderConfig{ Name: "k8s", Kubernetes: &provider.KubernetesConfig{Image: "myregistry/squid:custom"}, }) if custom.image != "myregistry/squid:custom" { t.Errorf("custom image = %q, want %q", custom.image, "myregistry/squid:custom") } } func TestClassify(t *testing.T) { t.Parallel() gr := schema.GroupResource{Group: "", Resource: "pods"} tests := []struct { name string err error want error }{ {"not found", apierrors.NewNotFound(gr, "x"), provider.ErrNotFound}, {"too many requests", apierrors.NewTooManyRequests("slow down", 5), provider.ErrTransient}, {"server timeout", apierrors.NewServerTimeout(gr, "create", 5), provider.ErrTransient}, {"forbidden", apierrors.NewForbidden(gr, "x", errors.New("denied")), provider.ErrPermanent}, {"bad request", apierrors.NewBadRequest("bad"), provider.ErrPermanent}, {"unauthorized", apierrors.NewUnauthorized("no creds"), provider.ErrPermanent}, {"unclassified", errors.New("boom"), provider.ErrTransient}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() got := classify("op", "id", tc.err) if !errors.Is(got, tc.want) { t.Errorf("classify(%v) = %v, want class %v", tc.err, got, tc.want) } }) } }