@skyhook-io/radar-app 1.16.3 → 1.17.0
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/App.tsx +14 -7
- package/src/RadarApp.tsx +27 -17
- package/src/api/client.goneResource.test.ts +49 -0
- package/src/api/client.ts +115 -8
- package/src/api/diagnose.test.ts +40 -0
- package/src/api/diagnose.ts +12 -1
- package/src/api/radarFeatures.render.test.tsx +19 -1
- package/src/api/radarFeatures.ts +3 -0
- package/src/api/traffic.sources.test.ts +55 -0
- package/src/api/traffic.ts +173 -14
- package/src/components/applications/ApplicationsView.tsx +3 -0
- package/src/components/capacity/CapacityPoolDetail.tsx +18 -7
- package/src/components/capacity/CapacityView.test.tsx +31 -1
- package/src/components/capacity/shared.tsx +12 -21
- package/src/components/cnpg/CNPGProtection.tsx +2 -2
- package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
- package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
- package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
- package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
- package/src/components/diagnose/ApplyDialog.tsx +26 -28
- package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
- package/src/components/diagnose/DiagnoseContext.tsx +136 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
- package/src/components/diagnose/InvestigationView.tsx +39 -2
- package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
- package/src/components/diagnose/agentCatalog.ts +2 -2
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
- package/src/components/dock/TrafficFlowListTab.tsx +27 -6
- package/src/components/execution/BatchExecutionView.tsx +11 -11
- package/src/components/helm/ChartBrowser.tsx +3 -3
- package/src/components/helm/RevisionHistory.tsx +3 -2
- package/src/components/home/ClusterHealthCard.tsx +7 -0
- package/src/components/home/mcpToolCatalog.ts +1 -1
- package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
- package/src/components/settings/SettingsDialog.tsx +122 -19
- package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
- package/src/components/timeline/TimelineList.tsx +3 -0
- package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
- package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
- package/src/components/timeline/TimelineView.tsx +7 -0
- package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
- package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
- package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
- package/src/components/traffic/TrafficGraph.tsx +109 -26
- package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
- package/src/components/traffic/TrafficView.tsx +582 -149
- package/src/components/traffic/trafficFilters.test.ts +398 -1
- package/src/components/traffic/trafficFilters.ts +408 -0
- package/src/components/workload/WorkloadView.test.ts +2 -1
- package/src/components/workload/WorkloadView.tsx +135 -290
- package/src/context/TimelineCustomization.tsx +15 -0
- package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
- package/src/hooks/useGitOpsWriteGuard.ts +193 -0
- package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
|
@@ -19,26 +19,32 @@ import {
|
|
|
19
19
|
type WorkloadExtraTab,
|
|
20
20
|
type RendererOverrides,
|
|
21
21
|
type GitOpsOwnerRef,
|
|
22
|
-
type GitOpsStatus,
|
|
23
22
|
type HelmOwnerRef,
|
|
24
23
|
type AppRow,
|
|
25
24
|
type ResourceOwnershipContext,
|
|
26
25
|
type ServingResourceDetail,
|
|
27
26
|
type AuditFinding,
|
|
27
|
+
dedupeResourceRefs,
|
|
28
28
|
gitOpsRouteForOwner,
|
|
29
29
|
gitOpsOwnerFromRelationships,
|
|
30
|
-
getGitOpsResourceStatus,
|
|
31
30
|
isDiagnoseKind,
|
|
32
31
|
isRolloutKind,
|
|
33
32
|
canSetWorkloadImages,
|
|
34
33
|
isCoreBatchJob,
|
|
35
|
-
type
|
|
34
|
+
type SetImageOwnership,
|
|
35
|
+
SET_IMAGE_WRITES,
|
|
36
|
+
setImageWrites,
|
|
37
|
+
setImageSelectionKey,
|
|
38
|
+
gitOpsWriteEvidencePaths,
|
|
39
|
+
type SetImageSelection,
|
|
36
40
|
type WorkloadImageTarget,
|
|
37
41
|
} from '@skyhook-io/k8s-ui'
|
|
38
42
|
import type { ServicePortRenderProps } from '@skyhook-io/k8s-ui/components/resources/renderers/ServiceRenderer'
|
|
39
43
|
import { isJobSetV1Alpha2 } from '@skyhook-io/k8s-ui/components/resources/resource-utils-jobset-lws'
|
|
40
44
|
import type { SelectedResource, ResourceRef, Relationships, ResourceWithRelationships } from '../../types'
|
|
41
45
|
import { useHistoryPaging } from './historyPaging'
|
|
46
|
+
import { findInheritedGitOpsLookupRef, useResolvedGitOpsOwner } from '../../hooks/useResolvedGitOpsOwner'
|
|
47
|
+
import { useGitOpsWriteGuard } from '../../hooks/useGitOpsWriteGuard'
|
|
42
48
|
import {
|
|
43
49
|
kindToPlural,
|
|
44
50
|
kindToPluralWithGroup,
|
|
@@ -97,6 +103,7 @@ import { RightsizingPanel } from '../resource/RightsizingStrip'
|
|
|
97
103
|
import { WorkloadCostTab } from '../cost/WorkloadCostTab'
|
|
98
104
|
import { isOpenCostWorkloadKind } from '../cost/kinds'
|
|
99
105
|
import { isRadarFeatureUnsupported } from '../../api/radarFeatures'
|
|
106
|
+
import { useTrafficAvailable } from '../../api/traffic'
|
|
100
107
|
import { useResourceAudit, useResourceIssues, useResources, useTrace, fetchTraceWithProbes, fetchInClusterCapability, runInClusterMerged } from '../../api/client'
|
|
101
108
|
import { AuditAlerts, getRadarUpgradeRequirement, ResourceIssuesSection, ReachabilityView, TraceSummary, InClusterConsentDialog, traceFingerprint, staticPollUnreliable, summarizeInClusterTests, type Trace as NetworkTrace, type InClusterCapability, inClusterConsentGiven, consentRequestRows } from '@skyhook-io/k8s-ui'
|
|
102
109
|
import { WorkloadLogsViewer } from '../logs/WorkloadLogsViewer'
|
|
@@ -305,6 +312,10 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
|
|
|
305
312
|
)
|
|
306
313
|
}
|
|
307
314
|
|
|
315
|
+
// The kinds the traffic map draws as a node of their own: those that own
|
|
316
|
+
// pods, which it groups under them.
|
|
317
|
+
const TRAFFIC_WORKLOAD_PLURALS = new Set(['deployments', 'statefulsets', 'daemonsets', 'rollouts', 'cronjobs', 'jobs'])
|
|
318
|
+
|
|
308
319
|
// ============================================================================
|
|
309
320
|
// WORKLOAD VIEW WRAPPER — injects data fetching hooks
|
|
310
321
|
// ============================================================================
|
|
@@ -340,7 +351,6 @@ interface ImageTargetOwnershipContext {
|
|
|
340
351
|
}
|
|
341
352
|
target: WorkloadImageTarget
|
|
342
353
|
response: ResourceWithRelationships<Record<string, unknown>>
|
|
343
|
-
inheritedResponse?: ResourceWithRelationships<Record<string, unknown>>
|
|
344
354
|
}
|
|
345
355
|
|
|
346
356
|
function useActionsBarProps(
|
|
@@ -560,6 +570,8 @@ export function WorkloadView({
|
|
|
560
570
|
const [imageTargetOwnership, setImageTargetOwnership] =
|
|
561
571
|
useState<ImageTargetOwnershipContext | null>(null)
|
|
562
572
|
const imageOwnershipRequestRef = useRef(0)
|
|
573
|
+
// The image guard's evidence is read only once the user opens the dialog.
|
|
574
|
+
const [imageGuardRequested, setImageGuardRequested] = useState(false)
|
|
563
575
|
|
|
564
576
|
// Tab state from URL query param — migrate legacy tab names
|
|
565
577
|
const rawTab = searchParams.get('tab')
|
|
@@ -674,125 +686,33 @@ export function WorkloadView({
|
|
|
674
686
|
: null,
|
|
675
687
|
[apiKind, imageTargetOwnership, name, namespace],
|
|
676
688
|
)
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
()
|
|
703
|
-
nativeHelmOwnerFromRelationships(relationships, resource?.metadata?.namespace ?? namespace),
|
|
704
|
-
[relationships, resource?.metadata?.namespace, namespace],
|
|
705
|
-
)
|
|
706
|
-
const inheritedHelmOwner = useMemo(
|
|
707
|
-
() =>
|
|
708
|
-
nativeHelmOwnerFromRelationships(
|
|
709
|
-
inheritedGitOpsResponse.data?.relationships,
|
|
710
|
-
inheritedGitOpsResponse.data?.resource?.metadata?.namespace ?? namespace,
|
|
711
|
-
),
|
|
712
|
-
[
|
|
713
|
-
inheritedGitOpsResponse.data?.relationships,
|
|
714
|
-
inheritedGitOpsResponse.data?.resource?.metadata?.namespace,
|
|
715
|
-
namespace,
|
|
716
|
-
],
|
|
717
|
-
)
|
|
718
|
-
const rawGitopsOwner = relationshipGitopsOwner ?? inheritedGitopsOwner
|
|
719
|
-
const gitOpsSourceResource = relationshipGitopsOwner
|
|
720
|
-
? resource
|
|
721
|
-
: inheritedGitOpsResponse.data?.resource
|
|
722
|
-
const helmOwner = relationshipHelmOwner ?? inheritedHelmOwner
|
|
723
|
-
const helmSourceResource = relationshipHelmOwner
|
|
724
|
-
? resource
|
|
725
|
-
: inheritedGitOpsResponse.data?.resource
|
|
726
|
-
const targetRelationshipGitopsOwner = useMemo(
|
|
727
|
-
() => gitOpsOwnerFromRelationships(activeImageTargetOwnership?.response.relationships),
|
|
728
|
-
[activeImageTargetOwnership?.response.relationships],
|
|
729
|
-
)
|
|
730
|
-
const targetInheritedGitopsOwner = useMemo(
|
|
731
|
-
() =>
|
|
732
|
-
gitOpsOwnerFromRelationships(
|
|
733
|
-
activeImageTargetOwnership?.inheritedResponse?.relationships,
|
|
734
|
-
),
|
|
735
|
-
[activeImageTargetOwnership?.inheritedResponse?.relationships],
|
|
736
|
-
)
|
|
737
|
-
const targetRawGitopsOwner = targetRelationshipGitopsOwner ?? targetInheritedGitopsOwner
|
|
738
|
-
const targetRelationshipHelmOwner = nativeHelmOwnerFromRelationships(
|
|
739
|
-
activeImageTargetOwnership?.response.relationships,
|
|
740
|
-
activeImageTargetOwnership?.target.namespace ?? namespace,
|
|
741
|
-
)
|
|
742
|
-
const targetInheritedHelmOwner = nativeHelmOwnerFromRelationships(
|
|
743
|
-
activeImageTargetOwnership?.inheritedResponse?.relationships,
|
|
744
|
-
activeImageTargetOwnership?.target.namespace ?? namespace,
|
|
745
|
-
)
|
|
746
|
-
const targetHelmOwner = targetRelationshipHelmOwner ?? targetInheritedHelmOwner
|
|
747
|
-
const shouldResolveArgoOwner =
|
|
748
|
-
(rawGitopsOwner?.tool === 'argocd' && !rawGitopsOwner.namespace) ||
|
|
749
|
-
(targetRawGitopsOwner?.tool === 'argocd' && !targetRawGitopsOwner.namespace)
|
|
750
|
-
const { data: argoApplications } = useResources<any>('applications', undefined, 'argoproj.io', {
|
|
751
|
-
enabled: shouldResolveArgoOwner,
|
|
689
|
+
const workloadOwnership = useResolvedGitOpsOwner({
|
|
690
|
+
kind: apiKind,
|
|
691
|
+
group: rest.group,
|
|
692
|
+
namespace,
|
|
693
|
+
name,
|
|
694
|
+
relationships,
|
|
695
|
+
resource,
|
|
696
|
+
})
|
|
697
|
+
const {
|
|
698
|
+
owner: gitopsOwner,
|
|
699
|
+
ownerObject: gitopsOwnerObject,
|
|
700
|
+
ownerVerified: gitOpsOwnerVerified,
|
|
701
|
+
ownerPending: gitOpsOwnerPending,
|
|
702
|
+
ownerSource: gitOpsOwnerSource,
|
|
703
|
+
ownerStatus: gitOpsOwnerStatus,
|
|
704
|
+
helmOwner,
|
|
705
|
+
helmSource: helmOwnerSource,
|
|
706
|
+
} = workloadOwnership
|
|
707
|
+
const imageTargetOwnershipResolution = useResolvedGitOpsOwner({
|
|
708
|
+
kind: activeImageTargetOwnership?.target.resource ?? '',
|
|
709
|
+
group: activeImageTargetOwnership?.target.group,
|
|
710
|
+
namespace: activeImageTargetOwnership?.target.namespace ?? '',
|
|
711
|
+
name: activeImageTargetOwnership?.target.name ?? '',
|
|
712
|
+
relationships: activeImageTargetOwnership?.response.relationships,
|
|
713
|
+
resource: activeImageTargetOwnership?.response.resource,
|
|
714
|
+
enabled: Boolean(activeImageTargetOwnership),
|
|
752
715
|
})
|
|
753
|
-
const gitopsOwner = useMemo(
|
|
754
|
-
() => resolveGitOpsOwner(rawGitopsOwner, argoApplications),
|
|
755
|
-
[rawGitopsOwner, argoApplications],
|
|
756
|
-
)
|
|
757
|
-
const targetGitopsOwner = useMemo(
|
|
758
|
-
() => resolveGitOpsOwner(targetRawGitopsOwner, argoApplications),
|
|
759
|
-
[argoApplications, targetRawGitopsOwner],
|
|
760
|
-
)
|
|
761
|
-
const gitopsOwnerGroup = gitopsOwner ? gitOpsOwnerGroup(gitopsOwner) : ''
|
|
762
|
-
const shouldFetchGitOpsOwner = Boolean(gitopsOwner?.namespace)
|
|
763
|
-
const gitopsOwnerQuery = useResource<any>(
|
|
764
|
-
shouldFetchGitOpsOwner ? gitopsOwner!.kind : '',
|
|
765
|
-
gitopsOwner?.namespace ?? '',
|
|
766
|
-
gitopsOwner?.name ?? '',
|
|
767
|
-
gitopsOwnerGroup,
|
|
768
|
-
)
|
|
769
|
-
const targetGitopsOwnerGroup = targetGitopsOwner ? gitOpsOwnerGroup(targetGitopsOwner) : ''
|
|
770
|
-
const shouldFetchTargetGitOpsOwner = Boolean(
|
|
771
|
-
activeImageTargetOwnership && targetGitopsOwner?.namespace,
|
|
772
|
-
)
|
|
773
|
-
const targetGitopsOwnerQuery = useResource<Record<string, unknown>>(
|
|
774
|
-
shouldFetchTargetGitOpsOwner ? targetGitopsOwner!.kind : '',
|
|
775
|
-
targetGitopsOwner?.namespace ?? '',
|
|
776
|
-
targetGitopsOwner?.name ?? '',
|
|
777
|
-
targetGitopsOwnerGroup,
|
|
778
|
-
{ enabled: shouldFetchTargetGitOpsOwner },
|
|
779
|
-
)
|
|
780
|
-
const gitOpsOwnerStatus = useMemo(
|
|
781
|
-
() => deriveGitOpsOwnerStatus(gitopsOwner, gitopsOwnerQuery.data),
|
|
782
|
-
[gitopsOwner, gitopsOwnerQuery.data],
|
|
783
|
-
)
|
|
784
|
-
const gitOpsOwnerVerified = Boolean(gitopsOwner?.namespace && gitopsOwnerQuery.data)
|
|
785
|
-
const gitOpsOwnerPending = Boolean(
|
|
786
|
-
gitopsOwner?.namespace && gitopsOwnerQuery.isLoading && !gitopsOwnerQuery.data,
|
|
787
|
-
)
|
|
788
|
-
const gitOpsOwnerSource = useMemo(
|
|
789
|
-
() => describeGitOpsOwnerSource(rawGitopsOwner, gitOpsSourceResource),
|
|
790
|
-
[rawGitopsOwner, gitOpsSourceResource],
|
|
791
|
-
)
|
|
792
|
-
const helmOwnerSource = useMemo(
|
|
793
|
-
() => describeHelmOwnerSource(helmOwner, helmSourceResource),
|
|
794
|
-
[helmOwner, helmSourceResource],
|
|
795
|
-
)
|
|
796
716
|
|
|
797
717
|
// Fetch topology for hierarchy building (only when expanded). Polled like
|
|
798
718
|
// useTrace's "drawer feeling live" pattern — without this, a resource
|
|
@@ -989,6 +909,23 @@ export function WorkloadView({
|
|
|
989
909
|
(path: string) => navigateRouter(path),
|
|
990
910
|
[navigateRouter],
|
|
991
911
|
)
|
|
912
|
+
// Offered for the kinds the traffic map draws as one node, and only once a
|
|
913
|
+
// traffic source is known to be available on this cluster. A Job something
|
|
914
|
+
// controls (a CronJob, a JobSet) is not one: the map groups its pods under
|
|
915
|
+
// that owner, so it is offered only once the Job is known to have none.
|
|
916
|
+
const isJob = apiKind.toLowerCase() === 'jobs'
|
|
917
|
+
const controlledJob = isJob && (!resource ||
|
|
918
|
+
(resource.metadata?.ownerReferences ?? []).some((ref: { controller?: boolean }) => ref.controller))
|
|
919
|
+
const trafficKind = !!namespace && TRAFFIC_WORKLOAD_PLURALS.has(apiKind.toLowerCase()) && !controlledJob
|
|
920
|
+
const trafficAvailable = useTrafficAvailable(trafficKind)
|
|
921
|
+
const openLiveTraffic = useCallback(() => {
|
|
922
|
+
const params = new URLSearchParams()
|
|
923
|
+
const namespaces = searchParams.get('namespaces')
|
|
924
|
+
if (namespaces) params.set('namespaces', namespaces)
|
|
925
|
+
params.set('focus', `${namespace}/${name}`)
|
|
926
|
+
navigateRouter({ pathname: '/traffic', search: params.toString() })
|
|
927
|
+
}, [navigateRouter, searchParams, namespace, name])
|
|
928
|
+
const offerLiveTraffic = trafficKind && trafficAvailable
|
|
992
929
|
// Drawer TraceSummary CTA → open the full resource view ON the Reachability tab.
|
|
993
930
|
// The generic onExpand navigates to the workload path but drops the query, so we
|
|
994
931
|
// navigate directly to that path WITH ?tab=reachability - the deeplink the
|
|
@@ -1013,6 +950,10 @@ export function WorkloadView({
|
|
|
1013
950
|
const loadImagesWithTargetOwnership = useCallback(
|
|
1014
951
|
async (params: { kind: string; namespace: string; name: string }) => {
|
|
1015
952
|
const request = ++imageOwnershipRequestRef.current
|
|
953
|
+
setImageGuardRequested(true)
|
|
954
|
+
// Each open and refresh re-reads the evidence: the owner's sync policy
|
|
955
|
+
// or ignore rules may have changed since the dialog last opened.
|
|
956
|
+
void queryClient.invalidateQueries({ queryKey: ['gitops-write-evidence'] })
|
|
1016
957
|
const inventory = await baseActionsBarProps.onLoadImages!(params)
|
|
1017
958
|
const targetDiffers =
|
|
1018
959
|
inventory.target.resource.toLowerCase() !== params.kind.toLowerCase() ||
|
|
@@ -1058,14 +999,17 @@ export function WorkloadView({
|
|
|
1058
999
|
group: inventory.target.group,
|
|
1059
1000
|
},
|
|
1060
1001
|
)
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1002
|
+
// Warm the cache the target's ownership resolution reads, so the
|
|
1003
|
+
// dialog doesn't open on a still-pending inherited lookup. A failed
|
|
1004
|
+
// read surfaces there as unknown ownership, not as a failed load.
|
|
1005
|
+
if (inheritedRef) {
|
|
1006
|
+
await fetchRelationships(
|
|
1007
|
+
kindToPluralWithGroup(inheritedRef.kind, inheritedRef.group ?? ''),
|
|
1008
|
+
inheritedRef.namespace,
|
|
1009
|
+
inheritedRef.name,
|
|
1010
|
+
inheritedRef.group,
|
|
1011
|
+
).catch(() => undefined)
|
|
1012
|
+
}
|
|
1069
1013
|
if (request === imageOwnershipRequestRef.current) {
|
|
1070
1014
|
setImageTargetOwnership({
|
|
1071
1015
|
root: {
|
|
@@ -1075,52 +1019,74 @@ export function WorkloadView({
|
|
|
1075
1019
|
},
|
|
1076
1020
|
target: inventory.target,
|
|
1077
1021
|
response,
|
|
1078
|
-
inheritedResponse,
|
|
1079
1022
|
})
|
|
1080
1023
|
}
|
|
1081
1024
|
return inventory
|
|
1082
1025
|
},
|
|
1083
1026
|
[baseActionsBarProps.onLoadImages, queryClient, setImageTargetOwnership],
|
|
1084
1027
|
)
|
|
1085
|
-
const
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1028
|
+
const imageOwnershipSource = activeImageTargetOwnership ? imageTargetOwnershipResolution : workloadOwnership
|
|
1029
|
+
const imageGuardTarget = activeImageTargetOwnership
|
|
1030
|
+
? {
|
|
1031
|
+
kind: activeImageTargetOwnership.target.kind,
|
|
1032
|
+
group: activeImageTargetOwnership.target.group,
|
|
1033
|
+
namespace: activeImageTargetOwnership.target.namespace,
|
|
1034
|
+
name: activeImageTargetOwnership.target.name,
|
|
1035
|
+
}
|
|
1036
|
+
: { kind: resource?.kind ?? pluralToKind(apiKind), group: effectiveGroup ?? '', namespace, name }
|
|
1037
|
+
// Once the dialog lists the containers, ask about each by name (so the
|
|
1038
|
+
// evidence can't predate the list) and judge the ones being changed.
|
|
1039
|
+
const [imageSelection, setImageSelection] = useState<SetImageSelection | null>(null)
|
|
1040
|
+
const imageWrites = useMemo(
|
|
1041
|
+
() =>
|
|
1042
|
+
imageSelection
|
|
1043
|
+
? setImageWrites(imageSelection.changed.length ? imageSelection.changed : imageSelection.containers)
|
|
1044
|
+
: SET_IMAGE_WRITES,
|
|
1045
|
+
[imageSelection],
|
|
1046
|
+
)
|
|
1047
|
+
const imageEvidencePaths = useMemo(
|
|
1048
|
+
() => (imageSelection ? gitOpsWriteEvidencePaths(setImageWrites(imageSelection.containers)) : undefined),
|
|
1049
|
+
[imageSelection],
|
|
1050
|
+
)
|
|
1051
|
+
const { guard: imageGuard, ownership: imageGuardOwnership } = useGitOpsWriteGuard({
|
|
1052
|
+
target: imageGuardTarget,
|
|
1053
|
+
writes: imageWrites,
|
|
1054
|
+
evidencePaths: imageEvidencePaths,
|
|
1055
|
+
ownership: imageOwnershipSource,
|
|
1056
|
+
relationshipsUnavailable: activeImageTargetOwnership
|
|
1057
|
+
? !activeImageTargetOwnership.response.relationships
|
|
1058
|
+
: Boolean(resource) && !relationships,
|
|
1059
|
+
enabled: imageGuardRequested,
|
|
1060
|
+
})
|
|
1061
|
+
const {
|
|
1062
|
+
owner: imageOwner,
|
|
1063
|
+
helmOwner: imageHelmOwner,
|
|
1064
|
+
ownerVerified: imageOwnerVerified,
|
|
1065
|
+
} = imageGuardOwnership
|
|
1066
|
+
const imageOwnership = useMemo<SetImageOwnership | undefined>(() => {
|
|
1067
|
+
if (!imageGuard) return undefined
|
|
1068
|
+
const owner = imageOwner
|
|
1069
|
+
const helmOwner = imageHelmOwner
|
|
1070
|
+
return {
|
|
1071
|
+
guard: imageGuard,
|
|
1072
|
+
onSelectionChange: setImageSelection,
|
|
1073
|
+
selectionKey: setImageSelectionKey(imageSelection),
|
|
1074
|
+
onOpenOwner: owner
|
|
1075
|
+
? imageOwnerVerified
|
|
1076
|
+
? () => handleOpenGitOpsResource(owner)
|
|
1077
|
+
: undefined
|
|
1078
|
+
: helmOwner
|
|
1079
|
+
? () => handleOpenHelmRelease(helmOwner)
|
|
1103
1080
|
: undefined,
|
|
1104
|
-
})
|
|
1105
|
-
}
|
|
1106
|
-
if (imageHelmOwner) {
|
|
1107
|
-
sources.push({
|
|
1108
|
-
type: 'Helm',
|
|
1109
|
-
label: `${imageHelmOwner.namespace}/${imageHelmOwner.name}`,
|
|
1110
|
-
onOpen: () => handleOpenHelmRelease(imageHelmOwner),
|
|
1111
|
-
})
|
|
1112
1081
|
}
|
|
1113
|
-
return sources
|
|
1114
1082
|
}, [
|
|
1115
1083
|
handleOpenGitOpsResource,
|
|
1116
1084
|
handleOpenHelmRelease,
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
imageGitopsOwnerData,
|
|
1085
|
+
imageGuard,
|
|
1086
|
+
imageOwner,
|
|
1120
1087
|
imageHelmOwner,
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
inheritedGitOpsResponse.isPending,
|
|
1088
|
+
imageOwnerVerified,
|
|
1089
|
+
imageSelection,
|
|
1124
1090
|
])
|
|
1125
1091
|
const actionsBarProps = useMemo(
|
|
1126
1092
|
() => ({
|
|
@@ -1130,12 +1096,12 @@ export function WorkloadView({
|
|
|
1130
1096
|
: undefined,
|
|
1131
1097
|
onCompareTo,
|
|
1132
1098
|
onCompareAcrossClusters,
|
|
1133
|
-
|
|
1099
|
+
imageOwnership,
|
|
1134
1100
|
}),
|
|
1135
1101
|
[
|
|
1136
1102
|
baseActionsBarProps,
|
|
1137
1103
|
loadImagesWithTargetOwnership,
|
|
1138
|
-
|
|
1104
|
+
imageOwnership,
|
|
1139
1105
|
onCompareTo,
|
|
1140
1106
|
onCompareAcrossClusters,
|
|
1141
1107
|
],
|
|
@@ -1395,7 +1361,7 @@ export function WorkloadView({
|
|
|
1395
1361
|
)}
|
|
1396
1362
|
hasOperationalIssues={hasOperationalIssues}
|
|
1397
1363
|
operationalIssuesPending={issuesPending}
|
|
1398
|
-
onOpenGitOpsResource={
|
|
1364
|
+
onOpenGitOpsResource={gitopsOwnerObject ? handleOpenGitOpsResource : undefined}
|
|
1399
1365
|
resolvedGitOpsOwner={gitopsOwner}
|
|
1400
1366
|
gitOpsOwnerVerified={gitOpsOwnerVerified}
|
|
1401
1367
|
gitOpsOwnerPending={gitOpsOwnerPending}
|
|
@@ -1405,6 +1371,7 @@ export function WorkloadView({
|
|
|
1405
1371
|
helmOwnerSource={helmOwnerSource}
|
|
1406
1372
|
onOpenHelmRelease={handleOpenHelmRelease}
|
|
1407
1373
|
onNavigateGitOpsPath={handleNavigateGitOpsPath}
|
|
1374
|
+
onOpenLiveTraffic={offerLiveTraffic ? openLiveTraffic : undefined}
|
|
1408
1375
|
/>
|
|
1409
1376
|
<CreateResourceDialog
|
|
1410
1377
|
open={duplicateDialogOpen}
|
|
@@ -1428,7 +1395,7 @@ export function WorkloadView({
|
|
|
1428
1395
|
|
|
1429
1396
|
function collectServingRefs(relationships: Relationships | undefined): ResourceRef[] {
|
|
1430
1397
|
if (!relationships) return []
|
|
1431
|
-
return
|
|
1398
|
+
return dedupeResourceRefs([
|
|
1432
1399
|
...(relationships.services ?? []),
|
|
1433
1400
|
...(relationships.ingresses ?? []),
|
|
1434
1401
|
...(relationships.gateways ?? []),
|
|
@@ -1436,45 +1403,6 @@ function collectServingRefs(relationships: Relationships | undefined): ResourceR
|
|
|
1436
1403
|
])
|
|
1437
1404
|
}
|
|
1438
1405
|
|
|
1439
|
-
function dedupeRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
1440
|
-
const seen = new Set<string>()
|
|
1441
|
-
return refs.filter((ref) => {
|
|
1442
|
-
const key = `${ref.kind}/${ref.namespace}/${ref.name}/${ref.group ?? ''}`
|
|
1443
|
-
if (seen.has(key)) return false
|
|
1444
|
-
seen.add(key)
|
|
1445
|
-
return true
|
|
1446
|
-
})
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
function resolveGitOpsOwner(
|
|
1450
|
-
owner: GitOpsOwnerRef | null,
|
|
1451
|
-
argoApplications: any[] | undefined,
|
|
1452
|
-
): GitOpsOwnerRef | null {
|
|
1453
|
-
if (!owner || owner.namespace || owner.tool !== 'argocd') return owner
|
|
1454
|
-
const matches = (argoApplications ?? []).filter((app) => app?.metadata?.name === owner.name)
|
|
1455
|
-
if (matches.length !== 1) return owner
|
|
1456
|
-
const namespace = matches[0]?.metadata?.namespace
|
|
1457
|
-
return namespace ? { ...owner, namespace } : owner
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
export function findInheritedGitOpsLookupRef(
|
|
1461
|
-
relationships: Relationships | undefined,
|
|
1462
|
-
directOwner: GitOpsOwnerRef | null,
|
|
1463
|
-
current: ResourceRef,
|
|
1464
|
-
): ResourceRef | null {
|
|
1465
|
-
if (directOwner) return null
|
|
1466
|
-
const inheritedManagerRefs = (relationships?.managedBy ?? []).filter(
|
|
1467
|
-
(ref) => !gitOpsOwnerFromRelationships({ managedBy: [ref] }) && !isNativeHelmManager(ref),
|
|
1468
|
-
)
|
|
1469
|
-
const candidates = [
|
|
1470
|
-
relationships?.deployment,
|
|
1471
|
-
...inheritedManagerRefs,
|
|
1472
|
-
relationships?.owner,
|
|
1473
|
-
].filter(Boolean) as ResourceRef[]
|
|
1474
|
-
|
|
1475
|
-
return candidates.find((ref) => !isCurrentResource(ref, current)) ?? null
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
1406
|
const POD_OWNERSHIP_WORKLOAD_KINDS = new Set([
|
|
1479
1407
|
'deployments',
|
|
1480
1408
|
'statefulsets',
|
|
@@ -1554,89 +1482,6 @@ function sameWorkload(
|
|
|
1554
1482
|
)
|
|
1555
1483
|
}
|
|
1556
1484
|
|
|
1557
|
-
function nativeHelmOwnerFromRelationships(
|
|
1558
|
-
relationships: Relationships | undefined,
|
|
1559
|
-
fallbackNamespace: string,
|
|
1560
|
-
): HelmOwnerRef | null {
|
|
1561
|
-
const ref = relationships?.managedBy?.[0]
|
|
1562
|
-
if (!ref || !isNativeHelmManager(ref)) return null
|
|
1563
|
-
return {
|
|
1564
|
-
namespace: ref.namespace || fallbackNamespace,
|
|
1565
|
-
name: ref.name,
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
function isCurrentResource(ref: ResourceRef, current: ResourceRef): boolean {
|
|
1570
|
-
return (
|
|
1571
|
-
kindToPluralWithGroup(ref.kind, ref.group ?? '') ===
|
|
1572
|
-
kindToPluralWithGroup(current.kind, current.group ?? '') &&
|
|
1573
|
-
ref.namespace === current.namespace &&
|
|
1574
|
-
ref.name === current.name &&
|
|
1575
|
-
(ref.group ?? '') === (current.group ?? '')
|
|
1576
|
-
)
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
function isNativeHelmManager(ref: ResourceRef): boolean {
|
|
1580
|
-
return ref.kind === 'HelmRelease' && ref.group !== 'helm.toolkit.fluxcd.io'
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
function describeGitOpsOwnerSource(owner: GitOpsOwnerRef | null, resource: any): string | null {
|
|
1584
|
-
if (!owner || !resource) return null
|
|
1585
|
-
const labels = resource.metadata?.labels ?? {}
|
|
1586
|
-
const annotations = resource.metadata?.annotations ?? {}
|
|
1587
|
-
|
|
1588
|
-
if (owner.tool === 'fluxcd') {
|
|
1589
|
-
const nameKey =
|
|
1590
|
-
owner.kind === 'helmreleases'
|
|
1591
|
-
? 'helm.toolkit.fluxcd.io/name'
|
|
1592
|
-
: 'kustomize.toolkit.fluxcd.io/name'
|
|
1593
|
-
const nsKey =
|
|
1594
|
-
owner.kind === 'helmreleases'
|
|
1595
|
-
? 'helm.toolkit.fluxcd.io/namespace'
|
|
1596
|
-
: 'kustomize.toolkit.fluxcd.io/namespace'
|
|
1597
|
-
if (labels[nameKey] || labels[nsKey]) {
|
|
1598
|
-
return `${nameKey}=${labels[nameKey] ?? ''}, ${nsKey}=${labels[nsKey] ?? ''}`
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
const trackingID = annotations['argocd.argoproj.io/tracking-id']
|
|
1603
|
-
if (trackingID) return `argocd.argoproj.io/tracking-id=${trackingID}`
|
|
1604
|
-
const argoInstance = labels['argocd.argoproj.io/instance']
|
|
1605
|
-
if (argoInstance) return `argocd.argoproj.io/instance=${argoInstance}`
|
|
1606
|
-
return null
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
function describeHelmOwnerSource(owner: HelmOwnerRef | null, resource: any): string | null {
|
|
1610
|
-
if (!owner || !resource) return null
|
|
1611
|
-
const annotations = resource.metadata?.annotations ?? {}
|
|
1612
|
-
const releaseName = annotations['meta.helm.sh/release-name']
|
|
1613
|
-
const releaseNamespace = annotations['meta.helm.sh/release-namespace']
|
|
1614
|
-
if (releaseName || releaseNamespace) {
|
|
1615
|
-
return `meta.helm.sh/release-name=${releaseName ?? ''}, meta.helm.sh/release-namespace=${releaseNamespace ?? ''}`
|
|
1616
|
-
}
|
|
1617
|
-
return null
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
function gitOpsOwnerGroup(owner: GitOpsOwnerRef): string {
|
|
1621
|
-
if (owner.tool === 'argocd') return 'argoproj.io'
|
|
1622
|
-
if (owner.kind === 'kustomizations') return 'kustomize.toolkit.fluxcd.io'
|
|
1623
|
-
return 'helm.toolkit.fluxcd.io'
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
function deriveGitOpsOwnerStatus(owner: GitOpsOwnerRef | null, resource: any): GitOpsStatus | null {
|
|
1627
|
-
if (!owner || !resource || !hasGitOpsStatusPayload(owner, resource)) return null
|
|
1628
|
-
return getGitOpsResourceStatus(owner.kind, resource)
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
function hasGitOpsStatusPayload(owner: GitOpsOwnerRef, resource: any): boolean {
|
|
1632
|
-
if (owner.kind === 'applications') {
|
|
1633
|
-
const status = resource.status ?? {}
|
|
1634
|
-
return Boolean(status.sync?.status || status.health?.status || status.operationState?.phase)
|
|
1635
|
-
}
|
|
1636
|
-
if (resource.spec?.suspend === true) return true
|
|
1637
|
-
return Array.isArray(resource.status?.conditions) && resource.status.conditions.length > 0
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
1485
|
// ============================================================================
|
|
1641
1486
|
// LOGS TAB — platform-specific (uses data-fetching hooks)
|
|
1642
1487
|
// ============================================================================
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A note from the host about how far back the timeline goes. The host owns
|
|
6
|
+
* the wording and decides when there is one. Standalone Radar serves
|
|
7
|
+
* everything it holds and never sets it.
|
|
8
|
+
*/
|
|
9
|
+
const TimelineRetentionNoteContext = createContext<ReactNode>(undefined)
|
|
10
|
+
|
|
11
|
+
export const TimelineRetentionNoteProvider = TimelineRetentionNoteContext.Provider
|
|
12
|
+
|
|
13
|
+
export function useTimelineRetentionNote(): ReactNode {
|
|
14
|
+
return useContext(TimelineRetentionNoteContext)
|
|
15
|
+
}
|