internal/resources/namespacescope_test.go
Rama main · réplica de lectura del repositorio original.
package resources
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"testing"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic/fake"
k8sfake "k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
"github.com/sophotechlabs/spinoza/internal/access"
"github.com/sophotechlabs/spinoza/internal/api"
)
type listCounter struct {
mu sync.Mutex
byNamespace map[string]int
}
func (c *listCounter) seen(namespace string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.byNamespace[namespace]
}
func namespaceBoundClient(t *testing.T, allowed map[string]bool, objs ...runtime.Object) (*fake.FakeDynamicClient, *listCounter) {
t.Helper()
dyn := newClient(t, objs...)
counts := &listCounter{byNamespace: map[string]int{}}
dyn.PrependReactor("list", "deployments", func(action k8stesting.Action) (bool, runtime.Object, error) {
counts.mu.Lock()
counts.byNamespace[action.GetNamespace()]++
counts.mu.Unlock()
if allowed[action.GetNamespace()] {
return false, nil, nil
}
return true, nil, apierrors.NewForbidden(depGVR.GroupResource(), "", errors.New("deployments.apps is forbidden: this account reads named namespaces only"))
})
return dyn, counts
}
func scopedManager(t *testing.T, dyn *fake.FakeDynamicClient) *Manager {
t.Helper()
mgr, cancel := newManager(t, dyn)
t.Cleanup(cancel)
mgr.syncTimeout = time.Second
return mgr
}
func TestANamespaceOnlyCredentialCanBrowseItsNamespace(t *testing.T) {
dyn, counts := namespaceBoundClient(t, map[string]bool{"review": true}, newDeployment("review", "first"))
mgr := scopedManager(t, dyn)
sub, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "review", 0, nil)
if err != nil {
t.Fatalf("a valid namespace list succeeded but the subscription failed: %v", err)
}
t.Cleanup(sub.Close)
if len(sub.Rows) != 1 || sub.Rows[0].Name != "first" {
t.Fatalf("rows = %+v, want the one deployment the account may read", sub.Rows)
}
if counts.seen("") != 1 {
t.Fatalf("cluster-wide lists = %d, want exactly one attempt before scoping down", counts.seen(""))
}
if counts.seen("review") != 1 {
t.Fatalf("namespace lists = %d, want one informer for the namespace", counts.seen("review"))
}
_, err = dyn.Resource(depGVR).Namespace("review").Create(context.Background(), newDeployment("review", "second"), metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
ev := recvEvent(t, sub.Events)
if ev.Kind != "added" || ev.Row.Name != "second" {
t.Fatalf("event = %+v, want the deployment that just appeared", ev)
}
}
func TestANamespaceOnlyCredentialIsStillRefusedElsewhere(t *testing.T) {
dyn, _ := namespaceBoundClient(t, map[string]bool{"review": true})
mgr := scopedManager(t, dyn)
_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "other", 0, nil)
if !apierrors.IsForbidden(err) {
t.Fatalf("error = %v, want the cluster's own refusal for the other namespace", err)
}
}
func TestANamespaceOnlyCredentialMustPickANamespaceForTheWholeCluster(t *testing.T) {
dyn, _ := namespaceBoundClient(t, map[string]bool{"review": true})
mgr := scopedManager(t, dyn)
_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "", 0, nil)
if !errors.Is(err, ErrNamespaceNeeded) {
t.Fatalf("error = %v, want ErrNamespaceNeeded", err)
}
want := "your account cannot read this kind across every namespace; pick one namespace: Deployment: "
if !strings.HasPrefix(err.Error(), want) {
t.Fatalf("error = %q, want it to start with %q", err.Error(), want)
}
if !apierrors.IsForbidden(err) || !strings.Contains(err.Error(), "forbidden") {
t.Fatalf("error = %q, want the cluster's own denial kept inside it", err.Error())
}
}
func TestAKindInNoNamespaceKeepsTheClustersOwnRefusal(t *testing.T) {
dyn := newClient(t)
dyn.PrependReactor("list", "nodes", func(k8stesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewForbidden(nodeGVR.GroupResource(), "", errors.New("nodes is forbidden"))
})
mgr := scopedManager(t, dyn)
_, err := mgr.Subscribe(t.Context(), "", "v1", "nodes", "", 0, nil)
if !apierrors.IsForbidden(err) || errors.Is(err, ErrNamespaceNeeded) {
t.Fatalf("error = %v, want the plain refusal, since no namespace could help", err)
}
}
func TestScopedStreamsAreSharedPerNamespaceNotPerSubscriber(t *testing.T) {
dyn, counts := namespaceBoundClient(t, map[string]bool{"a": true, "b": true},
newDeployment("a", "one"), newDeployment("b", "two"))
mgr := scopedManager(t, dyn)
for _, namespace := range []string{"a", "b", "a"} {
sub, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", namespace, 0, nil)
if err != nil {
t.Fatalf("subscribe %s: %v", namespace, err)
}
t.Cleanup(sub.Close)
}
if counts.seen("a") != 1 || counts.seen("b") != 1 {
t.Fatalf("lists = a:%d b:%d, want one informer per namespace", counts.seen("a"), counts.seen("b"))
}
if streamCount(mgr) != 2 {
t.Fatalf("streams = %d, want one per namespace the account can read", streamCount(mgr))
}
}
func TestAScopedStreamIsNotOfferedAsTheWholeKind(t *testing.T) {
dyn, _ := namespaceBoundClient(t, map[string]bool{"review": true}, newDeployment("review", "first"))
mgr := scopedManager(t, dyn)
sub, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "review", 0, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(sub.Close)
if _, offered := mgr.syncedTypes()[gvrKey(depGVR)]; offered {
t.Fatal("a namespace-scoped informer was offered as a view of the whole kind")
}
}
func TestEqualTimestampsSelectTheSameWindowWhateverTheInputOrder(t *testing.T) {
first := newDeployment("review", "first")
second := newDeployment("review", "second")
stamp := metav1.NewTime(time.Date(2026, 9, 11, 10, 0, 0, 0, time.UTC))
first.SetCreationTimestamp(stamp)
second.SetCreationTimestamp(stamp)
left := newestFirst("Deployment", []*unstructured.Unstructured{first, second}, 1)
right := newestFirst("Deployment", []*unstructured.Unstructured{second, first}, 1)
if left[0].GetUID() != right[0].GetUID() {
t.Fatalf("identical objects selected different windows: %s versus %s", left[0].GetName(), right[0].GetName())
}
if left[0].GetName() != "first" {
t.Fatalf("window = %s, want the alphabetically first of two equal-time objects", left[0].GetName())
}
}
func TestAWiderWindowKeepsTheNarrowerOneAsItsPrefix(t *testing.T) {
stamp := metav1.NewTime(time.Date(2026, 9, 11, 10, 0, 0, 0, time.UTC))
held := make([]*unstructured.Unstructured, 0, 5)
for _, name := range []string{"e", "c", "a", "d", "b"} {
obj := newDeployment("review", name)
obj.SetCreationTimestamp(stamp)
held = append(held, obj)
}
narrow := newestFirst("Deployment", held, 2)
wide := newestFirst("Deployment", held, 4)
for at, obj := range narrow {
if wide[at].GetName() != obj.GetName() {
t.Fatalf("position %d = %s in the wide window, %s in the narrow one", at, wide[at].GetName(), obj.GetName())
}
}
}
func eventSeenAt(name, seen string) *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": "Event",
"metadata": map[string]any{"name": name, "namespace": "review", "uid": "uid-" + name},
"lastTimestamp": seen,
}}
}
func TestEventsThatWereLastSeenTogetherBreakTheTieByName(t *testing.T) {
seen := "2026-09-11T10:00:00Z"
late := eventSeenAt("late", seen)
early := eventSeenAt("early", seen)
left := newestFirst(eventKind, []*unstructured.Unstructured{late, early}, 1)
right := newestFirst(eventKind, []*unstructured.Unstructured{early, late}, 1)
if left[0].GetName() != "early" || right[0].GetName() != "early" {
t.Fatalf("windows = %s and %s, want the same event from either order", left[0].GetName(), right[0].GetName())
}
}
func adviceManager(t *testing.T, perms *access.Service, namespaces ...string) *Manager {
t.Helper()
dyn, _ := namespaceBoundClient(t, map[string]bool{})
objects := make([]runtime.Object, 0, len(namespaces))
for _, name := range namespaces {
objects = append(objects, meta("", "v1", "Namespace", "", name))
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
mgr := NewManager(ctx, Deps{
Dynamic: dyn,
Metadata: fakeMeta(t, objects...),
Perms: perms,
Categories: []api.Category{{Name: "Workloads"}},
Descriptors: testDescs(),
Limits: Limits{IdleGrace: time.Millisecond},
})
mgr.syncTimeout = time.Second
return mgr
}
func TestAWholeClusterRefusalNamesTheNamespacesThatWouldWork(t *testing.T) {
mgr := adviceManager(t, boundTo("review", "staging"), "other", "review", "staging")
_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "", 0, nil)
if !errors.Is(err, ErrNamespaceNeeded) {
t.Fatalf("error = %v, want ErrNamespaceNeeded", err)
}
if !strings.Contains(err.Error(), "Deployment, readable in review, staging: ") {
t.Fatalf("error = %q, want the namespaces that would work named", err.Error())
}
if !apierrors.IsForbidden(err) {
t.Fatalf("error = %q, want the cluster's own denial kept inside it", err.Error())
}
}
func TestAWholeClusterRefusalSaysWhenNoNamespaceWouldHelp(t *testing.T) {
mgr := adviceManager(t, boundTo(), "other", "review")
_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "", 0, nil)
if !errors.Is(err, ErrKindUnreadable) || errors.Is(err, ErrNamespaceNeeded) {
t.Fatalf("error = %v, want ErrKindUnreadable rather than advice to pick a namespace", err)
}
want := "your account cannot read this kind in any namespace spinoza knows of: Deployment: "
if !strings.HasPrefix(err.Error(), want) {
t.Fatalf("error = %q, want it to start with %q", err.Error(), want)
}
if !apierrors.IsForbidden(err) {
t.Fatalf("error = %q, want the cluster's own denial kept inside it", err.Error())
}
}
func TestAWholeClusterRefusalKeepsTheAdviceWhenTheAnswerIsUnknown(t *testing.T) {
cs := k8sfake.NewClientset()
cs.PrependReactor("create", "selfsubjectaccessreviews", func(k8stesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("authorization webhook timed out")
})
mgr := adviceManager(t, access.New(cs), "other", "review")
_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "", 0, nil)
if !errors.Is(err, ErrNamespaceNeeded) {
t.Fatalf("error = %v, want the advice kept when no namespace could be ruled out", err)
}
}
func TestAWholeClusterRefusalChecksOnlyTheFirstNamespacesItKnows(t *testing.T) {
names := make([]string, 0, adviceLimit+1)
for at := range adviceLimit + 1 {
names = append(names, fmt.Sprintf("ns-%03d", at))
}
mgr := adviceManager(t, boundTo(), names...)
_, err := mgr.Subscribe(t.Context(), "apps", "v1", "deployments", "", 0, nil)
if !errors.Is(err, ErrNamespaceNeeded) {
t.Fatalf("error = %v, want the advice kept when some namespaces went unchecked", err)
}
}
func TestALongListOfReadableNamespacesIsShortened(t *testing.T) {
got := fewOf([]string{"a", "b", "c", "d", "e", "f", "g"})
if got != "a, b, c, d, e and 2 more" {
t.Fatalf("names = %q, want the first five and a count", got)
}
}