272 lines
9.4 KiB
Go
272 lines
9.4 KiB
Go
package sessions
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"sync/atomic"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
// runningReadyPod returns minimal k8s pod JSON: phase Running + Ready condition.
|
||
|
|
func runningReadyPod(name string) map[string]any {
|
||
|
|
return map[string]any{
|
||
|
|
"metadata": map[string]any{"name": name},
|
||
|
|
"status": map[string]any{
|
||
|
|
"phase": "Running",
|
||
|
|
"conditions": []any{
|
||
|
|
map[string]any{"type": "Ready", "status": "True"},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// respondK8sJSON writes a JSON body with the given status code.
|
||
|
|
func respondK8sJSON(w http.ResponseWriter, status int, body any) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.WriteHeader(status)
|
||
|
|
json.NewEncoder(w).Encode(body) //nolint:errcheck
|
||
|
|
}
|
||
|
|
|
||
|
|
// newTestSM builds a SandboxManager pointed at the given test server URL.
|
||
|
|
func newTestSM(url string) *SandboxManager {
|
||
|
|
return &SandboxManager{
|
||
|
|
k8sHost: url,
|
||
|
|
k8sClient: &http.Client{Timeout: 5 * time.Second},
|
||
|
|
pvcSize: "1Gi",
|
||
|
|
readyTimeout: 3 * time.Second,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── TestSandboxCreateOrResume_CreateNew ───────────────────────────────────────
|
||
|
|
|
||
|
|
// No existing resources → creates PVC, NetworkPolicy, Pod, Service; Resumed=false.
|
||
|
|
func TestSandboxCreateOrResume_CreateNew(t *testing.T) {
|
||
|
|
id := "newid"
|
||
|
|
var podGetCount int32
|
||
|
|
|
||
|
|
mux := http.NewServeMux()
|
||
|
|
|
||
|
|
// PVC: GET → 404, POST → 201
|
||
|
|
mux.HandleFunc("GET /api/v1/namespaces/poimen/persistentvolumeclaims/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusNotFound)
|
||
|
|
})
|
||
|
|
mux.HandleFunc("POST /api/v1/namespaces/poimen/persistentvolumeclaims", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusCreated)
|
||
|
|
})
|
||
|
|
|
||
|
|
// NetworkPolicy (fixed name "pi-session-sandbox"): GET → 404, POST → 201
|
||
|
|
mux.HandleFunc("GET /apis/networking.k8s.io/v1/namespaces/poimen/networkpolicies/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusNotFound)
|
||
|
|
})
|
||
|
|
mux.HandleFunc("POST /apis/networking.k8s.io/v1/namespaces/poimen/networkpolicies", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusCreated)
|
||
|
|
})
|
||
|
|
|
||
|
|
// Pod: first GET (getPodPhase) → 404; second GET (waitPodReady) → Running+Ready; POST → 201
|
||
|
|
mux.HandleFunc("GET /api/v1/namespaces/poimen/pods/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
if atomic.AddInt32(&podGetCount, 1) == 1 {
|
||
|
|
w.WriteHeader(http.StatusNotFound)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
respondK8sJSON(w, http.StatusOK, runningReadyPod(podName(id)))
|
||
|
|
})
|
||
|
|
mux.HandleFunc("POST /api/v1/namespaces/poimen/pods", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusCreated)
|
||
|
|
})
|
||
|
|
|
||
|
|
// Service: GET → 404, POST → 201
|
||
|
|
mux.HandleFunc("GET /api/v1/namespaces/poimen/services/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusNotFound)
|
||
|
|
})
|
||
|
|
mux.HandleFunc("POST /api/v1/namespaces/poimen/services", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusCreated)
|
||
|
|
})
|
||
|
|
|
||
|
|
srv := httptest.NewServer(mux)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
result, err := newTestSM(srv.URL).CreateOrResume(context.Background(), id)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CreateOrResume: %v", err)
|
||
|
|
}
|
||
|
|
if want := svcDNS(id); result.SvcDNS != want {
|
||
|
|
t.Errorf("SvcDNS: want %q, got %q", want, result.SvcDNS)
|
||
|
|
}
|
||
|
|
if result.Resumed {
|
||
|
|
t.Error("want Resumed=false, got true")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── TestSandboxCreateOrResume_Resume ─────────────────────────────────────────
|
||
|
|
|
||
|
|
// PVC exists + pod Running → Resumed=true, no POST calls.
|
||
|
|
func TestSandboxCreateOrResume_Resume(t *testing.T) {
|
||
|
|
id := "resumeid"
|
||
|
|
|
||
|
|
mux := http.NewServeMux()
|
||
|
|
|
||
|
|
// PVC exists
|
||
|
|
mux.HandleFunc("GET /api/v1/namespaces/poimen/persistentvolumeclaims/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
respondK8sJSON(w, http.StatusOK, map[string]any{"metadata": map[string]any{"name": pvcName(id)}})
|
||
|
|
})
|
||
|
|
|
||
|
|
// Pod exists and Running (getPodPhase uses GET /{name})
|
||
|
|
mux.HandleFunc("GET /api/v1/namespaces/poimen/pods/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
respondK8sJSON(w, http.StatusOK, map[string]any{
|
||
|
|
"metadata": map[string]any{"name": podName(id)},
|
||
|
|
"status": map[string]any{"phase": "Running"},
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
// Any POST → 500 to assert no creates are issued
|
||
|
|
mux.HandleFunc("POST /", func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
t.Errorf("unexpected POST to %s", r.URL.Path)
|
||
|
|
w.WriteHeader(http.StatusInternalServerError)
|
||
|
|
})
|
||
|
|
|
||
|
|
srv := httptest.NewServer(mux)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
result, err := newTestSM(srv.URL).CreateOrResume(context.Background(), id)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("CreateOrResume: %v", err)
|
||
|
|
}
|
||
|
|
if !result.Resumed {
|
||
|
|
t.Error("want Resumed=true, got false")
|
||
|
|
}
|
||
|
|
if want := svcDNS(id); result.SvcDNS != want {
|
||
|
|
t.Errorf("SvcDNS: want %q, got %q", want, result.SvcDNS)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── TestSandboxDelete ─────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
// All DELETEs succeed → no error returned.
|
||
|
|
func TestSandboxDelete(t *testing.T) {
|
||
|
|
id := "deleteid"
|
||
|
|
|
||
|
|
mux := http.NewServeMux()
|
||
|
|
mux.HandleFunc("DELETE /api/v1/namespaces/poimen/pods/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
})
|
||
|
|
mux.HandleFunc("DELETE /api/v1/namespaces/poimen/services/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
})
|
||
|
|
mux.HandleFunc("DELETE /api/v1/namespaces/poimen/persistentvolumeclaims/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
})
|
||
|
|
|
||
|
|
srv := httptest.NewServer(mux)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
if err := newTestSM(srv.URL).Delete(context.Background(), id); err != nil {
|
||
|
|
t.Errorf("Delete: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── TestSandboxDelete_NotFound ────────────────────────────────────────────────
|
||
|
|
|
||
|
|
// All DELETEs return 404 → isNotFound errors swallowed → no error returned.
|
||
|
|
func TestSandboxDelete_NotFound(t *testing.T) {
|
||
|
|
id := "ghostid"
|
||
|
|
|
||
|
|
mux := http.NewServeMux()
|
||
|
|
mux.HandleFunc("DELETE /api/v1/namespaces/poimen/pods/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusNotFound)
|
||
|
|
})
|
||
|
|
mux.HandleFunc("DELETE /api/v1/namespaces/poimen/services/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusNotFound)
|
||
|
|
})
|
||
|
|
mux.HandleFunc("DELETE /api/v1/namespaces/poimen/persistentvolumeclaims/{name}", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusNotFound)
|
||
|
|
})
|
||
|
|
|
||
|
|
srv := httptest.NewServer(mux)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
if err := newTestSM(srv.URL).Delete(context.Background(), id); err != nil {
|
||
|
|
t.Errorf("Delete_NotFound: want nil, got %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── TestSandboxReapIdlePods ───────────────────────────────────────────────────
|
||
|
|
|
||
|
|
// pod-a=Succeeded → deleted; pod-b=Running → skipped.
|
||
|
|
func TestSandboxReapIdlePods(t *testing.T) {
|
||
|
|
var deletedA int32
|
||
|
|
|
||
|
|
mux := http.NewServeMux()
|
||
|
|
|
||
|
|
// List all pi-session pods (query string not part of mux path matching)
|
||
|
|
mux.HandleFunc("GET /api/v1/namespaces/poimen/pods", func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
respondK8sJSON(w, http.StatusOK, map[string]any{
|
||
|
|
"items": []any{
|
||
|
|
map[string]any{
|
||
|
|
"metadata": map[string]any{"name": "pod-a"},
|
||
|
|
"status": map[string]any{"phase": "Succeeded"},
|
||
|
|
},
|
||
|
|
map[string]any{
|
||
|
|
"metadata": map[string]any{"name": "pod-b"},
|
||
|
|
"status": map[string]any{"phase": "Running"},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
mux.HandleFunc("DELETE /api/v1/namespaces/poimen/pods/{name}", func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
name := r.PathValue("name")
|
||
|
|
if name == "pod-b" {
|
||
|
|
t.Errorf("pod-b (Running) must NOT be deleted")
|
||
|
|
}
|
||
|
|
if name == "pod-a" {
|
||
|
|
atomic.AddInt32(&deletedA, 1)
|
||
|
|
}
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
})
|
||
|
|
|
||
|
|
srv := httptest.NewServer(mux)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
newTestSM(srv.URL).ReapIdlePods(context.Background())
|
||
|
|
|
||
|
|
if got := atomic.LoadInt32(&deletedA); got != 1 {
|
||
|
|
t.Errorf("pod-a delete count: want 1, got %d", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── TestSandboxNameHelpers ────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
func TestSandboxNameHelpers(t *testing.T) {
|
||
|
|
cases := []struct{ fn, want, got string }{
|
||
|
|
{"pvcName", "workspace-session-abc", pvcName("abc")},
|
||
|
|
{"podName", "pi-session-abc", podName("abc")},
|
||
|
|
{"svcName", "pi-session-abc", svcName("abc")},
|
||
|
|
{"svcDNS", "pi-session-abc.poimen.svc.cluster.local:8787", svcDNS("abc")},
|
||
|
|
}
|
||
|
|
for _, c := range cases {
|
||
|
|
if c.got != c.want {
|
||
|
|
t.Errorf("%s: want %q, got %q", c.fn, c.want, c.got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── TestSandboxIsNotFound ─────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
func TestSandboxIsNotFound(t *testing.T) {
|
||
|
|
if !isNotFound(&k8sNotFound{}) {
|
||
|
|
t.Error("isNotFound(&k8sNotFound{}) must be true")
|
||
|
|
}
|
||
|
|
if isNotFound(fmt.Errorf("other")) {
|
||
|
|
t.Error("isNotFound(fmt.Errorf) must be false")
|
||
|
|
}
|
||
|
|
if isNotFound(nil) {
|
||
|
|
t.Error("isNotFound(nil) must be false")
|
||
|
|
}
|
||
|
|
}
|