@oneuptime/common 14.0.8 → 14.0.10
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/AutoRemediationSuggestion.ts +60 -0
- package/Models/DatabaseModels/CephCluster.ts +213 -0
- package/Models/DatabaseModels/DatabaseServer.ts +213 -0
- package/Models/DatabaseModels/DockerHost.ts +213 -0
- package/Models/DatabaseModels/DockerSwarmCluster.ts +213 -0
- package/Models/DatabaseModels/GlobalConfig.ts +1 -1
- package/Models/DatabaseModels/Host.ts +213 -0
- package/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
- package/Models/DatabaseModels/PodmanHost.ts +213 -0
- package/Models/DatabaseModels/Project.ts +55 -5
- package/Models/DatabaseModels/ProxmoxCluster.ts +213 -0
- package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
- package/Models/DatabaseModels/ResourceAiAgent.ts +419 -0
- package/Models/DatabaseModels/RunnerJob.ts +219 -0
- package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
- package/Models/DatabaseModels/VMwareVCenter.ts +213 -0
- 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 +598 -25
- package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
- package/Server/API/MicrosoftTeamsAPI.ts +93 -14
- package/Server/API/ProxmoxResourceAPI.ts +244 -14
- package/Server/API/ResourceAiAccessAPI.ts +1669 -0
- 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/1796300000000-AddResourceAiAgents.ts +423 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/Semaphore.ts +22 -0
- package/Server/Middleware/TelemetryIngest.ts +51 -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/AnalyticsDatabaseService.ts +45 -1
- package/Server/Services/AutoRemediationRuleEngineService.ts +940 -112
- package/Server/Services/AutoRemediationRuleService.ts +1 -1
- package/Server/Services/CephClusterService.ts +109 -1
- package/Server/Services/DatabaseServerService.ts +116 -1
- package/Server/Services/DockerHostService.ts +109 -1
- package/Server/Services/DockerSwarmClusterService.ts +113 -1
- package/Server/Services/HostService.ts +126 -4
- 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/MetricRecordingRuleService.ts +47 -0
- package/Server/Services/PodmanHostService.ts +109 -1
- package/Server/Services/ProjectService.ts +32 -0
- package/Server/Services/ProxmoxClusterService.ts +110 -1
- package/Server/Services/ProxmoxResourceService.ts +322 -5
- package/Server/Services/ResourceAiAccessService.ts +1439 -0
- package/Server/Services/ResourceAiAgentJobService.ts +202 -0
- package/Server/Services/ResourceAiAgentService.ts +2432 -0
- package/Server/Services/RunnerJobService.ts +852 -108
- 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/Services/TelemetryUsageBillingService.ts +28 -0
- package/Server/Services/TraceRecordingRuleService.ts +33 -0
- package/Server/Services/VMwareVCenterService.ts +110 -1
- 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 +1475 -51
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +1000 -43
- package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +25 -1
- package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +577 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAccessContext.ts +271 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.ts +45 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.ts +964 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.ts +333 -0
- package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +873 -0
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +120 -7
- package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +72 -5
- package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +72 -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 +402 -14
- package/Server/Utils/AutoRemediation/RemediationVerifier.ts +35 -2
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +9 -1
- package/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.ts +920 -0
- package/Server/Utils/SessionReplay/SessionReplayUsage.ts +84 -6
- 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/Actions/Alert.ts +35 -15
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +33 -15
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +21 -1
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +298 -224
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +35 -15
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +389 -175
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +1073 -213
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.ts +163 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.ts +396 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.ts +418 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.ts +152 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.ts +251 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.ts +214 -0
- 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 +184 -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/DatabaseDocumentationMarkdown.test.ts +18 -6
- 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/InvestigationInfrastructureTools.test.tsx +506 -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/RemediationSuggestionCardDescription.test.tsx +209 -0
- package/Tests/App/Dashboard/RemediationSuggestionCardResource.test.tsx +317 -0
- package/Tests/App/Dashboard/ResourceAiAccessSettingsUtil.test.ts +921 -0
- package/Tests/App/Dashboard/ResourceAiAgentInstall.test.ts +860 -0
- package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +1721 -0
- package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +1002 -0
- package/Tests/App/Dashboard/ResourceAiInsightsPage.test.tsx +931 -0
- package/Tests/App/Dashboard/ResourceAiNavigation.test.tsx +575 -0
- package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
- package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
- package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +38 -7
- 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/DatabaseServerModels.test.ts +33 -1
- package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
- package/Tests/Models/DatabaseModels/ResourceAiAccessColumns.test.ts +738 -0
- package/Tests/Models/DatabaseModels/ResourceAiAgentModel.test.ts +842 -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/AutoRemediationApproveResourceRound.test.ts +835 -0
- 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/API/ResourceAiAccessAPI.test.ts +2730 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
- package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +60 -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/AddResourceAiAgentsMigration.test.ts +659 -0
- package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
- package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +31 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +119 -4
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +196 -2
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +123 -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/AutoRemediationResourceRuleEngine.test.ts +1279 -0
- package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
- package/Tests/Server/Services/DatabaseServerService.test.ts +55 -0
- package/Tests/Server/Services/GroupTelemetryUsageExcludeNames.test.ts +299 -0
- package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +44 -0
- 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/MonitorProbeServiceIntervalScheduling.test.ts +45 -6
- 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/RecordingRuleReservedMetricName.test.ts +171 -0
- package/Tests/Server/Services/ResourceAiAccessChangeAuthorization.test.ts +256 -0
- package/Tests/Server/Services/ResourceAiAccessService.test.ts +1373 -0
- package/Tests/Server/Services/ResourceAiAgentJobService.test.ts +375 -0
- package/Tests/Server/Services/ResourceAiAgentServiceHelpers.test.ts +1114 -0
- package/Tests/Server/Services/ResourceAiAgentServiceLifecycle.test.ts +1050 -0
- package/Tests/Server/Services/ResourceAiAgentServiceRegister.test.ts +2251 -0
- package/Tests/Server/Services/ResourceAiSettingsCreate.test.ts +373 -0
- package/Tests/Server/Services/ResourceAiSettingsPermission.test.ts +1042 -0
- package/Tests/Server/Services/ResourceServiceDeleteCleansUpAi.test.ts +319 -0
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +153 -15
- package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
- package/Tests/Server/Services/RunnerJobEnqueueResourceCommand.test.ts +986 -0
- package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
- package/Tests/Server/Services/RunnerJobResourceCommandLane.test.ts +211 -0
- package/Tests/Server/Services/RunnerJobResourceCommandTimeoutAndRedaction.test.ts +352 -0
- 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/Services/TelemetryUsageBillingSloExclusion.test.ts +218 -0
- package/Tests/Server/TestingUtils/Services/FakeRunnerJobCount.ts +145 -0
- 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/InvestigationInfrastructureAccessWiring.test.ts +453 -0
- package/Tests/Server/Utils/AI/InvestigationInfrastructureReport.test.ts +721 -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/RemediationCommandTools.test.ts +94 -1
- 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/RemediationCommandToolsResource.test.ts +1530 -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/RemediationExecutionRunnerResourceMode.test.ts +1249 -0
- package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +117 -0
- package/Tests/Server/Utils/AI/RemediationResourceCopyParity.test.ts +463 -0
- package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +722 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceAccessContext.test.ts +266 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.test.ts +1220 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.test.ts +569 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +830 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorResource.test.ts +717 -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/AutoRemediation/RemediationVerifierResourceFollowUp.test.ts +338 -0
- package/Tests/Server/Utils/Monitor/Criteria/SessionReplayBudgetTemplateCriteria.test.ts +422 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.test.ts +1647 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +143 -1
- 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 +75 -3
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsAccountNotLinked.test.ts +1093 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.test.ts +1049 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.test.ts +1676 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCards.test.ts +2884 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.test.ts +2490 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmit.test.ts +2293 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmitServerTimezone.test.ts +386 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.test.ts +1477 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.test.ts +1967 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsStripHtmlTags.test.ts +92 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsSubmittedFormRemoval.test.ts +1819 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.test.ts +1033 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezoneServerZone.test.ts +610 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsBotMessageHandling.test.ts +2610 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsCreateCommandsEndToEnd.test.ts +4672 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
- package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +190 -65
- package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +10 -4
- package/Tests/Types/AutoRemediation/AiRemediationCommandPlanResourceCommand.test.ts +305 -0
- package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
- package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +277 -13
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +113 -0
- package/Tests/Types/Monitor/RumAlertTemplates.test.ts +773 -2
- package/Tests/Types/ResourceAiAgent/AiResourceType.test.ts +419 -0
- package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +517 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +147 -8
- 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/RecordingHealthDashboard.test.tsx +769 -1
- 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/AiRemediation/Resource/CephCommandPolicy.test.ts +1653 -0
- package/Tests/Utils/AiRemediation/Resource/CephOutputRedaction.test.ts +204 -0
- package/Tests/Utils/AiRemediation/Resource/DatabaseCommandPolicy.test.ts +1077 -0
- package/Tests/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.test.ts +558 -0
- package/Tests/Utils/AiRemediation/Resource/DatabaseQueryRedactor.test.ts +834 -0
- package/Tests/Utils/AiRemediation/Resource/DockerCliGrammar.test.ts +747 -0
- package/Tests/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.test.ts +1259 -0
- package/Tests/Utils/AiRemediation/Resource/DockerOutputRedaction.test.ts +386 -0
- package/Tests/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.test.ts +782 -0
- package/Tests/Utils/AiRemediation/Resource/GovcCommandPolicy.test.ts +2259 -0
- package/Tests/Utils/AiRemediation/Resource/HostCommandPolicy.test.ts +2019 -0
- package/Tests/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.test.ts +2497 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicy.test.ts +1305 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.test.ts +585 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactor.test.ts +334 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactorCopyParity.test.ts +98 -0
- package/Tests/Utils/AiRemediation/Resource/ResourcePolicyImportClosure.test.ts +391 -0
- package/Tests/Utils/AiRemediation/ResourceAiAgentPolicyCopyParity.test.ts +497 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
- package/Tests/Utils/SessionReplay/SessionReplayBudgetMetricType.test.ts +383 -0
- package/Types/AI/AIRunType.ts +9 -4
- package/Types/AI/InvestigationNotStartedReason.ts +6 -0
- package/Types/AI/ResourceAiAccessApi.ts +140 -0
- package/Types/AI/ResourceAiAccessPermissions.ts +126 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +197 -18
- 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/Monitor/Recommendation/MonitorRecommendationCatalog.ts +55 -22
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +35 -3
- package/Types/Monitor/RumAlertTemplates.ts +351 -4
- package/Types/ResourceAiAgent/AiResourceType.ts +310 -0
- package/Types/ResourceAiAgent/ResourceAiAccess.ts +574 -0
- package/Types/Rum/SessionReplayBudgetMetricType.ts +47 -0
- package/Types/Runbook/RunbookStepType.ts +29 -0
- 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 +40 -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/AI/InvestigationReport.ts +121 -15
- package/Utils/AiRemediation/CommandPolicy.ts +1 -1
- package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
- package/Utils/AiRemediation/Resource/CephCommandPolicy.ts +1936 -0
- package/Utils/AiRemediation/Resource/DatabaseCommandPolicy.ts +166 -0
- package/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.ts +1532 -0
- package/Utils/AiRemediation/Resource/DatabaseQueryRedactor.ts +1394 -0
- package/Utils/AiRemediation/Resource/DockerCliGrammar.ts +1839 -0
- package/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.ts +490 -0
- package/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.ts +687 -0
- package/Utils/AiRemediation/Resource/GovcCommandPolicy.ts +2096 -0
- package/Utils/AiRemediation/Resource/HostCommandPolicy.ts +2982 -0
- package/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.ts +1679 -0
- package/Utils/AiRemediation/Resource/ResourceCommandPolicy.ts +851 -0
- package/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.ts +593 -0
- package/Utils/AiRemediation/Resource/ResourceOutputRedactor.ts +2812 -0
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
- package/Utils/SessionReplay/SessionReplayBudgetMetricType.ts +184 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CephCluster.js +219 -0
- package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DatabaseServer.js +219 -0
- package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerHost.js +219 -0
- package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +219 -0
- package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Host.js +219 -0
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -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/PodmanHost.js +219 -0
- package/build/dist/Models/DatabaseModels/PodmanHost.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/ProxmoxCluster.js +219 -0
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.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/ResourceAiAgent.js +439 -0
- package/build/dist/Models/DatabaseModels/ResourceAiAgent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js +219 -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/VMwareVCenter.js +219 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenter.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 +374 -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/API/ResourceAiAccessAPI.js +1119 -0
- package/build/dist/Server/API/ResourceAiAccessAPI.js.map +1 -0
- 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/1796300000000-AddResourceAiAgents.js +168 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +6 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +41 -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/AnalyticsDatabaseService.js +26 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +601 -3
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.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/CephClusterService.js +104 -0
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseServerService.js +99 -0
- package/build/dist/Server/Services/DatabaseServerService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +104 -0
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +104 -0
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +114 -2
- package/build/dist/Server/Services/HostService.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/MetricRecordingRuleService.js +46 -0
- package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +104 -0
- package/build/dist/Server/Services/PodmanHostService.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/ProxmoxClusterService.js +104 -0
- package/build/dist/Server/Services/ProxmoxClusterService.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/ResourceAiAccessService.js +1028 -0
- package/build/dist/Server/Services/ResourceAiAccessService.js.map +1 -0
- package/build/dist/Server/Services/ResourceAiAgentJobService.js +173 -0
- package/build/dist/Server/Services/ResourceAiAgentJobService.js.map +1 -0
- package/build/dist/Server/Services/ResourceAiAgentService.js +1687 -0
- package/build/dist/Server/Services/ResourceAiAgentService.js.map +1 -0
- package/build/dist/Server/Services/RunnerJobService.js +522 -97
- 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/Services/TelemetryUsageBillingService.js +26 -0
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Services/TraceRecordingRuleService.js +37 -0
- package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -1
- package/build/dist/Server/Services/VMwareVCenterService.js +104 -0
- package/build/dist/Server/Services/VMwareVCenterService.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 +1056 -43
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +683 -42
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +21 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +330 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js +160 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js +33 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js +640 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js +226 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +600 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +89 -9
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +55 -4
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +55 -4
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.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 +294 -15
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +29 -1
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +9 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js +627 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +60 -5
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.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/Actions/Alert.js +19 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +19 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +17 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +183 -209
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +19 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +233 -167
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +768 -152
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js +129 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js +266 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js +258 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js +104 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js +183 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js +123 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js.map +1 -0
- package/build/dist/Types/AI/AIRunType.js +9 -4
- package/build/dist/Types/AI/AIRunType.js.map +1 -1
- package/build/dist/Types/AI/ResourceAiAccessApi.js +30 -0
- package/build/dist/Types/AI/ResourceAiAccessApi.js.map +1 -0
- package/build/dist/Types/AI/ResourceAiAccessPermissions.js +98 -0
- package/build/dist/Types/AI/ResourceAiAccessPermissions.js.map +1 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +126 -45
- 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/Monitor/Recommendation/MonitorRecommendationCatalog.js +47 -21
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +5 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/RumAlertTemplates.js +199 -3
- package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
- package/build/dist/Types/ResourceAiAgent/AiResourceType.js +242 -0
- package/build/dist/Types/ResourceAiAgent/AiResourceType.js.map +1 -0
- package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +275 -0
- package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js +45 -0
- package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +27 -0
- package/build/dist/Types/Runbook/RunbookStepType.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 +36 -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/AI/InvestigationReport.js +71 -11
- package/build/dist/Utils/AI/InvestigationReport.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/AiRemediation/Resource/CephCommandPolicy.js +1462 -0
- package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js +122 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js +1139 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js +1060 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js +1301 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js +370 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js +498 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js +1565 -0
- package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js +2206 -0
- package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js +1129 -0
- package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js +565 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js +408 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js +1910 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js.map +1 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
- package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js +160 -0
- package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js.map +1 -0
- package/package.json +1 -1
|
@@ -7,10 +7,17 @@ import {
|
|
|
7
7
|
test,
|
|
8
8
|
} from "@jest/globals";
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
AI_AGENT_CLUSTER_WIDE_NAMESPACES_FLAG,
|
|
11
|
+
AI_AGENT_EXAMPLE_WRITE_NAMESPACES,
|
|
12
|
+
AiAgentHelmCommands,
|
|
13
|
+
formatNameList,
|
|
14
|
+
getAiAgentClusterWideCommandNote,
|
|
15
|
+
getAiAgentHelmCommands,
|
|
16
|
+
getAiAgentLogsCommand,
|
|
17
|
+
getAiAgentScopedCommandNote,
|
|
18
|
+
getAiAgentWriteDisclosure,
|
|
19
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesAiAccessSetup";
|
|
20
|
+
import {
|
|
14
21
|
KubernetesAiAccessConfirmation,
|
|
15
22
|
KubernetesAiAccessOfferedFields,
|
|
16
23
|
KubernetesAiAccessSavedSettings,
|
|
@@ -18,47 +25,62 @@ import {
|
|
|
18
25
|
KubernetesAiRunnerDirectoryEntry,
|
|
19
26
|
REMEDIATION_MODES_BY_AUTONOMY,
|
|
20
27
|
REMEDIATION_MODE_LABELS,
|
|
28
|
+
REMEDIATION_MODE_SHORT_NAMES,
|
|
29
|
+
REMEDIATION_MODE_SUMMARIES,
|
|
21
30
|
SavedKubectlAllowlist,
|
|
22
31
|
buildKubernetesAiCredentialDirectory,
|
|
23
32
|
buildKubernetesAiCredentialOptions,
|
|
24
33
|
buildKubernetesAiRunnerDirectory,
|
|
25
34
|
buildKubernetesAiRunnerOptions,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
canReadKubectlJobs,
|
|
29
|
-
describeRunnerWriteAccess,
|
|
30
|
-
getAccessTestPermissionGate,
|
|
31
|
-
getAccessTestPermissionMessage,
|
|
32
|
-
getAiAccessClusterWideCommandNote,
|
|
33
|
-
getAiAccessConnectCardMode,
|
|
34
|
-
getAiAccessHelmCommands,
|
|
35
|
-
getAiAccessScopedCommandNote,
|
|
36
|
-
getAiAccessWriteDisclosure,
|
|
35
|
+
capitalizeFirst,
|
|
36
|
+
getAllowlistInEffect,
|
|
37
37
|
getEveryModeProtectionsSentence,
|
|
38
38
|
getKubectlAllowlistRemovalOnlyError,
|
|
39
|
-
getKubectlJobsPermissionTitles,
|
|
40
39
|
getKubernetesAiAccessAdminPermissionTitles,
|
|
41
40
|
getKubernetesAiAccessBindingError,
|
|
42
41
|
getKubernetesAiAccessChosenRunnerId,
|
|
43
42
|
getKubernetesAiAccessConfirmation,
|
|
44
|
-
getKubernetesAiAccessEditCapabilities,
|
|
45
43
|
getKubernetesAiAccessLooseningChanges,
|
|
46
44
|
getKubernetesAiAccessOfferedFields,
|
|
47
45
|
getKubernetesAiAccessSettingsChanges,
|
|
48
46
|
getKubernetesAiAccessSettingsInitialValues,
|
|
47
|
+
getKubernetesAiAccessSubmittedFields,
|
|
49
48
|
getKubernetesAiCredentialAssignmentError,
|
|
50
49
|
getKubernetesAiCredentialFieldDescription,
|
|
51
|
-
|
|
50
|
+
getPermissionTitles,
|
|
52
51
|
getRemediationModeFieldDescription,
|
|
52
|
+
isAllowlistFieldShown,
|
|
53
53
|
isRemediationModeOpenToEveryEditor,
|
|
54
|
-
isThisClustersAgentRunner,
|
|
55
54
|
normalizeSavedKubectlAllowlist,
|
|
56
55
|
parseKubectlAllowlistText,
|
|
56
|
+
readDropdownId,
|
|
57
57
|
readKubernetesAiAccessSavedSettings,
|
|
58
|
+
readRemediationMode,
|
|
58
59
|
readStoredKubectlAllowlist,
|
|
59
60
|
validateKubectlAllowlistText,
|
|
60
|
-
} from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/
|
|
61
|
-
import {
|
|
61
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesAiAccessSettings";
|
|
62
|
+
import {
|
|
63
|
+
PROJECT_AI_SETTINGS_PERMISSIONS,
|
|
64
|
+
canChangeProjectAiSettings,
|
|
65
|
+
canConfigureUnattendedKubernetesAiAccess,
|
|
66
|
+
canPickKubernetesCredential,
|
|
67
|
+
canPickKubernetesRunner,
|
|
68
|
+
canReadKubectlJobs,
|
|
69
|
+
canResetKubernetesAiAgent,
|
|
70
|
+
getAccessTestPermissionGate,
|
|
71
|
+
getAccessTestPermissionMessage,
|
|
72
|
+
getAccessTestPermissionRequirement,
|
|
73
|
+
getKubectlJobsPermissionTitles,
|
|
74
|
+
getKubernetesAiAccessEditCapabilities,
|
|
75
|
+
getKubernetesCredentialPermissionTitles,
|
|
76
|
+
getKubernetesRunnerPermissionTitles,
|
|
77
|
+
holdsKubernetesAiAccessPermission,
|
|
78
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesAiAccessPermissions";
|
|
79
|
+
import {
|
|
80
|
+
KUBERNETES_AGENT_HELM_NAMESPACE,
|
|
81
|
+
KUBERNETES_AGENT_HELM_RELEASE,
|
|
82
|
+
getKubernetesInstallationMarkdown,
|
|
83
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/DocumentationMarkdown";
|
|
62
84
|
import { isKubernetesAgentRunnerRow } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesAgentRunner";
|
|
63
85
|
import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
|
|
64
86
|
import RunbookCredential from "../../../Models/DatabaseModels/RunbookCredential";
|
|
@@ -66,18 +88,21 @@ import Runner from "../../../Models/DatabaseModels/Runner";
|
|
|
66
88
|
import { AiRemediationCommandPolicyVerdict } from "../../../Types/AutoRemediation/AiRemediationCommandPlan";
|
|
67
89
|
import { JSONObject } from "../../../Types/JSON";
|
|
68
90
|
import {
|
|
69
|
-
KubernetesAiAccessGap,
|
|
70
91
|
KubernetesAiRemediationMode,
|
|
71
|
-
KubernetesClusterAiAccessStatus,
|
|
72
92
|
PROTECTED_KUBERNETES_NAMESPACES,
|
|
73
93
|
} from "../../../Types/Kubernetes/KubernetesClusterAiAccess";
|
|
74
|
-
import {
|
|
94
|
+
import {
|
|
95
|
+
KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS,
|
|
96
|
+
KUBERNETES_AI_ACCESS_CREDENTIAL_PERMISSIONS,
|
|
97
|
+
} from "../../../Types/Kubernetes/KubernetesClusterAiAccessPermissions";
|
|
75
98
|
import ObjectID from "../../../Types/ObjectID";
|
|
76
99
|
import Permission from "../../../Types/Permission";
|
|
77
100
|
import RunbookCredentialType from "../../../Types/Runbook/RunbookCredentialType";
|
|
78
101
|
import { DropdownOption } from "../../../UI/Components/Dropdown/Dropdown";
|
|
79
102
|
import FormValues from "../../../UI/Components/Forms/Types/FormValues";
|
|
80
|
-
import {
|
|
103
|
+
import PermissionGate, {
|
|
104
|
+
PermissionGateResult,
|
|
105
|
+
} from "../../../UI/Utils/PermissionGate";
|
|
81
106
|
import PermissionUtil from "../../../UI/Utils/Permission";
|
|
82
107
|
import User from "../../../UI/Utils/User";
|
|
83
108
|
import KubectlPolicy, {
|
|
@@ -88,13 +113,16 @@ import fs from "fs";
|
|
|
88
113
|
import path from "path";
|
|
89
114
|
|
|
90
115
|
/*
|
|
91
|
-
* The pure pieces behind the cluster AI page
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
116
|
+
* The pure pieces behind the cluster's AI agent page (AI → Agent): the
|
|
117
|
+
* helm commands it prints (KubernetesAiAccessSetup), who may loosen what AI
|
|
118
|
+
* does — relative to the saved settings, as the server decides it — what a
|
|
119
|
+
* Change actually sends, when a save is confirmed first, how the allowlist
|
|
120
|
+
* is read and checked (by KubectlPolicy, the matcher's own rules), which
|
|
121
|
+
* Runners and credentials an advanced binding's pickers offer
|
|
122
|
+
* (KubernetesAiAccessSettings), and the permission reads
|
|
123
|
+
* (KubernetesAiAccessPermissions). The status helpers are covered by
|
|
124
|
+
* KubernetesClusterAiAgentStatus.test.ts, the rendered page by
|
|
125
|
+
* KubernetesClusterAiPage.test.tsx, and the server-side parity checks by
|
|
98
126
|
* KubernetesClusterAiPageServerParity.test.ts.
|
|
99
127
|
*/
|
|
100
128
|
|
|
@@ -112,10 +140,9 @@ const BASE_PERMISSIONS: Array<Permission> = [
|
|
|
112
140
|
const SET_IMAGE_PATTERN: string = "kubectl set image deployment/web * -n web";
|
|
113
141
|
const PATCH_PATTERN: string = "kubectl patch deployment/web -n web -p *";
|
|
114
142
|
/*
|
|
115
|
-
* A broad but valid entry: deleting any Deployment in any namespace.
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* skipped by the matcher, so it is never broad and never confirmed.
|
|
143
|
+
* A broad but valid entry: deleting any Deployment in any namespace. A
|
|
144
|
+
* wildcard verb ("kubectl * * * -n *") is invalid since KubectlPolicy made
|
|
145
|
+
* it so: refused where it is typed and skipped by the matcher.
|
|
119
146
|
*/
|
|
120
147
|
const BROAD_PATTERN: string = "kubectl delete deployment * -n *";
|
|
121
148
|
// Wildcard verbs (and a lone "*"): invalid, refused by the form.
|
|
@@ -129,22 +156,9 @@ const VERB_WILDCARD_PATTERNS: Array<string> = [
|
|
|
129
156
|
// A line continuation left dangling at the end of a command.
|
|
130
157
|
const TRAILING_CONTINUATION_REGEX: RegExp = /\\\s*$/;
|
|
131
158
|
const CHART_VERSION_FLAG_REGEX: RegExp = /--version/;
|
|
132
|
-
const OLD_CHART_VERSION_REGEX: RegExp = /0\.7\.0/;
|
|
133
159
|
const MATCHES_NEARLY_EVERYTHING_REGEX: RegExp = /nearly/;
|
|
134
160
|
const ANY_IMAGE_ANY_SERVICE_ACCOUNT_REGEX: RegExp =
|
|
135
|
-
/any image as any ServiceAccount/;
|
|
136
|
-
|
|
137
|
-
/*
|
|
138
|
-
* The reset of aiAccess.remediation.namespaces that works under
|
|
139
|
-
* `helm upgrade --reuse-values`, as the page prints it. `--set ...=null`
|
|
140
|
-
* does not: Helm drops a null override whose key the release's stored
|
|
141
|
-
* values hold (the stored list survives), and on a release from a chart
|
|
142
|
-
* without aiAccess the null reaches the values schema and fails the
|
|
143
|
-
* upgrade. Reproduced with the real helm binary against a stored release;
|
|
144
|
-
* helm-unittest renders from values files and cannot show it.
|
|
145
|
-
*/
|
|
146
|
-
const EMPTY_LIST_RESET_FLAG: string =
|
|
147
|
-
"--set-json 'aiAccess.remediation.namespaces=[]'";
|
|
161
|
+
/equivalent to running any image as any ServiceAccount in that namespace and reading its Secrets/;
|
|
148
162
|
// A backslash-newline line continuation, which the shell reads as a space.
|
|
149
163
|
const LINE_CONTINUATION_REGEX: RegExp = /\\\n/g;
|
|
150
164
|
// One shell word: a run of unquoted, single-quoted or double-quoted parts.
|
|
@@ -153,7 +167,30 @@ const SHELL_WORD_REGEX: RegExp = /(?:[^\s'"]+|'[^']*'|"[^"]*")+/g;
|
|
|
153
167
|
const SHELL_QUOTED_PART_REGEX: RegExp = /'([^']*)'|"([^"]*)"/g;
|
|
154
168
|
// A --set value that sets the namespace list to null.
|
|
155
169
|
const NULL_NAMESPACES_VALUE_REGEX: RegExp =
|
|
156
|
-
/^
|
|
170
|
+
/^aiAgent\.remediation\.namespaces=null$/;
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* The shared exact strings every slice prints (phase-b-slices.md): the
|
|
174
|
+
* dashboard, the server's ai_agent_not_connected gap and the docs all show
|
|
175
|
+
* these, so they are pinned character for character.
|
|
176
|
+
*/
|
|
177
|
+
const EXACT_INSTALL_COMMAND: string = `helm repo update
|
|
178
|
+
helm upgrade kubernetes-agent oneuptime/kubernetes-agent \\
|
|
179
|
+
--namespace oneuptime-agent --reuse-values \\
|
|
180
|
+
--set aiAgent.enabled=true`;
|
|
181
|
+
const EXACT_SCOPED_COMMAND: string = `helm repo update
|
|
182
|
+
helm upgrade kubernetes-agent oneuptime/kubernetes-agent \\
|
|
183
|
+
--namespace oneuptime-agent --reuse-values \\
|
|
184
|
+
--set aiAgent.enabled=true \\
|
|
185
|
+
--set aiAgent.remediation.enabled=true \\
|
|
186
|
+
--set "aiAgent.remediation.namespaces={web,api}" \\
|
|
187
|
+
--set aiAgent.remediation.nodeOperations=false`;
|
|
188
|
+
const EXACT_CLUSTER_WIDE_COMMAND: string = `helm repo update
|
|
189
|
+
helm upgrade kubernetes-agent oneuptime/kubernetes-agent \\
|
|
190
|
+
--namespace oneuptime-agent --reuse-values \\
|
|
191
|
+
--set aiAgent.enabled=true \\
|
|
192
|
+
--set aiAgent.remediation.enabled=true \\
|
|
193
|
+
--set-json 'aiAgent.remediation.namespaces=[]'`;
|
|
157
194
|
|
|
158
195
|
/*
|
|
159
196
|
* The argv of the `helm upgrade` line in a copy-paste command, the way a
|
|
@@ -232,6 +269,7 @@ function grantNothingYet(): void {
|
|
|
232
269
|
jest.spyOn(PermissionUtil, "getProjectPermissions").mockReturnValue(null);
|
|
233
270
|
}
|
|
234
271
|
|
|
272
|
+
// Saved settings of a cluster bound to an advanced (outside-the-chart) Runner.
|
|
235
273
|
function makeSaved(
|
|
236
274
|
overrides: Partial<KubernetesAiAccessSavedSettings> = {},
|
|
237
275
|
): KubernetesAiAccessSavedSettings {
|
|
@@ -240,13 +278,24 @@ function makeSaved(
|
|
|
240
278
|
aiRemediationMode: KubernetesAiRemediationMode.Automatic,
|
|
241
279
|
aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN],
|
|
242
280
|
aiAccessRunnerId: RUNNER_ID,
|
|
243
|
-
aiAccessRunnerName: "
|
|
281
|
+
aiAccessRunnerName: "bash-runner",
|
|
244
282
|
aiAccessCredentialId: null,
|
|
245
283
|
aiAccessCredentialName: null,
|
|
246
284
|
...overrides,
|
|
247
285
|
};
|
|
248
286
|
}
|
|
249
287
|
|
|
288
|
+
// Saved settings of a cluster reached through its Kubernetes AI agent.
|
|
289
|
+
function makeAgentSaved(
|
|
290
|
+
overrides: Partial<KubernetesAiAccessSavedSettings> = {},
|
|
291
|
+
): KubernetesAiAccessSavedSettings {
|
|
292
|
+
return makeSaved({
|
|
293
|
+
aiAccessRunnerId: null,
|
|
294
|
+
aiAccessRunnerName: null,
|
|
295
|
+
...overrides,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
250
299
|
const NOTHING_OFFERED: KubernetesAiAccessOfferedFields = {
|
|
251
300
|
allowlist: false,
|
|
252
301
|
allowlistRemoveOnly: false,
|
|
@@ -364,15 +413,7 @@ afterEach(() => {
|
|
|
364
413
|
jest.restoreAllMocks();
|
|
365
414
|
});
|
|
366
415
|
|
|
367
|
-
describe("helm
|
|
368
|
-
/*
|
|
369
|
-
* The finding: the page printed `helm upgrade oneuptime-agent ...
|
|
370
|
-
* --namespace oneuptime-kubernetes-agent`, while the Dashboard's own
|
|
371
|
-
* install instructions install `kubernetes-agent` into `oneuptime-agent`.
|
|
372
|
-
* helm answers a missing release with "has no deployed releases", so a
|
|
373
|
-
* cluster installed the way the product said could not connect. The pair
|
|
374
|
-
* is now one exported constant pair in DocumentationMarkdown.ts.
|
|
375
|
-
*/
|
|
416
|
+
describe("the AI agent helm commands", () => {
|
|
376
417
|
const installMarkdown: string = getKubernetesInstallationMarkdown({
|
|
377
418
|
clusterName: "prod-east",
|
|
378
419
|
oneuptimeUrl: "https://oneuptime.example.com",
|
|
@@ -380,7 +421,7 @@ describe("helm upgrades", () => {
|
|
|
380
421
|
});
|
|
381
422
|
|
|
382
423
|
function allCommands(): Array<string> {
|
|
383
|
-
const commands:
|
|
424
|
+
const commands: AiAgentHelmCommands = getAiAgentHelmCommands();
|
|
384
425
|
return Object.values(commands);
|
|
385
426
|
}
|
|
386
427
|
|
|
@@ -396,7 +437,24 @@ describe("helm upgrades", () => {
|
|
|
396
437
|
return pairs;
|
|
397
438
|
}
|
|
398
439
|
|
|
399
|
-
test("
|
|
440
|
+
test("are exactly the shared strings every slice prints", () => {
|
|
441
|
+
const commands: AiAgentHelmCommands = getAiAgentHelmCommands();
|
|
442
|
+
expect(commands.install).toBe(EXACT_INSTALL_COMMAND);
|
|
443
|
+
expect(commands.enableRemediationScoped).toBe(EXACT_SCOPED_COMMAND);
|
|
444
|
+
expect(commands.enableRemediation).toBe(EXACT_CLUSTER_WIDE_COMMAND);
|
|
445
|
+
expect(Object.keys(commands).sort()).toEqual([
|
|
446
|
+
"enableRemediation",
|
|
447
|
+
"enableRemediationScoped",
|
|
448
|
+
"install",
|
|
449
|
+
]);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
/*
|
|
453
|
+
* `helm upgrade` of a release that does not exist fails with "has no
|
|
454
|
+
* deployed releases", so the page's release and namespace must be the
|
|
455
|
+
* install instructions' — one exported pair in DocumentationMarkdown.ts.
|
|
456
|
+
*/
|
|
457
|
+
test("use the release and namespace of the Dashboard's install instructions", () => {
|
|
400
458
|
const pairs: Array<{ release: string; namespace: string }> = installPairs();
|
|
401
459
|
|
|
402
460
|
// Harness guard: the parse found the install commands to compare with.
|
|
@@ -414,104 +472,71 @@ describe("helm upgrades", () => {
|
|
|
414
472
|
`helm upgrade ${pairs[0]!.release} oneuptime/kubernetes-agent`,
|
|
415
473
|
);
|
|
416
474
|
expect(command).toContain(`--namespace ${pairs[0]!.namespace}`);
|
|
417
|
-
expect(command).not.toContain("oneuptime-kubernetes-agent");
|
|
418
475
|
expect(command).toContain("--reuse-values");
|
|
419
476
|
}
|
|
420
477
|
});
|
|
421
478
|
|
|
422
|
-
test("
|
|
423
|
-
// Every -n / --namespace / uninstall in the instructions uses the pair.
|
|
424
|
-
const namespaces: Array<string> = Array.from(
|
|
425
|
-
installMarkdown.matchAll(/(?:--namespace|-n) ([\w.-]+)/g),
|
|
426
|
-
).map((match: RegExpMatchArray): string => {
|
|
427
|
-
return match[1]!;
|
|
428
|
-
});
|
|
429
|
-
expect(namespaces.length).toBeGreaterThan(5);
|
|
430
|
-
for (const namespace of namespaces) {
|
|
431
|
-
expect(namespace).toBe(KUBERNETES_AGENT_HELM_NAMESPACE);
|
|
432
|
-
}
|
|
433
|
-
expect(installMarkdown).toContain(
|
|
434
|
-
`helm uninstall ${KUBERNETES_AGENT_HELM_RELEASE} --namespace ${KUBERNETES_AGENT_HELM_NAMESPACE}`,
|
|
435
|
-
);
|
|
436
|
-
expect(installMarkdown).toContain(
|
|
437
|
-
`deployment/${KUBERNETES_AGENT_HELM_RELEASE}`,
|
|
438
|
-
);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
/*
|
|
442
|
-
* Every command, not only the read-only one: the write-access commands
|
|
443
|
-
* are offered on their own, so an operator who runs one directly on an
|
|
444
|
-
* old chart index would otherwise hit "Additional property aiAccess is
|
|
445
|
-
* not allowed".
|
|
446
|
-
*/
|
|
447
|
-
test("every command updates the chart index first", () => {
|
|
479
|
+
test("every command updates the chart index first and turns the agent on", () => {
|
|
448
480
|
expect(allCommands()).toHaveLength(3);
|
|
449
481
|
for (const command of allCommands()) {
|
|
450
482
|
expect(command.startsWith("helm repo update\n")).toBe(true);
|
|
451
|
-
|
|
452
|
-
|
|
483
|
+
/*
|
|
484
|
+
* Always aiAgent.enabled=true, harmless when the agent is on: nobody
|
|
485
|
+
* has to decide whether theirs was installed with it turned off.
|
|
486
|
+
*/
|
|
487
|
+
expect(getFlagValues(getHelmUpgradeArgv(command), "--set")).toContain(
|
|
488
|
+
"aiAgent.enabled=true",
|
|
453
489
|
);
|
|
454
490
|
}
|
|
455
491
|
});
|
|
456
492
|
|
|
457
|
-
test("the
|
|
458
|
-
const
|
|
459
|
-
expect(
|
|
460
|
-
|
|
493
|
+
test("the install command is read-only and names no deprecated aiAccess value", () => {
|
|
494
|
+
const install: string = getAiAgentHelmCommands().install;
|
|
495
|
+
expect(install).not.toContain("remediation");
|
|
496
|
+
for (const command of allCommands()) {
|
|
497
|
+
expect(command).not.toContain("aiAccess");
|
|
498
|
+
}
|
|
461
499
|
});
|
|
462
500
|
|
|
463
|
-
test("the recommended write
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
expect(scoped).toContain("--set aiAccess.remediation.enabled=true");
|
|
467
|
-
expect(scoped).toContain(
|
|
468
|
-
`--set "aiAccess.remediation.namespaces=${AI_ACCESS_EXAMPLE_WRITE_NAMESPACES}"`,
|
|
501
|
+
test("the recommended write command binds only the listed namespaces and keeps fixes off nodes", () => {
|
|
502
|
+
const argv: Array<string> = getHelmUpgradeArgv(
|
|
503
|
+
getAiAgentHelmCommands().enableRemediationScoped,
|
|
469
504
|
);
|
|
470
|
-
expect(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
* force, so "write access across the cluster" bound the role only
|
|
478
|
-
* where the old list said. It now resets the list — with an empty JSON
|
|
479
|
-
* list, not `=null` (round four: under --reuse-values Helm dropped the
|
|
480
|
-
* null and kept a stored list, and on an install from a chart without
|
|
481
|
-
* aiAccess the null failed the values schema, so the command failed
|
|
482
|
-
* for every existing install).
|
|
483
|
-
*/
|
|
484
|
-
expect(clusterWide).toContain(EMPTY_LIST_RESET_FLAG);
|
|
485
|
-
expect(clusterWide).not.toContain("namespaces=null");
|
|
486
|
-
expect(clusterWide).not.toContain(AI_ACCESS_EXAMPLE_WRITE_NAMESPACES);
|
|
487
|
-
expect(clusterWide).not.toContain("nodeOperations");
|
|
505
|
+
expect(getFlagValues(argv, "--set")).toEqual([
|
|
506
|
+
"aiAgent.enabled=true",
|
|
507
|
+
"aiAgent.remediation.enabled=true",
|
|
508
|
+
`aiAgent.remediation.namespaces=${AI_AGENT_EXAMPLE_WRITE_NAMESPACES}`,
|
|
509
|
+
"aiAgent.remediation.nodeOperations=false",
|
|
510
|
+
]);
|
|
511
|
+
expect(getFlagValues(argv, "--set-json")).toEqual([]);
|
|
488
512
|
});
|
|
489
513
|
|
|
490
514
|
/*
|
|
491
|
-
* What helm receives
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
* value is an empty JSON list — a value the chart's schema (namespaces is
|
|
495
|
-
* an array) accepts and that replaces a stored list under --reuse-values.
|
|
515
|
+
* What helm receives: the shell hands `--set-json` one word whose value
|
|
516
|
+
* is an empty JSON list — a value the schema accepts and that replaces a
|
|
517
|
+
* stored list under --reuse-values (a `=null` override would be dropped).
|
|
496
518
|
*/
|
|
497
519
|
test("the cluster-wide command hands helm an empty JSON list for the namespaces", () => {
|
|
498
520
|
const argv: Array<string> = getHelmUpgradeArgv(
|
|
499
|
-
|
|
521
|
+
getAiAgentHelmCommands().enableRemediation,
|
|
500
522
|
);
|
|
501
523
|
expect(argv).toContain("--reuse-values");
|
|
502
524
|
|
|
503
525
|
const setJson: Array<string> = getFlagValues(argv, "--set-json");
|
|
504
|
-
expect(setJson).toEqual(["
|
|
526
|
+
expect(setJson).toEqual(["aiAgent.remediation.namespaces=[]"]);
|
|
505
527
|
const assignment: string = setJson[0]!;
|
|
506
528
|
const separator: number = assignment.indexOf("=");
|
|
507
529
|
expect(assignment.slice(0, separator)).toBe(
|
|
508
|
-
"
|
|
530
|
+
"aiAgent.remediation.namespaces",
|
|
509
531
|
);
|
|
510
532
|
expect(JSON.parse(assignment.slice(separator + 1))).toEqual([]);
|
|
533
|
+
expect(AI_AGENT_CLUSTER_WIDE_NAMESPACES_FLAG).toBe(
|
|
534
|
+
"--set-json 'aiAgent.remediation.namespaces=[]'",
|
|
535
|
+
);
|
|
511
536
|
|
|
512
537
|
// Negative control: the helper does read a null reset where one is.
|
|
513
538
|
const withNull: Array<string> = getHelmUpgradeArgv(
|
|
514
|
-
"helm repo update\nhelm upgrade r c --reuse-values \\\n --set
|
|
539
|
+
"helm repo update\nhelm upgrade r c --reuse-values \\\n --set aiAgent.remediation.namespaces=null",
|
|
515
540
|
);
|
|
516
541
|
expect(
|
|
517
542
|
getFlagValues(withNull, "--set").some((setting: string): boolean => {
|
|
@@ -520,14 +545,7 @@ describe("helm upgrades", () => {
|
|
|
520
545
|
).toBe(true);
|
|
521
546
|
});
|
|
522
547
|
|
|
523
|
-
/*
|
|
524
|
-
* No command the page prints pairs --reuse-values (which every one of
|
|
525
|
-
* them uses) with a null namespace list: that pairing is the one that
|
|
526
|
-
* keeps a stored list, or fails the upgrade on an install from a chart
|
|
527
|
-
* without aiAccess.
|
|
528
|
-
*/
|
|
529
548
|
test("no command pairs --reuse-values with a null namespace list", () => {
|
|
530
|
-
expect(allCommands()).toHaveLength(3);
|
|
531
549
|
for (const command of allCommands()) {
|
|
532
550
|
const argv: Array<string> = getHelmUpgradeArgv(command);
|
|
533
551
|
expect({
|
|
@@ -548,65 +566,9 @@ describe("helm upgrades", () => {
|
|
|
548
566
|
}
|
|
549
567
|
});
|
|
550
568
|
|
|
551
|
-
/*
|
|
552
|
-
* What the page says under the two write-access commands: every listed
|
|
553
|
-
* namespace must already exist (the chart never creates one, and a
|
|
554
|
-
* missing one fails the whole upgrade), the empty-list `--set-json`
|
|
555
|
-
* resets a stored list (and `=null` does not, under --reuse-values),
|
|
556
|
-
* turning node operations back on takes `=true` under --reuse-values,
|
|
557
|
-
* and a write outside the list is refused before it reaches the Runner.
|
|
558
|
-
*/
|
|
559
|
-
test("the scoped command's note says what the chart and the server do with the list", () => {
|
|
560
|
-
const note: string = getAiAccessScopedCommandNote();
|
|
561
|
-
expect(note).toContain(AI_ACCESS_EXAMPLE_WRITE_NAMESPACES);
|
|
562
|
-
expect(note).toContain("Every namespace you list must already exist");
|
|
563
|
-
expect(note).toContain("never creates a namespace");
|
|
564
|
-
expect(note).toContain('namespaces "<name>" not found');
|
|
565
|
-
expect(note).toContain(
|
|
566
|
-
`${EMPTY_LIST_RESET_FLAG} resets it to cluster-wide, as the next command does.`,
|
|
567
|
-
);
|
|
568
|
-
// The round-three claim, which Helm does not honour under --reuse-values.
|
|
569
|
-
expect(note).not.toContain("=null resets it to cluster-wide");
|
|
570
|
-
expect(note).toContain(
|
|
571
|
-
"--set aiAccess.remediation.namespaces=null does not reset a stored list under --reuse-values",
|
|
572
|
-
);
|
|
573
|
-
expect(note).toContain(
|
|
574
|
-
"it only works with --reset-then-reuse-values (Helm 3.14+) in place of --reuse-values",
|
|
575
|
-
);
|
|
576
|
-
expect(note).toContain(
|
|
577
|
-
"a fix anywhere else is refused when it is proposed or approved, and the Runner refuses it again",
|
|
578
|
-
);
|
|
579
|
-
expect(note).toContain("set it to true to let AI cordon");
|
|
580
|
-
/*
|
|
581
|
-
* Round four: a patch of a node joins the drain and the taint (a taint
|
|
582
|
-
* written as a node patch is the same change), and it still waits for
|
|
583
|
-
* a human when node operations are turned on.
|
|
584
|
-
*/
|
|
585
|
-
expect(note).toContain(
|
|
586
|
-
"a drain, a taint or a patch of a node still waits for a human",
|
|
587
|
-
);
|
|
588
|
-
// The old advice: dropping the line keeps a stored false under --reuse-values.
|
|
589
|
-
expect(note).not.toContain("leave that line out");
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
test("the cluster-wide command's note says the list is reset and node operations are kept", () => {
|
|
593
|
-
const note: string = getAiAccessClusterWideCommandNote();
|
|
594
|
-
expect(note.startsWith(`${EMPTY_LIST_RESET_FLAG} resets`)).toBe(true);
|
|
595
|
-
expect(note).toContain("bound cluster-wide");
|
|
596
|
-
expect(note).toContain("--set-json needs Helm 3.10 or later");
|
|
597
|
-
expect(note).toContain(
|
|
598
|
-
"--set aiAccess.remediation.namespaces=null does not reset a stored list under --reuse-values",
|
|
599
|
-
);
|
|
600
|
-
expect(note).toContain("--reset-then-reuse-values (Helm 3.14+)");
|
|
601
|
-
expect(note).toContain("Node operations keep the release's setting");
|
|
602
|
-
});
|
|
603
|
-
|
|
604
569
|
test("every command is complete, never a line to append", () => {
|
|
605
570
|
for (const command of allCommands()) {
|
|
606
|
-
// No trailing line continuation: the shell would wait for more input.
|
|
607
571
|
expect(TRAILING_CONTINUATION_REGEX.test(command)).toBe(false);
|
|
608
|
-
|
|
609
|
-
// Every continued line is followed by a real one.
|
|
610
572
|
const lines: Array<string> = command.split("\n");
|
|
611
573
|
lines.forEach((line: string, index: number) => {
|
|
612
574
|
if (line.trimEnd().endsWith("\\")) {
|
|
@@ -619,151 +581,71 @@ describe("helm upgrades", () => {
|
|
|
619
581
|
test("names no chart version (published charts carry the OneUptime version)", () => {
|
|
620
582
|
for (const command of allCommands()) {
|
|
621
583
|
expect(command).not.toMatch(CHART_VERSION_FLAG_REGEX);
|
|
622
|
-
expect(command).not.toMatch(OLD_CHART_VERSION_REGEX);
|
|
623
584
|
}
|
|
624
585
|
});
|
|
625
586
|
|
|
587
|
+
test("the scoped note says a listed namespace must exist and how to go back to cluster-wide", () => {
|
|
588
|
+
const note: string = getAiAgentScopedCommandNote();
|
|
589
|
+
expect(note).toContain(AI_AGENT_EXAMPLE_WRITE_NAMESPACES);
|
|
590
|
+
expect(note).toContain("must already exist");
|
|
591
|
+
expect(note).toContain("never creates a namespace");
|
|
592
|
+
expect(note).toContain("fails the whole upgrade");
|
|
593
|
+
expect(note).toContain(AI_AGENT_CLUSTER_WIDE_NAMESPACES_FLAG);
|
|
594
|
+
expect(note).toContain("not =null");
|
|
595
|
+
expect(note).toContain("set it to true to allow cordon");
|
|
596
|
+
// A drain, a taint and a node patch still ask, even with node operations on.
|
|
597
|
+
expect(note).toContain(
|
|
598
|
+
"a drain, a taint or a node patch still waits for a person",
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("the cluster-wide note says the list is cleared and node operations are kept", () => {
|
|
603
|
+
const note: string = getAiAgentClusterWideCommandNote();
|
|
604
|
+
expect(
|
|
605
|
+
note.startsWith(`${AI_AGENT_CLUSTER_WIDE_NAMESPACES_FLAG} clears`),
|
|
606
|
+
).toBe(true);
|
|
607
|
+
expect(note).toContain("Helm 3.10 or later");
|
|
608
|
+
expect(note).toContain("Node operations keep the release's setting");
|
|
609
|
+
});
|
|
610
|
+
|
|
626
611
|
/*
|
|
627
|
-
* The
|
|
628
|
-
*
|
|
612
|
+
* The docs suite holds this disclosure to the chart docs' rules: what
|
|
613
|
+
* write RBAC amounts to, every protected namespace, the agent's own
|
|
614
|
+
* namespace, and the value that scopes it.
|
|
629
615
|
*/
|
|
630
|
-
test("the write-access disclosure says what
|
|
631
|
-
const disclosure: string =
|
|
616
|
+
test("the write-access disclosure says what write access amounts to and where OneUptime holds the line", () => {
|
|
617
|
+
const disclosure: string = getAiAgentWriteDisclosure();
|
|
632
618
|
expect(disclosure).toMatch(ANY_IMAGE_ANY_SERVICE_ACCOUNT_REGEX);
|
|
633
|
-
expect(disclosure).toContain("
|
|
634
|
-
expect(disclosure).toContain("
|
|
635
|
-
expect(disclosure).toContain("
|
|
636
|
-
expect(disclosure).toContain("own namespace");
|
|
619
|
+
expect(disclosure).toContain("aiAgent.remediation.namespaces");
|
|
620
|
+
expect(disclosure).toContain("aiAgent.remediation.nodeOperations=false");
|
|
621
|
+
expect(disclosure).toContain("the agent's own namespace");
|
|
637
622
|
for (const namespace of PROTECTED_KUBERNETES_NAMESPACES) {
|
|
638
623
|
expect(disclosure).toContain(namespace);
|
|
639
624
|
}
|
|
640
|
-
|
|
641
|
-
expect(disclosure).toContain(
|
|
642
|
-
|
|
643
|
-
);
|
|
644
|
-
expect(disclosure).not.toContain(
|
|
645
|
-
"and the Runner refuses a write anywhere else.",
|
|
646
|
-
);
|
|
647
|
-
});
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
describe("the bound Runner's write access", () => {
|
|
651
|
-
test("says where an in-cluster Runner may write", () => {
|
|
652
|
-
expect(describeRunnerWriteAccess(undefined)).toBeNull();
|
|
653
|
-
// A Runner outside the cluster: its credential's RBAC decides.
|
|
654
|
-
expect(describeRunnerWriteAccess({ inCluster: false })).toBeNull();
|
|
655
|
-
expect(
|
|
656
|
-
describeRunnerWriteAccess({ inCluster: true, allowWrites: false }),
|
|
657
|
-
).toBe("read-only RBAC");
|
|
658
|
-
// Absent: an older Runner that did not say.
|
|
659
|
-
expect(
|
|
660
|
-
describeRunnerWriteAccess({ inCluster: true, allowWrites: true }),
|
|
661
|
-
).toBe("writes allowed");
|
|
662
|
-
expect(
|
|
663
|
-
describeRunnerWriteAccess({
|
|
664
|
-
inCluster: true,
|
|
665
|
-
allowWrites: true,
|
|
666
|
-
writeNamespaces: [],
|
|
667
|
-
}),
|
|
668
|
-
).toBe("writes allowed cluster-wide");
|
|
669
|
-
expect(
|
|
670
|
-
describeRunnerWriteAccess({
|
|
671
|
-
inCluster: true,
|
|
672
|
-
allowWrites: true,
|
|
673
|
-
writeNamespaces: ["web", "api"],
|
|
674
|
-
allowNodeOperations: false,
|
|
675
|
-
}),
|
|
676
|
-
).toBe("writes allowed in web, api, no node operations");
|
|
625
|
+
expect(disclosure).toContain("always needs a person");
|
|
626
|
+
expect(disclosure).not.toContain("aiAccess");
|
|
627
|
+
expect(disclosure).not.toContain("Runner");
|
|
677
628
|
});
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
describe("the connect card", () => {
|
|
681
|
-
const baseStatus: KubernetesClusterAiAccessStatus = {
|
|
682
|
-
clusterId: "c1",
|
|
683
|
-
clusterName: "prod-east",
|
|
684
|
-
clusterIdentifier: "prod-east",
|
|
685
|
-
runner: {
|
|
686
|
-
id: RUNNER_ID,
|
|
687
|
-
name: "kubernetes-agent/prod-east",
|
|
688
|
-
isOnline: true,
|
|
689
|
-
canRunAiCommands: true,
|
|
690
|
-
posture: { inCluster: true, allowWrites: false },
|
|
691
|
-
},
|
|
692
|
-
accessMethod: "in_cluster",
|
|
693
|
-
kubectlAllowlist: [],
|
|
694
|
-
isInvestigationEnabled: true,
|
|
695
|
-
isInvestigationReady: true,
|
|
696
|
-
remediationMode: KubernetesAiRemediationMode.RequireApproval,
|
|
697
|
-
isRemediationReady: false,
|
|
698
|
-
gaps: [],
|
|
699
|
-
evaluatedAt: "2026-09-22T10:00:00.000Z",
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
function gap(
|
|
703
|
-
code: KubernetesAiAccessGap["code"],
|
|
704
|
-
nextStep: string,
|
|
705
|
-
): KubernetesAiAccessGap {
|
|
706
|
-
return { code, title: code, description: code, nextStep, blocks: "both" };
|
|
707
|
-
}
|
|
708
629
|
|
|
709
|
-
test("
|
|
710
|
-
expect(
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
).toBe("connect");
|
|
722
|
-
expect(
|
|
723
|
-
getAiAccessConnectCardMode({
|
|
724
|
-
...baseStatus,
|
|
725
|
-
gaps: [gap("runner_missing", "Reload this page.")],
|
|
726
|
-
}),
|
|
727
|
-
).toBe("connect");
|
|
630
|
+
test("the logs command uses the namespace the agent reported, else the install namespace", () => {
|
|
631
|
+
expect(getAiAgentLogsCommand("monitoring")).toBe(
|
|
632
|
+
"kubectl logs -n monitoring -l component=ai-agent --tail=100",
|
|
633
|
+
);
|
|
634
|
+
expect(getAiAgentLogsCommand(" monitoring ")).toBe(
|
|
635
|
+
"kubectl logs -n monitoring -l component=ai-agent --tail=100",
|
|
636
|
+
);
|
|
637
|
+
for (const missing of [undefined, "", " "]) {
|
|
638
|
+
expect(getAiAgentLogsCommand(missing)).toBe(
|
|
639
|
+
`kubectl logs -n ${KUBERNETES_AGENT_HELM_NAMESPACE} -l component=ai-agent --tail=100`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
728
642
|
});
|
|
729
643
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
test("asks to select the Runner when this cluster's in-cluster Runner is installed but not selected", () => {
|
|
736
|
-
expect(
|
|
737
|
-
getAiAccessConnectCardMode({
|
|
738
|
-
...baseStatus,
|
|
739
|
-
runner: null,
|
|
740
|
-
gaps: [
|
|
741
|
-
gap(
|
|
742
|
-
"no_runner_bound",
|
|
743
|
-
'Select the kubernetes-agent Runner "kubernetes-agent/prod-east" as this cluster\'s Runner on this page (leave the credential empty). No helm change is needed.',
|
|
744
|
-
),
|
|
745
|
-
],
|
|
746
|
-
}),
|
|
747
|
-
).toBe("select_agent_runner");
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
test("shows no connect card once a Runner is bound, whatever else is missing", () => {
|
|
751
|
-
expect(getAiAccessConnectCardMode(baseStatus)).toBe("none");
|
|
752
|
-
expect(
|
|
753
|
-
getAiAccessConnectCardMode({
|
|
754
|
-
...baseStatus,
|
|
755
|
-
gaps: [
|
|
756
|
-
gap(
|
|
757
|
-
"credential_on_agent_runner",
|
|
758
|
-
"Create a Runner under Project Settings → Runners, assign the Kubernetes credential to it and select both on this page — or install the in-cluster Runner on THIS cluster with --set aiAccess.enabled=true, which needs no credential.",
|
|
759
|
-
),
|
|
760
|
-
gap(
|
|
761
|
-
"remediation_write_access_missing",
|
|
762
|
-
"Upgrade the agent with --set aiAccess.remediation.enabled=true.",
|
|
763
|
-
),
|
|
764
|
-
],
|
|
765
|
-
}),
|
|
766
|
-
).toBe("none");
|
|
644
|
+
test("formatNameList joins names the way the copy reads them", () => {
|
|
645
|
+
expect(formatNameList([], "and")).toBe("");
|
|
646
|
+
expect(formatNameList(["a"], "and")).toBe("a");
|
|
647
|
+
expect(formatNameList(["a", "b"], "or")).toBe("a or b");
|
|
648
|
+
expect(formatNameList(["a", "b", "c"], "and")).toBe("a, b and c");
|
|
767
649
|
});
|
|
768
650
|
});
|
|
769
651
|
|
|
@@ -795,12 +677,6 @@ describe("kubectl allowlist text", () => {
|
|
|
795
677
|
).toBeNull();
|
|
796
678
|
});
|
|
797
679
|
|
|
798
|
-
/*
|
|
799
|
-
* KubectlPolicy made a wildcard verb invalid: it would pre-approve every
|
|
800
|
-
* KIND of change, and the matcher skips it. The form refuses it with the
|
|
801
|
-
* policy's words; this replaces the pin that accepted "*" and
|
|
802
|
-
* "kubectl * * * -n *" as broad but well-formed.
|
|
803
|
-
*/
|
|
804
680
|
test("refuses a wildcard verb, which the matcher never reads", () => {
|
|
805
681
|
for (const pattern of VERB_WILDCARD_PATTERNS) {
|
|
806
682
|
expect({
|
|
@@ -814,11 +690,6 @@ describe("kubectl allowlist text", () => {
|
|
|
814
690
|
}
|
|
815
691
|
});
|
|
816
692
|
|
|
817
|
-
/*
|
|
818
|
-
* The matcher reads a pattern with or without the leading "kubectl" (it
|
|
819
|
-
* tokenizes patterns like commands), so the form accepts both; the old
|
|
820
|
-
* form refused "set image …" although the matcher would have used it.
|
|
821
|
-
*/
|
|
822
693
|
test("accepts a pattern without the leading kubectl, as the matcher does", () => {
|
|
823
694
|
expect(
|
|
824
695
|
validateKubectlAllowlistText("set image deployment/web * -n web"),
|
|
@@ -827,11 +698,6 @@ describe("kubectl allowlist text", () => {
|
|
|
827
698
|
|
|
828
699
|
test("refuses a bare kubectl, which names no command", () => {
|
|
829
700
|
expect(validateKubectlAllowlistText("kubectl")).toMatch(/^Pattern 1: /);
|
|
830
|
-
// The old message taught the whole-string glob reading.
|
|
831
|
-
expect(validateKubectlAllowlistText("kubectl")).not.toMatch(
|
|
832
|
-
/matched against the whole command/,
|
|
833
|
-
);
|
|
834
|
-
// Negative control: a real command right next to it passes.
|
|
835
701
|
expect(validateKubectlAllowlistText("kubectl get pods")).toBeNull();
|
|
836
702
|
expect(
|
|
837
703
|
validateKubectlAllowlistText(`${SET_IMAGE_PATTERN}\nkubectl`),
|
|
@@ -864,10 +730,6 @@ describe("kubectl allowlist text", () => {
|
|
|
864
730
|
expect(validateKubectlAllowlistText(tooLong)).toMatch(/at most 500/);
|
|
865
731
|
});
|
|
866
732
|
|
|
867
|
-
/*
|
|
868
|
-
* One definition of validity: the form refuses a line exactly when
|
|
869
|
-
* KubectlPolicy says the matcher cannot use it.
|
|
870
|
-
*/
|
|
871
733
|
test("refuses a line exactly when KubectlPolicy.describeAllowlistPatternProblem does", () => {
|
|
872
734
|
for (const pattern of [
|
|
873
735
|
"kubectl",
|
|
@@ -906,7 +768,6 @@ describe("the stored allowlist", () => {
|
|
|
906
768
|
value: JSON.stringify([SET_IMAGE_PATTERN]),
|
|
907
769
|
expected: { patterns: [SET_IMAGE_PATTERN], isClean: true },
|
|
908
770
|
},
|
|
909
|
-
// Unusable: the server uses these as an empty allowlist.
|
|
910
771
|
{
|
|
911
772
|
value: { patterns: [SET_IMAGE_PATTERN] },
|
|
912
773
|
expected: { patterns: [], isClean: false },
|
|
@@ -916,11 +777,7 @@ describe("the stored allowlist", () => {
|
|
|
916
777
|
value: [1, "", SET_IMAGE_PATTERN],
|
|
917
778
|
expected: { patterns: [SET_IMAGE_PATTERN], isClean: false },
|
|
918
779
|
},
|
|
919
|
-
|
|
920
|
-
* A string that is not JSON is ONE pattern to the server (its
|
|
921
|
-
* readStoredKubectlAllowlist and the status's normalizeAllowlist),
|
|
922
|
-
* so the policy uses it; it used to read here as no pattern at all.
|
|
923
|
-
*/
|
|
780
|
+
// A string that is not JSON is ONE pattern to the server.
|
|
924
781
|
{
|
|
925
782
|
value: "kubectl *",
|
|
926
783
|
expected: { patterns: ["kubectl *"], isClean: false },
|
|
@@ -943,6 +800,14 @@ describe("the stored allowlist", () => {
|
|
|
943
800
|
.patterns,
|
|
944
801
|
).toEqual(["kubectl scale deployment/web"]);
|
|
945
802
|
});
|
|
803
|
+
|
|
804
|
+
test("the allowlist in effect is the status's non-blank strings only", () => {
|
|
805
|
+
expect(getAllowlistInEffect([SET_IMAGE_PATTERN, "", " ", 3])).toEqual([
|
|
806
|
+
SET_IMAGE_PATTERN,
|
|
807
|
+
]);
|
|
808
|
+
expect(getAllowlistInEffect(undefined)).toEqual([]);
|
|
809
|
+
expect(getAllowlistInEffect("kubectl *")).toEqual([]);
|
|
810
|
+
});
|
|
946
811
|
});
|
|
947
812
|
|
|
948
813
|
describe("reading the saved settings", () => {
|
|
@@ -954,12 +819,12 @@ describe("reading the saved settings", () => {
|
|
|
954
819
|
aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
|
|
955
820
|
aiAccessRunner: Object.assign(new Runner(), {
|
|
956
821
|
_id: RUNNER_ID,
|
|
957
|
-
name: "
|
|
822
|
+
name: "bash-runner",
|
|
958
823
|
}),
|
|
959
824
|
}),
|
|
960
825
|
);
|
|
961
826
|
expect(fromRelation.aiAccessRunnerId).toBe(RUNNER_ID);
|
|
962
|
-
expect(fromRelation.aiAccessRunnerName).toBe("
|
|
827
|
+
expect(fromRelation.aiAccessRunnerName).toBe("bash-runner");
|
|
963
828
|
expect(fromRelation.aiAccessCredentialId).toBeNull();
|
|
964
829
|
|
|
965
830
|
const fromColumn: KubernetesAiAccessSavedSettings =
|
|
@@ -971,13 +836,51 @@ describe("reading the saved settings", () => {
|
|
|
971
836
|
);
|
|
972
837
|
expect(fromColumn.aiAccessRunnerId).toBe(RUNNER_ID);
|
|
973
838
|
expect(fromColumn.aiAccessCredentialId).toBe(CREDENTIAL_ID);
|
|
974
|
-
// Missing switches read as the column defaults.
|
|
975
|
-
expect(fromColumn.isAiInvestigationEnabled).toBe(false);
|
|
976
839
|
expect(fromColumn.aiRemediationMode).toBe(
|
|
977
840
|
KubernetesAiRemediationMode.Disabled,
|
|
978
841
|
);
|
|
979
842
|
});
|
|
980
843
|
|
|
844
|
+
/*
|
|
845
|
+
* Regression: the column now defaults to true (AI on out of the box).
|
|
846
|
+
* The page read a missing value as false, so an untouched form showed
|
|
847
|
+
* investigation off and a save that changed nothing else turned it on
|
|
848
|
+
* "again" — or, worse, the page told the operator it was off.
|
|
849
|
+
*/
|
|
850
|
+
test("reads a missing investigation switch as on, the column's default", () => {
|
|
851
|
+
expect(
|
|
852
|
+
readKubernetesAiAccessSavedSettings(new KubernetesCluster())
|
|
853
|
+
.isAiInvestigationEnabled,
|
|
854
|
+
).toBe(true);
|
|
855
|
+
expect(
|
|
856
|
+
readKubernetesAiAccessSavedSettings(
|
|
857
|
+
Object.assign(new KubernetesCluster(), {
|
|
858
|
+
isAiInvestigationEnabled: null,
|
|
859
|
+
}),
|
|
860
|
+
).isAiInvestigationEnabled,
|
|
861
|
+
).toBe(true);
|
|
862
|
+
// Negative control: an explicit false stays false.
|
|
863
|
+
expect(
|
|
864
|
+
readKubernetesAiAccessSavedSettings(
|
|
865
|
+
Object.assign(new KubernetesCluster(), {
|
|
866
|
+
isAiInvestigationEnabled: false,
|
|
867
|
+
}),
|
|
868
|
+
).isAiInvestigationEnabled,
|
|
869
|
+
).toBe(false);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test("an unknown stored mode reads as Off, the server's reading", () => {
|
|
873
|
+
expect(readRemediationMode("automatic")).toBe(
|
|
874
|
+
KubernetesAiRemediationMode.Disabled,
|
|
875
|
+
);
|
|
876
|
+
expect(readRemediationMode(undefined)).toBe(
|
|
877
|
+
KubernetesAiRemediationMode.Disabled,
|
|
878
|
+
);
|
|
879
|
+
for (const mode of Object.values(KubernetesAiRemediationMode)) {
|
|
880
|
+
expect(readRemediationMode(mode)).toBe(mode);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
981
884
|
test("seeds the form with the allowlist as lines and both clear switches off", () => {
|
|
982
885
|
const initial: SettingsFormValues =
|
|
983
886
|
getKubernetesAiAccessSettingsInitialValues(
|
|
@@ -990,48 +893,127 @@ describe("reading the saved settings", () => {
|
|
|
990
893
|
);
|
|
991
894
|
expect(initial.clearAiAccessRunner).toBe(false);
|
|
992
895
|
expect(initial.clearAiAccessCredential).toBe(false);
|
|
896
|
+
expect(initial.aiAccessRunnerId).toBe(RUNNER_ID);
|
|
897
|
+
expect(initial.aiAccessCredentialId).toBe("");
|
|
993
898
|
});
|
|
994
899
|
});
|
|
995
900
|
|
|
996
|
-
describe("what the
|
|
997
|
-
test("an admin with both pickers gets the pickers and a free allowlist", () => {
|
|
901
|
+
describe("what the Change modal offers", () => {
|
|
902
|
+
test("an admin on an advanced binding with both pickers gets the pickers and a free allowlist", () => {
|
|
998
903
|
expect(
|
|
999
904
|
getKubernetesAiAccessOfferedFields({
|
|
1000
905
|
saved: makeSaved({ aiKubectlCommandAllowlist: [] }),
|
|
1001
906
|
canConfigureUnattended: true,
|
|
1002
907
|
isRunnerPickerAvailable: true,
|
|
1003
908
|
isCredentialPickerAvailable: true,
|
|
909
|
+
isAdvancedBinding: true,
|
|
910
|
+
hasAiAgent: true,
|
|
1004
911
|
}),
|
|
1005
912
|
).toEqual(EVERYTHING_OFFERED);
|
|
1006
913
|
});
|
|
1007
914
|
|
|
1008
915
|
/*
|
|
1009
|
-
* The
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
916
|
+
* The pickers are removed for every cluster without an advanced binding:
|
|
917
|
+
* it is reached through its Kubernetes AI agent, and there is nothing to
|
|
918
|
+
* pick. Even an admin who could list Runners gets none.
|
|
1012
919
|
*/
|
|
1013
|
-
test("a cluster
|
|
920
|
+
test("a cluster without an advanced binding gets no Runner or credential field at all", () => {
|
|
921
|
+
for (const saved of [
|
|
922
|
+
makeAgentSaved(),
|
|
923
|
+
// Still bound to the previous in-cluster Runner.
|
|
924
|
+
makeSaved({ aiAccessRunnerName: "kubernetes-agent/prod-east" }),
|
|
925
|
+
]) {
|
|
926
|
+
for (const canConfigureUnattended of [true, false]) {
|
|
927
|
+
const offered: KubernetesAiAccessOfferedFields =
|
|
928
|
+
getKubernetesAiAccessOfferedFields({
|
|
929
|
+
saved,
|
|
930
|
+
canConfigureUnattended,
|
|
931
|
+
isRunnerPickerAvailable: true,
|
|
932
|
+
isCredentialPickerAvailable: true,
|
|
933
|
+
isAdvancedBinding: false,
|
|
934
|
+
hasAiAgent: true,
|
|
935
|
+
});
|
|
936
|
+
expect({
|
|
937
|
+
runner: offered.runner,
|
|
938
|
+
credential: offered.credential,
|
|
939
|
+
runnerClear: offered.runnerClear,
|
|
940
|
+
credentialClear: offered.credentialClear,
|
|
941
|
+
}).toEqual({
|
|
942
|
+
runner: false,
|
|
943
|
+
credential: false,
|
|
944
|
+
runnerClear: false,
|
|
945
|
+
credentialClear: false,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
/*
|
|
952
|
+
* Unbinding an advanced Runner used to be a tightening every editor could
|
|
953
|
+
* make. With an AI agent row it moves the cluster to the agent — which
|
|
954
|
+
* may hold broader write RBAC — so the switch is no longer offered to a
|
|
955
|
+
* cluster editor without the admin set (critique finding 2).
|
|
956
|
+
*/
|
|
957
|
+
test("a cluster editor may unbind an advanced binding only while the cluster has no AI agent", () => {
|
|
958
|
+
const saved: KubernetesAiAccessSavedSettings = makeSaved({
|
|
959
|
+
aiAccessCredentialId: CREDENTIAL_ID,
|
|
960
|
+
});
|
|
961
|
+
|
|
1014
962
|
expect(
|
|
1015
963
|
getKubernetesAiAccessOfferedFields({
|
|
1016
|
-
saved
|
|
964
|
+
saved,
|
|
1017
965
|
canConfigureUnattended: false,
|
|
1018
966
|
isRunnerPickerAvailable: false,
|
|
1019
967
|
isCredentialPickerAvailable: false,
|
|
968
|
+
isAdvancedBinding: true,
|
|
969
|
+
hasAiAgent: false,
|
|
1020
970
|
}),
|
|
1021
971
|
).toEqual(TIGHTEN_ONLY_OFFERED);
|
|
972
|
+
|
|
973
|
+
expect(
|
|
974
|
+
getKubernetesAiAccessOfferedFields({
|
|
975
|
+
saved,
|
|
976
|
+
canConfigureUnattended: false,
|
|
977
|
+
isRunnerPickerAvailable: false,
|
|
978
|
+
isCredentialPickerAvailable: false,
|
|
979
|
+
isAdvancedBinding: true,
|
|
980
|
+
hasAiAgent: true,
|
|
981
|
+
}),
|
|
982
|
+
).toEqual({
|
|
983
|
+
...TIGHTEN_ONLY_OFFERED,
|
|
984
|
+
runnerClear: false,
|
|
985
|
+
credentialClear: false,
|
|
986
|
+
});
|
|
1022
987
|
});
|
|
1023
988
|
|
|
1024
|
-
test("
|
|
989
|
+
test("an admin who may not list Runners may still unbind, with or without an agent", () => {
|
|
990
|
+
for (const hasAiAgent of [true, false]) {
|
|
991
|
+
const offered: KubernetesAiAccessOfferedFields =
|
|
992
|
+
getKubernetesAiAccessOfferedFields({
|
|
993
|
+
saved: makeSaved(),
|
|
994
|
+
canConfigureUnattended: true,
|
|
995
|
+
isRunnerPickerAvailable: false,
|
|
996
|
+
isCredentialPickerAvailable: true,
|
|
997
|
+
isAdvancedBinding: true,
|
|
998
|
+
hasAiAgent,
|
|
999
|
+
});
|
|
1000
|
+
expect(offered.runner).toBe(false);
|
|
1001
|
+
expect(offered.runnerClear).toBe(true);
|
|
1002
|
+
expect(offered.credential).toBe(true);
|
|
1003
|
+
// Nothing bound: nothing to unbind.
|
|
1004
|
+
expect(offered.credentialClear).toBe(false);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
test("nothing to remove means no allowlist field for a cluster editor", () => {
|
|
1025
1009
|
expect(
|
|
1026
1010
|
getKubernetesAiAccessOfferedFields({
|
|
1027
|
-
saved:
|
|
1028
|
-
aiKubectlCommandAllowlist: [],
|
|
1029
|
-
aiAccessRunnerId: null,
|
|
1030
|
-
aiAccessRunnerName: null,
|
|
1031
|
-
}),
|
|
1011
|
+
saved: makeAgentSaved({ aiKubectlCommandAllowlist: [] }),
|
|
1032
1012
|
canConfigureUnattended: false,
|
|
1033
1013
|
isRunnerPickerAvailable: false,
|
|
1034
1014
|
isCredentialPickerAvailable: false,
|
|
1015
|
+
isAdvancedBinding: false,
|
|
1016
|
+
hasAiAgent: true,
|
|
1035
1017
|
}),
|
|
1036
1018
|
).toEqual({
|
|
1037
1019
|
...NOTHING_OFFERED,
|
|
@@ -1041,40 +1023,74 @@ describe("what the edit modal offers", () => {
|
|
|
1041
1023
|
// An unclean stored allowlist is offered, so it can be cleared.
|
|
1042
1024
|
expect(
|
|
1043
1025
|
getKubernetesAiAccessOfferedFields({
|
|
1044
|
-
saved:
|
|
1026
|
+
saved: makeAgentSaved({ aiKubectlCommandAllowlist: { a: 1 } }),
|
|
1045
1027
|
canConfigureUnattended: false,
|
|
1046
1028
|
isRunnerPickerAvailable: false,
|
|
1047
1029
|
isCredentialPickerAvailable: false,
|
|
1030
|
+
isAdvancedBinding: false,
|
|
1031
|
+
hasAiAgent: true,
|
|
1048
1032
|
}).allowlist,
|
|
1049
1033
|
).toBe(true);
|
|
1050
1034
|
});
|
|
1051
1035
|
|
|
1052
|
-
test("
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1036
|
+
test("the allowlist field shows, and is sent, only while Automatic is chosen", () => {
|
|
1037
|
+
const saved: KubernetesAiAccessSavedSettings = makeAgentSaved();
|
|
1038
|
+
|
|
1039
|
+
expect(isAllowlistFieldShown(formValues(saved))).toBe(true);
|
|
1040
|
+
expect(
|
|
1041
|
+
isAllowlistFieldShown(
|
|
1042
|
+
formValues(saved, {
|
|
1043
|
+
aiRemediationMode: {
|
|
1044
|
+
value: KubernetesAiRemediationMode.Automatic,
|
|
1045
|
+
label: "Automatic",
|
|
1046
|
+
},
|
|
1047
|
+
}),
|
|
1048
|
+
),
|
|
1049
|
+
).toBe(true);
|
|
1050
|
+
for (const mode of [
|
|
1051
|
+
KubernetesAiRemediationMode.Disabled,
|
|
1052
|
+
KubernetesAiRemediationMode.RequireApproval,
|
|
1053
|
+
KubernetesAiRemediationMode.BypassApproval,
|
|
1054
|
+
]) {
|
|
1055
|
+
expect(
|
|
1056
|
+
isAllowlistFieldShown(formValues(saved, { aiRemediationMode: mode })),
|
|
1057
|
+
).toBe(false);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Text typed while Automatic, then the mode changed: nothing is sent for it.
|
|
1061
|
+
const values: SettingsFormValues = formValues(saved, {
|
|
1062
|
+
aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
|
|
1063
|
+
kubectlAllowlistText: "kubectl",
|
|
1064
|
+
});
|
|
1065
|
+
const submitted: KubernetesAiAccessOfferedFields =
|
|
1066
|
+
getKubernetesAiAccessSubmittedFields({
|
|
1067
|
+
offered: EVERYTHING_OFFERED,
|
|
1068
|
+
values,
|
|
1059
1069
|
});
|
|
1060
|
-
expect(
|
|
1061
|
-
expect(
|
|
1062
|
-
|
|
1063
|
-
|
|
1070
|
+
expect(submitted).toEqual({ ...EVERYTHING_OFFERED, allowlist: false });
|
|
1071
|
+
expect(
|
|
1072
|
+
getKubernetesAiAccessSettingsChanges({
|
|
1073
|
+
saved,
|
|
1074
|
+
values,
|
|
1075
|
+
offered: submitted,
|
|
1076
|
+
}),
|
|
1077
|
+
).toEqual({
|
|
1078
|
+
aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
|
|
1079
|
+
});
|
|
1064
1080
|
});
|
|
1065
1081
|
});
|
|
1066
1082
|
|
|
1067
1083
|
describe("which modes a cluster editor without the admin set may choose", () => {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
(mode
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1084
|
+
function openModes(saved: KubernetesAiRemediationMode): Array<string> {
|
|
1085
|
+
return REMEDIATION_MODES_BY_AUTONOMY.filter(
|
|
1086
|
+
(mode: KubernetesAiRemediationMode): boolean => {
|
|
1087
|
+
return isRemediationModeOpenToEveryEditor(mode, saved);
|
|
1088
|
+
},
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1076
1091
|
|
|
1077
|
-
|
|
1092
|
+
test("every mode at or below the saved one", () => {
|
|
1093
|
+
expect(openModes(KubernetesAiRemediationMode.RequireApproval)).toEqual([
|
|
1078
1094
|
KubernetesAiRemediationMode.Disabled,
|
|
1079
1095
|
KubernetesAiRemediationMode.RequireApproval,
|
|
1080
1096
|
]);
|
|
@@ -1088,15 +1104,26 @@ describe("which modes a cluster editor without the admin set may choose", () =>
|
|
|
1088
1104
|
REMEDIATION_MODES_BY_AUTONOMY,
|
|
1089
1105
|
);
|
|
1090
1106
|
});
|
|
1091
|
-
});
|
|
1092
1107
|
|
|
1093
|
-
describe("what an edit sends", () => {
|
|
1094
1108
|
/*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
* the admin permissions. Only the change may be sent.
|
|
1109
|
+
* Regression (§11 "Disabled→enabled needs admin"): Off -> Ask for
|
|
1110
|
+
* approval was open to every editor. Turning fixes on at all now needs
|
|
1111
|
+
* the admin set, so an Off cluster offers a cluster editor only Off.
|
|
1099
1112
|
*/
|
|
1113
|
+
test("turning fixes on from Off is not open to every editor", () => {
|
|
1114
|
+
expect(openModes(KubernetesAiRemediationMode.Disabled)).toEqual([
|
|
1115
|
+
KubernetesAiRemediationMode.Disabled,
|
|
1116
|
+
]);
|
|
1117
|
+
expect(
|
|
1118
|
+
isRemediationModeOpenToEveryEditor(
|
|
1119
|
+
KubernetesAiRemediationMode.RequireApproval,
|
|
1120
|
+
KubernetesAiRemediationMode.Disabled,
|
|
1121
|
+
),
|
|
1122
|
+
).toBe(false);
|
|
1123
|
+
});
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
describe("what an edit sends", () => {
|
|
1100
1127
|
test("an editor who only toggles investigation sends only that", () => {
|
|
1101
1128
|
const saved: KubernetesAiAccessSavedSettings = makeSaved();
|
|
1102
1129
|
for (const offered of [
|
|
@@ -1118,20 +1145,15 @@ describe("what an edit sends", () => {
|
|
|
1118
1145
|
const saved: KubernetesAiAccessSavedSettings = makeSaved({
|
|
1119
1146
|
aiAccessCredentialId: CREDENTIAL_ID,
|
|
1120
1147
|
});
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
saved,
|
|
1131
|
-
values: formValues(saved),
|
|
1132
|
-
offered: TIGHTEN_ONLY_OFFERED,
|
|
1133
|
-
}),
|
|
1134
|
-
).toEqual({});
|
|
1148
|
+
for (const offered of [EVERYTHING_OFFERED, TIGHTEN_ONLY_OFFERED]) {
|
|
1149
|
+
expect(
|
|
1150
|
+
getKubernetesAiAccessSettingsChanges({
|
|
1151
|
+
saved,
|
|
1152
|
+
values: formValues(saved),
|
|
1153
|
+
offered,
|
|
1154
|
+
}),
|
|
1155
|
+
).toEqual({});
|
|
1156
|
+
}
|
|
1135
1157
|
});
|
|
1136
1158
|
|
|
1137
1159
|
test("tightening the mode sends the mode", () => {
|
|
@@ -1163,6 +1185,9 @@ describe("what an edit sends", () => {
|
|
|
1163
1185
|
).toEqual({
|
|
1164
1186
|
aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
|
|
1165
1187
|
});
|
|
1188
|
+
expect(readDropdownId({ value: { value: "x" } })).toBe("x");
|
|
1189
|
+
expect(readDropdownId("")).toBeNull();
|
|
1190
|
+
expect(readDropdownId(null)).toBeNull();
|
|
1166
1191
|
});
|
|
1167
1192
|
|
|
1168
1193
|
test("ignores a mode that is not a mode", () => {
|
|
@@ -1212,11 +1237,6 @@ describe("what an edit sends", () => {
|
|
|
1212
1237
|
).toEqual({ aiKubectlCommandAllowlist: [] });
|
|
1213
1238
|
});
|
|
1214
1239
|
|
|
1215
|
-
/*
|
|
1216
|
-
* The server decides "does this write add a pattern?" on the stored
|
|
1217
|
-
* strings. A line the form shows collapsed is sent in its stored
|
|
1218
|
-
* spelling, or removing one line would read as adding another.
|
|
1219
|
-
*/
|
|
1220
1240
|
test("sends every kept line in its stored spelling", () => {
|
|
1221
1241
|
const oddlySpaced: string =
|
|
1222
1242
|
"kubectl scale deployment/web --replicas=* -n web";
|
|
@@ -1318,7 +1338,6 @@ describe("what an edit sends", () => {
|
|
|
1318
1338
|
aiKubectlCommandAllowlist: [],
|
|
1319
1339
|
});
|
|
1320
1340
|
|
|
1321
|
-
// Switches off: nothing is sent.
|
|
1322
1341
|
expect(
|
|
1323
1342
|
getKubernetesAiAccessSettingsChanges({
|
|
1324
1343
|
saved,
|
|
@@ -1380,13 +1399,11 @@ describe("what an edit sends", () => {
|
|
|
1380
1399
|
|
|
1381
1400
|
/*
|
|
1382
1401
|
* The server's rule (KubernetesClusterService.getAiAccessLoosening) is
|
|
1383
|
-
* RELATIVE to the saved settings
|
|
1384
|
-
*
|
|
1385
|
-
* server accepts. KubernetesClusterAiPageServerParity.test.ts runs the same
|
|
1386
|
-
* cases through the server.
|
|
1402
|
+
* RELATIVE to the saved settings. KubernetesClusterAiPageServerParity.test.ts
|
|
1403
|
+
* runs the same cases through the server.
|
|
1387
1404
|
*/
|
|
1388
1405
|
describe("loosening changes", () => {
|
|
1389
|
-
test("
|
|
1406
|
+
test("any move of the mode up loosens, Off -> Ask for approval included; moving it down never does", () => {
|
|
1390
1407
|
const fromAutomatic: KubernetesAiAccessSavedSettings = makeSaved();
|
|
1391
1408
|
const fromBypass: KubernetesAiAccessSavedSettings = makeSaved({
|
|
1392
1409
|
aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
|
|
@@ -1401,58 +1418,72 @@ describe("loosening changes", () => {
|
|
|
1401
1418
|
changes: {
|
|
1402
1419
|
aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
|
|
1403
1420
|
},
|
|
1421
|
+
hasAiAgent: false,
|
|
1404
1422
|
}),
|
|
1405
|
-
).toEqual(["switching
|
|
1423
|
+
).toEqual(["switching fixes to Bypass approval"]);
|
|
1406
1424
|
expect(
|
|
1407
1425
|
getKubernetesAiAccessLooseningChanges({
|
|
1408
1426
|
saved: fromOff,
|
|
1409
1427
|
changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
|
|
1428
|
+
hasAiAgent: false,
|
|
1429
|
+
}),
|
|
1430
|
+
).toEqual(["switching fixes to Automatic"]);
|
|
1431
|
+
// Regression (§11): turning fixes on at all needs the admin set now.
|
|
1432
|
+
expect(
|
|
1433
|
+
getKubernetesAiAccessLooseningChanges({
|
|
1434
|
+
saved: fromOff,
|
|
1435
|
+
changes: {
|
|
1436
|
+
aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
|
|
1437
|
+
},
|
|
1438
|
+
hasAiAgent: false,
|
|
1410
1439
|
}),
|
|
1411
|
-
).toEqual(["switching
|
|
1440
|
+
).toEqual(["switching fixes to Ask for approval"]);
|
|
1412
1441
|
|
|
1413
1442
|
// Bypass approval -> Automatic is a tightening.
|
|
1414
1443
|
expect(
|
|
1415
1444
|
getKubernetesAiAccessLooseningChanges({
|
|
1416
1445
|
saved: fromBypass,
|
|
1417
1446
|
changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
|
|
1447
|
+
hasAiAgent: false,
|
|
1418
1448
|
}),
|
|
1419
1449
|
).toEqual([]);
|
|
1420
1450
|
for (const mode of [
|
|
1421
1451
|
KubernetesAiRemediationMode.Disabled,
|
|
1422
1452
|
KubernetesAiRemediationMode.RequireApproval,
|
|
1453
|
+
KubernetesAiRemediationMode.Automatic,
|
|
1423
1454
|
]) {
|
|
1424
1455
|
expect(
|
|
1425
1456
|
getKubernetesAiAccessLooseningChanges({
|
|
1426
|
-
saved:
|
|
1457
|
+
saved: fromAutomatic,
|
|
1427
1458
|
changes: { aiRemediationMode: mode },
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
test("adding a pattern loosens; removing patterns or clearing the list never does", () => {
|
|
1434
|
-
const saved: KubernetesAiAccessSavedSettings = makeSaved({
|
|
1435
|
-
aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, PATCH_PATTERN],
|
|
1436
|
-
});
|
|
1437
|
-
|
|
1438
|
-
expect(
|
|
1439
|
-
getKubernetesAiAccessLooseningChanges({
|
|
1440
|
-
saved,
|
|
1441
|
-
changes: { aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN] },
|
|
1442
|
-
}),
|
|
1443
|
-
).toEqual([]);
|
|
1444
|
-
expect(
|
|
1445
|
-
getKubernetesAiAccessLooseningChanges({
|
|
1446
|
-
saved,
|
|
1447
|
-
changes: { aiKubectlCommandAllowlist: [] },
|
|
1448
|
-
}),
|
|
1449
|
-
).toEqual([]);
|
|
1459
|
+
hasAiAgent: true,
|
|
1460
|
+
}),
|
|
1461
|
+
).toEqual([]);
|
|
1462
|
+
}
|
|
1463
|
+
// Not a mode: nothing to judge.
|
|
1450
1464
|
expect(
|
|
1451
1465
|
getKubernetesAiAccessLooseningChanges({
|
|
1452
|
-
saved,
|
|
1453
|
-
changes: {
|
|
1466
|
+
saved: fromOff,
|
|
1467
|
+
changes: { aiRemediationMode: "automatic" },
|
|
1468
|
+
hasAiAgent: false,
|
|
1454
1469
|
}),
|
|
1455
1470
|
).toEqual([]);
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
test("adding a pattern loosens; removing patterns or clearing the list never does", () => {
|
|
1474
|
+
const saved: KubernetesAiAccessSavedSettings = makeSaved({
|
|
1475
|
+
aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, PATCH_PATTERN],
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
for (const allowlist of [[SET_IMAGE_PATTERN], [], null]) {
|
|
1479
|
+
expect(
|
|
1480
|
+
getKubernetesAiAccessLooseningChanges({
|
|
1481
|
+
saved,
|
|
1482
|
+
changes: { aiKubectlCommandAllowlist: allowlist },
|
|
1483
|
+
hasAiAgent: false,
|
|
1484
|
+
}),
|
|
1485
|
+
).toEqual([]);
|
|
1486
|
+
}
|
|
1456
1487
|
|
|
1457
1488
|
expect(
|
|
1458
1489
|
getKubernetesAiAccessLooseningChanges({
|
|
@@ -1463,25 +1494,69 @@ describe("loosening changes", () => {
|
|
|
1463
1494
|
"kubectl delete pod * -n web",
|
|
1464
1495
|
],
|
|
1465
1496
|
},
|
|
1497
|
+
hasAiAgent: false,
|
|
1466
1498
|
}),
|
|
1467
1499
|
).toEqual([
|
|
1468
1500
|
'adding the kubectl allowlist pattern "kubectl delete pod * -n web"',
|
|
1469
1501
|
]);
|
|
1470
1502
|
|
|
1471
|
-
// Editing a pattern into another string is adding one.
|
|
1472
1503
|
expect(
|
|
1473
1504
|
getKubernetesAiAccessLooseningChanges({
|
|
1474
1505
|
saved,
|
|
1475
1506
|
changes: {
|
|
1476
1507
|
aiKubectlCommandAllowlist: [
|
|
1477
1508
|
"kubectl set image deployment/web * -n prod",
|
|
1509
|
+
"kubectl set image deployment/api * -n prod",
|
|
1478
1510
|
],
|
|
1479
1511
|
},
|
|
1512
|
+
hasAiAgent: false,
|
|
1480
1513
|
}),
|
|
1481
|
-
).
|
|
1514
|
+
).toEqual([
|
|
1515
|
+
'adding the kubectl allowlist patterns "kubectl set image deployment/web * -n prod", "kubectl set image deployment/api * -n prod"',
|
|
1516
|
+
]);
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
test("binding another Runner or a credential loosens; re-sending the bound one never does", () => {
|
|
1520
|
+
const saved: KubernetesAiAccessSavedSettings = makeSaved({
|
|
1521
|
+
aiAccessCredentialId: CREDENTIAL_ID,
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
for (const hasAiAgent of [true, false]) {
|
|
1525
|
+
expect(
|
|
1526
|
+
getKubernetesAiAccessLooseningChanges({
|
|
1527
|
+
saved,
|
|
1528
|
+
changes: { aiAccessRunnerId: OTHER_RUNNER_ID },
|
|
1529
|
+
hasAiAgent,
|
|
1530
|
+
}),
|
|
1531
|
+
).toEqual(["binding a different Runner"]);
|
|
1532
|
+
expect(
|
|
1533
|
+
getKubernetesAiAccessLooseningChanges({
|
|
1534
|
+
saved: makeSaved(),
|
|
1535
|
+
changes: { aiAccessCredentialId: CREDENTIAL_ID },
|
|
1536
|
+
hasAiAgent,
|
|
1537
|
+
}),
|
|
1538
|
+
).toEqual(["binding a Kubernetes credential"]);
|
|
1539
|
+
|
|
1540
|
+
for (const changes of [
|
|
1541
|
+
{ aiAccessRunnerId: RUNNER_ID },
|
|
1542
|
+
{ aiAccessCredentialId: CREDENTIAL_ID },
|
|
1543
|
+
{ isAiInvestigationEnabled: true },
|
|
1544
|
+
{ isAiInvestigationEnabled: false },
|
|
1545
|
+
] as Array<JSONObject>) {
|
|
1546
|
+
expect(
|
|
1547
|
+
getKubernetesAiAccessLooseningChanges({ saved, changes, hasAiAgent }),
|
|
1548
|
+
).toEqual([]);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1482
1551
|
});
|
|
1483
1552
|
|
|
1484
|
-
|
|
1553
|
+
/*
|
|
1554
|
+
* Regression (§11 "clearing a Runner binding with an agent row needs
|
|
1555
|
+
* admin"): clearing the binding used to be a tightening every editor
|
|
1556
|
+
* could make. With an agent row it changes the resolved access target to
|
|
1557
|
+
* the agent, so it loosens; without one it only takes access away.
|
|
1558
|
+
*/
|
|
1559
|
+
test("clearing the Runner or credential loosens exactly when the cluster has an AI agent", () => {
|
|
1485
1560
|
const saved: KubernetesAiAccessSavedSettings = makeSaved({
|
|
1486
1561
|
aiAccessCredentialId: CREDENTIAL_ID,
|
|
1487
1562
|
});
|
|
@@ -1489,28 +1564,36 @@ describe("loosening changes", () => {
|
|
|
1489
1564
|
expect(
|
|
1490
1565
|
getKubernetesAiAccessLooseningChanges({
|
|
1491
1566
|
saved,
|
|
1492
|
-
changes: { aiAccessRunnerId:
|
|
1567
|
+
changes: { aiAccessRunnerId: null, aiAccessCredentialId: null },
|
|
1568
|
+
hasAiAgent: true,
|
|
1493
1569
|
}),
|
|
1494
|
-
).toEqual([
|
|
1570
|
+
).toEqual([
|
|
1571
|
+
"switching this cluster to its Kubernetes AI agent",
|
|
1572
|
+
"removing the Kubernetes credential",
|
|
1573
|
+
]);
|
|
1495
1574
|
expect(
|
|
1496
1575
|
getKubernetesAiAccessLooseningChanges({
|
|
1497
|
-
saved
|
|
1498
|
-
changes: { aiAccessCredentialId:
|
|
1576
|
+
saved,
|
|
1577
|
+
changes: { aiAccessRunnerId: null, aiAccessCredentialId: null },
|
|
1578
|
+
hasAiAgent: false,
|
|
1499
1579
|
}),
|
|
1500
|
-
).toEqual([
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
{
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
]
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1580
|
+
).toEqual([]);
|
|
1581
|
+
|
|
1582
|
+
// Clearing what is not bound changes nothing.
|
|
1583
|
+
expect(
|
|
1584
|
+
getKubernetesAiAccessLooseningChanges({
|
|
1585
|
+
saved: makeAgentSaved(),
|
|
1586
|
+
changes: { aiAccessRunnerId: null, aiAccessCredentialId: null },
|
|
1587
|
+
hasAiAgent: true,
|
|
1588
|
+
}),
|
|
1589
|
+
).toEqual([]);
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
test("capitalizes the first loosening for the refusal", () => {
|
|
1593
|
+
expect(capitalizeFirst("switching fixes to Automatic")).toBe(
|
|
1594
|
+
"Switching fixes to Automatic",
|
|
1595
|
+
);
|
|
1596
|
+
expect(capitalizeFirst("")).toBe("");
|
|
1514
1597
|
});
|
|
1515
1598
|
});
|
|
1516
1599
|
|
|
@@ -1546,15 +1629,6 @@ describe("the remove-only allowlist", () => {
|
|
|
1546
1629
|
});
|
|
1547
1630
|
});
|
|
1548
1631
|
|
|
1549
|
-
/*
|
|
1550
|
-
* The confirmation is the UI's safety net for an allowlist that behaves
|
|
1551
|
-
* like Bypass approval. It used a whole-string glob reading ("*",
|
|
1552
|
-
* "kubectl *" were broad; anything with a literal "-n" was not), while the
|
|
1553
|
-
* matcher is word by word: "kubectl * * * -n *" auto-approves deleting any
|
|
1554
|
-
* Deployment in any non-protected namespace and was never confirmed. Broad
|
|
1555
|
-
* is now KubectlPolicy.isBroadAllowlistPattern, decided on the matcher's
|
|
1556
|
-
* own tokens, and the parity check below runs the real matcher.
|
|
1557
|
-
*/
|
|
1558
1632
|
describe("confirming a save that lets riskier changes run unattended", () => {
|
|
1559
1633
|
function expectNamesWhatItUnlocks(
|
|
1560
1634
|
confirmation: KubernetesAiAccessConfirmation | null,
|
|
@@ -1564,10 +1638,8 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1564
1638
|
for (const word of ["set image", "patch", "drain", "deleting workloads"]) {
|
|
1565
1639
|
expect(text).toContain(word);
|
|
1566
1640
|
}
|
|
1567
|
-
// What still never runs unattended is said too.
|
|
1568
1641
|
expect(text).toMatch(/never run/);
|
|
1569
1642
|
expect(text).toContain("kube-system");
|
|
1570
|
-
// The old copy taught the whole-string glob reading.
|
|
1571
1643
|
expect(text).not.toMatch(MATCHES_NEARLY_EVERYTHING_REGEX);
|
|
1572
1644
|
}
|
|
1573
1645
|
|
|
@@ -1594,7 +1666,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1594
1666
|
"kubectl set image deployment/* * --namespace=*",
|
|
1595
1667
|
"kubectl patch deployment * -p * -n *",
|
|
1596
1668
|
"kubectl rollout restart deployment -n *",
|
|
1597
|
-
// Without the leading "kubectl", as the matcher reads it too.
|
|
1598
1669
|
"set image deployment/web * -n *",
|
|
1599
1670
|
]) {
|
|
1600
1671
|
const confirmation: KubernetesAiAccessConfirmation | null =
|
|
@@ -1610,8 +1681,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1610
1681
|
expect(confirmation!.description).toContain(
|
|
1611
1682
|
"a wildcard for an object, the namespace, a selector or a --from source",
|
|
1612
1683
|
);
|
|
1613
|
-
// A wildcard verb is invalid, so the copy no longer names it.
|
|
1614
|
-
expect(confirmation!.description).not.toContain("for the verb");
|
|
1615
1684
|
}
|
|
1616
1685
|
});
|
|
1617
1686
|
|
|
@@ -1657,7 +1726,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1657
1726
|
saved: KubernetesAiAccessSavedSettings;
|
|
1658
1727
|
changes: JSONObject;
|
|
1659
1728
|
}> = [
|
|
1660
|
-
// Narrow allowlist, any mode change short of Bypass.
|
|
1661
1729
|
{
|
|
1662
1730
|
saved: makeSaved(),
|
|
1663
1731
|
changes: { aiRemediationMode: KubernetesAiRemediationMode.Disabled },
|
|
@@ -1668,7 +1736,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1668
1736
|
}),
|
|
1669
1737
|
changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
|
|
1670
1738
|
},
|
|
1671
|
-
// Narrow patterns: one object in one namespace, a wildcard only in a value.
|
|
1672
1739
|
{
|
|
1673
1740
|
saved: makeSaved(),
|
|
1674
1741
|
changes: {
|
|
@@ -1680,7 +1747,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1680
1747
|
],
|
|
1681
1748
|
},
|
|
1682
1749
|
},
|
|
1683
|
-
// An already-saved broad pattern is not re-confirmed on unrelated edits.
|
|
1684
1750
|
{
|
|
1685
1751
|
saved: makeSaved({ aiKubectlCommandAllowlist: [BROAD_PATTERN] }),
|
|
1686
1752
|
changes: { isAiInvestigationEnabled: false },
|
|
@@ -1691,7 +1757,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1691
1757
|
aiKubectlCommandAllowlist: [BROAD_PATTERN, SET_IMAGE_PATTERN],
|
|
1692
1758
|
},
|
|
1693
1759
|
},
|
|
1694
|
-
// Re-sent in its stored spelling: still the saved pattern.
|
|
1695
1760
|
{
|
|
1696
1761
|
saved: makeSaved({
|
|
1697
1762
|
aiKubectlCommandAllowlist: ["kubectl delete deployment * -n *"],
|
|
@@ -1700,7 +1765,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1700
1765
|
aiKubectlCommandAllowlist: ["kubectl delete deployment * -n *"],
|
|
1701
1766
|
},
|
|
1702
1767
|
},
|
|
1703
|
-
// Leaving Bypass approval — to Automatic too — or clearing the allowlist.
|
|
1704
1768
|
{
|
|
1705
1769
|
saved: makeSaved({
|
|
1706
1770
|
aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
|
|
@@ -1731,11 +1795,11 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1731
1795
|
});
|
|
1732
1796
|
|
|
1733
1797
|
/*
|
|
1734
|
-
* The parity check
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1738
|
-
*
|
|
1798
|
+
* The parity check on the real matcher. Every probe names an object and
|
|
1799
|
+
* namespaces nobody would type into a pattern, so a pattern that
|
|
1800
|
+
* auto-approves a probe reaches objects and namespaces it does not name.
|
|
1801
|
+
* Each such pattern must be confirmed; each narrow one must neither be
|
|
1802
|
+
* confirmed nor reach the probes.
|
|
1739
1803
|
*/
|
|
1740
1804
|
describe("agrees with the matcher that actually runs", () => {
|
|
1741
1805
|
const PROBES: Array<string> = [];
|
|
@@ -1762,7 +1826,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1762
1826
|
"kubectl taint nodes zz-probe-obj k=v:NoSchedule",
|
|
1763
1827
|
);
|
|
1764
1828
|
|
|
1765
|
-
// Probes the empty allowlist leaves for a human: what a pattern could promote.
|
|
1766
1829
|
const promotable: Array<string> = PROBES.filter(
|
|
1767
1830
|
(command: string): boolean => {
|
|
1768
1831
|
return (
|
|
@@ -1798,15 +1861,8 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1798
1861
|
"*",
|
|
1799
1862
|
"kubectl *",
|
|
1800
1863
|
"kubectl * *",
|
|
1801
|
-
"kubectl * * *",
|
|
1802
|
-
"kubectl * * * *",
|
|
1803
1864
|
"kubectl * * -n *",
|
|
1804
1865
|
"kubectl * * * -n *",
|
|
1805
|
-
"kubectl * * * * -n *",
|
|
1806
|
-
"kubectl * * * -p * -n *",
|
|
1807
|
-
"kubectl * * * --namespace *",
|
|
1808
|
-
"kubectl * * * --namespace=*",
|
|
1809
|
-
"* * * -n *",
|
|
1810
1866
|
"kubectl delete * * -n *",
|
|
1811
1867
|
"kubectl delete deployment * -n *",
|
|
1812
1868
|
"kubectl delete deployment *",
|
|
@@ -1833,17 +1889,8 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1833
1889
|
|
|
1834
1890
|
test("harness guard: the probes are riskier changes a pattern can promote", () => {
|
|
1835
1891
|
expect(promotable.length).toBeGreaterThanOrEqual(15);
|
|
1836
|
-
/*
|
|
1837
|
-
* Broad entries that really do promote probes (the wildcard-verb
|
|
1838
|
-
* entries this guard used promote nothing since KubectlPolicy made
|
|
1839
|
-
* them invalid).
|
|
1840
|
-
*/
|
|
1841
1892
|
expect(promotedBy(BROAD_PATTERN).length).toBeGreaterThan(0);
|
|
1842
1893
|
expect(promotedBy("kubectl delete * * -n *").length).toBeGreaterThan(0);
|
|
1843
|
-
expect(
|
|
1844
|
-
promotedBy("kubectl patch deployment * -p * -n *").length,
|
|
1845
|
-
).toBeGreaterThan(0);
|
|
1846
|
-
// An invalid entry promotes nothing: the matcher skips it.
|
|
1847
1894
|
for (const pattern of VERB_WILDCARD_PATTERNS) {
|
|
1848
1895
|
expect({ pattern, promoted: promotedBy(pattern) }).toEqual({
|
|
1849
1896
|
pattern,
|
|
@@ -1878,183 +1925,72 @@ describe("confirming a save that lets riskier changes run unattended", () => {
|
|
|
1878
1925
|
});
|
|
1879
1926
|
});
|
|
1880
1927
|
|
|
1881
|
-
|
|
1928
|
+
/*
|
|
1929
|
+
* The Runner picker exists only for a cluster already bound to a Runner
|
|
1930
|
+
* outside the chart. The Kubernetes AI agent replaces the chart's previous
|
|
1931
|
+
* in-cluster Runner, so no kubernetes-agent row is ever offered — this
|
|
1932
|
+
* cluster's included — and switching back to the agent is "leave the
|
|
1933
|
+
* Runner empty".
|
|
1934
|
+
*/
|
|
1935
|
+
describe("Runner picker options (advanced bindings)", () => {
|
|
1882
1936
|
const runners: Array<Runner> = [
|
|
1883
1937
|
makeRunner("r-this", "kubernetes-agent/prod-east", true, "prod-east"),
|
|
1884
1938
|
makeRunner("r-other-cluster", "kubernetes-agent/prod-eu", true, "prod-eu"),
|
|
1939
|
+
makeRunner("r-renamed", "prod-eu-kubectl", true, "prod-eu"),
|
|
1885
1940
|
makeRunner("r-host", "bash-runner"),
|
|
1886
1941
|
makeRunner("r-off", "ops-runner", false),
|
|
1887
1942
|
];
|
|
1888
1943
|
|
|
1889
|
-
test("offer only Runners
|
|
1944
|
+
test("offer only Runners outside the chart that run AI commands", () => {
|
|
1890
1945
|
const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
|
|
1891
1946
|
runners,
|
|
1892
|
-
clusterIdentifier: "prod-east",
|
|
1893
1947
|
boundRunnerId: null,
|
|
1894
1948
|
boundRunnerName: null,
|
|
1895
1949
|
});
|
|
1896
1950
|
|
|
1897
|
-
expect(
|
|
1898
|
-
expect(options[0]!.label).toMatch(/in-cluster Runner for this cluster/);
|
|
1951
|
+
expect(options).toEqual([{ value: "r-host", label: "bash-runner" }]);
|
|
1899
1952
|
});
|
|
1900
1953
|
|
|
1901
|
-
test("
|
|
1954
|
+
test("keep the bound Runner, saying why it cannot serve the cluster", () => {
|
|
1902
1955
|
expect(
|
|
1903
|
-
|
|
1956
|
+
optionLabel(
|
|
1904
1957
|
buildKubernetesAiRunnerOptions({
|
|
1905
1958
|
runners,
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
boundRunnerName: null,
|
|
1959
|
+
boundRunnerId: "r-this",
|
|
1960
|
+
boundRunnerName: "kubernetes-agent/prod-east",
|
|
1909
1961
|
}),
|
|
1962
|
+
"r-this",
|
|
1910
1963
|
),
|
|
1911
|
-
).
|
|
1912
|
-
|
|
1964
|
+
).toBe(
|
|
1965
|
+
"kubernetes-agent/prod-east (currently bound — installed by the Kubernetes agent chart, which now uses the Kubernetes AI agent)",
|
|
1966
|
+
);
|
|
1913
1967
|
|
|
1914
|
-
|
|
1968
|
+
// A posture-only agent row is an agent row too (the server's rule).
|
|
1915
1969
|
expect(
|
|
1916
|
-
|
|
1970
|
+
optionLabel(
|
|
1917
1971
|
buildKubernetesAiRunnerOptions({
|
|
1918
1972
|
runners,
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
boundRunnerName: null,
|
|
1973
|
+
boundRunnerId: "r-renamed",
|
|
1974
|
+
boundRunnerName: "prod-eu-kubectl",
|
|
1922
1975
|
}),
|
|
1976
|
+
"r-renamed",
|
|
1923
1977
|
),
|
|
1924
|
-
).
|
|
1925
|
-
});
|
|
1926
|
-
|
|
1927
|
-
/*
|
|
1928
|
-
* The finding (dashboard-ai-page-2): the server names an agent Runner
|
|
1929
|
-
* "kubernetes-agent/<id>" only while that fits 100 characters, and
|
|
1930
|
-
* "kubernetes-agent/<head>-<hash>" beyond. The picker rebuilt the name
|
|
1931
|
-
* from the identifier, so for an identifier of 84+ characters it hid
|
|
1932
|
-
* this cluster's own Runner, or called it another cluster's. It now
|
|
1933
|
-
* matches the Runner's posture, as the server does.
|
|
1934
|
-
*/
|
|
1935
|
-
describe("a cluster identifier long enough for the server to shorten the Runner name", () => {
|
|
1936
|
-
const longIdentifier: string =
|
|
1937
|
-
"arn:aws:eks:ap-southeast-2:123456789012:cluster/payments-platform-production-blue-green";
|
|
1938
|
-
const otherLongIdentifier: string =
|
|
1939
|
-
"arn:aws:eks:ap-southeast-2:123456789012:cluster/payments-platform-producti-other-region";
|
|
1940
|
-
const head: string = `kubernetes-agent/${longIdentifier.slice(0, 74)}`;
|
|
1941
|
-
const thisRunner: Runner = makeRunner(
|
|
1942
|
-
"r-long",
|
|
1943
|
-
`${head}-fe7fc1b3`,
|
|
1944
|
-
true,
|
|
1945
|
-
longIdentifier,
|
|
1946
|
-
);
|
|
1947
|
-
// Same head, another hash: another long-named cluster's Runner.
|
|
1948
|
-
const otherRunner: Runner = makeRunner(
|
|
1949
|
-
"r-long-other",
|
|
1950
|
-
`${head}-0a1b2c3d`,
|
|
1951
|
-
true,
|
|
1952
|
-
otherLongIdentifier,
|
|
1953
|
-
);
|
|
1954
|
-
|
|
1955
|
-
test("harness guard: the identifier is past the length the name keeps whole", () => {
|
|
1956
|
-
expect(longIdentifier.length).toBe(87);
|
|
1957
|
-
expect(`kubernetes-agent/${longIdentifier}`.length).toBeGreaterThan(100);
|
|
1958
|
-
expect(thisRunner.name!.endsWith(longIdentifier)).toBe(false);
|
|
1959
|
-
});
|
|
1960
|
-
|
|
1961
|
-
test("lists this cluster's Runner first, as its in-cluster Runner, bound or not", () => {
|
|
1962
|
-
for (const boundRunnerId of [null, "r-long"]) {
|
|
1963
|
-
const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
|
|
1964
|
-
runners: [
|
|
1965
|
-
otherRunner,
|
|
1966
|
-
thisRunner,
|
|
1967
|
-
makeRunner("r-host", "bash-runner"),
|
|
1968
|
-
],
|
|
1969
|
-
clusterIdentifier: longIdentifier,
|
|
1970
|
-
boundRunnerId,
|
|
1971
|
-
boundRunnerName: boundRunnerId ? thisRunner.name! : null,
|
|
1972
|
-
});
|
|
1973
|
-
expect(optionValues(options)).toEqual(["r-long", "r-host"]);
|
|
1974
|
-
expect(options[0]!.label).toBe(
|
|
1975
|
-
`${thisRunner.name} (in-cluster Runner for this cluster)`,
|
|
1976
|
-
);
|
|
1977
|
-
}
|
|
1978
|
-
});
|
|
1979
|
-
|
|
1980
|
-
test("still calls another long-named cluster's Runner another cluster's", () => {
|
|
1981
|
-
const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
|
|
1982
|
-
runners: [otherRunner, thisRunner],
|
|
1983
|
-
clusterIdentifier: longIdentifier,
|
|
1984
|
-
boundRunnerId: "r-long-other",
|
|
1985
|
-
boundRunnerName: otherRunner.name!,
|
|
1986
|
-
});
|
|
1987
|
-
expect(optionLabel(options, "r-long-other")).toMatch(
|
|
1988
|
-
/currently bound — runs inside another cluster/,
|
|
1989
|
-
);
|
|
1990
|
-
});
|
|
1991
|
-
});
|
|
1978
|
+
).toMatch(/currently bound — installed by the Kubernetes agent chart/);
|
|
1992
1979
|
|
|
1993
|
-
test("an agent-named Runner whose posture names another cluster, or none, is not this cluster's", () => {
|
|
1994
|
-
expect(
|
|
1995
|
-
isThisClustersAgentRunner(
|
|
1996
|
-
makeRunner("a", "kubernetes-agent/prod-east", true, "prod-eu"),
|
|
1997
|
-
"prod-east",
|
|
1998
|
-
),
|
|
1999
|
-
).toBe(false);
|
|
2000
|
-
expect(
|
|
2001
|
-
isThisClustersAgentRunner(
|
|
2002
|
-
makeRunner("a", "kubernetes-agent/prod-east"),
|
|
2003
|
-
"prod-east",
|
|
2004
|
-
),
|
|
2005
|
-
).toBe(false);
|
|
2006
|
-
/*
|
|
2007
|
-
* A row without the name marker that reports an agent posture for this
|
|
2008
|
-
* cluster IS this cluster's agent: only the kubernetes-agent Runner
|
|
2009
|
-
* binary reports such a posture, and RunnerService.isKubernetesAgent
|
|
2010
|
-
* RunnerRow / isKubernetesAgentRunnerOfCluster read it so. This replaces
|
|
2011
|
-
* the pin that called it "not an agent row" (the page read the name
|
|
2012
|
-
* alone); a Runner in a pod that names no cluster is still not one.
|
|
2013
|
-
*/
|
|
2014
|
-
expect(
|
|
2015
|
-
isThisClustersAgentRunner(
|
|
2016
|
-
makeRunner("a", "office-runner", true, "prod-east"),
|
|
2017
|
-
"prod-east",
|
|
2018
|
-
),
|
|
2019
|
-
).toBe(true);
|
|
2020
1980
|
expect(
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
1981
|
+
optionLabel(
|
|
1982
|
+
buildKubernetesAiRunnerOptions({
|
|
1983
|
+
runners,
|
|
1984
|
+
boundRunnerId: "r-off",
|
|
1985
|
+
boundRunnerName: "ops-runner",
|
|
2026
1986
|
}),
|
|
2027
|
-
"
|
|
1987
|
+
"r-off",
|
|
2028
1988
|
),
|
|
2029
|
-
).
|
|
2030
|
-
});
|
|
2031
|
-
|
|
2032
|
-
test("keep the bound Runner, saying why it cannot serve the cluster", () => {
|
|
2033
|
-
const boundToOther: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
|
|
2034
|
-
runners,
|
|
2035
|
-
clusterIdentifier: "prod-east",
|
|
2036
|
-
boundRunnerId: "r-other-cluster",
|
|
2037
|
-
boundRunnerName: "kubernetes-agent/prod-eu",
|
|
2038
|
-
});
|
|
2039
|
-
expect(optionValues(boundToOther)).toContain("r-other-cluster");
|
|
2040
|
-
expect(optionLabel(boundToOther, "r-other-cluster")).toMatch(
|
|
2041
|
-
/currently bound — runs inside another cluster/,
|
|
2042
|
-
);
|
|
2043
|
-
|
|
2044
|
-
const boundToOff: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
|
|
2045
|
-
runners,
|
|
2046
|
-
clusterIdentifier: "prod-east",
|
|
2047
|
-
boundRunnerId: "r-off",
|
|
2048
|
-
boundRunnerName: "ops-runner",
|
|
2049
|
-
});
|
|
2050
|
-
expect(optionLabel(boundToOff, "r-off")).toMatch(
|
|
2051
|
-
/Runs AI Remediation Commands/,
|
|
2052
|
-
);
|
|
1989
|
+
).toMatch(/Runs AI Remediation Commands/);
|
|
2053
1990
|
|
|
2054
1991
|
const boundToUnlisted: Array<DropdownOption> =
|
|
2055
1992
|
buildKubernetesAiRunnerOptions({
|
|
2056
1993
|
runners,
|
|
2057
|
-
clusterIdentifier: "prod-east",
|
|
2058
1994
|
boundRunnerId: "r-gone",
|
|
2059
1995
|
boundRunnerName: "old-runner",
|
|
2060
1996
|
});
|
|
@@ -2062,6 +1998,25 @@ describe("Runner picker options", () => {
|
|
|
2062
1998
|
value: "r-gone",
|
|
2063
1999
|
label: "old-runner (currently bound)",
|
|
2064
2000
|
});
|
|
2001
|
+
expect(
|
|
2002
|
+
buildKubernetesAiRunnerOptions({
|
|
2003
|
+
runners: [],
|
|
2004
|
+
boundRunnerId: "r-gone",
|
|
2005
|
+
boundRunnerName: null,
|
|
2006
|
+
}),
|
|
2007
|
+
).toEqual([
|
|
2008
|
+
{ value: "r-gone", label: "The bound Runner (currently bound)" },
|
|
2009
|
+
]);
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
test("skip a row without an id", () => {
|
|
2013
|
+
expect(
|
|
2014
|
+
buildKubernetesAiRunnerOptions({
|
|
2015
|
+
runners: [Object.assign(new Runner(), { name: "no-id" })],
|
|
2016
|
+
boundRunnerId: null,
|
|
2017
|
+
boundRunnerName: null,
|
|
2018
|
+
}),
|
|
2019
|
+
).toEqual([]);
|
|
2065
2020
|
});
|
|
2066
2021
|
|
|
2067
2022
|
test("the directory names each Runner and says which are agent rows", () => {
|
|
@@ -2071,6 +2026,7 @@ describe("Runner picker options", () => {
|
|
|
2071
2026
|
name: "kubernetes-agent/prod-east",
|
|
2072
2027
|
isAgent: true,
|
|
2073
2028
|
});
|
|
2029
|
+
expect(directory["r-renamed"]!.isAgent).toBe(true);
|
|
2074
2030
|
expect(directory["r-host"]).toEqual({
|
|
2075
2031
|
name: "bash-runner",
|
|
2076
2032
|
isAgent: false,
|
|
@@ -2078,13 +2034,6 @@ describe("Runner picker options", () => {
|
|
|
2078
2034
|
});
|
|
2079
2035
|
});
|
|
2080
2036
|
|
|
2081
|
-
/*
|
|
2082
|
-
* The finding (dashboard-ai-page-7): the picker offered every Kubernetes
|
|
2083
|
-
* credential, so an admin could save a Runner with a credential that is
|
|
2084
|
-
* not assigned to it (the status then blocks everything with a
|
|
2085
|
-
* credential_missing gap), or a credential for a kubernetes-agent Runner,
|
|
2086
|
-
* which is never given one.
|
|
2087
|
-
*/
|
|
2088
2037
|
describe("credential picker options", () => {
|
|
2089
2038
|
const credentials: Array<RunbookCredential> = [
|
|
2090
2039
|
makeCredential("c-ssh", "ssh key", ["r-host"], RunbookCredentialType.SSH),
|
|
@@ -2110,7 +2059,6 @@ describe("credential picker options", () => {
|
|
|
2110
2059
|
),
|
|
2111
2060
|
).toEqual(["c-host", "c-both"]);
|
|
2112
2061
|
|
|
2113
|
-
// Negative control: another Runner gets its own credentials.
|
|
2114
2062
|
expect(
|
|
2115
2063
|
optionValues(
|
|
2116
2064
|
buildKubernetesAiCredentialOptions({
|
|
@@ -2144,7 +2092,7 @@ describe("credential picker options", () => {
|
|
|
2144
2092
|
name: "kubernetes-agent/prod-east",
|
|
2145
2093
|
}),
|
|
2146
2094
|
).toMatch(
|
|
2147
|
-
|
|
2095
|
+
/^No credential can be chosen: "kubernetes-agent\/prod-east" is an in-cluster Runner/,
|
|
2148
2096
|
);
|
|
2149
2097
|
expect(
|
|
2150
2098
|
getKubernetesAiCredentialFieldDescription({ id: null, name: null }),
|
|
@@ -2214,45 +2162,64 @@ describe("credential picker options", () => {
|
|
|
2214
2162
|
),
|
|
2215
2163
|
).toEqual(["c-unknown"]);
|
|
2216
2164
|
});
|
|
2165
|
+
|
|
2166
|
+
test("the directory names each credential and its Runners", () => {
|
|
2167
|
+
const directory: Record<string, KubernetesAiCredentialDirectoryEntry> =
|
|
2168
|
+
buildKubernetesAiCredentialDirectory([
|
|
2169
|
+
...credentials,
|
|
2170
|
+
makeCredential("c-unknown", "", undefined),
|
|
2171
|
+
]);
|
|
2172
|
+
expect(directory["c-both"]).toEqual({
|
|
2173
|
+
name: "shared token",
|
|
2174
|
+
runnerIds: ["r-host", "r-other-host"],
|
|
2175
|
+
});
|
|
2176
|
+
expect(directory["c-unknown"]).toEqual({
|
|
2177
|
+
name: "c-unknown",
|
|
2178
|
+
runnerIds: undefined,
|
|
2179
|
+
});
|
|
2180
|
+
});
|
|
2217
2181
|
});
|
|
2218
2182
|
|
|
2219
2183
|
describe("a Runner and credential that cannot work together", () => {
|
|
2220
2184
|
test("a credential not assigned to the Runner is refused, naming both", () => {
|
|
2221
|
-
const error: string | null = getKubernetesAiCredentialAssignmentError({
|
|
2222
|
-
runner: { id: "r-host", name: "bash-runner" },
|
|
2223
|
-
credentialId: "c-other",
|
|
2224
|
-
credentialName: "staging token",
|
|
2225
|
-
credentialRunnerIds: ["r-other-host"],
|
|
2226
|
-
});
|
|
2227
|
-
expect(error).toMatch(
|
|
2228
|
-
/"staging token" is not assigned to Runner "bash-runner"/,
|
|
2229
|
-
);
|
|
2230
|
-
|
|
2231
|
-
// Negative controls: assigned, unknown assignments, or no credential.
|
|
2232
2185
|
expect(
|
|
2233
2186
|
getKubernetesAiCredentialAssignmentError({
|
|
2234
2187
|
runner: { id: "r-host", name: "bash-runner" },
|
|
2235
|
-
credentialId: "c-
|
|
2236
|
-
credentialName: "
|
|
2237
|
-
credentialRunnerIds: ["r-host"],
|
|
2188
|
+
credentialId: "c-other",
|
|
2189
|
+
credentialName: "staging token",
|
|
2190
|
+
credentialRunnerIds: ["r-other-host"],
|
|
2238
2191
|
}),
|
|
2239
|
-
).
|
|
2192
|
+
).toMatch(/"staging token" is not assigned to Runner "bash-runner"/);
|
|
2193
|
+
|
|
2194
|
+
for (const credentialRunnerIds of [["r-host"], undefined]) {
|
|
2195
|
+
expect(
|
|
2196
|
+
getKubernetesAiCredentialAssignmentError({
|
|
2197
|
+
runner: { id: "r-host", name: "bash-runner" },
|
|
2198
|
+
credentialId: "c-host",
|
|
2199
|
+
credentialName: "prod-east token",
|
|
2200
|
+
credentialRunnerIds,
|
|
2201
|
+
}),
|
|
2202
|
+
).toBeNull();
|
|
2203
|
+
}
|
|
2240
2204
|
expect(
|
|
2241
2205
|
getKubernetesAiCredentialAssignmentError({
|
|
2242
2206
|
runner: { id: "r-host", name: "bash-runner" },
|
|
2243
|
-
credentialId:
|
|
2244
|
-
credentialName:
|
|
2207
|
+
credentialId: null,
|
|
2208
|
+
credentialName: null,
|
|
2245
2209
|
credentialRunnerIds: undefined,
|
|
2246
2210
|
}),
|
|
2247
2211
|
).toBeNull();
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
test("a credential without a Runner asks for one", () => {
|
|
2248
2215
|
expect(
|
|
2249
2216
|
getKubernetesAiCredentialAssignmentError({
|
|
2250
|
-
runner: { id:
|
|
2251
|
-
credentialId:
|
|
2217
|
+
runner: { id: null, name: null },
|
|
2218
|
+
credentialId: "c-host",
|
|
2252
2219
|
credentialName: null,
|
|
2253
|
-
credentialRunnerIds:
|
|
2220
|
+
credentialRunnerIds: ["r-host"],
|
|
2254
2221
|
}),
|
|
2255
|
-
).
|
|
2222
|
+
).toMatch(/^The Kubernetes credential is only used through a Runner/);
|
|
2256
2223
|
});
|
|
2257
2224
|
|
|
2258
2225
|
test("a kubernetes-agent Runner with a credential asks for the credential to be cleared", () => {
|
|
@@ -2260,7 +2227,6 @@ describe("a Runner and credential that cannot work together", () => {
|
|
|
2260
2227
|
runner: { id: "r-this", name: "kubernetes-agent/prod-east" },
|
|
2261
2228
|
credentialId: "c-host",
|
|
2262
2229
|
credentialName: "prod-east token",
|
|
2263
|
-
// Even assigned: an agent Runner is never given a credential.
|
|
2264
2230
|
credentialRunnerIds: ["r-this"],
|
|
2265
2231
|
});
|
|
2266
2232
|
expect(error).toMatch(/is an in-cluster Runner/);
|
|
@@ -2352,6 +2318,8 @@ describe("who may loosen a cluster's AI access", () => {
|
|
|
2352
2318
|
for (const permission of KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS) {
|
|
2353
2319
|
grant([...BASE_PERMISSIONS, permission]);
|
|
2354
2320
|
expect(canConfigureUnattendedKubernetesAiAccess()).toBe(true);
|
|
2321
|
+
// Resetting the agent takes the same set (the server's route gate).
|
|
2322
|
+
expect(canResetKubernetesAiAgent()).toBe(true);
|
|
2355
2323
|
}
|
|
2356
2324
|
|
|
2357
2325
|
expect(getKubernetesAiAccessAdminPermissionTitles()).toEqual(
|
|
@@ -2373,6 +2341,7 @@ describe("who may loosen a cluster's AI access", () => {
|
|
|
2373
2341
|
]) {
|
|
2374
2342
|
grant([...BASE_PERMISSIONS, permission]);
|
|
2375
2343
|
expect(canConfigureUnattendedKubernetesAiAccess()).toBe(false);
|
|
2344
|
+
expect(canResetKubernetesAiAgent()).toBe(false);
|
|
2376
2345
|
}
|
|
2377
2346
|
});
|
|
2378
2347
|
|
|
@@ -2382,6 +2351,9 @@ describe("who may loosen a cluster's AI access", () => {
|
|
|
2382
2351
|
[Permission.ProjectAdmin, Permission.EditAutoRemediationRule],
|
|
2383
2352
|
);
|
|
2384
2353
|
expect(canConfigureUnattendedKubernetesAiAccess()).toBe(false);
|
|
2354
|
+
expect(holdsKubernetesAiAccessPermission([Permission.ProjectMember])).toBe(
|
|
2355
|
+
true,
|
|
2356
|
+
);
|
|
2385
2357
|
});
|
|
2386
2358
|
|
|
2387
2359
|
test("is allowed to a master admin and withheld before the snapshot lands", () => {
|
|
@@ -2392,7 +2364,7 @@ describe("who may loosen a cluster's AI access", () => {
|
|
|
2392
2364
|
expect(canConfigureUnattendedKubernetesAiAccess()).toBe(true);
|
|
2393
2365
|
});
|
|
2394
2366
|
|
|
2395
|
-
test("decides what the
|
|
2367
|
+
test("decides what the Change modal offers", () => {
|
|
2396
2368
|
grant([...BASE_PERMISSIONS, Permission.ProjectAdmin]);
|
|
2397
2369
|
expect(getKubernetesAiAccessEditCapabilities()).toEqual({
|
|
2398
2370
|
canConfigureUnattended: true,
|
|
@@ -2400,7 +2372,6 @@ describe("who may loosen a cluster's AI access", () => {
|
|
|
2400
2372
|
canPickCredential: true,
|
|
2401
2373
|
});
|
|
2402
2374
|
|
|
2403
|
-
// May loosen and list Runners, may not read credentials.
|
|
2404
2375
|
grant([
|
|
2405
2376
|
...BASE_PERMISSIONS,
|
|
2406
2377
|
Permission.ProjectMember,
|
|
@@ -2412,7 +2383,6 @@ describe("who may loosen a cluster's AI access", () => {
|
|
|
2412
2383
|
canPickCredential: false,
|
|
2413
2384
|
});
|
|
2414
2385
|
|
|
2415
|
-
// May loosen, but may not list Runners.
|
|
2416
2386
|
grant([
|
|
2417
2387
|
...BASE_PERMISSIONS,
|
|
2418
2388
|
Permission.EditAutoRemediationRule,
|
|
@@ -2425,7 +2395,6 @@ describe("who may loosen a cluster's AI access", () => {
|
|
|
2425
2395
|
canPickCredential: false,
|
|
2426
2396
|
});
|
|
2427
2397
|
|
|
2428
|
-
// Reading credentials alone does not let a member bind one.
|
|
2429
2398
|
grant([
|
|
2430
2399
|
...BASE_PERMISSIONS,
|
|
2431
2400
|
Permission.ProjectMember,
|
|
@@ -2436,11 +2405,64 @@ describe("who may loosen a cluster's AI access", () => {
|
|
|
2436
2405
|
canPickRunner: false,
|
|
2437
2406
|
canPickCredential: false,
|
|
2438
2407
|
});
|
|
2408
|
+
expect(KUBERNETES_AI_ACCESS_CREDENTIAL_PERMISSIONS.length).toBeGreaterThan(
|
|
2409
|
+
0,
|
|
2410
|
+
);
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
// The page's titles and PermissionGate's must never disagree.
|
|
2414
|
+
test("names permissions exactly as PermissionGate does, without a browser", () => {
|
|
2415
|
+
for (const permissions of [
|
|
2416
|
+
KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS,
|
|
2417
|
+
KUBERNETES_AI_ACCESS_CREDENTIAL_PERMISSIONS,
|
|
2418
|
+
PROJECT_AI_SETTINGS_PERMISSIONS,
|
|
2419
|
+
[Permission.ProjectOwner, Permission.ProjectOwner],
|
|
2420
|
+
]) {
|
|
2421
|
+
expect(getPermissionTitles(permissions)).toEqual(
|
|
2422
|
+
PermissionGate.getPermissionTitles(permissions),
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
/*
|
|
2429
|
+
* The project's AI switches (AI Features, LLM providers, AI credits,
|
|
2430
|
+
* automatic investigation) are changed by project owners and admins — the
|
|
2431
|
+
* update ACL of Project.enableAutomaticIncidentInvestigation. Everyone else
|
|
2432
|
+
* is told who to ask.
|
|
2433
|
+
*/
|
|
2434
|
+
describe("who may change the project's AI settings from the cluster page", () => {
|
|
2435
|
+
test("project owners, admins and a master admin", () => {
|
|
2436
|
+
expect(PROJECT_AI_SETTINGS_PERMISSIONS).toEqual([
|
|
2437
|
+
Permission.ProjectOwner,
|
|
2438
|
+
Permission.ProjectAdmin,
|
|
2439
|
+
]);
|
|
2440
|
+
for (const permission of PROJECT_AI_SETTINGS_PERMISSIONS) {
|
|
2441
|
+
grant([...BASE_PERMISSIONS, permission]);
|
|
2442
|
+
expect(canChangeProjectAiSettings()).toBe(true);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
grantNothingYet();
|
|
2446
|
+
expect(canChangeProjectAiSettings()).toBe(false);
|
|
2447
|
+
jest.spyOn(User, "isMasterAdmin").mockReturnValue(true);
|
|
2448
|
+
expect(canChangeProjectAiSettings()).toBe(true);
|
|
2449
|
+
});
|
|
2450
|
+
|
|
2451
|
+
test("not members, cluster editors or the auto-remediation admin set", () => {
|
|
2452
|
+
for (const permission of [
|
|
2453
|
+
Permission.ProjectMember,
|
|
2454
|
+
Permission.EditKubernetesCluster,
|
|
2455
|
+
Permission.EditAutoRemediationRule,
|
|
2456
|
+
Permission.SettingsAdmin,
|
|
2457
|
+
]) {
|
|
2458
|
+
grant([...BASE_PERMISSIONS, permission]);
|
|
2459
|
+
expect(canChangeProjectAiSettings()).toBe(false);
|
|
2460
|
+
}
|
|
2439
2461
|
});
|
|
2440
2462
|
});
|
|
2441
2463
|
|
|
2442
|
-
describe("Runner picker
|
|
2443
|
-
test("is withheld from cluster editors who may not read Runners", () => {
|
|
2464
|
+
describe("Runner and credential picker permissions", () => {
|
|
2465
|
+
test("the Runner picker is withheld from cluster editors who may not read Runners", () => {
|
|
2444
2466
|
for (const permissions of [
|
|
2445
2467
|
[Permission.SettingsAdmin],
|
|
2446
2468
|
[Permission.SettingsMember],
|
|
@@ -2451,7 +2473,7 @@ describe("Runner picker permission", () => {
|
|
|
2451
2473
|
}
|
|
2452
2474
|
});
|
|
2453
2475
|
|
|
2454
|
-
test("is offered to Runner readers and a master admin", () => {
|
|
2476
|
+
test("the Runner picker is offered to Runner readers and a master admin", () => {
|
|
2455
2477
|
for (const permission of [
|
|
2456
2478
|
Permission.ProjectMember,
|
|
2457
2479
|
Permission.ReadRunner,
|
|
@@ -2468,13 +2490,26 @@ describe("Runner picker permission", () => {
|
|
|
2468
2490
|
expect(canPickKubernetesRunner()).toBe(true);
|
|
2469
2491
|
});
|
|
2470
2492
|
|
|
2471
|
-
test("
|
|
2493
|
+
test("the credential picker needs permission to read credentials", () => {
|
|
2494
|
+
expect(canPickKubernetesCredential()).toBe(false);
|
|
2495
|
+
grant([...BASE_PERMISSIONS, Permission.ReadRunbookCredential]);
|
|
2496
|
+
expect(canPickKubernetesCredential()).toBe(true);
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
test("name the permissions that would unlock them", () => {
|
|
2472
2500
|
expect(getKubernetesRunnerPermissionTitles()).toEqual(
|
|
2473
2501
|
expect.arrayContaining(["Read Runbook Agent", "Project Member"]),
|
|
2474
2502
|
);
|
|
2503
|
+
expect(getKubernetesCredentialPermissionTitles().join(", ")).toMatch(
|
|
2504
|
+
/Runbook Credential/,
|
|
2505
|
+
);
|
|
2475
2506
|
});
|
|
2476
2507
|
});
|
|
2477
2508
|
|
|
2509
|
+
/*
|
|
2510
|
+
* The kubectl command history (now on the AI Insights page) is RunnerJob
|
|
2511
|
+
* rows, which roles that may open the cluster's pages may not read.
|
|
2512
|
+
*/
|
|
2478
2513
|
describe("command history permission", () => {
|
|
2479
2514
|
test("is withheld from roles that may open the page but not read Runner jobs", () => {
|
|
2480
2515
|
for (const permission of [
|
|
@@ -2511,7 +2546,7 @@ describe("command history permission", () => {
|
|
|
2511
2546
|
});
|
|
2512
2547
|
});
|
|
2513
2548
|
|
|
2514
|
-
describe("
|
|
2549
|
+
describe("connection test permission", () => {
|
|
2515
2550
|
test("requires edit access to the cluster, as the /test route does", () => {
|
|
2516
2551
|
grant([...BASE_PERMISSIONS, Permission.ReadKubernetesCluster]);
|
|
2517
2552
|
const refused: PermissionGateResult = getAccessTestPermissionGate();
|
|
@@ -2532,9 +2567,11 @@ describe("access test permission", () => {
|
|
|
2532
2567
|
expect(getAccessTestPermissionGate()).toEqual({ isAllowed: false });
|
|
2533
2568
|
});
|
|
2534
2569
|
|
|
2535
|
-
test("a refusal talks about
|
|
2570
|
+
test("a refusal talks about testing the connection, not changing settings", () => {
|
|
2571
|
+
expect(getAccessTestPermissionRequirement()).toMatch(
|
|
2572
|
+
/^Testing the connection needs permission to edit this cluster/,
|
|
2573
|
+
);
|
|
2536
2574
|
const message: string = getAccessTestPermissionMessage();
|
|
2537
|
-
expect(message).toMatch(/Running the access test needs permission/);
|
|
2538
2575
|
expect(message).toMatch(/Nothing .* was changed/);
|
|
2539
2576
|
expect(message).not.toMatch(/permission to change/);
|
|
2540
2577
|
});
|
|
@@ -2542,8 +2579,7 @@ describe("access test permission", () => {
|
|
|
2542
2579
|
|
|
2543
2580
|
/*
|
|
2544
2581
|
* The page's mode copy against the canonical description on
|
|
2545
|
-
* KubernetesAiRemediationMode
|
|
2546
|
-
* KubernetesClusterAiAccess.ts). The shared phrases are checked in the
|
|
2582
|
+
* KubernetesAiRemediationMode. The shared phrases are checked in the
|
|
2547
2583
|
* canonical comment too, so rewording it flags this copy for review.
|
|
2548
2584
|
*/
|
|
2549
2585
|
describe("the mode copy follows the canonical description", () => {
|
|
@@ -2566,20 +2602,11 @@ describe("the mode copy follows the canonical description", () => {
|
|
|
2566
2602
|
.replace(COMMENT_LINE_BREAK_REGEX, " ");
|
|
2567
2603
|
}
|
|
2568
2604
|
|
|
2569
|
-
// Clauses of the canonical "In EVERY mode" paragraph the page repeats verbatim.
|
|
2570
2605
|
const EVERY_MODE_CLAUSES: Array<string> = [
|
|
2571
2606
|
"never changes its own namespace or anything outside the namespaces its chart may write",
|
|
2572
2607
|
"the hourly per-cluster circuit breaker trips or another unattended round already holds the cluster",
|
|
2573
2608
|
];
|
|
2574
2609
|
|
|
2575
|
-
/*
|
|
2576
|
-
* The node clause. The canonical comment names a node drain and a node
|
|
2577
|
-
* taint ("a node drain and a node taint always need a human"); the page
|
|
2578
|
-
* adds a patch of a node, which the policy holds to the same rule since
|
|
2579
|
-
* round four (a NoExecute taint written with `kubectl patch node` is the
|
|
2580
|
-
* same change as `kubectl taint`). Matched as a shape, so the canonical
|
|
2581
|
-
* comment may name the node patch too without flagging this copy.
|
|
2582
|
-
*/
|
|
2583
2610
|
const NODE_CLAUSE_SHAPE_REGEX: RegExp =
|
|
2584
2611
|
/a node drain(,| and) a node taint[^;]{0,120} always need a human/;
|
|
2585
2612
|
const PAGE_NODE_CLAUSE: string =
|
|
@@ -2601,21 +2628,14 @@ describe("the mode copy follows the canonical description", () => {
|
|
|
2601
2628
|
expect(canonical).toMatch(NODE_CLAUSE_SHAPE_REGEX);
|
|
2602
2629
|
});
|
|
2603
2630
|
|
|
2604
|
-
// Negative control: the shape does not accept a clause that drops the taint.
|
|
2605
2631
|
test("the node clause shape needs both the drain and the taint", () => {
|
|
2606
2632
|
expect(PAGE_NODE_CLAUSE).toMatch(NODE_CLAUSE_SHAPE_REGEX);
|
|
2607
|
-
expect("a node drain and a node taint always need a human").toMatch(
|
|
2608
|
-
NODE_CLAUSE_SHAPE_REGEX,
|
|
2609
|
-
);
|
|
2610
2633
|
expect("a node drain always needs a human").not.toMatch(
|
|
2611
2634
|
NODE_CLAUSE_SHAPE_REGEX,
|
|
2612
2635
|
);
|
|
2613
|
-
expect(
|
|
2614
|
-
"a node drain and a patch of a node always need a human",
|
|
2615
|
-
).not.toMatch(NODE_CLAUSE_SHAPE_REGEX);
|
|
2616
2636
|
});
|
|
2617
2637
|
|
|
2618
|
-
test("every mode's protections name the drain, the taint, the
|
|
2638
|
+
test("every mode's protections name the drain, the taint, the agent's scope and both proposal cases", () => {
|
|
2619
2639
|
const sentence: string = getEveryModeProtectionsSentence();
|
|
2620
2640
|
for (const clause of EVERY_MODE_CLAUSES) {
|
|
2621
2641
|
expect({ clause, said: sentence.includes(clause) }).toEqual({
|
|
@@ -2623,39 +2643,78 @@ describe("the mode copy follows the canonical description", () => {
|
|
|
2623
2643
|
said: true,
|
|
2624
2644
|
});
|
|
2625
2645
|
}
|
|
2626
|
-
/*
|
|
2627
|
-
* Replaces the pin on "a node drain and a node taint always need a
|
|
2628
|
-
* human": round four adds a patch of a node, which a human must
|
|
2629
|
-
* approve in every mode too.
|
|
2630
|
-
*/
|
|
2631
2646
|
expect(sentence).toContain(PAGE_NODE_CLAUSE);
|
|
2632
|
-
expect(sentence).
|
|
2647
|
+
expect(sentence).toContain("the in-cluster agent never changes");
|
|
2633
2648
|
for (const namespace of PROTECTED_KUBERNETES_NAMESPACES) {
|
|
2634
2649
|
expect(sentence).toContain(namespace);
|
|
2635
2650
|
}
|
|
2636
|
-
// The earlier copy: only the breaker, and "a taint" without "node".
|
|
2637
|
-
expect(sentence).not.toContain(
|
|
2638
|
-
"the hourly circuit breaker turns an unattended run into a proposal",
|
|
2639
|
-
);
|
|
2640
2651
|
});
|
|
2641
2652
|
|
|
2642
|
-
test("the mode field names the
|
|
2653
|
+
test("the mode field names the riskier changes, the one-click proposal and Bypass's exceptions", () => {
|
|
2643
2654
|
const description: string = getRemediationModeFieldDescription();
|
|
2644
2655
|
expect(description).toContain(
|
|
2645
2656
|
"A riskier change (patch, set image, drain, taint, scale to zero,",
|
|
2646
2657
|
);
|
|
2658
|
+
expect(description).toMatch(
|
|
2659
|
+
/when the round could only find riskier fixes, it ends by proposing exactly those for one-click approval/,
|
|
2660
|
+
);
|
|
2661
|
+
expect(description).toContain("delete a named pod");
|
|
2662
|
+
expect(description).not.toMatch(/delete a named pod or job/);
|
|
2647
2663
|
expect(description).toContain(
|
|
2648
2664
|
"follow-up rounds included, except for what always asks. In every mode, Bypass approval included:",
|
|
2649
2665
|
);
|
|
2650
2666
|
expect(description).toContain(getEveryModeProtectionsSentence());
|
|
2651
2667
|
});
|
|
2652
2668
|
|
|
2669
|
+
test("every mode has its own label, short name and one-line summary", () => {
|
|
2670
|
+
for (const record of [
|
|
2671
|
+
REMEDIATION_MODE_LABELS,
|
|
2672
|
+
REMEDIATION_MODE_SHORT_NAMES,
|
|
2673
|
+
REMEDIATION_MODE_SUMMARIES,
|
|
2674
|
+
]) {
|
|
2675
|
+
const values: Array<string> = Object.values(
|
|
2676
|
+
KubernetesAiRemediationMode,
|
|
2677
|
+
).map((mode: KubernetesAiRemediationMode): string => {
|
|
2678
|
+
return record[mode];
|
|
2679
|
+
});
|
|
2680
|
+
expect(
|
|
2681
|
+
values.every((value: string): boolean => {
|
|
2682
|
+
return value.trim().length > 0;
|
|
2683
|
+
}),
|
|
2684
|
+
).toBe(true);
|
|
2685
|
+
expect(new Set(values).size).toBe(values.length);
|
|
2686
|
+
}
|
|
2687
|
+
expect(REMEDIATION_MODE_SHORT_NAMES).toEqual({
|
|
2688
|
+
[KubernetesAiRemediationMode.Disabled]: "Off",
|
|
2689
|
+
[KubernetesAiRemediationMode.RequireApproval]: "Ask for approval",
|
|
2690
|
+
[KubernetesAiRemediationMode.Automatic]: "Automatic",
|
|
2691
|
+
[KubernetesAiRemediationMode.BypassApproval]: "Bypass approval",
|
|
2692
|
+
});
|
|
2693
|
+
// Each label starts with its short name, so the dropdown and card agree.
|
|
2694
|
+
for (const mode of Object.values(KubernetesAiRemediationMode)) {
|
|
2695
|
+
expect(
|
|
2696
|
+
REMEDIATION_MODE_LABELS[mode].startsWith(
|
|
2697
|
+
REMEDIATION_MODE_SHORT_NAMES[mode],
|
|
2698
|
+
),
|
|
2699
|
+
).toBe(true);
|
|
2700
|
+
}
|
|
2701
|
+
});
|
|
2702
|
+
|
|
2703
|
+
test("Automatic says a riskier fix waits for one-click approval", () => {
|
|
2704
|
+
for (const text of [
|
|
2705
|
+
REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Automatic],
|
|
2706
|
+
REMEDIATION_MODE_SUMMARIES[KubernetesAiRemediationMode.Automatic],
|
|
2707
|
+
]) {
|
|
2708
|
+
expect(text).toMatch(/one-click approval/);
|
|
2709
|
+
expect(text).not.toMatch(/left for you/);
|
|
2710
|
+
}
|
|
2711
|
+
});
|
|
2712
|
+
|
|
2653
2713
|
test("the Bypass approval label names every exception, another unattended round included", () => {
|
|
2654
2714
|
const label: string =
|
|
2655
2715
|
REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.BypassApproval];
|
|
2656
2716
|
for (const exception of [
|
|
2657
2717
|
"protected namespaces",
|
|
2658
|
-
// Round four: a patch of a node joins the drain and the taint.
|
|
2659
2718
|
"node drains, taints and patches",
|
|
2660
2719
|
"circuit breaker",
|
|
2661
2720
|
"another unattended round",
|
|
@@ -2666,6 +2725,10 @@ describe("the mode copy follows the canonical description", () => {
|
|
|
2666
2725
|
});
|
|
2667
2726
|
}
|
|
2668
2727
|
expect(label).not.toMatch(/\bonly\b/);
|
|
2728
|
+
expect(label).not.toMatch(/nobody is asked/);
|
|
2729
|
+
expect(
|
|
2730
|
+
REMEDIATION_MODE_SUMMARIES[KubernetesAiRemediationMode.BypassApproval],
|
|
2731
|
+
).toContain("still ask a person");
|
|
2669
2732
|
});
|
|
2670
2733
|
|
|
2671
2734
|
test("the Bypass approval confirmation says AI does not ask, then what still asks", () => {
|
|
@@ -2681,7 +2744,6 @@ describe("the mode copy follows the canonical description", () => {
|
|
|
2681
2744
|
expect(confirmation!.description).toContain(
|
|
2682
2745
|
"With Bypass approval OneUptime AI does not ask",
|
|
2683
2746
|
);
|
|
2684
|
-
expect(confirmation!.description).not.toContain("without asking anyone");
|
|
2685
2747
|
expect(confirmation!.description).toContain(
|
|
2686
2748
|
getEveryModeProtectionsSentence(),
|
|
2687
2749
|
);
|
|
@@ -2691,32 +2753,20 @@ describe("the mode copy follows the canonical description", () => {
|
|
|
2691
2753
|
/*
|
|
2692
2754
|
* Which Runner rows are kubernetes-agent Runners: the server's one rule
|
|
2693
2755
|
* (RunnerService.isKubernetesAgentRunnerRow) — the name marker, compared
|
|
2694
|
-
* case-insensitively, OR an agent posture.
|
|
2695
|
-
* so a row whose posture says it is an agent (a legacy rename) was
|
|
2696
|
-
* offered like a host Runner and given credentials the server refuses.
|
|
2697
|
-
* KubernetesClusterAiPageServerParity.test.ts runs the same table against
|
|
2698
|
-
* the real RunnerService.
|
|
2756
|
+
* case-insensitively, OR an agent posture.
|
|
2699
2757
|
*/
|
|
2700
2758
|
describe("a kubernetes-agent Runner, by the server's rule", () => {
|
|
2701
|
-
// An agent posture without the name marker: in a cluster, naming it.
|
|
2702
2759
|
const postureOnly: Runner = makeRunner(
|
|
2703
2760
|
"r-renamed",
|
|
2704
2761
|
"prod-eu-kubectl",
|
|
2705
2762
|
true,
|
|
2706
2763
|
"prod-eu",
|
|
2707
2764
|
);
|
|
2708
|
-
const postureOnlyHere: Runner = makeRunner(
|
|
2709
|
-
"r-renamed-here",
|
|
2710
|
-
"east-kubectl",
|
|
2711
|
-
true,
|
|
2712
|
-
"prod-east",
|
|
2713
|
-
);
|
|
2714
2765
|
const upperCaseName: Runner = makeRunner(
|
|
2715
2766
|
"r-upper",
|
|
2716
2767
|
"KUBERNETES-AGENT/prod-eu",
|
|
2717
2768
|
true,
|
|
2718
2769
|
);
|
|
2719
|
-
// In a pod, but naming no cluster: an ordinary Runner, not an agent.
|
|
2720
2770
|
const podNoCluster: Runner = Object.assign(new Runner(), {
|
|
2721
2771
|
_id: "r-pod",
|
|
2722
2772
|
name: "pod-runner",
|
|
@@ -2731,7 +2781,6 @@ describe("a kubernetes-agent Runner, by the server's rule", () => {
|
|
|
2731
2781
|
expect(
|
|
2732
2782
|
isKubernetesAgentRunnerRow({ name: " Kubernetes-Agent/prod-eu " }),
|
|
2733
2783
|
).toBe(true);
|
|
2734
|
-
// Negative controls: an ordinary Runner, in a pod or not.
|
|
2735
2784
|
expect(isKubernetesAgentRunnerRow(podNoCluster)).toBe(false);
|
|
2736
2785
|
expect(isKubernetesAgentRunnerRow(hostRunner)).toBe(false);
|
|
2737
2786
|
expect(isKubernetesAgentRunnerRow({ name: "kubernetes-agent" })).toBe(
|
|
@@ -2741,68 +2790,23 @@ describe("a kubernetes-agent Runner, by the server's rule", () => {
|
|
|
2741
2790
|
expect(isKubernetesAgentRunnerRow({})).toBe(false);
|
|
2742
2791
|
});
|
|
2743
2792
|
|
|
2744
|
-
test("the
|
|
2745
|
-
const directory: Record<string, KubernetesAiRunnerDirectoryEntry> =
|
|
2746
|
-
buildKubernetesAiRunnerDirectory([
|
|
2747
|
-
postureOnly,
|
|
2748
|
-
upperCaseName,
|
|
2749
|
-
podNoCluster,
|
|
2750
|
-
hostRunner,
|
|
2751
|
-
]);
|
|
2752
|
-
expect(directory["r-renamed"]!.isAgent).toBe(true);
|
|
2753
|
-
expect(directory["r-upper"]!.isAgent).toBe(true);
|
|
2754
|
-
expect(directory["r-pod"]!.isAgent).toBe(false);
|
|
2755
|
-
expect(directory["r-host"]!.isAgent).toBe(false);
|
|
2756
|
-
});
|
|
2757
|
-
|
|
2758
|
-
test("the Runner picker never offers another cluster's agent, whatever it is named", () => {
|
|
2793
|
+
test("the Runner picker never offers an agent row, whatever it is named", () => {
|
|
2759
2794
|
const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
|
|
2760
2795
|
runners: [postureOnly, upperCaseName, podNoCluster, hostRunner],
|
|
2761
|
-
clusterIdentifier: "prod-east",
|
|
2762
2796
|
boundRunnerId: null,
|
|
2763
2797
|
boundRunnerName: null,
|
|
2764
2798
|
});
|
|
2765
|
-
//
|
|
2799
|
+
// A Runner in a pod that names no cluster is an ordinary Runner.
|
|
2766
2800
|
expect(optionValues(options)).toEqual(["r-pod", "r-host"]);
|
|
2767
2801
|
});
|
|
2768
2802
|
|
|
2769
|
-
test("the Runner picker lists this cluster's agent first even without the name marker", () => {
|
|
2770
|
-
expect(isThisClustersAgentRunner(postureOnlyHere, "prod-east")).toBe(true);
|
|
2771
|
-
expect(isThisClustersAgentRunner(postureOnly, "prod-east")).toBe(false);
|
|
2772
|
-
expect(isThisClustersAgentRunner(podNoCluster, "prod-east")).toBe(false);
|
|
2773
|
-
|
|
2774
|
-
const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
|
|
2775
|
-
runners: [hostRunner, postureOnlyHere],
|
|
2776
|
-
clusterIdentifier: "prod-east",
|
|
2777
|
-
boundRunnerId: null,
|
|
2778
|
-
boundRunnerName: null,
|
|
2779
|
-
});
|
|
2780
|
-
expect(optionValues(options)).toEqual(["r-renamed-here", "r-host"]);
|
|
2781
|
-
expect(options[0]!.label).toBe(
|
|
2782
|
-
"east-kubectl (in-cluster Runner for this cluster)",
|
|
2783
|
-
);
|
|
2784
|
-
});
|
|
2785
|
-
|
|
2786
|
-
test("a bound agent of another cluster stays listed, labelled why it cannot serve", () => {
|
|
2787
|
-
const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
|
|
2788
|
-
runners: [postureOnly, hostRunner],
|
|
2789
|
-
clusterIdentifier: "prod-east",
|
|
2790
|
-
boundRunnerId: "r-renamed",
|
|
2791
|
-
boundRunnerName: "prod-eu-kubectl",
|
|
2792
|
-
});
|
|
2793
|
-
expect(optionValues(options)).toEqual(["r-renamed", "r-host"]);
|
|
2794
|
-
expect(options[0]!.label).toMatch(
|
|
2795
|
-
/currently bound — runs inside another cluster/,
|
|
2796
|
-
);
|
|
2797
|
-
});
|
|
2798
|
-
|
|
2799
2803
|
test("the credential picker offers nothing for an agent row the name does not mark", () => {
|
|
2800
2804
|
const credentials: Array<RunbookCredential> = [
|
|
2801
|
-
makeCredential(CREDENTIAL_ID, "prod token", ["r-renamed
|
|
2805
|
+
makeCredential(CREDENTIAL_ID, "prod token", ["r-renamed"]),
|
|
2802
2806
|
];
|
|
2803
2807
|
const runner: { id: string; name: string; isAgent: boolean } = {
|
|
2804
|
-
id: "r-renamed
|
|
2805
|
-
name: "
|
|
2808
|
+
id: "r-renamed",
|
|
2809
|
+
name: "prod-eu-kubectl",
|
|
2806
2810
|
isAgent: true,
|
|
2807
2811
|
};
|
|
2808
2812
|
|
|
@@ -2814,19 +2818,15 @@ describe("a kubernetes-agent Runner, by the server's rule", () => {
|
|
|
2814
2818
|
runner,
|
|
2815
2819
|
}),
|
|
2816
2820
|
).toEqual([]);
|
|
2817
|
-
expect(getKubernetesAiCredentialFieldDescription(runner)).toMatch(
|
|
2818
|
-
/^No credential can be chosen: "east-kubectl" is an in-cluster Runner/,
|
|
2819
|
-
);
|
|
2820
2821
|
expect(
|
|
2821
2822
|
getKubernetesAiCredentialAssignmentError({
|
|
2822
2823
|
runner,
|
|
2823
2824
|
credentialId: CREDENTIAL_ID,
|
|
2824
2825
|
credentialName: "prod token",
|
|
2825
|
-
credentialRunnerIds: ["r-renamed
|
|
2826
|
+
credentialRunnerIds: ["r-renamed"],
|
|
2826
2827
|
}),
|
|
2827
2828
|
).toMatch(/is an in-cluster Runner/);
|
|
2828
2829
|
|
|
2829
|
-
// Negative control: the same row read as an ordinary Runner.
|
|
2830
2830
|
const ordinary: { id: string; name: string; isAgent: boolean } = {
|
|
2831
2831
|
...runner,
|
|
2832
2832
|
isAgent: false,
|
|
@@ -2841,46 +2841,26 @@ describe("a kubernetes-agent Runner, by the server's rule", () => {
|
|
|
2841
2841
|
}),
|
|
2842
2842
|
),
|
|
2843
2843
|
).toEqual([CREDENTIAL_ID]);
|
|
2844
|
-
expect(
|
|
2845
|
-
getKubernetesAiCredentialAssignmentError({
|
|
2846
|
-
runner: ordinary,
|
|
2847
|
-
credentialId: CREDENTIAL_ID,
|
|
2848
|
-
credentialName: "prod token",
|
|
2849
|
-
credentialRunnerIds: ["r-renamed-here"],
|
|
2850
|
-
}),
|
|
2851
|
-
).toBeNull();
|
|
2852
|
-
});
|
|
2853
|
-
|
|
2854
|
-
test("an upper-case agent name is an agent even when the row was not read", () => {
|
|
2855
|
-
expect(
|
|
2856
|
-
getKubernetesAiCredentialFieldDescription({
|
|
2857
|
-
id: "r-upper",
|
|
2858
|
-
name: "KUBERNETES-AGENT/prod-eu",
|
|
2859
|
-
}),
|
|
2860
|
-
).toMatch(/^No credential can be chosen/);
|
|
2861
2844
|
});
|
|
2862
2845
|
|
|
2863
2846
|
test("saving a credential with an agent row the name does not mark is refused", () => {
|
|
2864
2847
|
const runners: Record<string, KubernetesAiRunnerDirectoryEntry> =
|
|
2865
|
-
buildKubernetesAiRunnerDirectory([
|
|
2848
|
+
buildKubernetesAiRunnerDirectory([postureOnly]);
|
|
2866
2849
|
const credentials: Record<string, KubernetesAiCredentialDirectoryEntry> =
|
|
2867
2850
|
buildKubernetesAiCredentialDirectory([
|
|
2868
|
-
makeCredential(CREDENTIAL_ID, "prod token", ["r-renamed
|
|
2851
|
+
makeCredential(CREDENTIAL_ID, "prod token", ["r-renamed"]),
|
|
2869
2852
|
]);
|
|
2870
2853
|
|
|
2871
2854
|
expect(
|
|
2872
2855
|
getKubernetesAiAccessBindingError({
|
|
2873
|
-
saved:
|
|
2874
|
-
aiAccessRunnerId: null,
|
|
2875
|
-
aiAccessCredentialId: null,
|
|
2876
|
-
}),
|
|
2856
|
+
saved: makeAgentSaved(),
|
|
2877
2857
|
changes: {
|
|
2878
|
-
aiAccessRunnerId: "r-renamed
|
|
2858
|
+
aiAccessRunnerId: "r-renamed",
|
|
2879
2859
|
aiAccessCredentialId: CREDENTIAL_ID,
|
|
2880
2860
|
},
|
|
2881
2861
|
runners,
|
|
2882
2862
|
credentials,
|
|
2883
2863
|
}),
|
|
2884
|
-
).toMatch(/"
|
|
2864
|
+
).toMatch(/"prod-eu-kubectl" is an in-cluster Runner/);
|
|
2885
2865
|
});
|
|
2886
2866
|
});
|