@oneuptime/common 14.0.8 → 14.0.9
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/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
- package/Models/DatabaseModels/Project.ts +55 -5
- package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
- package/Models/DatabaseModels/RunnerJob.ts +75 -0
- package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
- package/Server/API/AIChatAPI.ts +6 -3
- package/Server/API/AIInvestigationAPI.ts +17 -10
- package/Server/API/AutoRemediationAPI.ts +207 -25
- package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
- package/Server/API/MicrosoftTeamsAPI.ts +93 -14
- package/Server/API/ProxmoxResourceAPI.ts +244 -14
- package/Server/Infrastructure/GlobalCache.ts +32 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/TelemetryIngest.ts +41 -18
- package/Server/Services/AIService.ts +70 -1
- package/Server/Services/AlertService.ts +352 -11
- package/Server/Services/AlertSeverityService.ts +65 -15
- package/Server/Services/AutoRemediationRuleService.ts +1 -1
- package/Server/Services/IncidentService.ts +280 -104
- package/Server/Services/IncidentSeverityService.ts +66 -15
- package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
- package/Server/Services/KubernetesAiAgentService.ts +1634 -0
- package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
- package/Server/Services/KubernetesClusterService.ts +129 -21
- package/Server/Services/ProjectService.ts +32 -0
- package/Server/Services/ProxmoxResourceService.ts +322 -5
- package/Server/Services/RunnerJobService.ts +241 -104
- package/Server/Services/RunnerService.ts +1 -1
- package/Server/Services/ScheduledMaintenanceService.ts +737 -1
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
- package/Server/Services/TeamComplianceSettingService.ts +1125 -9
- package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
- package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
- package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
- package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
- package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
- package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
- package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
- package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
- package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
- package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
- package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
- package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
- package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
- package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
- package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
- package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
- package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
- package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
- package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
- package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
- package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
- package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
- package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
- package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
- package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
- package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
- package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
- package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
- package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
- package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
- package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
- package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
- package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
- package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
- package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
- package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
- package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
- package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
- package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
- package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
- package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
- package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
- package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
- package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
- package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
- package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
- package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
- package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
- package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
- package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
- package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
- package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
- package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
- package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
- package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
- package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
- package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
- package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
- package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
- package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
- package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
- package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
- package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
- package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
- package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
- package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
- package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
- package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
- package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
- package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
- package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
- package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
- package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
- package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
- package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
- package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
- package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
- package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
- package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
- package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
- package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
- package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
- package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
- package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
- package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
- package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
- package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
- package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
- package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
- package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
- package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
- package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
- package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
- package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
- package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
- package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
- package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
- package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
- package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
- package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
- package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
- package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
- package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
- package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
- package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
- package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
- package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
- package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
- package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
- package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
- package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
- package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
- package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
- package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
- package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
- package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
- package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
- package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
- package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
- package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
- package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
- package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
- package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
- package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
- package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
- package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
- package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
- package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
- package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
- package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
- package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
- package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
- package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
- package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
- package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
- package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
- package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
- package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
- package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
- package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
- package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
- package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
- package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
- package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
- package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
- package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
- package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
- package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
- package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
- package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
- package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
- package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
- package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
- package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
- package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
- package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
- package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
- package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
- package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
- package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
- package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
- package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
- package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
- package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
- package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
- package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
- package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
- package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
- package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
- package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
- package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
- package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
- package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
- package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
- package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
- package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
- package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
- package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
- package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
- package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
- package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
- package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
- package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
- package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
- package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
- package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
- package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
- package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
- package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
- package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
- package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
- package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
- package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
- package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
- package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
- package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
- package/Tests/UI/Components/Input.test.tsx +80 -1
- package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
- package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
- package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
- package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
- package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
- package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
- package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
- package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
- package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
- package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
- package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
- package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
- package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
- package/Tests/UI/Components/Toggle.test.tsx +66 -0
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
- package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
- package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
- package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
- package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
- package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
- package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
- package/Types/AI/AIRunType.ts +9 -4
- package/Types/AI/InvestigationNotStartedReason.ts +6 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
- package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
- package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
- package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
- package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
- package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
- package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
- package/Types/Team/ComplianceNotificationChannel.ts +22 -0
- package/Types/Team/ComplianceRule.ts +434 -0
- package/Types/Team/ComplianceRuleType.ts +21 -0
- package/Types/Team/TeamComplianceStatus.ts +81 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
- package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
- package/UI/Components/ActionButton/RowActions.tsx +238 -0
- package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
- package/UI/Components/Charts/Line/LineChart.tsx +32 -2
- package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
- package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
- package/UI/Components/Charts/Utils/XAxis.ts +75 -0
- package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
- package/UI/Components/Forms/Fields/FormField.tsx +10 -0
- package/UI/Components/Input/Input.tsx +90 -57
- package/UI/Components/List/ListRow.tsx +8 -74
- package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
- package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
- package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
- package/UI/Components/OrderedStatesList/Item.tsx +7 -72
- package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
- package/UI/Components/Table/TableBody.tsx +8 -2
- package/UI/Components/Table/TableRow.tsx +20 -140
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
- package/UI/Components/TelemetryViewer/types.ts +2 -0
- package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
- package/UI/Components/Toggle/Toggle.tsx +26 -0
- package/UI/Components/Tooltip/Tooltip.tsx +8 -1
- package/UI/Styles/Theme.css +14 -2
- package/Utils/AiRemediation/CommandPolicy.ts +1 -1
- package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +56 -5
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +6 -1
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
- package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +59 -1
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +248 -13
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AlertSeverityService.js +51 -15
- package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +199 -72
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
- package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +22 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +178 -92
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +1 -1
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
- package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/AI/AIRunType.js +9 -4
- package/build/dist/Types/AI/AIRunType.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
- package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
- package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRule.js +304 -0
- package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
- package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
- package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
- package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
- package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +69 -54
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/List/ListRow.js +4 -39
- package/build/dist/UI/Components/List/ListRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
- package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/Table/TableBody.js +6 -2
- package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +12 -75
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1634 @@
|
|
|
1
|
+
import AutoRemediationRuleService from "./AutoRemediationRuleService";
|
|
2
|
+
import DatabaseService from "./DatabaseService";
|
|
3
|
+
import KubernetesClusterAiAccessService from "./KubernetesClusterAiAccessService";
|
|
4
|
+
import KubernetesClusterFeedService from "./KubernetesClusterFeedService";
|
|
5
|
+
import KubernetesClusterService from "./KubernetesClusterService";
|
|
6
|
+
import RunnerService from "./RunnerService";
|
|
7
|
+
import UserService from "./UserService";
|
|
8
|
+
import Model from "../../Models/DatabaseModels/KubernetesAiAgent";
|
|
9
|
+
import KubernetesCluster from "../../Models/DatabaseModels/KubernetesCluster";
|
|
10
|
+
import { KubernetesClusterFeedEventType } from "../../Models/DatabaseModels/KubernetesClusterFeed";
|
|
11
|
+
import Runner from "../../Models/DatabaseModels/Runner";
|
|
12
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
13
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
14
|
+
import logger from "../Utils/Logger";
|
|
15
|
+
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
16
|
+
import Select from "../Types/Database/Select";
|
|
17
|
+
import ColumnLength from "../../Types/Database/ColumnLength";
|
|
18
|
+
import OneUptimeDate from "../../Types/Date";
|
|
19
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
20
|
+
import ForbiddenException from "../../Types/Exception/ForbiddenException";
|
|
21
|
+
import { Blue500, Green500 } from "../../Types/BrandColors";
|
|
22
|
+
import { JSONObject } from "../../Types/JSON";
|
|
23
|
+
import {
|
|
24
|
+
KUBERNETES_AI_AGENT_ALIVE_WINDOW_IN_MINUTES,
|
|
25
|
+
KUBERNETES_AI_AGENT_DISPLAY_NAME,
|
|
26
|
+
KubernetesAgentPosture,
|
|
27
|
+
KubernetesAiAgentConnectionStatus,
|
|
28
|
+
KubernetesAiAgentRegistrationRefusalReason,
|
|
29
|
+
KubernetesAiAgentSummary,
|
|
30
|
+
KubernetesAiRemediationMode,
|
|
31
|
+
parseKubernetesAgentPosture,
|
|
32
|
+
} from "../../Types/Kubernetes/KubernetesClusterAiAccess";
|
|
33
|
+
import ObjectID from "../../Types/ObjectID";
|
|
34
|
+
import crypto from "crypto";
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Every column of a KubernetesAiAgent row a reader may need — everything
|
|
38
|
+
* except keyHash. The key hash is read only by the agent authentication
|
|
39
|
+
* path, which selects it explicitly; nothing that builds a status, a list
|
|
40
|
+
* or a response ever loads it, so it cannot leak into one by accident.
|
|
41
|
+
*/
|
|
42
|
+
export const KUBERNETES_AI_AGENT_SELECT_WITHOUT_KEY: Select<Model> = {
|
|
43
|
+
_id: true,
|
|
44
|
+
createdAt: true,
|
|
45
|
+
updatedAt: true,
|
|
46
|
+
projectId: true,
|
|
47
|
+
kubernetesClusterId: true,
|
|
48
|
+
agentVersion: true,
|
|
49
|
+
posture: true,
|
|
50
|
+
lastAliveAt: true,
|
|
51
|
+
connectionStatus: true,
|
|
52
|
+
lastRegisteredAt: true,
|
|
53
|
+
registeredWithIngestionKeyId: true,
|
|
54
|
+
lastRefusedRegistrationAt: true,
|
|
55
|
+
lastRefusedRegistrationReason: true,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// 32 random bytes, hex: 64 characters, 256 bits.
|
|
59
|
+
const AGENT_KEY_BYTES: number = 32;
|
|
60
|
+
|
|
61
|
+
// What a stored key hash looks like: sha256, hex.
|
|
62
|
+
const SHA256_HEX_PATTERN: RegExp = /^[0-9a-f]{64}$/i;
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
* The subset of a row isOnline / toSummary read. Dates may arrive as Date
|
|
66
|
+
* objects (a hydrated model) or ISO strings (a JSON payload).
|
|
67
|
+
*/
|
|
68
|
+
export interface KubernetesAiAgentPresenceRow {
|
|
69
|
+
connectionStatus?: string | null | undefined;
|
|
70
|
+
lastAliveAt?: Date | string | null | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type DateLike = Date | string | null | undefined;
|
|
74
|
+
|
|
75
|
+
function toDate(value: DateLike): Date | null {
|
|
76
|
+
if (!value) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const date: Date = value instanceof Date ? value : new Date(value);
|
|
81
|
+
|
|
82
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toIsoString(value: DateLike): string | undefined {
|
|
86
|
+
const date: Date | null = toDate(value);
|
|
87
|
+
|
|
88
|
+
return date ? OneUptimeDate.toString(date) : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
* Ceiling on agent rows one project may hold, and on how many new ones may
|
|
93
|
+
* be created per hour. Registration is authenticated by the project's
|
|
94
|
+
* telemetry ingestion key — a credential every collector and CI job holds —
|
|
95
|
+
* and every distinct clusterName creates a row that is handed kubectl jobs,
|
|
96
|
+
* so the rows are bounded like anything else an ingestion key can create.
|
|
97
|
+
* The same numbers as the in-cluster Runner's brakes: a fleet-wide install
|
|
98
|
+
* that trips the hourly one is only delayed, the agent keeps retrying.
|
|
99
|
+
*/
|
|
100
|
+
export const MAX_KUBERNETES_AI_AGENTS_PER_PROJECT: number = 250;
|
|
101
|
+
export const MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR: number = 30;
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* When a transient refusal (previous_instance_online, legacy_runner_online)
|
|
105
|
+
* tells the agent to try again. Both clear within seconds on a helm upgrade
|
|
106
|
+
* or a pod restart, and an agent that is refused for longer just keeps
|
|
107
|
+
* asking at this pace.
|
|
108
|
+
*/
|
|
109
|
+
export const KUBERNETES_AI_AGENT_REGISTRATION_RETRY_AFTER_SECONDS: number = 20;
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
* The longest cluster name registration accepts: KubernetesCluster's
|
|
113
|
+
* clusterIdentifier is a ShortText column, so a longer name could never
|
|
114
|
+
* become a cluster row.
|
|
115
|
+
*/
|
|
116
|
+
export const MAX_KUBERNETES_AI_AGENT_CLUSTER_NAME_LENGTH: number =
|
|
117
|
+
ColumnLength.ShortText;
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* A 403 from POST /kubernetes-ai-agent-ingest/register that says WHICH
|
|
121
|
+
* refusal it is, so the agent can tell a wait that clears on its own (the
|
|
122
|
+
* transient reasons, which also carry retryAfterSeconds) from one that
|
|
123
|
+
* needs an operator. The ingress sends { message, reason,
|
|
124
|
+
* retryAfterSeconds? } and a Retry-After header for the transient ones.
|
|
125
|
+
*/
|
|
126
|
+
export class KubernetesAiAgentRegistrationRefusedException extends ForbiddenException {
|
|
127
|
+
public readonly reason: KubernetesAiAgentRegistrationRefusalReason;
|
|
128
|
+
// Set only for a refusal that clears on its own: when to try again.
|
|
129
|
+
public readonly retryAfterSeconds: number | undefined;
|
|
130
|
+
|
|
131
|
+
public constructor(data: {
|
|
132
|
+
message: string;
|
|
133
|
+
reason: KubernetesAiAgentRegistrationRefusalReason;
|
|
134
|
+
retryAfterSeconds?: number | undefined;
|
|
135
|
+
}) {
|
|
136
|
+
super(data.message);
|
|
137
|
+
this.reason = data.reason;
|
|
138
|
+
this.retryAfterSeconds = data.retryAfterSeconds;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/*
|
|
143
|
+
* How a registration came to hold the cluster's agent key.
|
|
144
|
+
*
|
|
145
|
+
* created: the cluster had no agent row; this registration created it.
|
|
146
|
+
* continuity: it presented the row's current key — the same agent.
|
|
147
|
+
* reset: an admin reset the agent (no key is valid), so the first
|
|
148
|
+
* registration after that takes the row.
|
|
149
|
+
* signed_off: the previous instance signed off (/disconnect), as a pod
|
|
150
|
+
* does on a clean shutdown.
|
|
151
|
+
* offline: the previous instance stopped heartbeating.
|
|
152
|
+
*
|
|
153
|
+
* Only "continuity" is proof. The rest are what a restarted pod looks like —
|
|
154
|
+
* and also what anyone holding the ingestion key looks like, which is why
|
|
155
|
+
* none of them can take a row whose agent is online.
|
|
156
|
+
*/
|
|
157
|
+
export type KubernetesAiAgentRegistrationAdmission =
|
|
158
|
+
| "created"
|
|
159
|
+
| "continuity"
|
|
160
|
+
| "reset"
|
|
161
|
+
| "signed_off"
|
|
162
|
+
| "offline";
|
|
163
|
+
|
|
164
|
+
export interface KubernetesAiAgentRegistrationResult {
|
|
165
|
+
agentId: ObjectID;
|
|
166
|
+
// The new agent key. Returned once, here; only its hash is stored.
|
|
167
|
+
agentKey: string;
|
|
168
|
+
clusterId: ObjectID;
|
|
169
|
+
admission: KubernetesAiAgentRegistrationAdmission;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* What the server changed on the cluster when it applied the agent's
|
|
174
|
+
* first-connection defaults. Empty when nothing needed changing.
|
|
175
|
+
*/
|
|
176
|
+
export interface KubernetesAiAgentDefaultsApplied {
|
|
177
|
+
turnedOnInvestigation: boolean;
|
|
178
|
+
remediationMode?: KubernetesAiRemediationMode | undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// What registration and heartbeat read off the agent's cluster.
|
|
182
|
+
const CLUSTER_SELECT: Select<KubernetesCluster> = {
|
|
183
|
+
_id: true,
|
|
184
|
+
projectId: true,
|
|
185
|
+
clusterIdentifier: true,
|
|
186
|
+
isAiInvestigationEnabled: true,
|
|
187
|
+
aiRemediationMode: true,
|
|
188
|
+
aiAccessConfiguredAt: true,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* A key hash that no key can have (its preimage is unknown), compared
|
|
193
|
+
* against when the presented agent id matches no row, so an unknown id and
|
|
194
|
+
* a wrong key cost the same.
|
|
195
|
+
*/
|
|
196
|
+
const UNMATCHABLE_KEY_HASH: string = "0".repeat(64);
|
|
197
|
+
|
|
198
|
+
/*
|
|
199
|
+
* When the previous in-cluster Runner of a cluster may be retired (see
|
|
200
|
+
* retireLegacyRunnerIfUnused): the agent has existed for a day, the Runner
|
|
201
|
+
* has been silent for a week, and this is checked at most hourly.
|
|
202
|
+
*/
|
|
203
|
+
export const LEGACY_RUNNER_RETIREMENT_AGENT_AGE_HOURS: number = 24;
|
|
204
|
+
export const LEGACY_RUNNER_RETIREMENT_OFFLINE_DAYS: number = 7;
|
|
205
|
+
export const LEGACY_RUNNER_RETIREMENT_CHECK_INTERVAL_MS: number =
|
|
206
|
+
60 * 60 * 1000;
|
|
207
|
+
const MAX_LEGACY_RUNNER_RETIREMENT_CHECKS_REMEMBERED: number = 10_000;
|
|
208
|
+
|
|
209
|
+
// What retireLegacyRunnerIfUnused decided.
|
|
210
|
+
export type LegacyRunnerRetirementOutcome =
|
|
211
|
+
| "not_due"
|
|
212
|
+
| "agent_too_new"
|
|
213
|
+
| "no_legacy_runner"
|
|
214
|
+
| "legacy_runner_recently_online"
|
|
215
|
+
| "legacy_runner_holds_more"
|
|
216
|
+
| "legacy_runner_in_rule"
|
|
217
|
+
| "retired"
|
|
218
|
+
| "failed";
|
|
219
|
+
|
|
220
|
+
/*
|
|
221
|
+
* The Kubernetes AI agent of a cluster (see the model): its identity, and
|
|
222
|
+
* everything the server does about it.
|
|
223
|
+
*
|
|
224
|
+
* - Shared helpers every caller needs: key minting and hashing, the one
|
|
225
|
+
* online rule, the summary the dashboard sees and the reads the status
|
|
226
|
+
* builder uses.
|
|
227
|
+
* - The identity lifecycle behind /kubernetes-ai-agent-ingest: register
|
|
228
|
+
* (with the project's telemetry ingestion key; hands out an agent key),
|
|
229
|
+
* authenticate (every later request, by agent id and key), heartbeat and
|
|
230
|
+
* sign-off — plus the admin reset on the cluster's AI agent page.
|
|
231
|
+
*
|
|
232
|
+
* Registration never deletes or unbinds anything: the cluster's settings,
|
|
233
|
+
* its Runner binding and the previous in-cluster Runner are left as they
|
|
234
|
+
* are. The only cluster setting it ever writes is the first-connection
|
|
235
|
+
* defaults, and only on a cluster nobody configured. The previous
|
|
236
|
+
* in-cluster Runner is removed only later, lazily, once it is certainly
|
|
237
|
+
* unused (retireLegacyRunnerIfUnused).
|
|
238
|
+
*/
|
|
239
|
+
export class Service extends DatabaseService<Model> {
|
|
240
|
+
// Agent id → when this process last checked its previous Runner (ms).
|
|
241
|
+
private legacyRunnerRetirementCheckedAt: Map<string, number> = new Map<
|
|
242
|
+
string,
|
|
243
|
+
number
|
|
244
|
+
>();
|
|
245
|
+
|
|
246
|
+
public constructor() {
|
|
247
|
+
super(Model);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
* sha256 (hex) of an agent key: what KubernetesAiAgent.keyHash stores.
|
|
252
|
+
* The key itself is never stored.
|
|
253
|
+
*/
|
|
254
|
+
public static hashKey(key: string): string {
|
|
255
|
+
return crypto.createHash("sha256").update(key, "utf8").digest("hex");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// A fresh agent key: 32 cryptographically random bytes, hex encoded.
|
|
259
|
+
public static generateKey(): string {
|
|
260
|
+
return crypto.randomBytes(AGENT_KEY_BYTES).toString("hex");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/*
|
|
264
|
+
* Does this presented key match the stored hash? Timing-safe, and false —
|
|
265
|
+
* never a throw — for anything that is not a non-empty key and a stored
|
|
266
|
+
* sha256 hex hash (a reset row's hash is null, which nothing matches).
|
|
267
|
+
*/
|
|
268
|
+
public static doesKeyMatchHash(key: unknown, keyHash: unknown): boolean {
|
|
269
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (typeof keyHash !== "string" || !SHA256_HEX_PATTERN.test(keyHash)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const presented: Buffer = Buffer.from(Service.hashKey(key), "hex");
|
|
278
|
+
const stored: Buffer = Buffer.from(keyHash.toLowerCase(), "hex");
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
presented.length === stored.length &&
|
|
282
|
+
crypto.timingSafeEqual(presented, stored)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Instance forms of the static helpers, for callers holding the default export.
|
|
287
|
+
public hashKey(key: string): string {
|
|
288
|
+
return Service.hashKey(key);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public generateKey(): string {
|
|
292
|
+
return Service.generateKey();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public doesKeyMatchHash(key: unknown, keyHash: unknown): boolean {
|
|
296
|
+
return Service.doesKeyMatchHash(key, keyHash);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/*
|
|
300
|
+
* The one "is the agent online?" rule: it says it is connected (it
|
|
301
|
+
* registered or heartbeated and has not signed off or been reset) AND it
|
|
302
|
+
* was heard from within KUBERNETES_AI_AGENT_ALIVE_WINDOW_IN_MINUTES. A
|
|
303
|
+
* heartbeat stamped slightly in the future (another server's clock) still
|
|
304
|
+
* counts as recent.
|
|
305
|
+
*/
|
|
306
|
+
public isOnline(row: KubernetesAiAgentPresenceRow, now?: Date): boolean {
|
|
307
|
+
if (row.connectionStatus !== "connected") {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const lastAliveAt: Date | null = toDate(row.lastAliveAt);
|
|
312
|
+
|
|
313
|
+
if (!lastAliveAt) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const nowTime: number = (now || OneUptimeDate.getCurrentDate()).getTime();
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
nowTime - lastAliveAt.getTime() <=
|
|
321
|
+
KUBERNETES_AI_AGENT_ALIVE_WINDOW_IN_MINUTES * 60 * 1000
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/*
|
|
326
|
+
* The row as the dashboard and the status builder see it. Never carries
|
|
327
|
+
* the key hash; the posture is re-validated on the way out, so a stored
|
|
328
|
+
* value of the wrong shape is dropped rather than trusted.
|
|
329
|
+
*/
|
|
330
|
+
public toSummary(
|
|
331
|
+
row: Pick<
|
|
332
|
+
Model,
|
|
333
|
+
| "id"
|
|
334
|
+
| "agentVersion"
|
|
335
|
+
| "posture"
|
|
336
|
+
| "lastRegisteredAt"
|
|
337
|
+
| "lastRefusedRegistrationAt"
|
|
338
|
+
| "lastRefusedRegistrationReason"
|
|
339
|
+
> &
|
|
340
|
+
KubernetesAiAgentPresenceRow,
|
|
341
|
+
now?: Date,
|
|
342
|
+
): KubernetesAiAgentSummary {
|
|
343
|
+
const connectionStatus: KubernetesAiAgentConnectionStatus =
|
|
344
|
+
row.connectionStatus === "connected" ? "connected" : "disconnected";
|
|
345
|
+
|
|
346
|
+
const posture: KubernetesAgentPosture | undefined =
|
|
347
|
+
parseKubernetesAgentPosture(row.posture);
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
id: row.id ? row.id.toString() : "",
|
|
351
|
+
isOnline: this.isOnline(row, now),
|
|
352
|
+
connectionStatus,
|
|
353
|
+
lastAliveAt: toIsoString(row.lastAliveAt),
|
|
354
|
+
lastRegisteredAt: toIsoString(row.lastRegisteredAt),
|
|
355
|
+
agentVersion: row.agentVersion || undefined,
|
|
356
|
+
posture,
|
|
357
|
+
lastRefusedRegistrationAt: toIsoString(row.lastRefusedRegistrationAt),
|
|
358
|
+
lastRefusedRegistrationReason:
|
|
359
|
+
row.lastRefusedRegistrationReason || undefined,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// The cluster's agent row (as root, without the key hash), or null.
|
|
364
|
+
@CaptureSpan()
|
|
365
|
+
public async findForCluster(data: {
|
|
366
|
+
projectId: ObjectID;
|
|
367
|
+
kubernetesClusterId: ObjectID;
|
|
368
|
+
}): Promise<Model | null> {
|
|
369
|
+
return await this.findOneBy({
|
|
370
|
+
query: {
|
|
371
|
+
projectId: data.projectId,
|
|
372
|
+
kubernetesClusterId: data.kubernetesClusterId,
|
|
373
|
+
},
|
|
374
|
+
select: KUBERNETES_AI_AGENT_SELECT_WITHOUT_KEY,
|
|
375
|
+
props: {
|
|
376
|
+
isRoot: true,
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/*
|
|
382
|
+
* The agent rows of several clusters of one project in one query, keyed
|
|
383
|
+
* by cluster id (string). Clusters without an agent are simply absent.
|
|
384
|
+
*/
|
|
385
|
+
@CaptureSpan()
|
|
386
|
+
public async findForClusters(data: {
|
|
387
|
+
projectId: ObjectID;
|
|
388
|
+
kubernetesClusterIds: Array<ObjectID>;
|
|
389
|
+
}): Promise<Map<string, Model>> {
|
|
390
|
+
const byClusterId: Map<string, Model> = new Map<string, Model>();
|
|
391
|
+
|
|
392
|
+
const clusterIds: Array<string> = Array.from(
|
|
393
|
+
new Set(
|
|
394
|
+
data.kubernetesClusterIds.map((id: ObjectID): string => {
|
|
395
|
+
return id.toString();
|
|
396
|
+
}),
|
|
397
|
+
),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (clusterIds.length === 0) {
|
|
401
|
+
return byClusterId;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const rows: Array<Model> = await this.findBy({
|
|
405
|
+
query: {
|
|
406
|
+
projectId: data.projectId,
|
|
407
|
+
kubernetesClusterId: QueryHelper.any(clusterIds),
|
|
408
|
+
},
|
|
409
|
+
select: KUBERNETES_AI_AGENT_SELECT_WITHOUT_KEY,
|
|
410
|
+
limit: LIMIT_MAX,
|
|
411
|
+
skip: 0,
|
|
412
|
+
props: {
|
|
413
|
+
isRoot: true,
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
for (const row of rows) {
|
|
418
|
+
const clusterId: string | undefined = row.kubernetesClusterId?.toString();
|
|
419
|
+
|
|
420
|
+
if (clusterId && clusterIds.includes(clusterId)) {
|
|
421
|
+
byClusterId.set(clusterId, row);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return byClusterId;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/*
|
|
429
|
+
* ------------------------------------------------------------------
|
|
430
|
+
* Registration
|
|
431
|
+
* ------------------------------------------------------------------
|
|
432
|
+
*/
|
|
433
|
+
|
|
434
|
+
/*
|
|
435
|
+
* The agent registering for its cluster (POST
|
|
436
|
+
* /kubernetes-ai-agent-ingest/register, authenticated by the project's
|
|
437
|
+
* telemetry ingestion key). Hands out a fresh agent key; only its hash is
|
|
438
|
+
* kept.
|
|
439
|
+
*
|
|
440
|
+
* Refused (KubernetesAiAgentRegistrationRefusedException, a 403 with the
|
|
441
|
+
* reason) when:
|
|
442
|
+
* - the cluster name is empty or too long (cluster_name_invalid);
|
|
443
|
+
* - this cluster's previous in-cluster Runner is still online
|
|
444
|
+
* (legacy_runner_online) — a helm upgrade stops it within seconds, and
|
|
445
|
+
* an ingestion key alone must never take a cluster away from a live
|
|
446
|
+
* Runner;
|
|
447
|
+
* - the cluster's agent is online and the request did not present its
|
|
448
|
+
* current key (previous_instance_online) — recorded on the row so the
|
|
449
|
+
* AI agent page can say another agent tried;
|
|
450
|
+
* - a new row would pass the project's caps (agent_cap_reached).
|
|
451
|
+
*
|
|
452
|
+
* An existing row is re-keyed when the request proves continuity (its
|
|
453
|
+
* current key as previousAgentKey), or when the row was reset, or its
|
|
454
|
+
* agent signed off or went quiet: what a restarted pod looks like.
|
|
455
|
+
*
|
|
456
|
+
* On a cluster nobody configured it applies the first-connection defaults
|
|
457
|
+
* (applyFirstConnectionDefaults). It never deletes, unbinds or rebinds
|
|
458
|
+
* anything, and writes ONE feed item when a cluster's agent first
|
|
459
|
+
* connects.
|
|
460
|
+
*/
|
|
461
|
+
@CaptureSpan()
|
|
462
|
+
public async register(data: {
|
|
463
|
+
projectId: ObjectID;
|
|
464
|
+
clusterName: unknown;
|
|
465
|
+
agentVersion?: string | undefined;
|
|
466
|
+
// The key the agent currently holds, when it still has one.
|
|
467
|
+
previousAgentKey?: string | undefined;
|
|
468
|
+
// The bare posture the body reports; parsed here, never trusted as is.
|
|
469
|
+
posture?: unknown;
|
|
470
|
+
// The TelemetryIngestionKey the request was admitted with, when known.
|
|
471
|
+
ingestionKeyId?: ObjectID | undefined;
|
|
472
|
+
}): Promise<KubernetesAiAgentRegistrationResult> {
|
|
473
|
+
const clusterName: string = this.getValidClusterName(data.clusterName);
|
|
474
|
+
|
|
475
|
+
const cluster: KubernetesCluster = await this.findOrCreateCluster({
|
|
476
|
+
projectId: data.projectId,
|
|
477
|
+
clusterName,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// The cluster row's own spelling of its name, whatever the chart sent.
|
|
481
|
+
const clusterIdentifier: string =
|
|
482
|
+
cluster.clusterIdentifier?.trim() || clusterName;
|
|
483
|
+
|
|
484
|
+
await this.assertLegacyRunnerIsNotOnline({
|
|
485
|
+
projectId: data.projectId,
|
|
486
|
+
cluster,
|
|
487
|
+
clusterIdentifier,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const posture: KubernetesAgentPosture = this.buildStoredPosture({
|
|
491
|
+
reported: data.posture,
|
|
492
|
+
clusterIdentifier,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const agentVersion: string | undefined = Service.normalizeAgentVersion(
|
|
496
|
+
data.agentVersion,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
const agentKey: string = Service.generateKey();
|
|
500
|
+
const now: Date = OneUptimeDate.getCurrentDate();
|
|
501
|
+
|
|
502
|
+
const existing: Model | null = await this.findOneBy({
|
|
503
|
+
query: {
|
|
504
|
+
projectId: data.projectId,
|
|
505
|
+
kubernetesClusterId: cluster.id!,
|
|
506
|
+
},
|
|
507
|
+
select: { ...KUBERNETES_AI_AGENT_SELECT_WITHOUT_KEY, keyHash: true },
|
|
508
|
+
props: { isRoot: true },
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
let agentId: ObjectID;
|
|
512
|
+
let admission: KubernetesAiAgentRegistrationAdmission;
|
|
513
|
+
|
|
514
|
+
if (existing && existing.id) {
|
|
515
|
+
admission = await this.admitReRegistration({
|
|
516
|
+
agent: existing,
|
|
517
|
+
previousAgentKey: data.previousAgentKey,
|
|
518
|
+
clusterIdentifier,
|
|
519
|
+
projectId: data.projectId,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await this.updateOneById({
|
|
523
|
+
id: existing.id,
|
|
524
|
+
data: {
|
|
525
|
+
keyHash: Service.hashKey(agentKey),
|
|
526
|
+
posture: posture as JSONObject,
|
|
527
|
+
lastAliveAt: now,
|
|
528
|
+
connectionStatus: "connected",
|
|
529
|
+
lastRegisteredAt: now,
|
|
530
|
+
registeredWithIngestionKeyId: data.ingestionKeyId || null,
|
|
531
|
+
...(agentVersion ? { agentVersion } : {}),
|
|
532
|
+
} as never,
|
|
533
|
+
props: { isRoot: true },
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
agentId = existing.id;
|
|
537
|
+
} else {
|
|
538
|
+
await this.assertAgentMayBeCreated({
|
|
539
|
+
projectId: data.projectId,
|
|
540
|
+
clusterIdentifier,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const row: Model = new Model();
|
|
544
|
+
row.projectId = data.projectId;
|
|
545
|
+
row.kubernetesClusterId = cluster.id!;
|
|
546
|
+
row.keyHash = Service.hashKey(agentKey);
|
|
547
|
+
row.posture = posture as JSONObject;
|
|
548
|
+
row.lastAliveAt = now;
|
|
549
|
+
row.connectionStatus = "connected";
|
|
550
|
+
row.lastRegisteredAt = now;
|
|
551
|
+
if (agentVersion) {
|
|
552
|
+
row.agentVersion = agentVersion;
|
|
553
|
+
}
|
|
554
|
+
if (data.ingestionKeyId) {
|
|
555
|
+
row.registeredWithIngestionKeyId = data.ingestionKeyId;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const created: Model = await this.createAgentRow({
|
|
559
|
+
row,
|
|
560
|
+
clusterIdentifier,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
agentId = created.id!;
|
|
564
|
+
admission = "created";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
logger.info(
|
|
568
|
+
`KubernetesAiAgent: the Kubernetes AI agent of cluster "${clusterIdentifier}" in project ${data.projectId.toString()} registered (${admission}).`,
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
/*
|
|
572
|
+
* The new key's hash is stored: from here on nothing may fail the
|
|
573
|
+
* registration, or the agent never receives a key the row now demands
|
|
574
|
+
* (see applyFirstConnectionDefaultsSafely). The feed items are safe
|
|
575
|
+
* already — KubernetesClusterFeedService never throws.
|
|
576
|
+
*/
|
|
577
|
+
const defaultsApplied: KubernetesAiAgentDefaultsApplied =
|
|
578
|
+
await this.applyFirstConnectionDefaultsSafely({
|
|
579
|
+
cluster,
|
|
580
|
+
allowWrites: posture.allowWrites === true,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
if (admission === "created") {
|
|
584
|
+
await this.writeConnectedFeedItem({
|
|
585
|
+
cluster,
|
|
586
|
+
agentId,
|
|
587
|
+
posture,
|
|
588
|
+
agentVersion,
|
|
589
|
+
defaultsApplied,
|
|
590
|
+
});
|
|
591
|
+
} else if (Service.didApplyDefaults(defaultsApplied)) {
|
|
592
|
+
await this.writeDefaultsFeedItem({ cluster, posture, defaultsApplied });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
agentId,
|
|
597
|
+
agentKey,
|
|
598
|
+
clusterId: cluster.id!,
|
|
599
|
+
admission,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/*
|
|
604
|
+
* How a registration for a cluster that already has an agent row is
|
|
605
|
+
* admitted, or null when it must be refused (the row's agent is online
|
|
606
|
+
* and the request did not prove it is that agent). A reset row admits
|
|
607
|
+
* anyone: no key is valid for it, and the admin reset is exactly the
|
|
608
|
+
* "let the real pod back in" action.
|
|
609
|
+
*/
|
|
610
|
+
public getReRegistrationAdmission(data: {
|
|
611
|
+
agent: {
|
|
612
|
+
keyHash?: string | null | undefined;
|
|
613
|
+
} & KubernetesAiAgentPresenceRow;
|
|
614
|
+
previousAgentKey?: string | undefined;
|
|
615
|
+
now?: Date | undefined;
|
|
616
|
+
}): KubernetesAiAgentRegistrationAdmission | null {
|
|
617
|
+
if (!data.agent.keyHash) {
|
|
618
|
+
return "reset";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (Service.doesKeyMatchHash(data.previousAgentKey, data.agent.keyHash)) {
|
|
622
|
+
return "continuity";
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (data.agent.connectionStatus !== "connected") {
|
|
626
|
+
return "signed_off";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!this.isOnline(data.agent, data.now)) {
|
|
630
|
+
return "offline";
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/*
|
|
637
|
+
* The first-connection defaults, applied ONLY to a cluster nobody
|
|
638
|
+
* configured (aiAccessConfiguredAt is null — an operator's write of any AI
|
|
639
|
+
* setting stamps it): investigating with kubectl on, and, when the chart
|
|
640
|
+
* granted the agent write RBAC, fixes "Ask for approval" instead of Off.
|
|
641
|
+
* The chart's write RBAC is the cluster operator's consent for that; a
|
|
642
|
+
* human still approves every fix. Never sets aiAccessConfiguredAt, so
|
|
643
|
+
* these stay defaults — and never touches a mode an operator chose.
|
|
644
|
+
*
|
|
645
|
+
* Registration applies them every time; a heartbeat applies them when the
|
|
646
|
+
* agent's write access appears (the chart was upgraded to allow writes).
|
|
647
|
+
* The update is conditioned on the cluster still being unconfigured, so
|
|
648
|
+
* an operator's choice made a moment earlier is never overwritten.
|
|
649
|
+
*/
|
|
650
|
+
@CaptureSpan()
|
|
651
|
+
public async applyFirstConnectionDefaults(data: {
|
|
652
|
+
cluster: KubernetesCluster;
|
|
653
|
+
allowWrites: boolean;
|
|
654
|
+
}): Promise<KubernetesAiAgentDefaultsApplied> {
|
|
655
|
+
const defaults: KubernetesAiAgentDefaultsApplied =
|
|
656
|
+
this.getFirstConnectionDefaults(data);
|
|
657
|
+
|
|
658
|
+
if (!Service.didApplyDefaults(defaults) || !data.cluster.id) {
|
|
659
|
+
return { turnedOnInvestigation: false };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const updated: number = await KubernetesClusterService.updateOneBy({
|
|
663
|
+
query: {
|
|
664
|
+
_id: data.cluster.id.toString(),
|
|
665
|
+
aiAccessConfiguredAt: QueryHelper.isNull(),
|
|
666
|
+
...(data.cluster.projectId
|
|
667
|
+
? { projectId: data.cluster.projectId }
|
|
668
|
+
: {}),
|
|
669
|
+
},
|
|
670
|
+
data: {
|
|
671
|
+
...(defaults.turnedOnInvestigation
|
|
672
|
+
? { isAiInvestigationEnabled: true }
|
|
673
|
+
: {}),
|
|
674
|
+
...(defaults.remediationMode
|
|
675
|
+
? { aiRemediationMode: defaults.remediationMode }
|
|
676
|
+
: {}),
|
|
677
|
+
} as never,
|
|
678
|
+
props: { isRoot: true },
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
if (updated === 0) {
|
|
682
|
+
return { turnedOnInvestigation: false };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
logger.info(
|
|
686
|
+
`KubernetesAiAgent: applied the first-connection defaults to cluster ${data.cluster.id.toString()} (${[
|
|
687
|
+
defaults.turnedOnInvestigation ? "investigation on" : "",
|
|
688
|
+
defaults.remediationMode
|
|
689
|
+
? `remediation ${defaults.remediationMode}`
|
|
690
|
+
: "",
|
|
691
|
+
]
|
|
692
|
+
.filter(Boolean)
|
|
693
|
+
.join(", ")}).`,
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
return defaults;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/*
|
|
700
|
+
* applyFirstConnectionDefaults for a request whose real work is already
|
|
701
|
+
* written — registration stored the new key's hash and marked the row
|
|
702
|
+
* connected, a heartbeat stored the liveness. A failure here must not
|
|
703
|
+
* fail that request: a registration that answered 500 after storing the
|
|
704
|
+
* hash would leave a row that reads online and holds a key nobody
|
|
705
|
+
* received, so every retry would be refused as previous_instance_online
|
|
706
|
+
* (and recorded as another agent's attempt) until the alive window
|
|
707
|
+
* lapsed. Nothing is lost by skipping the defaults: the next
|
|
708
|
+
* registration applies them again. Logged, and reported as "nothing
|
|
709
|
+
* changed", so no feed item claims a change that did not happen.
|
|
710
|
+
*/
|
|
711
|
+
private async applyFirstConnectionDefaultsSafely(data: {
|
|
712
|
+
cluster: KubernetesCluster;
|
|
713
|
+
allowWrites: boolean;
|
|
714
|
+
}): Promise<KubernetesAiAgentDefaultsApplied> {
|
|
715
|
+
try {
|
|
716
|
+
return await this.applyFirstConnectionDefaults(data);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
logger.error(
|
|
719
|
+
`KubernetesAiAgent: could not apply the first-connection defaults to cluster ${data.cluster.id?.toString()}; the next registration tries again: ${error}`,
|
|
720
|
+
);
|
|
721
|
+
return { turnedOnInvestigation: false };
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// What applyFirstConnectionDefaults would change, without writing it.
|
|
726
|
+
public getFirstConnectionDefaults(data: {
|
|
727
|
+
cluster: Pick<
|
|
728
|
+
KubernetesCluster,
|
|
729
|
+
"aiAccessConfiguredAt" | "isAiInvestigationEnabled" | "aiRemediationMode"
|
|
730
|
+
>;
|
|
731
|
+
allowWrites: boolean;
|
|
732
|
+
}): KubernetesAiAgentDefaultsApplied {
|
|
733
|
+
if (data.cluster.aiAccessConfiguredAt) {
|
|
734
|
+
return { turnedOnInvestigation: false };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const isRemediationOff: boolean =
|
|
738
|
+
KubernetesClusterAiAccessService.normalizeRemediationMode(
|
|
739
|
+
data.cluster.aiRemediationMode,
|
|
740
|
+
) === KubernetesAiRemediationMode.Disabled;
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
turnedOnInvestigation: data.cluster.isAiInvestigationEnabled !== true,
|
|
744
|
+
...(data.allowWrites && isRemediationOff
|
|
745
|
+
? { remediationMode: KubernetesAiRemediationMode.RequireApproval }
|
|
746
|
+
: {}),
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
public static didApplyDefaults(
|
|
751
|
+
defaults: KubernetesAiAgentDefaultsApplied,
|
|
752
|
+
): boolean {
|
|
753
|
+
return defaults.turnedOnInvestigation || Boolean(defaults.remediationMode);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/*
|
|
757
|
+
* The posture a row stores: what the agent reported, validated field by
|
|
758
|
+
* field, with the two facts the server decides itself forced — it runs in
|
|
759
|
+
* the cluster (inCluster), and WHICH cluster (the cluster row's
|
|
760
|
+
* identifier). A body can never make the agent the in-cluster executor of
|
|
761
|
+
* another cluster.
|
|
762
|
+
*/
|
|
763
|
+
public buildStoredPosture(data: {
|
|
764
|
+
reported: unknown;
|
|
765
|
+
clusterIdentifier: string;
|
|
766
|
+
}): KubernetesAgentPosture {
|
|
767
|
+
const reported: KubernetesAgentPosture =
|
|
768
|
+
parseKubernetesAgentPosture(data.reported) || {};
|
|
769
|
+
|
|
770
|
+
const posture: KubernetesAgentPosture = {
|
|
771
|
+
...reported,
|
|
772
|
+
clusterIdentifier: data.clusterIdentifier.trim() || undefined,
|
|
773
|
+
inCluster: true,
|
|
774
|
+
allowWrites: reported.allowWrites === true,
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// A JSON column: store only what is known, never `undefined` keys.
|
|
778
|
+
for (const key of Object.keys(posture) as Array<
|
|
779
|
+
keyof KubernetesAgentPosture
|
|
780
|
+
>) {
|
|
781
|
+
if (posture[key] === undefined) {
|
|
782
|
+
delete posture[key];
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return posture;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/*
|
|
790
|
+
* The version string the agent reports, bounded to its ShortText column
|
|
791
|
+
* (an over-long value would fail the whole write); undefined when absent.
|
|
792
|
+
*/
|
|
793
|
+
public static normalizeAgentVersion(value: unknown): string | undefined {
|
|
794
|
+
if (typeof value !== "string") {
|
|
795
|
+
return undefined;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const trimmed: string = value.trim();
|
|
799
|
+
|
|
800
|
+
return trimmed ? trimmed.substring(0, ColumnLength.ShortText) : undefined;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private getValidClusterName(raw: unknown): string {
|
|
804
|
+
const clusterName: string = typeof raw === "string" ? raw.trim() : "";
|
|
805
|
+
|
|
806
|
+
if (!clusterName) {
|
|
807
|
+
throw new KubernetesAiAgentRegistrationRefusedException({
|
|
808
|
+
reason: "cluster_name_invalid",
|
|
809
|
+
message:
|
|
810
|
+
"The Kubernetes AI agent did not send a cluster name. Set clusterName on the kubernetes-agent chart.",
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (clusterName.length > MAX_KUBERNETES_AI_AGENT_CLUSTER_NAME_LENGTH) {
|
|
815
|
+
throw new KubernetesAiAgentRegistrationRefusedException({
|
|
816
|
+
reason: "cluster_name_invalid",
|
|
817
|
+
message: `The cluster name is ${clusterName.length} characters long; the limit is ${MAX_KUBERNETES_AI_AGENT_CLUSTER_NAME_LENGTH}. Set a shorter clusterName on the kubernetes-agent chart.`,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return clusterName;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/*
|
|
825
|
+
* The cluster the agent names, created when OneUptime has not seen it yet
|
|
826
|
+
* (the agent may register before the collector's first telemetry), then
|
|
827
|
+
* re-read for the AI columns registration needs.
|
|
828
|
+
*/
|
|
829
|
+
private async findOrCreateCluster(data: {
|
|
830
|
+
projectId: ObjectID;
|
|
831
|
+
clusterName: string;
|
|
832
|
+
}): Promise<KubernetesCluster> {
|
|
833
|
+
const found: KubernetesCluster =
|
|
834
|
+
await KubernetesClusterService.findOrCreateByClusterIdentifier({
|
|
835
|
+
projectId: data.projectId,
|
|
836
|
+
clusterIdentifier: data.clusterName,
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
const cluster: KubernetesCluster | null =
|
|
840
|
+
await KubernetesClusterService.findOneBy({
|
|
841
|
+
query: { _id: found.id!.toString(), projectId: data.projectId },
|
|
842
|
+
select: CLUSTER_SELECT,
|
|
843
|
+
props: { isRoot: true },
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
if (!cluster || !cluster.id) {
|
|
847
|
+
throw new BadDataException("Cluster could not be resolved.");
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return cluster;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/*
|
|
854
|
+
* The previous in-cluster Runner (the kubernetes-agent chart's ai-runner,
|
|
855
|
+
* which this agent replaces) keeps its cluster while it is online. During
|
|
856
|
+
* a helm upgrade the old pod stops within seconds of the new one starting,
|
|
857
|
+
* so the agent is only asked to wait; an ingestion key alone can never
|
|
858
|
+
* take a cluster away from a Runner that is still working.
|
|
859
|
+
*/
|
|
860
|
+
private async assertLegacyRunnerIsNotOnline(data: {
|
|
861
|
+
projectId: ObjectID;
|
|
862
|
+
cluster: KubernetesCluster;
|
|
863
|
+
clusterIdentifier: string;
|
|
864
|
+
}): Promise<void> {
|
|
865
|
+
const legacyRunner: Runner | null =
|
|
866
|
+
await KubernetesClusterAiAccessService.getLegacyAgentRunnerForCluster({
|
|
867
|
+
projectId: data.projectId,
|
|
868
|
+
kubernetesClusterId: data.cluster.id!,
|
|
869
|
+
clusterIdentifier: data.clusterIdentifier,
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
if (
|
|
873
|
+
!legacyRunner ||
|
|
874
|
+
!KubernetesClusterAiAccessService.isRunnerOnline(legacyRunner)
|
|
875
|
+
) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
logger.info(
|
|
880
|
+
`KubernetesAiAgent: asked the Kubernetes AI agent of cluster "${data.clusterIdentifier}" in project ${data.projectId.toString()} to wait: its previous in-cluster Runner (${legacyRunner.id?.toString()}) is still online.`,
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
throw new KubernetesAiAgentRegistrationRefusedException({
|
|
884
|
+
reason: "legacy_runner_online",
|
|
885
|
+
retryAfterSeconds: KUBERNETES_AI_AGENT_REGISTRATION_RETRY_AFTER_SECONDS,
|
|
886
|
+
message: `The previous in-cluster Runner of cluster "${data.clusterIdentifier}" is still online. This is normal for a few seconds during a helm upgrade, and the agent retries on its own. If it lasts, remove the old ai-runner pod.`,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/*
|
|
891
|
+
* An existing row: admitted per getReRegistrationAdmission, or refused
|
|
892
|
+
* with previous_instance_online — which is recorded on the row (when, and
|
|
893
|
+
* why) so the AI agent page can warn that another agent tried to register
|
|
894
|
+
* while this one was online. Nothing about the current key is disclosed.
|
|
895
|
+
*/
|
|
896
|
+
private async admitReRegistration(data: {
|
|
897
|
+
agent: Model;
|
|
898
|
+
previousAgentKey?: string | undefined;
|
|
899
|
+
clusterIdentifier: string;
|
|
900
|
+
projectId: ObjectID;
|
|
901
|
+
}): Promise<KubernetesAiAgentRegistrationAdmission> {
|
|
902
|
+
const admission: KubernetesAiAgentRegistrationAdmission | null =
|
|
903
|
+
this.getReRegistrationAdmission({
|
|
904
|
+
agent: data.agent,
|
|
905
|
+
previousAgentKey: data.previousAgentKey,
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
if (admission) {
|
|
909
|
+
return admission;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
logger.warn(
|
|
913
|
+
`KubernetesAiAgent: refused a registration for cluster "${data.clusterIdentifier}" in project ${data.projectId.toString()}: its agent (${data.agent.id?.toString()}) is online and the request did not present its current key.`,
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
await this.recordRefusedRegistration({
|
|
917
|
+
agentId: data.agent.id!,
|
|
918
|
+
reason: "previous_instance_online",
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
throw this.getPreviousInstanceOnlineRefusal(data.clusterIdentifier);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private getPreviousInstanceOnlineRefusal(
|
|
925
|
+
clusterIdentifier: string,
|
|
926
|
+
): KubernetesAiAgentRegistrationRefusedException {
|
|
927
|
+
return new KubernetesAiAgentRegistrationRefusedException({
|
|
928
|
+
reason: "previous_instance_online",
|
|
929
|
+
retryAfterSeconds: KUBERNETES_AI_AGENT_REGISTRATION_RETRY_AFTER_SECONDS,
|
|
930
|
+
message: `Another Kubernetes AI agent for cluster "${clusterIdentifier}" is online. This is normal for a moment while its pod restarts, and the agent retries on its own. If it lasts, make sure only one kubernetes-agent release uses clusterName "${clusterIdentifier}".`,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Best-effort: recording a refusal must never turn it into a 500.
|
|
935
|
+
private async recordRefusedRegistration(data: {
|
|
936
|
+
agentId: ObjectID;
|
|
937
|
+
reason: KubernetesAiAgentRegistrationRefusalReason;
|
|
938
|
+
}): Promise<void> {
|
|
939
|
+
try {
|
|
940
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
941
|
+
id: data.agentId,
|
|
942
|
+
data: {
|
|
943
|
+
lastRefusedRegistrationAt: OneUptimeDate.getCurrentDate(),
|
|
944
|
+
lastRefusedRegistrationReason: data.reason,
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
} catch (error) {
|
|
948
|
+
logger.error(
|
|
949
|
+
`KubernetesAiAgent: could not record a refused registration on agent ${data.agentId.toString()}: ${error}`,
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Bounds what an ingestion key can create (see the caps above).
|
|
955
|
+
private async assertAgentMayBeCreated(data: {
|
|
956
|
+
projectId: ObjectID;
|
|
957
|
+
clusterIdentifier: string;
|
|
958
|
+
}): Promise<void> {
|
|
959
|
+
const total: number = (
|
|
960
|
+
await this.countBy({
|
|
961
|
+
query: { projectId: data.projectId },
|
|
962
|
+
props: { isRoot: true },
|
|
963
|
+
})
|
|
964
|
+
).toNumber();
|
|
965
|
+
|
|
966
|
+
if (total >= MAX_KUBERNETES_AI_AGENTS_PER_PROJECT) {
|
|
967
|
+
logger.warn(
|
|
968
|
+
`KubernetesAiAgent: refused to create an agent for cluster "${data.clusterIdentifier}" in project ${data.projectId.toString()}: the project already has ${total} (limit ${MAX_KUBERNETES_AI_AGENTS_PER_PROJECT}).`,
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
throw new KubernetesAiAgentRegistrationRefusedException({
|
|
972
|
+
reason: "agent_cap_reached",
|
|
973
|
+
message: `This project already has ${total} Kubernetes AI agents, which is its limit (${MAX_KUBERNETES_AI_AGENTS_PER_PROJECT}). Delete the clusters you no longer use under Kubernetes; the agent then connects on its next retry.`,
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const createdInLastHour: number = (
|
|
978
|
+
await this.countBy({
|
|
979
|
+
query: {
|
|
980
|
+
projectId: data.projectId,
|
|
981
|
+
createdAt: QueryHelper.greaterThan(OneUptimeDate.getSomeHoursAgo(1)),
|
|
982
|
+
},
|
|
983
|
+
props: { isRoot: true },
|
|
984
|
+
})
|
|
985
|
+
).toNumber();
|
|
986
|
+
|
|
987
|
+
if (
|
|
988
|
+
createdInLastHour >= MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR
|
|
989
|
+
) {
|
|
990
|
+
logger.warn(
|
|
991
|
+
`KubernetesAiAgent: refused to create an agent for cluster "${data.clusterIdentifier}" in project ${data.projectId.toString()}: ${createdInLastHour} were created in the last hour (limit ${MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR}).`,
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
throw new KubernetesAiAgentRegistrationRefusedException({
|
|
995
|
+
reason: "agent_cap_reached",
|
|
996
|
+
message: `${createdInLastHour} Kubernetes AI agents connected to this project in the last hour, which is its limit (${MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR}). The agent connects on a later retry.`,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/*
|
|
1002
|
+
* Creates the cluster's agent row. The database allows one per cluster,
|
|
1003
|
+
* so when two pods register for a new cluster at once one create loses;
|
|
1004
|
+
* that pod is told the other agent is online (it is — it just
|
|
1005
|
+
* registered), exactly as if it had arrived a moment later.
|
|
1006
|
+
*/
|
|
1007
|
+
private async createAgentRow(data: {
|
|
1008
|
+
row: Model;
|
|
1009
|
+
clusterIdentifier: string;
|
|
1010
|
+
}): Promise<Model> {
|
|
1011
|
+
try {
|
|
1012
|
+
return await this.create({
|
|
1013
|
+
data: data.row,
|
|
1014
|
+
props: { isRoot: true },
|
|
1015
|
+
});
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
const winner: Model | null = await this.findOneBy({
|
|
1018
|
+
query: {
|
|
1019
|
+
projectId: data.row.projectId!,
|
|
1020
|
+
kubernetesClusterId: data.row.kubernetesClusterId!,
|
|
1021
|
+
},
|
|
1022
|
+
select: { _id: true },
|
|
1023
|
+
props: { isRoot: true },
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
if (!winner) {
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
logger.info(
|
|
1031
|
+
`KubernetesAiAgent: two agents registered for cluster "${data.clusterIdentifier}" at once; the other one was admitted.`,
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
throw this.getPreviousInstanceOnlineRefusal(data.clusterIdentifier);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/*
|
|
1039
|
+
* ------------------------------------------------------------------
|
|
1040
|
+
* The registered agent
|
|
1041
|
+
* ------------------------------------------------------------------
|
|
1042
|
+
*/
|
|
1043
|
+
|
|
1044
|
+
/*
|
|
1045
|
+
* The agent a request's id and key belong to, or null. Every request
|
|
1046
|
+
* after registration is authenticated here: the key's sha256 must match
|
|
1047
|
+
* the row's hash (timing-safe). A reset row (null hash) matches nothing,
|
|
1048
|
+
* and an unknown id costs the same comparison as a wrong key. The row is
|
|
1049
|
+
* returned without its key hash.
|
|
1050
|
+
*/
|
|
1051
|
+
@CaptureSpan()
|
|
1052
|
+
public async authenticate(data: {
|
|
1053
|
+
agentId: ObjectID | string;
|
|
1054
|
+
agentKey: string;
|
|
1055
|
+
}): Promise<Model | null> {
|
|
1056
|
+
const agentId: string =
|
|
1057
|
+
typeof data.agentId === "string"
|
|
1058
|
+
? data.agentId
|
|
1059
|
+
: data.agentId?.toString() || "";
|
|
1060
|
+
|
|
1061
|
+
if (
|
|
1062
|
+
!agentId ||
|
|
1063
|
+
!ObjectID.isValidUUID(agentId) ||
|
|
1064
|
+
typeof data.agentKey !== "string" ||
|
|
1065
|
+
!data.agentKey
|
|
1066
|
+
) {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const agent: Model | null = await this.findOneBy({
|
|
1071
|
+
query: { _id: agentId },
|
|
1072
|
+
select: { ...KUBERNETES_AI_AGENT_SELECT_WITHOUT_KEY, keyHash: true },
|
|
1073
|
+
props: { isRoot: true },
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
const matches: boolean = Service.doesKeyMatchHash(
|
|
1077
|
+
data.agentKey,
|
|
1078
|
+
agent?.keyHash || UNMATCHABLE_KEY_HASH,
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
if (!agent || !matches) {
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
delete agent.keyHash;
|
|
1086
|
+
|
|
1087
|
+
return agent;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/*
|
|
1091
|
+
* A heartbeat from the authenticated agent: it is alive and connected,
|
|
1092
|
+
* and this is what it can do now. The posture is stored the way
|
|
1093
|
+
* registration stores it (validated, with inCluster and the cluster's
|
|
1094
|
+
* identifier forced); a heartbeat without one keeps the stored posture.
|
|
1095
|
+
*
|
|
1096
|
+
* When the agent's write access appears (the chart was upgraded with
|
|
1097
|
+
* aiAgent.remediation.enabled=true) on a cluster nobody configured, the
|
|
1098
|
+
* first-connection defaults apply again, so fixes move from Off to "Ask
|
|
1099
|
+
* for approval" without anyone visiting the AI agent page. It is also
|
|
1100
|
+
* where the previous in-cluster Runner's retirement is checked (hourly).
|
|
1101
|
+
*
|
|
1102
|
+
* This runs every 30 seconds per cluster, so a normal heartbeat is one
|
|
1103
|
+
* UPDATE without hooks and no read: the cluster's identifier comes from
|
|
1104
|
+
* the stored posture, which registration took from the cluster row. The
|
|
1105
|
+
* cluster row is read only when the stored posture lacks it or write
|
|
1106
|
+
* access just appeared; the hourly Runner check reads it itself.
|
|
1107
|
+
*/
|
|
1108
|
+
@CaptureSpan()
|
|
1109
|
+
public async heartbeat(data: {
|
|
1110
|
+
// The row authenticate returned for this request.
|
|
1111
|
+
agent: Model;
|
|
1112
|
+
agentVersion?: string | undefined;
|
|
1113
|
+
posture?: unknown;
|
|
1114
|
+
}): Promise<void> {
|
|
1115
|
+
const agentId: ObjectID | null = data.agent.id;
|
|
1116
|
+
|
|
1117
|
+
if (!agentId) {
|
|
1118
|
+
throw new BadDataException("The agent is missing its id.");
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const storedPosture: KubernetesAgentPosture | undefined =
|
|
1122
|
+
parseKubernetesAgentPosture(data.agent.posture);
|
|
1123
|
+
|
|
1124
|
+
const isPostureReported: boolean = Boolean(
|
|
1125
|
+
data.posture && typeof data.posture === "object",
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
let cluster: KubernetesCluster | null = null;
|
|
1129
|
+
let clusterIdentifier: string =
|
|
1130
|
+
storedPosture?.clusterIdentifier?.trim() || "";
|
|
1131
|
+
|
|
1132
|
+
if (isPostureReported && !clusterIdentifier) {
|
|
1133
|
+
cluster = await this.findAgentCluster(data.agent);
|
|
1134
|
+
clusterIdentifier = cluster?.clusterIdentifier?.trim() || "";
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const posture: KubernetesAgentPosture | undefined = isPostureReported
|
|
1138
|
+
? this.buildStoredPosture({
|
|
1139
|
+
reported: data.posture,
|
|
1140
|
+
clusterIdentifier,
|
|
1141
|
+
})
|
|
1142
|
+
: undefined;
|
|
1143
|
+
|
|
1144
|
+
const agentVersion: string | undefined = Service.normalizeAgentVersion(
|
|
1145
|
+
data.agentVersion,
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
const update: JSONObject = {
|
|
1149
|
+
lastAliveAt: OneUptimeDate.getCurrentDate(),
|
|
1150
|
+
connectionStatus: "connected",
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
if (posture) {
|
|
1154
|
+
update["posture"] = posture as JSONObject;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (agentVersion) {
|
|
1158
|
+
update["agentVersion"] = agentVersion;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
1162
|
+
id: agentId,
|
|
1163
|
+
data: update as never,
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
/*
|
|
1167
|
+
* Off the heartbeat's path: it decides at most once an hour per agent
|
|
1168
|
+
* and never throws, and a heartbeat must not wait on (or fail because
|
|
1169
|
+
* of) housekeeping.
|
|
1170
|
+
*/
|
|
1171
|
+
this.retireLegacyRunnerIfUnused({
|
|
1172
|
+
agent: data.agent,
|
|
1173
|
+
...(cluster ? { cluster } : {}),
|
|
1174
|
+
}).catch((error: unknown) => {
|
|
1175
|
+
logger.error(
|
|
1176
|
+
`KubernetesAiAgent: the previous in-cluster Runner check for agent ${agentId.toString()} failed: ${error}`,
|
|
1177
|
+
);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
const writesAppeared: boolean =
|
|
1181
|
+
posture?.allowWrites === true && storedPosture?.allowWrites !== true;
|
|
1182
|
+
|
|
1183
|
+
if (!writesAppeared || !posture) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// The defaults read the cluster's current AI settings.
|
|
1188
|
+
cluster = cluster || (await this.findAgentCluster(data.agent));
|
|
1189
|
+
|
|
1190
|
+
if (!cluster) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// The liveness is written; a failure here must not fail the heartbeat.
|
|
1195
|
+
const defaultsApplied: KubernetesAiAgentDefaultsApplied =
|
|
1196
|
+
await this.applyFirstConnectionDefaultsSafely({
|
|
1197
|
+
cluster,
|
|
1198
|
+
allowWrites: true,
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
if (Service.didApplyDefaults(defaultsApplied)) {
|
|
1202
|
+
await this.writeDefaultsFeedItem({ cluster, posture, defaultsApplied });
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// The agent's own cluster (in its project, as root), or null.
|
|
1207
|
+
private async findAgentCluster(
|
|
1208
|
+
agent: Pick<Model, "kubernetesClusterId" | "projectId">,
|
|
1209
|
+
): Promise<KubernetesCluster | null> {
|
|
1210
|
+
if (!agent.kubernetesClusterId) {
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
return await KubernetesClusterService.findOneBy({
|
|
1215
|
+
query: {
|
|
1216
|
+
_id: agent.kubernetesClusterId.toString(),
|
|
1217
|
+
...(agent.projectId ? { projectId: agent.projectId } : {}),
|
|
1218
|
+
},
|
|
1219
|
+
select: CLUSTER_SELECT,
|
|
1220
|
+
props: { isRoot: true },
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/*
|
|
1225
|
+
* ------------------------------------------------------------------
|
|
1226
|
+
* Retiring the previous in-cluster Runner
|
|
1227
|
+
* ------------------------------------------------------------------
|
|
1228
|
+
*/
|
|
1229
|
+
|
|
1230
|
+
/*
|
|
1231
|
+
* The kubernetes-agent Runner row an older chart registered for this
|
|
1232
|
+
* cluster, deleted once it is certainly no longer needed — so the
|
|
1233
|
+
* Runners page stops listing a dead Runner nobody created by hand. Every
|
|
1234
|
+
* condition must hold, and anything else leaves it alone:
|
|
1235
|
+
*
|
|
1236
|
+
* - this agent has existed for LEGACY_RUNNER_RETIREMENT_AGENT_AGE_HOURS
|
|
1237
|
+
* and is online (it is heartbeating) — a rollback in the first day
|
|
1238
|
+
* after an upgrade still finds its Runner;
|
|
1239
|
+
* - the Runner has not been heard from for
|
|
1240
|
+
* LEGACY_RUNNER_RETIREMENT_OFFLINE_DAYS (a Runner that never
|
|
1241
|
+
* heartbeated is left alone: its age is unknown);
|
|
1242
|
+
* - it holds nothing beyond an in-cluster Runner's defaults (no
|
|
1243
|
+
* credentials or secrets, no runbooks or code fixes, not another
|
|
1244
|
+
* cluster's Runner) — what an operator entrusted to it is theirs;
|
|
1245
|
+
* - no auto-remediation rule names it: a rule scoped to only that Runner
|
|
1246
|
+
* would silently widen to "any Runner" once it is gone.
|
|
1247
|
+
*
|
|
1248
|
+
* Checked at most once an hour per agent per process, from the heartbeat.
|
|
1249
|
+
* The agent's cluster is read (once the check is due) unless the caller
|
|
1250
|
+
* already has it. Never throws; says what it decided.
|
|
1251
|
+
*/
|
|
1252
|
+
@CaptureSpan()
|
|
1253
|
+
public async retireLegacyRunnerIfUnused(data: {
|
|
1254
|
+
agent: Model;
|
|
1255
|
+
cluster?: KubernetesCluster | undefined;
|
|
1256
|
+
now?: Date | undefined;
|
|
1257
|
+
}): Promise<LegacyRunnerRetirementOutcome> {
|
|
1258
|
+
const now: Date = data.now || OneUptimeDate.getCurrentDate();
|
|
1259
|
+
const agentId: string = data.agent.id?.toString() || "";
|
|
1260
|
+
|
|
1261
|
+
if (!agentId || !this.isLegacyRunnerRetirementCheckDue(agentId, now)) {
|
|
1262
|
+
return "not_due";
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const clusterId: string =
|
|
1266
|
+
data.cluster?.id?.toString() ||
|
|
1267
|
+
data.agent.kubernetesClusterId?.toString() ||
|
|
1268
|
+
"";
|
|
1269
|
+
|
|
1270
|
+
try {
|
|
1271
|
+
if (!Service.isOldEnoughToRetireLegacyRunner(data.agent, now)) {
|
|
1272
|
+
return "agent_too_new";
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const cluster: KubernetesCluster | null =
|
|
1276
|
+
data.cluster || (await this.findAgentCluster(data.agent));
|
|
1277
|
+
|
|
1278
|
+
const projectId: ObjectID | undefined =
|
|
1279
|
+
cluster?.projectId || data.agent.projectId;
|
|
1280
|
+
|
|
1281
|
+
if (!cluster || !cluster.id || !projectId) {
|
|
1282
|
+
return "no_legacy_runner";
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const legacyRunner: Runner | null =
|
|
1286
|
+
await KubernetesClusterAiAccessService.getLegacyAgentRunnerForCluster({
|
|
1287
|
+
projectId,
|
|
1288
|
+
kubernetesClusterId: cluster.id,
|
|
1289
|
+
clusterIdentifier: cluster.clusterIdentifier,
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
if (!legacyRunner || !legacyRunner.id) {
|
|
1293
|
+
return "no_legacy_runner";
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (
|
|
1297
|
+
KubernetesClusterAiAccessService.isRunnerOnline(legacyRunner) ||
|
|
1298
|
+
!Service.hasBeenOfflineLongEnoughToRetire(legacyRunner, now)
|
|
1299
|
+
) {
|
|
1300
|
+
return "legacy_runner_recently_online";
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const holdings: Array<{ description: string }> =
|
|
1304
|
+
await KubernetesClusterAiAccessService.getRunnerHoldingsBeyondDefaults({
|
|
1305
|
+
runner: legacyRunner,
|
|
1306
|
+
clusterId: cluster.id,
|
|
1307
|
+
projectId,
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
if (holdings.length > 0) {
|
|
1311
|
+
logger.info(
|
|
1312
|
+
`KubernetesAiAgent: kept the previous in-cluster Runner ${legacyRunner.id.toString()} of cluster ${cluster.id.toString()}: it holds more than its defaults.`,
|
|
1313
|
+
);
|
|
1314
|
+
return "legacy_runner_holds_more";
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const rulesNamingIt: number = (
|
|
1318
|
+
await AutoRemediationRuleService.countBy({
|
|
1319
|
+
query: {
|
|
1320
|
+
projectId,
|
|
1321
|
+
commandRunners: QueryHelper.inRelationArray([legacyRunner.id]),
|
|
1322
|
+
},
|
|
1323
|
+
props: { isRoot: true },
|
|
1324
|
+
})
|
|
1325
|
+
).toNumber();
|
|
1326
|
+
|
|
1327
|
+
if (rulesNamingIt > 0) {
|
|
1328
|
+
logger.info(
|
|
1329
|
+
`KubernetesAiAgent: kept the previous in-cluster Runner ${legacyRunner.id.toString()} of cluster ${cluster.id.toString()}: ${rulesNamingIt} auto-remediation rule(s) name it.`,
|
|
1330
|
+
);
|
|
1331
|
+
return "legacy_runner_in_rule";
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
await RunnerService.deleteOneBy({
|
|
1335
|
+
query: { _id: legacyRunner.id.toString(), projectId },
|
|
1336
|
+
props: { isRoot: true },
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
logger.info(
|
|
1340
|
+
`KubernetesAiAgent: removed the previous in-cluster Runner ${legacyRunner.id.toString()} of cluster ${cluster.id.toString()}; the Kubernetes AI agent replaced it.`,
|
|
1341
|
+
);
|
|
1342
|
+
|
|
1343
|
+
await KubernetesClusterFeedService.createKubernetesClusterFeedItem({
|
|
1344
|
+
kubernetesClusterId: cluster.id,
|
|
1345
|
+
projectId,
|
|
1346
|
+
kubernetesClusterFeedEventType:
|
|
1347
|
+
KubernetesClusterFeedEventType.KubernetesClusterUpdated,
|
|
1348
|
+
displayColor: Blue500,
|
|
1349
|
+
feedInfoInMarkdown: `🧹 The previous in-cluster Runner **${
|
|
1350
|
+
legacyRunner.name || "kubernetes-agent"
|
|
1351
|
+
}** was removed. The ${KUBERNETES_AI_AGENT_DISPLAY_NAME} replaced it, and it had been offline for over ${LEGACY_RUNNER_RETIREMENT_OFFLINE_DAYS} days.`,
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
return "retired";
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
logger.error(
|
|
1357
|
+
`KubernetesAiAgent: could not check the previous in-cluster Runner of cluster ${clusterId}: ${error}`,
|
|
1358
|
+
);
|
|
1359
|
+
return "failed";
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// The agent has existed long enough that a rollback is no longer likely.
|
|
1364
|
+
public static isOldEnoughToRetireLegacyRunner(
|
|
1365
|
+
agent: Pick<Model, "createdAt">,
|
|
1366
|
+
now: Date,
|
|
1367
|
+
): boolean {
|
|
1368
|
+
const createdAt: Date | null = toDate(agent.createdAt);
|
|
1369
|
+
|
|
1370
|
+
return Boolean(
|
|
1371
|
+
createdAt &&
|
|
1372
|
+
now.getTime() - createdAt.getTime() >=
|
|
1373
|
+
LEGACY_RUNNER_RETIREMENT_AGENT_AGE_HOURS * 60 * 60 * 1000,
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// The Runner was last heard from long enough ago (never: not eligible).
|
|
1378
|
+
public static hasBeenOfflineLongEnoughToRetire(
|
|
1379
|
+
runner: Pick<Runner, "lastAlive">,
|
|
1380
|
+
now: Date,
|
|
1381
|
+
): boolean {
|
|
1382
|
+
const lastAlive: Date | null = toDate(runner.lastAlive);
|
|
1383
|
+
|
|
1384
|
+
return Boolean(
|
|
1385
|
+
lastAlive &&
|
|
1386
|
+
now.getTime() - lastAlive.getTime() >=
|
|
1387
|
+
LEGACY_RUNNER_RETIREMENT_OFFLINE_DAYS * 24 * 60 * 60 * 1000,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/*
|
|
1392
|
+
* At most one check an hour per agent in this process. Stamped before the
|
|
1393
|
+
* check runs, so heartbeats that overlap it do not start a second one.
|
|
1394
|
+
*/
|
|
1395
|
+
private isLegacyRunnerRetirementCheckDue(
|
|
1396
|
+
agentId: string,
|
|
1397
|
+
now: Date,
|
|
1398
|
+
): boolean {
|
|
1399
|
+
const lastCheckedAt: number | undefined =
|
|
1400
|
+
this.legacyRunnerRetirementCheckedAt.get(agentId);
|
|
1401
|
+
|
|
1402
|
+
if (
|
|
1403
|
+
lastCheckedAt !== undefined &&
|
|
1404
|
+
now.getTime() - lastCheckedAt < LEGACY_RUNNER_RETIREMENT_CHECK_INTERVAL_MS
|
|
1405
|
+
) {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Bounded: a long-lived process forgets old agents rather than growing.
|
|
1410
|
+
if (
|
|
1411
|
+
this.legacyRunnerRetirementCheckedAt.size >=
|
|
1412
|
+
MAX_LEGACY_RUNNER_RETIREMENT_CHECKS_REMEMBERED
|
|
1413
|
+
) {
|
|
1414
|
+
this.legacyRunnerRetirementCheckedAt.clear();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
this.legacyRunnerRetirementCheckedAt.set(agentId, now.getTime());
|
|
1418
|
+
|
|
1419
|
+
return true;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/*
|
|
1423
|
+
* The agent signing off on a clean shutdown. lastAliveAt is left alone
|
|
1424
|
+
* (it is when the agent was last heard from); the disconnected status is
|
|
1425
|
+
* what lets its replacement pod register at once instead of waiting for
|
|
1426
|
+
* the alive window to lapse, and what makes the AI agent page say
|
|
1427
|
+
* "Offline" straight away. Its next heartbeat or registration flips it
|
|
1428
|
+
* back.
|
|
1429
|
+
*/
|
|
1430
|
+
@CaptureSpan()
|
|
1431
|
+
public async markDisconnected(data: {
|
|
1432
|
+
kubernetesAiAgentId: ObjectID;
|
|
1433
|
+
}): Promise<void> {
|
|
1434
|
+
if (!data.kubernetesAiAgentId) {
|
|
1435
|
+
throw new BadDataException("kubernetesAiAgentId is required");
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
1439
|
+
id: data.kubernetesAiAgentId,
|
|
1440
|
+
data: { connectionStatus: "disconnected" },
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/*
|
|
1445
|
+
* An admin's "Reset agent" on the cluster's AI agent page: the agent's key
|
|
1446
|
+
* stops working at once (keyHash null matches nothing) and the row reads
|
|
1447
|
+
* as disconnected. The real pod fails authentication on its next request,
|
|
1448
|
+
* registers again and — the row being reset — is admitted, so it comes
|
|
1449
|
+
* back by itself within a few minutes. Whoever else held the key (a pod
|
|
1450
|
+
* that registered with a leaked ingestion key) is locked out the same
|
|
1451
|
+
* way; rotating that ingestion key keeps it out.
|
|
1452
|
+
*/
|
|
1453
|
+
@CaptureSpan()
|
|
1454
|
+
public async resetAgent(data: {
|
|
1455
|
+
projectId: ObjectID;
|
|
1456
|
+
kubernetesClusterId: ObjectID;
|
|
1457
|
+
userId?: ObjectID | undefined;
|
|
1458
|
+
}): Promise<void> {
|
|
1459
|
+
const agent: Model | null = await this.findForCluster({
|
|
1460
|
+
projectId: data.projectId,
|
|
1461
|
+
kubernetesClusterId: data.kubernetesClusterId,
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
if (!agent || !agent.id) {
|
|
1465
|
+
throw new BadDataException(
|
|
1466
|
+
"This cluster has no Kubernetes AI agent to reset.",
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
await this.updateOneById({
|
|
1471
|
+
id: agent.id,
|
|
1472
|
+
data: {
|
|
1473
|
+
keyHash: null,
|
|
1474
|
+
connectionStatus: "disconnected",
|
|
1475
|
+
} as never,
|
|
1476
|
+
props: { isRoot: true },
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
logger.info(
|
|
1480
|
+
`KubernetesAiAgent: the Kubernetes AI agent of cluster ${data.kubernetesClusterId.toString()} in project ${data.projectId.toString()} was reset${
|
|
1481
|
+
data.userId ? ` by user ${data.userId.toString()}` : ""
|
|
1482
|
+
}.`,
|
|
1483
|
+
);
|
|
1484
|
+
|
|
1485
|
+
const resetBy: string = data.userId
|
|
1486
|
+
? await this.getUserMarkdownForFeed({
|
|
1487
|
+
userId: data.userId,
|
|
1488
|
+
projectId: data.projectId,
|
|
1489
|
+
})
|
|
1490
|
+
: "";
|
|
1491
|
+
|
|
1492
|
+
await KubernetesClusterFeedService.createKubernetesClusterFeedItem({
|
|
1493
|
+
kubernetesClusterId: data.kubernetesClusterId,
|
|
1494
|
+
projectId: data.projectId,
|
|
1495
|
+
kubernetesClusterFeedEventType:
|
|
1496
|
+
KubernetesClusterFeedEventType.KubernetesClusterUpdated,
|
|
1497
|
+
displayColor: Blue500,
|
|
1498
|
+
feedInfoInMarkdown: `🔄 The ${KUBERNETES_AI_AGENT_DISPLAY_NAME} was reset${
|
|
1499
|
+
resetBy ? ` by **${resetBy}**` : ""
|
|
1500
|
+
}. It reconnects on its own within a few minutes.`,
|
|
1501
|
+
moreInformationInMarkdown: `**Agent**: ${KUBERNETES_AI_AGENT_DISPLAY_NAME} (${agent.id.toString()})`,
|
|
1502
|
+
...(data.userId ? { userId: data.userId } : {}),
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/*
|
|
1507
|
+
* The user's name as a feed link, or "" when it cannot be had (no such
|
|
1508
|
+
* user, or the lookup failed). Only ever wording: the action it describes
|
|
1509
|
+
* is already done, so a failed lookup must not fail it.
|
|
1510
|
+
*/
|
|
1511
|
+
private async getUserMarkdownForFeed(data: {
|
|
1512
|
+
userId: ObjectID;
|
|
1513
|
+
projectId: ObjectID;
|
|
1514
|
+
}): Promise<string> {
|
|
1515
|
+
try {
|
|
1516
|
+
return await UserService.getUserMarkdownString(data);
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
logger.error(
|
|
1519
|
+
`KubernetesAiAgent: could not look up user ${data.userId.toString()} for a feed item: ${error}`,
|
|
1520
|
+
);
|
|
1521
|
+
return "";
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/*
|
|
1526
|
+
* ------------------------------------------------------------------
|
|
1527
|
+
* Feed
|
|
1528
|
+
* ------------------------------------------------------------------
|
|
1529
|
+
*/
|
|
1530
|
+
|
|
1531
|
+
// What the agent may change, in the feed's words.
|
|
1532
|
+
public static describeWriteAccess(posture: KubernetesAgentPosture): string {
|
|
1533
|
+
if (posture.allowWrites !== true) {
|
|
1534
|
+
return "read-only";
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
return `can make changes ${Service.describeWriteScope(posture)}`;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// Where a writing agent may change things: its namespaces, or everywhere.
|
|
1541
|
+
public static describeWriteScope(posture: KubernetesAgentPosture): string {
|
|
1542
|
+
const namespaces: Array<string> = posture.writeNamespaces || [];
|
|
1543
|
+
|
|
1544
|
+
return namespaces.length > 0
|
|
1545
|
+
? `in ${namespaces.join(", ")}`
|
|
1546
|
+
: "anywhere in the cluster";
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// The first successful registration of a cluster's agent. Best-effort.
|
|
1550
|
+
private async writeConnectedFeedItem(data: {
|
|
1551
|
+
cluster: KubernetesCluster;
|
|
1552
|
+
agentId: ObjectID;
|
|
1553
|
+
posture: KubernetesAgentPosture;
|
|
1554
|
+
agentVersion?: string | undefined;
|
|
1555
|
+
defaultsApplied: KubernetesAiAgentDefaultsApplied;
|
|
1556
|
+
}): Promise<void> {
|
|
1557
|
+
const isInvestigationOn: boolean =
|
|
1558
|
+
data.cluster.isAiInvestigationEnabled === true ||
|
|
1559
|
+
data.defaultsApplied.turnedOnInvestigation;
|
|
1560
|
+
|
|
1561
|
+
const sentences: Array<string> = [
|
|
1562
|
+
`🤖 The ${KUBERNETES_AI_AGENT_DISPLAY_NAME} connected (${Service.describeWriteAccess(
|
|
1563
|
+
data.posture,
|
|
1564
|
+
)}).`,
|
|
1565
|
+
isInvestigationOn
|
|
1566
|
+
? "AI can now use it to investigate this cluster."
|
|
1567
|
+
: "Investigating with kubectl is off for this cluster; turn it on on the cluster's AI agent page.",
|
|
1568
|
+
];
|
|
1569
|
+
|
|
1570
|
+
if (data.defaultsApplied.remediationMode) {
|
|
1571
|
+
sentences.push(
|
|
1572
|
+
'Fixes are set to "Ask for approval", because the chart allows changes.',
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
await KubernetesClusterFeedService.createKubernetesClusterFeedItem({
|
|
1577
|
+
kubernetesClusterId: data.cluster.id!,
|
|
1578
|
+
projectId: data.cluster.projectId!,
|
|
1579
|
+
kubernetesClusterFeedEventType:
|
|
1580
|
+
KubernetesClusterFeedEventType.KubernetesClusterUpdated,
|
|
1581
|
+
displayColor: Green500,
|
|
1582
|
+
feedInfoInMarkdown: sentences.join(" "),
|
|
1583
|
+
moreInformationInMarkdown: [
|
|
1584
|
+
`**Agent**: ${KUBERNETES_AI_AGENT_DISPLAY_NAME} (${data.agentId.toString()})`,
|
|
1585
|
+
`**Cluster identifier**: \`${data.posture.clusterIdentifier || ""}\``,
|
|
1586
|
+
`**Agent version**: ${data.agentVersion || "unknown"}`,
|
|
1587
|
+
`**kubectl**: ${data.posture.kubectlVersion || "not detected"}`,
|
|
1588
|
+
`**Agent chart**: ${data.posture.agentChartVersion || "unknown"}`,
|
|
1589
|
+
].join("\n\n"),
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/*
|
|
1594
|
+
* The first-connection defaults changed a setting after the first
|
|
1595
|
+
* connection (the agent re-registered with write access, or its write
|
|
1596
|
+
* access appeared on a heartbeat). A setting never changes silently.
|
|
1597
|
+
*/
|
|
1598
|
+
private async writeDefaultsFeedItem(data: {
|
|
1599
|
+
cluster: KubernetesCluster;
|
|
1600
|
+
posture: KubernetesAgentPosture;
|
|
1601
|
+
defaultsApplied: KubernetesAiAgentDefaultsApplied;
|
|
1602
|
+
}): Promise<void> {
|
|
1603
|
+
const sentences: Array<string> = [];
|
|
1604
|
+
|
|
1605
|
+
if (data.defaultsApplied.turnedOnInvestigation) {
|
|
1606
|
+
sentences.push(
|
|
1607
|
+
`AI can now use the ${KUBERNETES_AI_AGENT_DISPLAY_NAME} to investigate this cluster.`,
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (data.defaultsApplied.remediationMode) {
|
|
1612
|
+
sentences.push(
|
|
1613
|
+
`The ${KUBERNETES_AI_AGENT_DISPLAY_NAME} can now make changes ${Service.describeWriteScope(
|
|
1614
|
+
data.posture,
|
|
1615
|
+
)}, so fixes are set to "Ask for approval".`,
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
sentences.push(
|
|
1620
|
+
"Nobody had chosen AI settings for this cluster yet; change them on the cluster's AI agent page.",
|
|
1621
|
+
);
|
|
1622
|
+
|
|
1623
|
+
await KubernetesClusterFeedService.createKubernetesClusterFeedItem({
|
|
1624
|
+
kubernetesClusterId: data.cluster.id!,
|
|
1625
|
+
projectId: data.cluster.projectId!,
|
|
1626
|
+
kubernetesClusterFeedEventType:
|
|
1627
|
+
KubernetesClusterFeedEventType.KubernetesClusterUpdated,
|
|
1628
|
+
displayColor: Blue500,
|
|
1629
|
+
feedInfoInMarkdown: `🤖 ${sentences.join(" ")}`,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
export default new Service();
|