@skyhook-io/radar-app 1.14.4 → 1.14.6
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/README.md +2 -9
- package/package.json +6 -6
- package/src/App.tsx +91 -328
- package/src/RadarApp.tsx +3 -8
- package/src/api/client.jobset.test.ts +18 -0
- package/src/api/client.resource-identity.test.ts +44 -0
- package/src/api/client.rightsizing.test.ts +7 -3
- package/src/api/client.ts +233 -78
- package/src/api/preferences.test.ts +54 -0
- package/src/api/preferences.ts +38 -0
- package/src/components/CloudFunnelButton.test.tsx +155 -0
- package/src/components/CloudFunnelButton.tsx +14 -8
- package/src/components/ContextSwitcher.tsx +13 -132
- package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
- package/src/components/applications/ApplicationsView.tsx +5 -3
- package/src/components/audit/AuditSettingsDialog.tsx +15 -11
- package/src/components/diagnose/AIMarkdown.tsx +8 -3
- package/src/components/diagnose/AgentControls.tsx +26 -3
- package/src/components/diagnose/AssessmentCard.tsx +249 -237
- package/src/components/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +47 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +15 -2
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +1 -1
- package/src/components/diagnose/InvestigationView.tsx +4 -1
- package/src/components/diagnose/agentCatalog.ts +6 -0
- package/src/components/diagnose/investigationCase.test.tsx +56 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +7 -3
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +1 -1
- package/src/components/diagnose/investigationEvidence/observations.ts +8 -5
- package/src/components/diagnose/investigationEvidence/types.ts +1 -1
- package/src/components/diagnose/launch.test.ts +23 -0
- package/src/components/diagnose/launch.ts +4 -0
- package/src/components/diagnose/parts.test.tsx +159 -2
- package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
- package/src/components/dock/NodeTerminalTab.tsx +5 -3
- package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
- package/src/components/dock/WorkloadLogsTab.tsx +2 -1
- package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
- package/src/components/execution/BatchExecutionView.test.ts +52 -1
- package/src/components/execution/BatchExecutionView.tsx +324 -65
- package/src/components/execution/JobSetAdmission.test.tsx +42 -0
- package/src/components/execution/JobSetAdmission.tsx +17 -0
- package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
- package/src/components/execution/JobSetMemberComparison.tsx +88 -0
- package/src/components/execution/batch-run-actions.test.ts +1 -0
- package/src/components/execution/batch-timeline.test.ts +3 -0
- package/src/components/execution/execution-definition.test.ts +15 -1
- package/src/components/execution/execution-definition.ts +4 -0
- package/src/components/execution/member-collection.render.test.tsx +142 -0
- package/src/components/gitops/GitOpsView.tsx +33 -2
- package/src/components/gitops/destination-toast.test.ts +33 -0
- package/src/components/gitops/destination-toast.ts +46 -0
- package/src/components/gitops/useDestinationCluster.ts +30 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +20 -11
- package/src/components/home/MCPSetupDialog.tsx +10 -0
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
- package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
- package/src/components/nav/PrimaryNavRail.tsx +2 -5
- package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
- package/src/components/resource/PVCUsageBar.tsx +62 -16
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +3 -3
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +207 -83
- package/src/components/rightsizing/copy.test.ts +7 -0
- package/src/components/rightsizing/empty-state.test.tsx +67 -0
- package/src/components/rightsizing/model.test.ts +120 -2
- package/src/components/rightsizing/model.ts +36 -13
- package/src/components/rightsizing/notices.test.tsx +176 -0
- package/src/components/settings/OperatorManagedNotice.tsx +17 -0
- package/src/components/settings/SettingsDialog.tsx +157 -42
- package/src/components/settings/settings-state.test.ts +18 -0
- package/src/components/settings/settings-state.ts +18 -0
- package/src/components/timeline/TimelineView.tsx +9 -3
- package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
- package/src/components/ui/Markdown.tsx +33 -6
- package/src/components/ui/command-items.ts +0 -3
- package/src/components/useContextSwitchFlow.tsx +147 -0
- package/src/components/workload/WorkloadView.test.ts +17 -1
- package/src/components/workload/WorkloadView.tsx +36 -18
- package/src/context/ContextSwitchContext.tsx +18 -2
- package/src/context/NavCustomization.tsx +4 -58
- package/src/context/ThemeContext.tsx +3 -11
- package/src/hooks/useClusterLoadState.ts +2 -2
- package/src/hooks/useFavorites.ts +3 -5
- package/src/utils/auditBadges.ts +1 -1
- package/src/utils/navigation.test.ts +18 -1
- package/src/utils/navigation.ts +12 -5
- package/src/utils/topology-selection.test.ts +56 -0
- package/src/utils/topology-selection.ts +9 -9
- package/src/components/ui/CommandPalette.tsx +0 -261
|
@@ -179,14 +179,14 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
179
179
|
|
|
180
180
|
// Cluster Audit findings for the selected kind, keyed by "namespace/name" for
|
|
181
181
|
// the resource list. The list shows ONE kind at a time, so ns/name is enough;
|
|
182
|
-
// we still match the finding's group
|
|
182
|
+
// we still match the finding's group for every resource so a
|
|
183
183
|
// kind shared across groups doesn't bleed findings across the two lists. Only
|
|
184
184
|
// "badge-worthy" findings count (reference-integrity / lifecycle) — posture
|
|
185
185
|
// and best-practice nags fire near-universally and would just be noise.
|
|
186
186
|
const audit = useAudit(namespaces)
|
|
187
187
|
const auditBadges = useMemo(() => {
|
|
188
188
|
if (!selectedKind || !audit.data?.findings) return undefined
|
|
189
|
-
const wantGroup =
|
|
189
|
+
const wantGroup = selectedKind.group
|
|
190
190
|
const map: Record<string, { danger: number; warning: number; messages: AuditBadgeMessage[] }> = {}
|
|
191
191
|
for (const f of audit.data.findings) {
|
|
192
192
|
if (f.kind !== selectedKindCanonical || (f.group ?? '') !== wantGroup) continue
|
|
@@ -202,7 +202,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
202
202
|
cur.messages.sort((a, b) => (a.severity === 'danger' ? 0 : 1) - (b.severity === 'danger' ? 0 : 1))
|
|
203
203
|
}
|
|
204
204
|
return map
|
|
205
|
-
}, [audit.data?.findings, audit.data?.checks, selectedKind, selectedKindCanonical
|
|
205
|
+
}, [audit.data?.findings, audit.data?.checks, selectedKind, selectedKindCanonical])
|
|
206
206
|
|
|
207
207
|
const selectedCountKey = selectedKind ? resourceCountKey(selectedKind) : ''
|
|
208
208
|
const selectedCount = selectedCountKey ? countsData?.counts[selectedCountKey] : undefined
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CAPIClusterRenderer as BaseCAPIClusterRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/CAPIClusterRenderer'
|
|
2
|
+
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { useCapabilities } from '../../../api/client'
|
|
4
|
+
|
|
5
|
+
export function CAPIClusterRenderer({ data, onNavigate }: { data: any; onNavigate?: (ref: ResourceRef) => void }) {
|
|
6
|
+
const { data: capabilities } = useCapabilities()
|
|
7
|
+
return <BaseCAPIClusterRenderer data={data} onNavigate={onNavigate} canConnect={capabilities != null && capabilities.configManagement !== 'operator'} />
|
|
8
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { KueueWorkloadRenderer } from './KueueWorkloadRenderer'
|
|
4
|
+
|
|
5
|
+
const mock = vi.hoisted(() => ({ query: {} as any }))
|
|
6
|
+
vi.mock('../../../api/client', () => ({ useKueueProvisioning: () => mock.query }))
|
|
7
|
+
const data = { metadata: { name: 'work', namespace: 'ml', uid: 'new' }, status: { admissionChecks: [{ name: 'capacity', state: 'Rejected', message: 'Capacity unavailable' }] } }
|
|
8
|
+
const render = () => renderToStaticMarkup(<KueueWorkloadRenderer data={data} />)
|
|
9
|
+
|
|
10
|
+
describe('Workload provisioning observation', () => {
|
|
11
|
+
beforeEach(() => { mock.query = {} })
|
|
12
|
+
it('never reports failed or old-generation lookups as an empty collection', () => {
|
|
13
|
+
mock.query = { error: new Error('no access to list ProvisioningRequests'), data: { uid: 'new', requests: [] } }
|
|
14
|
+
expect(render()).toContain('Could not observe')
|
|
15
|
+
expect(render()).not.toContain('No retained')
|
|
16
|
+
mock.query = { data: { uid: 'old', installed: true, requests: [{ metadata: { name: 'old-request' } }] } }
|
|
17
|
+
expect(render()).toContain('Looking for')
|
|
18
|
+
expect(render()).not.toContain('old-request')
|
|
19
|
+
})
|
|
20
|
+
it('keeps the rejected explanation when requests have already been cleaned up', () => {
|
|
21
|
+
mock.query = { data: { uid: 'new', installed: true, requests: [], total: 0 } }
|
|
22
|
+
expect(render()).toContain('Capacity unavailable')
|
|
23
|
+
expect(render()).toContain('may have been cleaned up')
|
|
24
|
+
mock.query.data.installed = false
|
|
25
|
+
expect(render()).toContain('API is not served')
|
|
26
|
+
expect(render()).not.toContain('No retained')
|
|
27
|
+
})
|
|
28
|
+
it('shows retained request state before PodSets without inferring check membership', () => {
|
|
29
|
+
mock.query = { data: { uid: 'new', installed: true, total: 21, truncated: true, requests: [{
|
|
30
|
+
metadata: { name: 'request-1', namespace: 'ml', uid: 'request-id' }, spec: { provisioningClassName: 'test.invalid' },
|
|
31
|
+
status: { conditions: [{ type: 'Failed', status: 'True', message: 'No capacity' }] },
|
|
32
|
+
}] } }
|
|
33
|
+
const html = render()
|
|
34
|
+
expect(html).toContain('request-1')
|
|
35
|
+
expect(html).toContain('Showing 1 of 21')
|
|
36
|
+
expect(html.indexOf('Provisioning Requests')).toBeLessThan(html.indexOf('Pod Sets'))
|
|
37
|
+
expect(html).toContain('not a retry history')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Boxes } from 'lucide-react'
|
|
2
|
+
import { KueueWorkloadRenderer as BaseRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/KueueWorkloadRenderer'
|
|
3
|
+
import { getProvisioningRequestStatus, getProvisioningRequestMessage } from '@skyhook-io/k8s-ui/components/resources/resource-utils-kueue'
|
|
4
|
+
import { Badge, ResourceLink, Section, type ResourceRef } from '@skyhook-io/k8s-ui'
|
|
5
|
+
import { useKueueProvisioning } from '../../../api/client'
|
|
6
|
+
|
|
7
|
+
export function KueueWorkloadRenderer({ data, onNavigate }: { data: any; onNavigate?: (ref: ResourceRef) => void }) {
|
|
8
|
+
const namespace = data.metadata?.namespace ?? ''
|
|
9
|
+
const uid = data.metadata?.uid
|
|
10
|
+
const hasChecks = (data.status?.admissionChecks?.length ?? 0) > 0
|
|
11
|
+
const requests = useKueueProvisioning(namespace, data.metadata?.name ?? '', uid, hasChecks)
|
|
12
|
+
const current = requests.data?.uid === uid ? requests.data : undefined
|
|
13
|
+
const provisioning = hasChecks ? <Section title="Provisioning Requests" icon={Boxes} defaultExpanded>
|
|
14
|
+
{requests.error ? <p className="text-sm text-warning-text">Could not observe provisioning requests: {requests.error.message}</p>
|
|
15
|
+
: !current ? <p className="text-sm text-theme-text-tertiary">Looking for provisioning requests…</p>
|
|
16
|
+
: !current.installed ? <p className="text-sm text-theme-text-tertiary">The ProvisioningRequest API is not served by this cluster.</p>
|
|
17
|
+
: current.requests.length === 0 ? <p className="text-sm text-theme-text-tertiary">No retained provisioning requests found on this cluster. A request may not be needed or may have been cleaned up. Check results above retain the controller’s explanation; requests on a MultiKueue worker cluster are not included.</p>
|
|
18
|
+
: <div className="space-y-2">
|
|
19
|
+
<p className="text-xs text-theme-text-tertiary">Currently retained requests owned by this Workload, not a retry history. Requests may be removed after completion or eviction.</p>
|
|
20
|
+
{current.requests.map(request => {
|
|
21
|
+
const status = getProvisioningRequestStatus(request)
|
|
22
|
+
const message = getProvisioningRequestMessage(request)
|
|
23
|
+
return <div key={request.metadata.uid} className="card-inner">
|
|
24
|
+
<div className="flex flex-wrap items-center gap-2 break-all [&_button]:text-left">
|
|
25
|
+
<ResourceLink kind="provisioningrequests" group="autoscaling.x-k8s.io" namespace={namespace} name={request.metadata.name} onNavigate={onNavigate} />
|
|
26
|
+
<Badge colorClass={status.color} size="sm">{status.text}</Badge>
|
|
27
|
+
{request.metadata.deletionTimestamp && <Badge severity="neutral" size="sm">Deleting</Badge>}
|
|
28
|
+
</div>
|
|
29
|
+
<p className="mt-1 break-all text-xs text-theme-text-tertiary">{request.spec.provisioningClassName}</p>
|
|
30
|
+
{message && <p className="mt-1 whitespace-pre-wrap break-words text-sm text-theme-text-secondary">{message}</p>}
|
|
31
|
+
</div>
|
|
32
|
+
})}
|
|
33
|
+
{current.truncated && <p className="text-xs text-theme-text-tertiary">Showing {current.requests.length} of {current.total} retained requests, newest first.</p>}
|
|
34
|
+
</div>}
|
|
35
|
+
</Section> : undefined
|
|
36
|
+
return <BaseRenderer data={data} onNavigate={onNavigate} provisioning={provisioning} />
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { RayClusterRenderer } from './RayClusterRenderer'
|
|
4
|
+
const mock = vi.hoisted(() => ({ result: {} as any, calls: [] as any[] }))
|
|
5
|
+
vi.mock('../../../api/client', () => ({ useWorkloadPods: (...args: any[]) => { mock.calls.push(args); return mock.result } }))
|
|
6
|
+
const root = { apiVersion: 'ray.io/v1', kind: 'RayCluster', metadata: { name: 'cluster', namespace: 'ml', uid: 'current' } }
|
|
7
|
+
describe('RayCluster host evidence', () => {
|
|
8
|
+
beforeEach(() => { mock.result = {}; mock.calls = [] })
|
|
9
|
+
it('makes one bounded UID-bound query with polling', () => {
|
|
10
|
+
renderToStaticMarkup(<RayClusterRenderer data={root} />)
|
|
11
|
+
expect(mock.calls).toEqual([['rayclusters', 'ml', 'cluster', { ownerUID: 'current', limit: 200, refetchInterval: 5000 }]])
|
|
12
|
+
})
|
|
13
|
+
it.each(['Forbidden', 'Cache warming', 'RayCluster was recreated'])('surfaces %s without claiming an empty cluster', message => {
|
|
14
|
+
mock.result = { error: new Error(message) }
|
|
15
|
+
const html = renderToStaticMarkup(<RayClusterRenderer data={root} />)
|
|
16
|
+
expect(html).toContain(message); expect(html).not.toContain('No directly owned Pods')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { RayClusterRenderer as BaseRenderer, type RayClusterPodSelection } from '@skyhook-io/k8s-ui/components/resources/renderers/RayClusterRenderer'
|
|
3
|
+
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { useWorkloadPods } from '../../../api/client'
|
|
5
|
+
|
|
6
|
+
export function RayClusterRenderer(props: { data: any; onNavigate?: (ref: ResourceRef) => void }) {
|
|
7
|
+
return <RayClusterEvidence key={props.data.metadata.uid} {...props} />
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function RayClusterEvidence({ data, onNavigate }: { data: any; onNavigate?: (ref: ResourceRef) => void }) {
|
|
11
|
+
const [requestedSelection, setSelection] = useState<RayClusterPodSelection>({})
|
|
12
|
+
const selection = requestedSelection.workerGroup != null && !(data.spec?.workerGroupSpecs ?? []).some((group: any) => group.groupName === requestedSelection.workerGroup) ? {} : requestedSelection
|
|
13
|
+
const query = useWorkloadPods('rayclusters', data.metadata.namespace, data.metadata.uid ? data.metadata.name : '', {
|
|
14
|
+
ownerUID: data.metadata.uid, ...selection, limit: 200, refetchInterval: 5000,
|
|
15
|
+
})
|
|
16
|
+
return <BaseRenderer data={data} onNavigate={onNavigate} selection={selection} onSelectPods={setSelection} podEvidence={query.data} podsLoading={query.isLoading} podsError={query.error?.message} />
|
|
17
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { RayServiceRenderer } from './RayServiceRenderer'
|
|
4
|
+
import { ApiError } from '../../../api/client'
|
|
5
|
+
|
|
6
|
+
const mock = vi.hoisted(() => ({ queries: {} as Record<string, any>, reads: [] as any[] }))
|
|
7
|
+
vi.mock('../../../api/client', () => ({
|
|
8
|
+
ApiError: class extends Error { constructor(message: string, public status: number) { super(message) } },
|
|
9
|
+
useResource: (...args: any[]) => { mock.reads.push(args); return mock.queries[args[2]] ?? {} },
|
|
10
|
+
}))
|
|
11
|
+
const root = { apiVersion: 'ray.io/v1', kind: 'RayService', metadata: { namespace: 'ml', name: 'serve', uid: 'root' }, status: { activeServiceStatus: { rayClusterName: 'active' }, pendingServiceStatus: { rayClusterName: 'pending' } } }
|
|
12
|
+
const cluster = (name: string, extra = {}) => ({ apiVersion: 'ray.io/v1', kind: 'RayCluster', metadata: { namespace: 'ml', name, generation: 1, ownerReferences: [{ controller: true, apiVersion: 'ray.io/v1', kind: 'RayService', name: 'serve', uid: 'root' }], ...extra }, status: { conditions: [{ type: 'HeadPodReady', status: 'False', reason: 'CrashLoopBackOff', message: 'Head container failed' }] } })
|
|
13
|
+
const render = (data = root) => renderToStaticMarkup(<RayServiceRenderer data={data} />)
|
|
14
|
+
|
|
15
|
+
describe('RayService runtime observation', () => {
|
|
16
|
+
beforeEach(() => { mock.queries = {}; mock.reads = [] })
|
|
17
|
+
it('makes only two exact named reads and reports direct pending failures', () => {
|
|
18
|
+
mock.queries.pending = { data: cluster('pending') }
|
|
19
|
+
const html = render()
|
|
20
|
+
expect(mock.reads).toEqual([['rayclusters', 'ml', 'active', 'ray.io'], ['rayclusters', 'ml', 'pending', 'ray.io']])
|
|
21
|
+
expect(html).toContain('Reading RayCluster status')
|
|
22
|
+
expect(html).toContain('Head container failed')
|
|
23
|
+
})
|
|
24
|
+
it.each([403, 500, 503])('shows %s read failure instead of empty/healthy runtime', status => {
|
|
25
|
+
mock.queries.pending = { error: new ApiError('Access unavailable', status), data: cluster('pending') }
|
|
26
|
+
const html = render()
|
|
27
|
+
expect(html).toContain('Could not read RayCluster: Access unavailable')
|
|
28
|
+
expect(html).not.toContain('Head container failed')
|
|
29
|
+
})
|
|
30
|
+
it('uses root freshness rather than unstamped RayCluster conditions', () => {
|
|
31
|
+
const data = cluster('pending')
|
|
32
|
+
mock.queries.pending = { data: { ...data, status: { observedGeneration: 1, conditions: [{ type: 'HeadPodReady', status: 'True', observedGeneration: 0 }] } } }
|
|
33
|
+
expect(render()).not.toContain('RayCluster status describes an earlier generation')
|
|
34
|
+
mock.queries.pending.data.metadata.generation = 2
|
|
35
|
+
expect(render()).toContain('RayCluster status describes an earlier generation')
|
|
36
|
+
})
|
|
37
|
+
it('treats missing and conditions-unreported as distinct observations', () => {
|
|
38
|
+
mock.queries.active = { error: new ApiError('Missing', 404) }
|
|
39
|
+
mock.queries.pending = { data: { ...cluster('pending'), status: {} } }
|
|
40
|
+
const html = render()
|
|
41
|
+
expect(html).toContain('may have been cleaned up')
|
|
42
|
+
expect(html).toContain('Runtime conditions are not reported')
|
|
43
|
+
})
|
|
44
|
+
it('does not attribute foreign, recreated or incorrectly owned children', () => {
|
|
45
|
+
for (const data of [
|
|
46
|
+
cluster('wrong-name'), cluster('pending', { namespace: 'other' }),
|
|
47
|
+
{ ...cluster('pending'), apiVersion: 'foreign.io/v1' },
|
|
48
|
+
cluster('pending', { ownerReferences: [{ controller: true, apiVersion: 'ray.io/v1', kind: 'RayService', name: 'serve', uid: 'old-root' }] }),
|
|
49
|
+
cluster('pending', { ownerReferences: [{ controller: false, apiVersion: 'ray.io/v1', kind: 'RayService', name: 'serve', uid: 'root' }] }),
|
|
50
|
+
]) {
|
|
51
|
+
mock.queries.pending = { data }
|
|
52
|
+
expect(render()).toContain('Runtime status is not attributed')
|
|
53
|
+
expect(render()).not.toContain('Head container failed')
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
it('accepts the same controller identity across served owner versions', () => {
|
|
57
|
+
const data = cluster('pending')
|
|
58
|
+
data.metadata.ownerReferences[0].apiVersion = 'ray.io/v1alpha1'
|
|
59
|
+
mock.queries.pending = { data }
|
|
60
|
+
expect(render()).toContain('Head container failed')
|
|
61
|
+
})
|
|
62
|
+
it('follows promotion and root recreation using current slot and owner identity', () => {
|
|
63
|
+
mock.queries.pending = { data: cluster('pending') }
|
|
64
|
+
const promoted = { ...root, status: { activeServiceStatus: { rayClusterName: 'pending' }, pendingServiceStatus: { rayClusterName: '' } } }
|
|
65
|
+
expect(render(promoted)).not.toContain('Pending Revision')
|
|
66
|
+
expect(mock.reads).toEqual([['rayclusters', 'ml', 'pending', 'ray.io']])
|
|
67
|
+
expect(render({ ...promoted, metadata: { ...root.metadata, uid: 'new-root' } })).toContain('Runtime status is not attributed')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
2
|
+
import { RayServiceRenderer as BaseRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/RayServiceRenderer'
|
|
3
|
+
import { isRayObservationStale } from '@skyhook-io/k8s-ui/components/resources/resource-utils-ray'
|
|
4
|
+
import { AlertBanner, ConditionsSection, type ConditionTone } from '@skyhook-io/k8s-ui/components/ui/drawer-components'
|
|
5
|
+
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
6
|
+
import { ApiError, useResource } from '../../../api/client'
|
|
7
|
+
|
|
8
|
+
function runtimeConditionTone(condition: any): ConditionTone {
|
|
9
|
+
if (!['True', 'False'].includes(condition.status)) return 'unknown'
|
|
10
|
+
if (condition.type === 'RayClusterReplicaFailure') return condition.status === 'True' ? 'fail' : 'unknown'
|
|
11
|
+
if (['HeadPodReady', 'RayClusterProvisioned'].includes(condition.type)) return condition.status === 'True' ? 'ok' : 'warning'
|
|
12
|
+
return 'unknown'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function RuntimeEvidence({ root, name }: { root: any; name: string }) {
|
|
16
|
+
const query = useResource<any>('rayclusters', root.metadata.namespace, name, 'ray.io')
|
|
17
|
+
if (query.error) return <AlertBanner variant="warning" title="Runtime evidence unavailable" message={query.error instanceof ApiError && query.error.status === 404 ? 'The named RayCluster was not found; it may have been cleaned up.' : `Could not read RayCluster: ${query.error.message}`} />
|
|
18
|
+
if (!query.data) return <p className="mt-2 text-sm text-theme-text-tertiary">Reading RayCluster status…</p>
|
|
19
|
+
const cluster = query.data
|
|
20
|
+
const owned = root.metadata.uid && cluster.apiVersion === 'ray.io/v1' && cluster.kind === 'RayCluster' && cluster.metadata?.namespace === root.metadata.namespace && cluster.metadata?.name === name && cluster.metadata.ownerReferences?.some((owner: any) => owner.controller === true && apiVersionToGroup(owner.apiVersion) === 'ray.io' && owner.kind === 'RayService' && owner.name === root.metadata.name && owner.uid === root.metadata.uid)
|
|
21
|
+
if (!owned) return <AlertBanner variant="warning" title="Runtime ownership mismatch" message="The named RayCluster does not report this RayService as its controller. Runtime status is not attributed to this revision." />
|
|
22
|
+
const conditions = (cluster.status?.conditions ?? []).filter((condition: any) => ['HeadPodReady', 'RayClusterProvisioned', 'RayClusterReplicaFailure', 'RayClusterSuspending', 'RayClusterSuspended'].includes(condition.type))
|
|
23
|
+
const stale = isRayObservationStale(cluster.metadata.generation, cluster.status?.observedGeneration)
|
|
24
|
+
return <div className="mt-2">
|
|
25
|
+
<p className="text-xs text-theme-text-tertiary">Directly observed RayCluster conditions, separate from the RayService application snapshot.</p>
|
|
26
|
+
{stale && <p className="mt-2 text-sm text-theme-text-secondary">RayCluster status describes an earlier generation.</p>}
|
|
27
|
+
{conditions.length === 0 ? <p className="mt-2 text-sm text-theme-text-secondary">Runtime conditions are not reported by this RayCluster.</p> : <ConditionsSection conditions={conditions} getConditionTone={condition => stale ? 'unknown' : runtimeConditionTone(condition)} defaultExpanded />}
|
|
28
|
+
</div>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function RayServiceRenderer({ data, onNavigate }: { data: any; onNavigate?: (ref: ResourceRef) => void }) {
|
|
32
|
+
const active = data.status?.activeServiceStatus?.rayClusterName
|
|
33
|
+
const pending = data.status?.pendingServiceStatus?.rayClusterName
|
|
34
|
+
return <BaseRenderer data={data} onNavigate={onNavigate} runtimeEvidence={{
|
|
35
|
+
active: active ? <RuntimeEvidence root={data} name={active} /> : undefined,
|
|
36
|
+
pending: pending ? <RuntimeEvidence root={data} name={pending} /> : undefined,
|
|
37
|
+
}} />
|
|
38
|
+
}
|