Skip to content

Commit 058552c

Browse files
authored
feat(resourcecontext): Build generator + /api/ai/resources/* GET wiring (T6) (#721)
1 parent 433743a commit 058552c

20 files changed

Lines changed: 2604 additions & 84 deletions

internal/issues/issues.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,21 @@ func condTypeReason(condType, reason string) string {
303303
// Source-specific normalization
304304
// ---------------------------------------------------------------------------
305305

306+
// resolveGroup returns the explicit group if set, else falls back to the
307+
// built-in (Kind→Group) table. Some legacy Problem emission sites in
308+
// k8s.DetectProblems still leave Group="" for built-in workloads
309+
// (Deployment, StatefulSet, etc.) — without this fallback, the
310+
// group-aware consumer (computeIssueSummaryForResource) would silently
311+
// drop those rows when looking up by canonical group like "apps".
312+
// Centralised here so the (Kind→Group) map lives in one place across
313+
// packages (pkg/audit owns the table; this is a pass-through).
314+
func resolveGroup(group, kind string) string {
315+
if group != "" {
316+
return group
317+
}
318+
return bp.GroupForBuiltinKind(kind)
319+
}
320+
306321
func fromProblem(p k8s.Problem, now time.Time) Issue {
307322
sev := SeverityWarning
308323
if p.Severity == "critical" {
@@ -313,7 +328,7 @@ func fromProblem(p k8s.Problem, now time.Time) Issue {
313328
Severity: sev,
314329
Source: SourceProblem,
315330
Kind: p.Kind,
316-
Group: p.Group,
331+
Group: resolveGroup(p.Group, p.Kind),
317332
Namespace: p.Namespace,
318333
Name: p.Name,
319334
Reason: p.Reason,
@@ -333,6 +348,7 @@ func fromAudit(fin bp.Finding, now time.Time) Issue {
333348
Severity: sev,
334349
Source: SourceAudit,
335350
Kind: fin.Kind,
351+
Group: resolveGroup(fin.Group, fin.Kind),
336352
Namespace: fin.Namespace,
337353
Name: fin.Name,
338354
Reason: fin.CheckID,
@@ -413,10 +429,19 @@ func fromWarningEvent(e *corev1.Event) Issue {
413429
if first.IsZero() {
414430
first = last
415431
}
432+
// Event.InvolvedObject carries apiVersion (group/version); split out
433+
// the group so cross-group consumers don't collide when a Knative
434+
// Service and a core Service share name+ns.
435+
group, _, _ := strings.Cut(e.InvolvedObject.APIVersion, "/")
436+
if e.InvolvedObject.APIVersion != "" && !strings.Contains(e.InvolvedObject.APIVersion, "/") {
437+
// "v1" → core group "".
438+
group = ""
439+
}
416440
return Issue{
417441
Severity: SeverityWarning,
418442
Source: SourceEvent,
419443
Kind: e.InvolvedObject.Kind,
444+
Group: resolveGroup(group, e.InvolvedObject.Kind),
420445
Namespace: e.Namespace,
421446
Name: e.InvolvedObject.Name,
422447
Reason: e.Reason,

internal/k8s/testing.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"sync"
55

66
"github.com/skyhook-io/radar/pkg/k8score"
7+
"k8s.io/client-go/dynamic"
78
"k8s.io/client-go/kubernetes"
9+
fakeclientset "k8s.io/client-go/kubernetes/fake"
810
)
911

1012
// InitTestResourceCache creates a resource cache from a fake or test client,
@@ -68,6 +70,56 @@ func InitTestResourceCache(client kubernetes.Interface) error {
6870
return nil
6971
}
7072

73+
// InitTestDynamicResourceCache wires the dynamic resource cache and discovery
74+
// singletons against test fakes. Pass a dynamic client (typically from
75+
// dynamicfake.NewSimpleDynamicClientWithCustomListKinds) and the set of
76+
// APIResources to register in discovery. Each registered resource gets a GVR
77+
// entry that group-qualified lookups (GetGVRWithGroup) and dynamic informers
78+
// can resolve.
79+
//
80+
// Callers should defer ResetTestDynamicState — without it, the dynamic
81+
// singletons leak into other tests that share TestMain state.
82+
//
83+
// This is intended for integration tests only.
84+
func InitTestDynamicResourceCache(dynClient dynamic.Interface, resources []APIResource) error {
85+
clientMu.Lock()
86+
dynamicClient = dynClient
87+
clientMu.Unlock()
88+
89+
// Bootstrap discovery from a fake clientset so NewResourceDiscovery has a
90+
// non-nil discovery client; AddAPIResource then registers the test-only
91+
// GVRs (e.g. serving.knative.dev/Service) the test depends on.
92+
fakeDisc := fakeclientset.NewSimpleClientset().Discovery()
93+
core, err := k8score.NewResourceDiscovery(fakeDisc)
94+
if err != nil {
95+
clientMu.Lock()
96+
dynamicClient = nil
97+
clientMu.Unlock()
98+
return err
99+
}
100+
for _, r := range resources {
101+
core.AddAPIResource(r)
102+
}
103+
104+
discoveryMu.Lock()
105+
resourceDiscovery = &ResourceDiscovery{ResourceDiscovery: core}
106+
discoveryOnce = new(sync.Once)
107+
discoveryOnce.Do(func() {})
108+
discoveryMu.Unlock()
109+
110+
return InitDynamicResourceCache(nil)
111+
}
112+
113+
// ResetTestDynamicState tears down the dynamic cache + discovery singletons
114+
// and clears the dynamic client. Pairs with InitTestDynamicResourceCache.
115+
func ResetTestDynamicState() {
116+
ResetDynamicResourceCache()
117+
ResetResourceDiscovery()
118+
clientMu.Lock()
119+
dynamicClient = nil
120+
clientMu.Unlock()
121+
}
122+
71123
// SetTestContextName is a test-only helper that overrides the package-level
72124
// kubeconfig context name. Used by tests that exercise per-context state
73125
// (e.g. namespace preferences) without needing to spin up a real client.

0 commit comments

Comments
 (0)