Ctrl K
ComunidadGuías…

pkg/k8score/request_deadlines_informer_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package k8score

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"sort"
	"strconv"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	clientfeatures "k8s.io/client-go/features"
	clientfeaturestesting "k8s.io/client-go/features/testing"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

// stallingSecretsAPI is a minimal apiserver for Secrets that refuses watches
// with 403, the way list-but-not-watch RBAC or a filtering proxy does, so the
// informer re-lists on a backoff loop. One LIST can be made to stall without
// sending a byte, like a proxy whose upstream connection dropped silently.
type stallingSecretsAPI struct {
	mu        sync.Mutex
	secrets   map[string]corev1.Secret
	rv        int
	stallNext bool
	stalled   atomic.Int64
}

func (f *stallingSecretsAPI) put(ns, name string) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.rv++
	f.secrets[ns+"/"+name] = corev1.Secret{
		TypeMeta:   metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
		ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name, ResourceVersion: strconv.Itoa(f.rv)},
	}
}

func (f *stallingSecretsAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/api/v1/secrets" {
		http.NotFound(w, r)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	if q := r.URL.Query().Get("watch"); q == "true" || q == "1" {
		w.WriteHeader(http.StatusForbidden)
		_ = json.NewEncoder(w).Encode(metav1.Status{
			TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"},
			Status:   metav1.StatusFailure, Reason: metav1.StatusReasonForbidden, Code: http.StatusForbidden,
			Message: "secrets is forbidden: watch is not allowed",
		})
		return
	}
	f.mu.Lock()
	stall := f.stallNext
	f.stallNext = false
	f.mu.Unlock()
	if stall {
		f.stalled.Add(1)
		<-r.Context().Done()
		return
	}
	f.mu.Lock()
	list := corev1.SecretList{TypeMeta: metav1.TypeMeta{Kind: "SecretList", APIVersion: "v1"}}
	list.ResourceVersion = strconv.Itoa(f.rv)
	keys := make([]string, 0, len(f.secrets))
	for k := range f.secrets {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		list.Items = append(list.Items, f.secrets[k])
	}
	f.mu.Unlock()
	_ = json.NewEncoder(w).Encode(list)
}

func waitUntil(t *testing.T, timeout time.Duration, what string, cond func() bool) {
	t.Helper()
	deadline := time.Now().Add(timeout)
	for !cond() {
		if time.Now().After(deadline) {
			t.Fatalf("timed out after %s waiting for %s", timeout, what)
		}
		time.Sleep(20 * time.Millisecond)
	}
}

// A real reflector whose LIST stalls recovers once the stall timeout ends
// that LIST: the reflector backs off, lists again, and the store catches up.
// Without a deadline the same stall would block the reflector for good.
func TestRequestDeadlines_StalledInformerListRecovers(t *testing.T) {
	for _, watchList := range []bool{true, false} {
		t.Run(fmt.Sprintf("watchlist=%v", watchList), func(t *testing.T) {
			clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, watchList)

			api := &stallingSecretsAPI{secrets: map[string]corev1.Secret{}}
			api.put("shop", "db-conn")
			srv := httptest.NewServer(api)
			cfg := &rest.Config{Host: srv.URL, QPS: 100, Burst: 200}
			cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
				return &requestDeadlineRoundTripper{next: rt, idle: 500 * time.Millisecond, headerTimeout: 500 * time.Millisecond, ceiling: time.Minute, watchGrace: time.Second}
			})
			rc, err := NewResourceCache(CacheConfig{
				Client:         kubernetes.NewForConfigOrDie(cfg),
				ResourceScopes: map[string]ResourceScope{"secrets": {Enabled: true}},
				Logger:         log.New(io.Discard, "", 0),
			})
			if err != nil {
				t.Fatalf("NewResourceCache: %v", err)
			}
			t.Cleanup(func() {
				rc.Stop()
				srv.CloseClientConnections()
				srv.Close()
			})
			waitUntil(t, 10*time.Second, "sync", func() bool { return rc.IsKindReady("secrets") })

			api.mu.Lock()
			api.stallNext = true
			api.mu.Unlock()
			waitUntil(t, 30*time.Second, "a stalled LIST", func() bool { return api.stalled.Load() > 0 })
			api.put("shop", "created-during-stall")
			waitUntil(t, 30*time.Second, "the new Secret in the cache", func() bool {
				_, err := rc.Secrets().Secrets("shop").Get("created-during-stall")
				return err == nil
			})
		})
	}
}