@skyhook-io/radar-app 1.14.2 → 1.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/api/client.ts +30 -0
- package/src/components/CloudConnectFlow.tsx +21 -7
- package/src/components/CloudFunnelButton.tsx +85 -8
- package/src/components/cloudConnectHandoff.test.ts +10 -1
- package/src/components/cloudConnectHandoff.ts +15 -1
- package/src/components/diagnose/AIMarkdown.tsx +87 -0
- package/src/components/diagnose/ActivityTurn.tsx +1244 -0
- package/src/components/diagnose/AgentControls.tsx +603 -0
- package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
- package/src/components/diagnose/AnalysisStory.tsx +13 -18
- package/src/components/diagnose/ApplyDialog.tsx +289 -0
- package/src/components/diagnose/AssessmentCard.tsx +1560 -0
- package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
- package/src/components/diagnose/EvidenceCard.tsx +611 -0
- package/src/components/diagnose/Home.test.tsx +1 -3
- package/src/components/diagnose/Home.tsx +2 -1
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
- package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
- package/src/components/diagnose/InvestigationView.tsx +11 -28
- package/src/components/diagnose/StoryExcerpt.tsx +118 -0
- package/src/components/diagnose/assessmentCopy.ts +169 -0
- package/src/components/diagnose/investigationCase.test.tsx +631 -2
- package/src/components/diagnose/investigationCase.ts +132 -24
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +660 -0
- package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
- package/src/components/diagnose/investigationEvidence/adapters/listings.ts +134 -0
- package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
- package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
- package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
- package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
- package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
- package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
- package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
- package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
- package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
- package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
- package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
- package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
- package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
- package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
- package/src/components/diagnose/investigationEvidence/index.ts +1 -1
- package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +11 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
- package/src/components/diagnose/investigationEvidence/types.ts +97 -2
- package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
- package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
- package/src/components/diagnose/parts.test.tsx +3 -1
- package/src/components/diagnose/parts.tsx +26 -3904
- package/src/components/diagnose/target.ts +9 -3
- package/src/components/diagnose/toolCallLabel.test.ts +47 -1
- package/src/components/diagnose/toolCallLabel.ts +41 -2
- package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
- package/src/components/resources/PodFilePreview.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +12 -8
- package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
- package/src/components/ui/Omnibar.tsx +33 -10
- package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
- package/src/components/ui/Disclosure.tsx +0 -47
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { pluralToKind } from "@skyhook-io/k8s-ui";
|
|
2
|
-
|
|
3
1
|
import type {
|
|
4
2
|
DiagnosisEvidenceItem,
|
|
5
3
|
DiagnosisEvidenceRole,
|
|
@@ -10,6 +8,8 @@ import {
|
|
|
10
8
|
investigationEvidenceSubjectRef,
|
|
11
9
|
investigationSourceArgs,
|
|
12
10
|
isInvestigationEvidenceRef,
|
|
11
|
+
sameKind,
|
|
12
|
+
type InvestigationEvidenceData,
|
|
13
13
|
type InvestigationEvidenceGroup,
|
|
14
14
|
type InvestigationEvidenceObservation,
|
|
15
15
|
type InvestigationEvidenceProjection,
|
|
@@ -140,22 +140,23 @@ export function resolveInvestigationCase(
|
|
|
140
140
|
source,
|
|
141
141
|
placement: "source",
|
|
142
142
|
};
|
|
143
|
+
const produced = projection.groups.flatMap((group) =>
|
|
144
|
+
group.observations
|
|
145
|
+
.filter((observation) => observation.source.id === source.id)
|
|
146
|
+
.map((observation) => ({ group, observation })),
|
|
147
|
+
);
|
|
143
148
|
const candidates = subjectUnusable
|
|
144
149
|
? []
|
|
145
|
-
:
|
|
146
|
-
group
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
targetPods,
|
|
156
|
-
)),
|
|
157
|
-
)
|
|
158
|
-
.map((observation) => ({ group, observation })),
|
|
150
|
+
: produced.filter(
|
|
151
|
+
({ group, observation }) =>
|
|
152
|
+
!subject ||
|
|
153
|
+
observationMatchesSubject(
|
|
154
|
+
group,
|
|
155
|
+
observation,
|
|
156
|
+
subject,
|
|
157
|
+
targetPods,
|
|
158
|
+
produced.length === 1,
|
|
159
|
+
),
|
|
159
160
|
);
|
|
160
161
|
if (candidates.length === 1) {
|
|
161
162
|
const { group, observation } = candidates[0];
|
|
@@ -276,6 +277,19 @@ function observationSubjectIdentity(
|
|
|
276
277
|
const args = investigationSourceArgs(observation.source);
|
|
277
278
|
// A listing whose call names no kind (list_namespaces) is still a listing
|
|
278
279
|
// of the one kind its rows carry.
|
|
280
|
+
// A ranking is a listing of what it ranked; a posture card is a listing of
|
|
281
|
+
// the resources its findings are about.
|
|
282
|
+
if (data.type === "ranking" || data.type === "posture") {
|
|
283
|
+
const rows = data.type === "ranking" ? data.rows : data.findings;
|
|
284
|
+
const kinds = new Set(rows.map((row) => row.kind));
|
|
285
|
+
if (kinds.size !== 1) return undefined;
|
|
286
|
+
return {
|
|
287
|
+
kind: [...kinds][0],
|
|
288
|
+
namespace: nonEmptyString(args?.namespace) ? args.namespace : undefined,
|
|
289
|
+
name: "",
|
|
290
|
+
listing: true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
279
293
|
if (data.type === "inventory" && !nonEmptyString(args?.kind)) {
|
|
280
294
|
const kinds = new Set(data.resources.map((resource) => resource.kind));
|
|
281
295
|
if (kinds.size !== 1) return undefined;
|
|
@@ -311,31 +325,75 @@ function isBuiltInGroup(group: string): boolean {
|
|
|
311
325
|
return LEGACY_BUILT_IN_GROUPS.has(group) || group.endsWith(".k8s.io");
|
|
312
326
|
}
|
|
313
327
|
|
|
314
|
-
export
|
|
315
|
-
|
|
328
|
+
export { sameKind };
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Whether a listing row is the entry a citation names. A package row carries
|
|
332
|
+
* a kind of Radar's own, so the agent names it by the kind it knows the
|
|
333
|
+
* package as (the Flux HelmRelease, the chart); every other row is named by
|
|
334
|
+
* its kind.
|
|
335
|
+
*/
|
|
336
|
+
export function listingRowNamesSubject(
|
|
337
|
+
row: { kind: string; name: string },
|
|
338
|
+
subject: { kind: string; name?: string },
|
|
339
|
+
): boolean {
|
|
340
|
+
return (
|
|
341
|
+
subject.name !== undefined &&
|
|
342
|
+
row.name === subject.name &&
|
|
343
|
+
(row.kind === "Package" || sameKind(row.kind, subject.kind))
|
|
344
|
+
);
|
|
316
345
|
}
|
|
317
346
|
|
|
347
|
+
// The vocabulary the agent places with names evidence kinds a reader would
|
|
348
|
+
// distinguish (logs, events, changes, metrics, alerts, issue); everything a
|
|
349
|
+
// call returns about a resource is "resource" to it, whether Radar draws that
|
|
350
|
+
// as a resource card, a listing, a ranking, a posture card, a Helm release, a
|
|
351
|
+
// permissions check or a neighborhood. The kinds a diagnose bundle yields
|
|
352
|
+
// beside its resource (startup, crash, dns, network, receipts) stay out so
|
|
353
|
+
// "resource" still picks one observation of that bundle.
|
|
354
|
+
const RESOURCE_SHAPED: ReadonlySet<InvestigationEvidenceData["type"]> = new Set<
|
|
355
|
+
InvestigationEvidenceData["type"]
|
|
356
|
+
>([
|
|
357
|
+
"resource",
|
|
358
|
+
"inventory",
|
|
359
|
+
"ranking",
|
|
360
|
+
"posture",
|
|
361
|
+
"helm",
|
|
362
|
+
"permissions",
|
|
363
|
+
"topology",
|
|
364
|
+
"relationships",
|
|
365
|
+
]);
|
|
366
|
+
|
|
318
367
|
/**
|
|
319
368
|
* A discriminator the agent omits is a wildcard, and so is one the producer
|
|
320
369
|
* did not state; every discriminator both sides supply must match. Uniqueness
|
|
321
370
|
* of the match, not completeness of the subject, is what places a claim.
|
|
371
|
+
*
|
|
372
|
+
* The evidence word tells observations of one call apart, and a receipt is
|
|
373
|
+
* not a kind of its own: it is what a call of any kind returned when it found
|
|
374
|
+
* nothing (a metric search, a scan with nothing about the target). A call
|
|
375
|
+
* whose only observation is a receipt is not held to the word; what the
|
|
376
|
+
* subject names still has to match.
|
|
322
377
|
*/
|
|
323
378
|
function observationMatchesSubject(
|
|
324
379
|
group: InvestigationEvidenceGroup,
|
|
325
380
|
observation: InvestigationEvidenceObservation,
|
|
326
381
|
subject: DiagnosisEvidenceSubject,
|
|
327
382
|
targetPods: ReadonlySet<string>,
|
|
383
|
+
sole: boolean,
|
|
328
384
|
): boolean {
|
|
329
|
-
|
|
385
|
+
const soleReceipt = sole && observation.data.type === "receipt";
|
|
386
|
+
if (subject.observation !== undefined && !soleReceipt) {
|
|
330
387
|
// A diagnose bundle captures several vitals charts for one resource, so
|
|
331
388
|
// "metrics" alone cannot name one; "metrics:<category>" picks the chart
|
|
332
389
|
// whose identity ends in that category. An agent-run query is one chart,
|
|
333
390
|
// so a qualifier on it carries no meaning.
|
|
334
391
|
const [kind, qualifier] = subject.observation.toLowerCase().split(":", 2);
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
kind === "resource" && observation.data.type
|
|
338
|
-
|
|
392
|
+
if (
|
|
393
|
+
kind !== observation.data.type &&
|
|
394
|
+
!(kind === "resource" && RESOURCE_SHAPED.has(observation.data.type))
|
|
395
|
+
)
|
|
396
|
+
return false;
|
|
339
397
|
if (
|
|
340
398
|
qualifier !== undefined &&
|
|
341
399
|
observation.data.type === "metrics" &&
|
|
@@ -371,6 +429,36 @@ function observationIdentities(
|
|
|
371
429
|
): ObservationSubjectIdentity[] {
|
|
372
430
|
const stated = observationSubjectIdentity(observation);
|
|
373
431
|
const identities = stated ? [stated] : [];
|
|
432
|
+
// A ranking is also each row it ranks and the workload each pod belongs
|
|
433
|
+
// to; a posture card each resource its findings name and the workload that
|
|
434
|
+
// owns it: a citation of the workload reaches its pods' rows and the
|
|
435
|
+
// finding on its HPA, and a card of mixed kinds accepts no name it does
|
|
436
|
+
// not hold.
|
|
437
|
+
if (
|
|
438
|
+
observation.data.type === "ranking" ||
|
|
439
|
+
observation.data.type === "posture"
|
|
440
|
+
) {
|
|
441
|
+
const rows =
|
|
442
|
+
observation.data.type === "ranking"
|
|
443
|
+
? observation.data.rows.flatMap((row) =>
|
|
444
|
+
row.owner ? [row, row.owner] : [row],
|
|
445
|
+
)
|
|
446
|
+
: observation.data.findings.flatMap((finding) =>
|
|
447
|
+
finding.managedBy ? [finding, finding.managedBy] : [finding],
|
|
448
|
+
);
|
|
449
|
+
const seen = new Set<string>();
|
|
450
|
+
for (const row of rows) {
|
|
451
|
+
const key = `${row.kind}/${row.namespace ?? ""}/${row.name}`;
|
|
452
|
+
if (seen.has(key)) continue;
|
|
453
|
+
seen.add(key);
|
|
454
|
+
identities.push({
|
|
455
|
+
kind: row.kind,
|
|
456
|
+
group: "group" in row ? row.group : undefined,
|
|
457
|
+
namespace: row.namespace ?? "",
|
|
458
|
+
name: row.name,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
374
462
|
if (observation.data.type === "logs" && stated) {
|
|
375
463
|
const args = investigationSourceArgs(observation.source);
|
|
376
464
|
if (args && nonEmptyString(args.kind) && nonEmptyString(args.name)) {
|
|
@@ -432,7 +520,15 @@ function identityMatchesSubject(
|
|
|
432
520
|
))
|
|
433
521
|
)
|
|
434
522
|
return true;
|
|
435
|
-
|
|
523
|
+
// Naming an entry the listing holds is naming the listing, whatever kind
|
|
524
|
+
// the listing itself goes by.
|
|
525
|
+
const namesHeldEntry =
|
|
526
|
+
identity.listing &&
|
|
527
|
+
observation.data.type === "inventory" &&
|
|
528
|
+
observation.data.resources.some((resource) =>
|
|
529
|
+
listingRowNamesSubject(resource, subject),
|
|
530
|
+
);
|
|
531
|
+
if (!namesHeldEntry && !sameKind(identity.kind, subject.kind)) return false;
|
|
436
532
|
// A subject with no name (a listing cited for what it does not contain) is
|
|
437
533
|
// a wildcard that uniqueness still gates. A listing has no name of its own; the agent naming the entry it means
|
|
438
534
|
// ("ConfigMap kube-root-ca.crt" in the ConfigMaps of a namespace) still
|
|
@@ -456,7 +552,19 @@ function identityMatchesSubject(
|
|
|
456
552
|
) {
|
|
457
553
|
return false;
|
|
458
554
|
}
|
|
555
|
+
// The object declaring a package lives where it lives, not in the
|
|
556
|
+
// listing's scope; every other held entry is named in the scope it has.
|
|
557
|
+
const namesDeclaredPackage =
|
|
558
|
+
namesHeldEntry &&
|
|
559
|
+
!sameKind(subject.kind, "Package") &&
|
|
560
|
+
observation.data.type === "inventory" &&
|
|
561
|
+
observation.data.resources.some(
|
|
562
|
+
(resource) =>
|
|
563
|
+
resource.kind === "Package" &&
|
|
564
|
+
listingRowNamesSubject(resource, subject),
|
|
565
|
+
);
|
|
459
566
|
if (
|
|
567
|
+
!namesDeclaredPackage &&
|
|
460
568
|
subject.namespace !== undefined &&
|
|
461
569
|
identity.namespace !== undefined &&
|
|
462
570
|
subject.namespace !== identity.namespace
|