pkg/k8score/node_debug_test.go
Rama main · réplica de lectura del repositorio original.
package k8score
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"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/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
func TestCreateNodeDebugPodIdentity(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/api/v1/namespaces/default/pods" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var pod corev1.Pod
if err := json.NewDecoder(r.Body).Decode(&pod); err != nil {
t.Error(err)
}
if pod.Spec.NodeName != "node-a" || pod.Spec.ActiveDeadlineSeconds == nil || *pod.Spec.ActiveDeadlineSeconds != 3600 {
t.Errorf("unexpected pod spec: %+v", pod.Spec)
}
pod.UID = types.UID("created-uid")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(pod)
}))
defer server.Close()
client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL, ContentConfig: rest.ContentConfig{ContentType: "application/json"}})
if err != nil {
t.Fatal(err)
}
result, err := CreateNodeDebugPod(context.Background(), client, "node-a", "")
if err != nil {
t.Fatal(err)
}
if result.UID != "created-uid" || result.Namespace != "default" || result.PodName == "" || result.ContainerName != "debug" {
t.Fatalf("unexpected result: %+v", result)
}
data, err := json.Marshal(result)
if err != nil {
t.Fatal(err)
}
var decoded map[string]any
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatal(err)
}
if decoded["uid"] != "created-uid" {
t.Fatalf("UID missing from JSON: %s", data)
}
}
func TestDeleteNodeDebugPod(t *testing.T) {
for _, tc := range []struct {
name string
status int
reason metav1.StatusReason
}{
{"success", http.StatusOK, ""},
{"already gone", http.StatusNotFound, metav1.StatusReasonNotFound},
{"UID conflict", http.StatusConflict, metav1.StatusReasonConflict},
{"forbidden", http.StatusForbidden, metav1.StatusReasonForbidden},
} {
t.Run(tc.name, func(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.Method != http.MethodDelete || r.URL.Path != "/api/v1/namespaces/default/pods/session-a" || r.URL.RawQuery != "" {
t.Errorf("must delete exact pod, not a collection: %s %s", r.Method, r.URL)
}
var options metav1.DeleteOptions
if err := json.NewDecoder(r.Body).Decode(&options); err != nil {
t.Error(err)
}
if options.Preconditions == nil || options.Preconditions.UID == nil || *options.Preconditions.UID != "fdde0bca-d4df-4263-bf04-88ce73cc90c1" {
t.Errorf("missing UID precondition: %+v", options)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(tc.status)
json.NewEncoder(w).Encode(metav1.Status{Status: "Failure", Reason: tc.reason, Code: int32(tc.status), Message: "test response"})
}))
defer server.Close()
client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL, ContentConfig: rest.ContentConfig{ContentType: "application/json"}})
if err != nil {
t.Fatal(err)
}
err = DeleteNodeDebugPod(context.Background(), client, "default", "session-a", "fdde0bca-d4df-4263-bf04-88ce73cc90c1")
switch tc.status {
case http.StatusOK, http.StatusNotFound:
if err != nil {
t.Fatal(err)
}
case http.StatusConflict:
if !apierrors.IsConflict(err) {
t.Fatalf("expected conflict, got %v", err)
}
case http.StatusForbidden:
if !apierrors.IsForbidden(err) {
t.Fatalf("expected forbidden, got %v", err)
}
}
if requests != 1 {
t.Fatalf("expected exactly one guarded request, got %d", requests)
}
})
}
}
func TestDeleteNodeDebugPodRequiresIdentity(t *testing.T) {
for i, identity := range [][3]string{{"", "pod", "uid"}, {"default", "", "uid"}, {"default", "pod", ""}} {
t.Run(fmt.Sprint(i), func(t *testing.T) {
// A nil client ensures validation cannot issue a delete or fall back to a collection.
if err := DeleteNodeDebugPod(context.Background(), nil, identity[0], identity[1], types.UID(identity[2])); err == nil || err.Error() != "debug pod namespace, name and UID are required" {
t.Fatalf("expected identity validation, got %v", err)
}
})
}
}
func TestDeleteNodeDebugPodRejectsMalformedIdentity(t *testing.T) {
const uid = "fdde0bca-d4df-4263-bf04-88ce73cc90c1"
for _, tc := range []struct{ name, namespace, podName, uid string }{
{"namespace whitespace", " default", "pod", uid},
{"namespace uppercase", "Default", "pod", uid},
{"namespace dotted", "team.a", "pod", uid},
{"namespace too long", strings.Repeat("a", 64), "pod", uid},
{"pod slash", "default", "a/b", uid},
{"pod percent", "default", "pod%2Fa", uid},
{"pod uppercase", "default", "Pod", uid},
{"pod too long", "default", strings.Repeat("a", 254), uid},
{"UID whitespace", "default", "pod", " " + uid},
{"UID malformed", "default", "pod", "not-a-uuid"},
{"UID invalid hex", "default", "pod", "zzzzzzzz-d4df-4263-bf04-88ce73cc90c1"},
{"UID URN", "default", "pod", "urn:uuid:" + uid},
{"UID braces", "default", "pod", "{" + uid + "}"},
{"UID no hyphens", "default", "pod", strings.ReplaceAll(uid, "-", "")},
} {
t.Run(tc.name, func(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requests++; w.WriteHeader(http.StatusOK) }))
defer server.Close()
client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL})
if err != nil {
t.Fatal(err)
}
err = DeleteNodeDebugPod(context.Background(), client, tc.namespace, tc.podName, types.UID(tc.uid))
if err == nil {
t.Fatal("expected invalid identity to be rejected")
}
if requests != 0 {
t.Fatalf("invalid identity issued %d Kubernetes requests", requests)
}
})
}
}
func TestValidateNodeDebugPodIdentityAcceptsKubernetesNames(t *testing.T) {
for _, tc := range []struct{ namespace, podName string }{
{"default", "radar-node-debug-worker-1-123"},
{"team-a", "debug.worker-1"},
{strings.Repeat("a", 63), strings.Repeat("a", 253)},
} {
if err := ValidateNodeDebugPodIdentity(tc.namespace, tc.podName, "fdde0bca-d4df-4263-bf04-88ce73cc90c1"); err != nil {
t.Fatalf("valid namespace/name rejected: %v", err)
}
}
}