@oneuptime/common 14.0.3 → 14.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/Alert.ts +55 -0
- package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
- package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
- package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
- package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
- package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
- package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
- package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
- package/Models/DatabaseModels/Incident.ts +55 -0
- package/Models/DatabaseModels/Index.ts +16 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
- package/Models/DatabaseModels/TelemetryException.ts +2 -0
- package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/MultipartFormData.ts +1 -1
- package/Server/Middleware/TelemetryIngest.ts +15 -8
- package/Server/Services/AlertReminderRuleService.ts +100 -24
- package/Server/Services/DatabaseServerEndpointService.ts +895 -0
- package/Server/Services/DatabaseServerFeedService.ts +128 -0
- package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
- package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
- package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
- package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
- package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
- package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
- package/Server/Services/DatabaseServerService.ts +4546 -0
- package/Server/Services/DatabaseService.ts +14 -8
- package/Server/Services/DockerResourceService.ts +43 -2
- package/Server/Services/IncidentReminderRuleService.ts +103 -23
- package/Server/Services/Index.ts +16 -0
- package/Server/Services/InventoryItemService.ts +113 -5
- package/Server/Services/IoTDeviceService.ts +145 -37
- package/Server/Services/MetricService.ts +1 -1
- package/Server/Services/OpenTelemetryIngestService.ts +19 -0
- package/Server/Services/PodmanResourceService.ts +43 -2
- package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
- package/Server/Services/ServiceService.ts +1 -1
- package/Server/Services/TelemetryUsageBillingService.ts +29 -2
- package/Server/Services/UserProjectSsoConsentService.ts +74 -0
- package/Server/Services/UserService.ts +14 -1
- package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
- package/Server/Types/Database/QueryHelper.ts +84 -2
- package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
- package/Server/Utils/AI/Toolbox/ResourceTools.ts +81 -2
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
- package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +192 -9
- package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2264 -755
- package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
- package/Server/Utils/Monitor/MonitorResource.ts +47 -11
- package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
- package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
- package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
- package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
- package/Server/Utils/StartServer.ts +26 -6
- package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
- package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
- package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
- package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
- package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
- package/Server/Utils/Telemetry/Telemetry.ts +16 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
- package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
- package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
- package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
- package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
- package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
- package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
- package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
- package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
- package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
- package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
- package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
- package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
- package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
- package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
- package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
- package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
- package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
- package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
- package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
- package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
- package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
- package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
- package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
- package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
- package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
- package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
- package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
- package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
- package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
- package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
- package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
- package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
- package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
- package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
- package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
- package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
- package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
- package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
- package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
- package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
- package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
- package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
- package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
- package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
- package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
- package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
- package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
- package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
- package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
- package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
- package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
- package/Tests/ClassStrings.ts +178 -0
- package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
- package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
- package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
- package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
- package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
- package/Tests/RenderedMarkup.ts +518 -0
- package/Tests/ResponsiveVisibility.test.ts +490 -4
- package/Tests/ResponsiveVisibility.ts +505 -52
- package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
- package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
- package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
- package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
- package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
- package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
- package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
- package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
- package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
- package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
- package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
- package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
- package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
- package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
- package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
- package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
- package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
- package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
- package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
- package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
- package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
- package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
- package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
- package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
- package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
- package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
- package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
- package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
- package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
- package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
- package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
- package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
- package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
- package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
- package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
- package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
- package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
- package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
- package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
- package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
- package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
- package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1815 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
- package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
- package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
- package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
- package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
- package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
- package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
- package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
- package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
- package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
- package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
- package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
- package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
- package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
- package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
- package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
- package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
- package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
- package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
- package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
- package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
- package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
- package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
- package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
- package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
- package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
- package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
- package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
- package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +292 -0
- package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +272 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
- package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
- package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
- package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +202 -0
- package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
- package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
- package/Tests/UI/Components/Card.test.tsx +93 -2
- package/Tests/UI/Components/FeedOptions.test.ts +6 -0
- package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
- package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
- package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
- package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
- package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
- package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
- package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
- package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
- package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
- package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
- package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
- package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
- package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
- package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
- package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +70 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +21 -0
- package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
- package/Tests/UI/Utils/Navigation.test.ts +102 -0
- package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
- package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
- package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
- package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
- package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
- package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
- package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
- package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
- package/Tests/Utils/ValueFormatter.test.ts +50 -0
- package/Tests/tsconfig.json +16 -0
- package/Types/AI/AIResourceContext.ts +7 -0
- package/Types/BaseDatabase/AggregationType.ts +7 -0
- package/Types/Dashboard/DashboardTemplates.ts +22 -8
- package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
- package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
- package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
- package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
- package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
- package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
- package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
- package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
- package/Types/Email/EmailTemplateType.ts +1 -0
- package/Types/Monitor/CephAlertTemplates.ts +18 -0
- package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
- package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
- package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
- package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
- package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
- package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
- package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
- package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
- package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
- package/Types/Permission.ts +267 -0
- package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
- package/Types/Rules/RuleRun.ts +12 -0
- package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
- package/Types/Rum/WebVitals.ts +13 -0
- package/Types/Telemetry/EntityType.ts +23 -0
- package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
- package/Types/Telemetry/ServiceType.ts +8 -0
- package/UI/Components/Banner/Banner.tsx +1 -1
- package/UI/Components/Card/Card.tsx +1 -1
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
- package/UI/Components/Charts/Line/LineChart.tsx +7 -0
- package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
- package/UI/Components/Date/StartAndEndDate.tsx +5 -5
- package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
- package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
- package/UI/Components/Forms/BasicForm.tsx +1 -1
- package/UI/Components/Header/Header.tsx +1 -1
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
- package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
- package/UI/Components/Input/Input.tsx +11 -0
- package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
- package/UI/Components/Modal/ModalFooter.tsx +1 -1
- package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
- package/UI/Components/ModelTable/BaseModelTable.tsx +4 -4
- package/UI/Components/Navbar/NavBar.tsx +9 -2
- package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
- package/UI/Components/Page/Page.tsx +1 -1
- package/UI/Components/Pagination/Pagination.tsx +3 -3
- package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
- package/UI/Components/SideMenu/SideMenu.tsx +4 -1
- package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
- package/UI/Components/StatusPage/ResourceList.tsx +6 -6
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
- package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
- package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
- package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
- package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
- package/UI/Styles/Theme.css +8 -1
- package/UI/Utils/Navigation.ts +89 -42
- package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
- package/UI/Utils/TelemetryService.ts +4 -3
- package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
- package/Utils/LabelRuleImportExport.ts +2 -0
- package/Utils/MetricUnitUtil.ts +103 -11
- package/Utils/Monitor/MetricValueFormatter.ts +66 -13
- package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
- package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
- package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
- package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
- package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
- package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
- package/Utils/Telemetry/EntityKey.ts +176 -0
- package/Utils/ValueFormatter.ts +16 -0
- package/build/dist/Models/DatabaseModels/Alert.js +54 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
- package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Incident.js +54 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +16 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
- package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
- package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
- package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
- package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
- package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
- package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
- package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
- package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
- package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
- package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
- package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseService.js +8 -4
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +30 -3
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
- package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +16 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +88 -5
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +111 -24
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +1 -1
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +30 -3
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +1 -1
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
- package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
- package/build/dist/Server/Services/UserService.js +13 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +73 -2
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +58 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
- package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
- package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +135 -14
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
- package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +20 -4
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
- package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
- package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
- package/build/dist/Types/AI/AIResourceContext.js +7 -0
- package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
- package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +22 -8
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
- package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
- package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
- package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
- package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
- package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
- package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
- package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
- package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
- package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
- package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
- package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
- package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
- package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
- package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
- package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
- package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
- package/build/dist/Types/Permission.js +234 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rules/RuleRun.js +4 -0
- package/build/dist/Types/Rules/RuleRun.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +12 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityType.js +23 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
- package/build/dist/Types/Telemetry/ServiceType.js +8 -0
- package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
- package/build/dist/UI/Components/Banner/Banner.js +1 -1
- package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
- package/build/dist/UI/Components/Card/Card.js +1 -1
- package/build/dist/UI/Components/Card/Card.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
- package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
- package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
- package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
- package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
- package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
- package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +4 -4
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Components/Page/Page.js +1 -1
- package/build/dist/UI/Components/Page/Page.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
- package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
- package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
- package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
- package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +66 -33
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
- package/build/dist/UI/Utils/TelemetryService.js +3 -2
- package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
- package/build/dist/Utils/LabelRuleImportExport.js +2 -0
- package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
- package/build/dist/Utils/MetricUnitUtil.js +77 -13
- package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
- package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
- package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
- package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
- package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
- package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
- package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
- package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- package/build/dist/Utils/ValueFormatter.js +15 -0
- package/build/dist/Utils/ValueFormatter.js.map +1 -1
- package/package.json +1 -1
- package/tsconfig.tests.json +7 -5
|
@@ -0,0 +1,2045 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import Entities from "../../../Models/DatabaseModels/Index";
|
|
3
|
+
import DatabaseServer from "../../../Models/DatabaseModels/DatabaseServer";
|
|
4
|
+
import DatabaseServerEndpoint from "../../../Models/DatabaseModels/DatabaseServerEndpoint";
|
|
5
|
+
import PostgresAppInstance from "../../../Server/Infrastructure/PostgresDatabase";
|
|
6
|
+
import DatabaseServerEndpointService from "../../../Server/Services/DatabaseServerEndpointService";
|
|
7
|
+
import DatabaseServerFeedService from "../../../Server/Services/DatabaseServerFeedService";
|
|
8
|
+
import DatabaseServerLabelRuleEngineService from "../../../Server/Services/DatabaseServerLabelRuleEngineService";
|
|
9
|
+
import DatabaseServerOwnerRuleEngineService from "../../../Server/Services/DatabaseServerOwnerRuleEngineService";
|
|
10
|
+
import DatabaseServerService from "../../../Server/Services/DatabaseServerService";
|
|
11
|
+
import logger from "../../../Server/Utils/Logger";
|
|
12
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
13
|
+
import PermissionScope from "../../../Types/Database/AccessControl/PermissionScope";
|
|
14
|
+
import DatabaseServerDiscoverySource from "../../../Types/DatabaseServer/DatabaseServerDiscoverySource";
|
|
15
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
16
|
+
import Permission, { UserPermission } from "../../../Types/Permission";
|
|
17
|
+
import { DataSource } from "typeorm";
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* The hand-written Postgres of the Databases product, EXECUTED - not string
|
|
21
|
+
* matched. The unit suites replace manager.query with a mock, so a renamed
|
|
22
|
+
* join table or column (IncidentDatabaseServer, automaticAssignments, ...)
|
|
23
|
+
* would pass them and then fail in production every five minutes, logged
|
|
24
|
+
* and swallowed by DatabaseServer:CleanupStaleResources. Here the real
|
|
25
|
+
* service methods run against structure-only clones of the MIGRATED tables:
|
|
26
|
+
*
|
|
27
|
+
* - the disconnect sweep, the auto-archive sweep (every "invested" rule,
|
|
28
|
+
* automatic labels and owners, a person's restore, a dark parent), the
|
|
29
|
+
* auto-create budget count and the auto-archive restore;
|
|
30
|
+
* - the automaticAssignments bookkeeping;
|
|
31
|
+
* - the endpoint lifecycle statements (refresh, hand-over, release) and
|
|
32
|
+
* a whole workload identity change end to end;
|
|
33
|
+
* - a person adding or removing an alias through the real create and
|
|
34
|
+
* delete pipelines, label- and Owned-scoped;
|
|
35
|
+
* - the engine compare-and-set, and a retired workload row a trace names;
|
|
36
|
+
* - the cluster names a manual create's refusal may list, read through
|
|
37
|
+
* the caller's own Kubernetes cluster permissions.
|
|
38
|
+
*
|
|
39
|
+
* Opt in with RUN_POSTGRES_DATABASE_SERVER_SQL_TESTS=true against a Postgres
|
|
40
|
+
* migrated to the current head (the clones copy public.<table>) - the
|
|
41
|
+
* Postgres Schema Drift workflow's database right after its drift check, as
|
|
42
|
+
* for AIInvestigationDecisionPostgres. Credentials from DATABASE_USERNAME /
|
|
43
|
+
* DATABASE_PASSWORD, database from DATABASE_SERVER_SQL_TEST_DATABASE_NAME or
|
|
44
|
+
* DATABASE_NAME, endpoint from DATABASE_SERVER_SQL_TEST_DATABASE_HOST /
|
|
45
|
+
* _PORT (default localhost:5400, Scripts/Dev/docker-compose.dev.yml). Every
|
|
46
|
+
* table and row lives in a
|
|
47
|
+
* unique schema that is dropped afterwards; search_path holds only that
|
|
48
|
+
* schema, so a statement naming a table that was not cloned fails loudly
|
|
49
|
+
* instead of touching real data.
|
|
50
|
+
*/
|
|
51
|
+
const describePostgres: typeof describe =
|
|
52
|
+
process.env["RUN_POSTGRES_DATABASE_SERVER_SQL_TESTS"] === "true"
|
|
53
|
+
? describe
|
|
54
|
+
: describe.skip;
|
|
55
|
+
|
|
56
|
+
const DAY_MS: number = 24 * 60 * 60 * 1000;
|
|
57
|
+
const HOUR_MS: number = 60 * 60 * 1000;
|
|
58
|
+
|
|
59
|
+
// Tables the SQL under test reads or writes.
|
|
60
|
+
const TABLES: Array<string> = [
|
|
61
|
+
"DatabaseServer",
|
|
62
|
+
"DatabaseServerEndpoint",
|
|
63
|
+
"DatabaseServerLabel",
|
|
64
|
+
"DatabaseServerOwnerUser",
|
|
65
|
+
"DatabaseServerOwnerTeam",
|
|
66
|
+
"Label",
|
|
67
|
+
"IncidentDatabaseServer",
|
|
68
|
+
"Incident",
|
|
69
|
+
"AlertDatabaseServer",
|
|
70
|
+
"Alert",
|
|
71
|
+
"ScheduledMaintenanceDatabaseServer",
|
|
72
|
+
"ScheduledMaintenance",
|
|
73
|
+
"KubernetesCluster",
|
|
74
|
+
"KubernetesClusterLabel",
|
|
75
|
+
"DockerHost",
|
|
76
|
+
"PodmanHost",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
* Tables only ever seeded by hand here: their NOT NULL columns are relaxed
|
|
81
|
+
* so a fixture names only what the SQL reads. DatabaseServer and
|
|
82
|
+
* DatabaseServerEndpoint keep every constraint - the ORM writes them.
|
|
83
|
+
*/
|
|
84
|
+
const FIXTURE_TABLES: Array<string> = TABLES.filter((table: string) => {
|
|
85
|
+
return table !== "DatabaseServer" && table !== "DatabaseServerEndpoint";
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describePostgres("Databases SQL against Postgres", () => {
|
|
89
|
+
const schema: string = `database_server_sql_${ObjectID.generate()
|
|
90
|
+
.toString()
|
|
91
|
+
.replace(/-/g, "")}`;
|
|
92
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
93
|
+
const otherProjectId: ObjectID = ObjectID.generate();
|
|
94
|
+
const userId: ObjectID = ObjectID.generate();
|
|
95
|
+
const teamId: ObjectID = ObjectID.generate();
|
|
96
|
+
let database: DataSource;
|
|
97
|
+
|
|
98
|
+
function silence(): void {
|
|
99
|
+
for (const level of ["debug", "info", "warn"] as const) {
|
|
100
|
+
jest.spyOn(logger, level).mockImplementation(() => {
|
|
101
|
+
return undefined as never;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
beforeAll(async () => {
|
|
107
|
+
database = new DataSource({
|
|
108
|
+
type: "postgres",
|
|
109
|
+
host:
|
|
110
|
+
process.env["DATABASE_SERVER_SQL_TEST_DATABASE_HOST"] || "localhost",
|
|
111
|
+
port: Number(
|
|
112
|
+
process.env["DATABASE_SERVER_SQL_TEST_DATABASE_PORT"] || "5400",
|
|
113
|
+
),
|
|
114
|
+
username: process.env["DATABASE_USERNAME"] || "postgres",
|
|
115
|
+
password: process.env["DATABASE_PASSWORD"] || "password",
|
|
116
|
+
database:
|
|
117
|
+
process.env["DATABASE_SERVER_SQL_TEST_DATABASE_NAME"] ||
|
|
118
|
+
process.env["DATABASE_NAME"] ||
|
|
119
|
+
"oneuptimedb",
|
|
120
|
+
entities: Entities,
|
|
121
|
+
schema,
|
|
122
|
+
synchronize: false,
|
|
123
|
+
extra: { options: `-c search_path=${schema}` },
|
|
124
|
+
});
|
|
125
|
+
await database.initialize();
|
|
126
|
+
await database.query(`CREATE SCHEMA "${schema}"`);
|
|
127
|
+
for (const table of TABLES) {
|
|
128
|
+
await database.query(
|
|
129
|
+
`CREATE TABLE "${schema}"."${table}" (LIKE public."${table}" INCLUDING ALL)`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
for (const table of FIXTURE_TABLES) {
|
|
133
|
+
// Primary-key columns (a join table's pair included) stay NOT NULL.
|
|
134
|
+
const columns: Array<{ column_name: string }> = await database.query(
|
|
135
|
+
`SELECT c.column_name FROM information_schema.columns c
|
|
136
|
+
WHERE c.table_schema = $1 AND c.table_name = $2 AND c.is_nullable = 'NO'
|
|
137
|
+
AND c.column_name NOT IN (
|
|
138
|
+
SELECT a.attname FROM pg_index i
|
|
139
|
+
INNER JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
140
|
+
WHERE i.indrelid = format('%I.%I', $1::text, $2::text)::regclass AND i.indisprimary
|
|
141
|
+
)`,
|
|
142
|
+
[schema, table],
|
|
143
|
+
);
|
|
144
|
+
for (const column of columns) {
|
|
145
|
+
await database.query(
|
|
146
|
+
`ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${column.column_name}" DROP NOT NULL`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
expect(
|
|
151
|
+
(await database.query("SELECT current_schema()"))[0].current_schema,
|
|
152
|
+
).toBe(schema);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
afterAll(async () => {
|
|
156
|
+
if (database?.isInitialized) {
|
|
157
|
+
await database.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
|
|
158
|
+
await database.destroy();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let feed: jest.SpyInstance;
|
|
163
|
+
|
|
164
|
+
beforeEach(async () => {
|
|
165
|
+
silence();
|
|
166
|
+
jest.spyOn(PostgresAppInstance, "isConnected").mockReturnValue(true);
|
|
167
|
+
jest.spyOn(PostgresAppInstance, "getDataSource").mockReturnValue(database);
|
|
168
|
+
feed = jest
|
|
169
|
+
.spyOn(DatabaseServerFeedService, "createDatabaseServerFeedItem")
|
|
170
|
+
.mockResolvedValue(undefined);
|
|
171
|
+
jest
|
|
172
|
+
.spyOn(DatabaseServerService, "getDatabaseServerMarkdownLink")
|
|
173
|
+
.mockResolvedValue("[Database x](/x)");
|
|
174
|
+
jest
|
|
175
|
+
.spyOn(DatabaseServerLabelRuleEngineService, "applyRulesToDatabaseServer")
|
|
176
|
+
.mockResolvedValue(undefined);
|
|
177
|
+
jest
|
|
178
|
+
.spyOn(DatabaseServerOwnerRuleEngineService, "applyRulesToDatabaseServer")
|
|
179
|
+
.mockResolvedValue(undefined);
|
|
180
|
+
for (const service of [
|
|
181
|
+
DatabaseServerService,
|
|
182
|
+
DatabaseServerEndpointService,
|
|
183
|
+
] as Array<any>) {
|
|
184
|
+
jest.spyOn(service, "onTriggerRealtime").mockResolvedValue(undefined);
|
|
185
|
+
jest.spyOn(service, "onTriggerWorkflow").mockResolvedValue(undefined);
|
|
186
|
+
}
|
|
187
|
+
DatabaseServerService.clearAutoCreateBudgetMemo();
|
|
188
|
+
await database.query(
|
|
189
|
+
TABLES.map((table: string): string => {
|
|
190
|
+
return `DELETE FROM "${schema}"."${table}"`;
|
|
191
|
+
}).join("; "),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
afterEach(() => {
|
|
196
|
+
jest.restoreAllMocks();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
function ago(ms: number): Date {
|
|
200
|
+
return new Date(Date.now() - ms);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function insertDatabase(data: {
|
|
204
|
+
name?: string;
|
|
205
|
+
description?: string | null;
|
|
206
|
+
dbSystem?: string;
|
|
207
|
+
serverAddress?: string | null;
|
|
208
|
+
serverPort?: number | null;
|
|
209
|
+
workloadLastSeenAt?: Date | null;
|
|
210
|
+
project?: ObjectID;
|
|
211
|
+
source?: string;
|
|
212
|
+
lastSeenAt?: Date | null;
|
|
213
|
+
createdAt?: Date;
|
|
214
|
+
isArchived?: boolean;
|
|
215
|
+
autoArchivedAt?: Date | null;
|
|
216
|
+
collectorStatus?: string;
|
|
217
|
+
collectorLastSeenAt?: Date | null;
|
|
218
|
+
retainDays?: number | null;
|
|
219
|
+
retentionConfig?: unknown;
|
|
220
|
+
manuallyRestoredAt?: Date | null;
|
|
221
|
+
automaticAssignments?: unknown;
|
|
222
|
+
kubernetesClusterId?: ObjectID | null;
|
|
223
|
+
dockerHostId?: ObjectID | null;
|
|
224
|
+
podmanHostId?: ObjectID | null;
|
|
225
|
+
workloadIdentifier?: string | null;
|
|
226
|
+
deletedAt?: Date | null;
|
|
227
|
+
}): Promise<ObjectID> {
|
|
228
|
+
const id: ObjectID = ObjectID.generate();
|
|
229
|
+
const name: string = data.name || `db-${id.toString().substring(0, 8)}`;
|
|
230
|
+
await database.query(
|
|
231
|
+
`INSERT INTO "${schema}"."DatabaseServer" (
|
|
232
|
+
"_id", "version", "projectId", "name", "slug", "databaseIdentifier",
|
|
233
|
+
"dbSystem", "discoverySource", "lastSeenAt", "createdAt", "isArchived",
|
|
234
|
+
"autoArchivedAt", "otelCollectorStatus", "collectorLastSeenAt",
|
|
235
|
+
"retainTelemetryDataForDays", "telemetryRetentionConfig",
|
|
236
|
+
"manuallyRestoredAt", "automaticAssignments", "kubernetesClusterId",
|
|
237
|
+
"dockerHostId", "podmanHostId", "workloadIdentifier", "deletedAt",
|
|
238
|
+
"description", "serverAddress", "serverPort", "workloadLastSeenAt"
|
|
239
|
+
) VALUES ($1, 1, $2, $3, $4, $5, $22, $6, $7, $8, $9, $10, $11,
|
|
240
|
+
$12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $23, $24, $25, $26)`,
|
|
241
|
+
[
|
|
242
|
+
id.toString(),
|
|
243
|
+
(data.project || projectId).toString(),
|
|
244
|
+
name,
|
|
245
|
+
`${name}-${id.toString()}`,
|
|
246
|
+
`postgresql|${id.toString()}.example.com:5432`,
|
|
247
|
+
data.source === undefined
|
|
248
|
+
? DatabaseServerDiscoverySource.ClientSpans
|
|
249
|
+
: data.source,
|
|
250
|
+
data.lastSeenAt === undefined ? ago(10 * DAY_MS) : data.lastSeenAt,
|
|
251
|
+
data.createdAt || ago(30 * DAY_MS),
|
|
252
|
+
Boolean(data.isArchived),
|
|
253
|
+
data.autoArchivedAt || null,
|
|
254
|
+
data.collectorStatus || "disconnected",
|
|
255
|
+
data.collectorLastSeenAt || null,
|
|
256
|
+
data.retainDays ?? null,
|
|
257
|
+
data.retentionConfig === undefined
|
|
258
|
+
? null
|
|
259
|
+
: JSON.stringify(data.retentionConfig),
|
|
260
|
+
data.manuallyRestoredAt || null,
|
|
261
|
+
data.automaticAssignments === undefined
|
|
262
|
+
? null
|
|
263
|
+
: JSON.stringify(data.automaticAssignments),
|
|
264
|
+
data.kubernetesClusterId?.toString() || null,
|
|
265
|
+
data.dockerHostId?.toString() || null,
|
|
266
|
+
data.podmanHostId?.toString() || null,
|
|
267
|
+
data.workloadIdentifier || null,
|
|
268
|
+
data.deletedAt || null,
|
|
269
|
+
data.dbSystem || "postgresql",
|
|
270
|
+
data.description ?? null,
|
|
271
|
+
data.serverAddress ?? null,
|
|
272
|
+
data.serverPort ?? null,
|
|
273
|
+
data.workloadLastSeenAt ?? null,
|
|
274
|
+
],
|
|
275
|
+
);
|
|
276
|
+
return id;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function insertRow(
|
|
280
|
+
table: string,
|
|
281
|
+
values: Record<string, unknown>,
|
|
282
|
+
): Promise<ObjectID> {
|
|
283
|
+
const id: ObjectID = ObjectID.generate();
|
|
284
|
+
const columns: Array<string> = ["_id", ...Object.keys(values)];
|
|
285
|
+
const parameters: Array<unknown> = [
|
|
286
|
+
id.toString(),
|
|
287
|
+
...Object.values(values),
|
|
288
|
+
];
|
|
289
|
+
await database.query(
|
|
290
|
+
`INSERT INTO "${schema}"."${table}" (${columns
|
|
291
|
+
.map((column: string): string => {
|
|
292
|
+
return `"${column}"`;
|
|
293
|
+
})
|
|
294
|
+
.join(", ")}) VALUES (${parameters
|
|
295
|
+
.map((_value: unknown, index: number): string => {
|
|
296
|
+
return `$${index + 1}`;
|
|
297
|
+
})
|
|
298
|
+
.join(", ")})`,
|
|
299
|
+
parameters.map((value: unknown): unknown => {
|
|
300
|
+
return value instanceof ObjectID ? value.toString() : value;
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
return id;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function link(
|
|
307
|
+
table: string,
|
|
308
|
+
columns: Record<string, ObjectID>,
|
|
309
|
+
): Promise<void> {
|
|
310
|
+
const names: Array<string> = Object.keys(columns);
|
|
311
|
+
await database.query(
|
|
312
|
+
`INSERT INTO "${schema}"."${table}" (${names
|
|
313
|
+
.map((name: string): string => {
|
|
314
|
+
return `"${name}"`;
|
|
315
|
+
})
|
|
316
|
+
.join(", ")}) VALUES (${names
|
|
317
|
+
.map((_name: string, index: number): string => {
|
|
318
|
+
return `$${index + 1}`;
|
|
319
|
+
})
|
|
320
|
+
.join(", ")})`,
|
|
321
|
+
Object.values(columns).map((value: ObjectID): string => {
|
|
322
|
+
return value.toString();
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function databaseState(id: ObjectID): Promise<any> {
|
|
328
|
+
const rows: Array<any> = await database.query(
|
|
329
|
+
`SELECT * FROM "${schema}"."DatabaseServer" WHERE "_id" = $1`,
|
|
330
|
+
[id.toString()],
|
|
331
|
+
);
|
|
332
|
+
return rows[0];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function endpointsOf(id: ObjectID): Promise<Array<any>> {
|
|
336
|
+
return await database.query(
|
|
337
|
+
`SELECT "endpoint", "source", "isPrimary", "lastMatchedAt" FROM "${schema}"."DatabaseServerEndpoint" WHERE "databaseServerId" = $1 ORDER BY "endpoint"`,
|
|
338
|
+
[id.toString()],
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function insertEndpoint(data: {
|
|
343
|
+
databaseServerId: ObjectID;
|
|
344
|
+
endpoint: string;
|
|
345
|
+
source: string;
|
|
346
|
+
isPrimary?: boolean;
|
|
347
|
+
lastMatchedAt?: Date | null;
|
|
348
|
+
project?: ObjectID;
|
|
349
|
+
}): Promise<ObjectID> {
|
|
350
|
+
const id: ObjectID = ObjectID.generate();
|
|
351
|
+
await database.query(
|
|
352
|
+
`INSERT INTO "${schema}"."DatabaseServerEndpoint" ("_id", "version", "projectId", "databaseServerId", "endpoint", "isPrimary", "source", "lastMatchedAt")
|
|
353
|
+
VALUES ($1, 1, $2, $3, $4, $5, $6, $7)`,
|
|
354
|
+
[
|
|
355
|
+
id.toString(),
|
|
356
|
+
(data.project || projectId).toString(),
|
|
357
|
+
data.databaseServerId.toString(),
|
|
358
|
+
data.endpoint,
|
|
359
|
+
Boolean(data.isPrimary),
|
|
360
|
+
data.source,
|
|
361
|
+
data.lastMatchedAt === undefined ? new Date() : data.lastMatchedAt,
|
|
362
|
+
],
|
|
363
|
+
);
|
|
364
|
+
return id;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/*
|
|
368
|
+
* -------------------------------------------------------------------------
|
|
369
|
+
* The sweeps
|
|
370
|
+
* -------------------------------------------------------------------------
|
|
371
|
+
*/
|
|
372
|
+
test("the disconnect sweep flips only live connected rows whose collector went quiet", async () => {
|
|
373
|
+
const quiet: ObjectID = await insertDatabase({
|
|
374
|
+
collectorStatus: "connected",
|
|
375
|
+
collectorLastSeenAt: ago(HOUR_MS),
|
|
376
|
+
});
|
|
377
|
+
const neverReported: ObjectID = await insertDatabase({
|
|
378
|
+
collectorStatus: "connected",
|
|
379
|
+
collectorLastSeenAt: null,
|
|
380
|
+
});
|
|
381
|
+
const live: ObjectID = await insertDatabase({
|
|
382
|
+
collectorStatus: "connected",
|
|
383
|
+
collectorLastSeenAt: ago(60 * 1000),
|
|
384
|
+
});
|
|
385
|
+
const deleted: ObjectID = await insertDatabase({
|
|
386
|
+
collectorStatus: "connected",
|
|
387
|
+
collectorLastSeenAt: ago(HOUR_MS),
|
|
388
|
+
deletedAt: new Date(),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await expect(
|
|
392
|
+
DatabaseServerService.markDisconnectedDatabaseServers(),
|
|
393
|
+
).resolves.toBe(2);
|
|
394
|
+
|
|
395
|
+
expect((await databaseState(quiet)).otelCollectorStatus).toBe(
|
|
396
|
+
"disconnected",
|
|
397
|
+
);
|
|
398
|
+
expect((await databaseState(neverReported)).otelCollectorStatus).toBe(
|
|
399
|
+
"disconnected",
|
|
400
|
+
);
|
|
401
|
+
expect((await databaseState(live)).otelCollectorStatus).toBe("connected");
|
|
402
|
+
expect((await databaseState(deleted)).otelCollectorStatus).toBe(
|
|
403
|
+
"connected",
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
/*
|
|
408
|
+
* e2e, the Memcached agent: its heartbeat (behind the ingest fence) last
|
|
409
|
+
* wrote collectorLastSeenAt 1m51s before its last data, and the sweep
|
|
410
|
+
* flipped it 14m56s after that data - short of the documented 15 minutes.
|
|
411
|
+
* A heartbeat 16 minutes old may still be trailing data from 9 minutes
|
|
412
|
+
* ago; one 23 minutes old is 16 minutes of silence at the least.
|
|
413
|
+
*/
|
|
414
|
+
test("the disconnect sweep waits for 15 minutes of silence, not 15 minutes after the fenced heartbeat", async () => {
|
|
415
|
+
const heartbeatSixteenMinutesAgo: ObjectID = await insertDatabase({
|
|
416
|
+
collectorStatus: "connected",
|
|
417
|
+
collectorLastSeenAt: ago(16 * 60 * 1000),
|
|
418
|
+
});
|
|
419
|
+
const heartbeatTwentyThreeMinutesAgo: ObjectID = await insertDatabase({
|
|
420
|
+
collectorStatus: "connected",
|
|
421
|
+
collectorLastSeenAt: ago(23 * 60 * 1000),
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
await expect(
|
|
425
|
+
DatabaseServerService.markDisconnectedDatabaseServers(),
|
|
426
|
+
).resolves.toBe(1);
|
|
427
|
+
|
|
428
|
+
expect(
|
|
429
|
+
(await databaseState(heartbeatSixteenMinutesAgo)).otelCollectorStatus,
|
|
430
|
+
).toBe("connected");
|
|
431
|
+
expect(
|
|
432
|
+
(await databaseState(heartbeatTwentyThreeMinutesAgo)).otelCollectorStatus,
|
|
433
|
+
).toBe("disconnected");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("a row the Database Agent creates stores the agent and engine versions in the same insert", async () => {
|
|
437
|
+
const created: DatabaseServer | null =
|
|
438
|
+
await DatabaseServerService.findOrCreateByEndpoint({
|
|
439
|
+
projectId: projectId,
|
|
440
|
+
dbSystem: "mysql",
|
|
441
|
+
endpoint: { host: "mariamysql.rcv-e2e.example.net", port: 3306 },
|
|
442
|
+
discoverySource: DatabaseServerDiscoverySource.Collector,
|
|
443
|
+
allowCreate: true,
|
|
444
|
+
collector: {
|
|
445
|
+
agentVersion: "0.161.0",
|
|
446
|
+
dbVersion: "11.4.13-MariaDB-ubu2404",
|
|
447
|
+
reportedByDatabaseAgent: true,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const state: any = await databaseState(created!.id!);
|
|
452
|
+
expect(state.agentVersion).toBe("0.161.0");
|
|
453
|
+
expect(state.dbVersion).toBe("11.4.13-MariaDB-ubu2404");
|
|
454
|
+
expect(state.otelCollectorStatus).toBe("connected");
|
|
455
|
+
expect(state.collectorLastSeenAt).not.toBeNull();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("the auto-archive sweep archives exactly the stale rows nobody invested in", async () => {
|
|
459
|
+
const archived: Map<string, ObjectID> = new Map();
|
|
460
|
+
const kept: Map<string, ObjectID> = new Map();
|
|
461
|
+
|
|
462
|
+
archived.set("plain stale", await insertDatabase({}));
|
|
463
|
+
kept.set(
|
|
464
|
+
"manual",
|
|
465
|
+
await insertDatabase({ source: DatabaseServerDiscoverySource.Manual }),
|
|
466
|
+
);
|
|
467
|
+
kept.set(
|
|
468
|
+
"seen yesterday",
|
|
469
|
+
await insertDatabase({ lastSeenAt: ago(DAY_MS) }),
|
|
470
|
+
);
|
|
471
|
+
archived.set(
|
|
472
|
+
"never seen, created long ago",
|
|
473
|
+
await insertDatabase({ lastSeenAt: null, createdAt: ago(20 * DAY_MS) }),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// Labels: a person's count, a rule's / ingest's do not.
|
|
477
|
+
const labelA: ObjectID = await insertRow("Label", {
|
|
478
|
+
projectId: projectId,
|
|
479
|
+
name: "a",
|
|
480
|
+
});
|
|
481
|
+
const labelB: ObjectID = await insertRow("Label", {
|
|
482
|
+
projectId: projectId,
|
|
483
|
+
name: "b",
|
|
484
|
+
});
|
|
485
|
+
const foreignLabel: ObjectID = await insertRow("Label", {
|
|
486
|
+
projectId: otherProjectId,
|
|
487
|
+
name: "x",
|
|
488
|
+
});
|
|
489
|
+
const deletedLabel: ObjectID = await insertRow("Label", {
|
|
490
|
+
projectId: projectId,
|
|
491
|
+
name: "d",
|
|
492
|
+
deletedAt: new Date(),
|
|
493
|
+
});
|
|
494
|
+
const humanLabelled: ObjectID = await insertDatabase({});
|
|
495
|
+
await link("DatabaseServerLabel", {
|
|
496
|
+
databaseServerId: humanLabelled,
|
|
497
|
+
labelId: labelA,
|
|
498
|
+
});
|
|
499
|
+
kept.set("labelled by a person", humanLabelled);
|
|
500
|
+
const ruleLabelled: ObjectID = await insertDatabase({
|
|
501
|
+
automaticAssignments: { labelIds: [labelA.toString()] },
|
|
502
|
+
});
|
|
503
|
+
await link("DatabaseServerLabel", {
|
|
504
|
+
databaseServerId: ruleLabelled,
|
|
505
|
+
labelId: labelA,
|
|
506
|
+
});
|
|
507
|
+
archived.set("labelled only by a rule", ruleLabelled);
|
|
508
|
+
const mixedLabels: ObjectID = await insertDatabase({
|
|
509
|
+
automaticAssignments: { labelIds: [labelA.toString()] },
|
|
510
|
+
});
|
|
511
|
+
await link("DatabaseServerLabel", {
|
|
512
|
+
databaseServerId: mixedLabels,
|
|
513
|
+
labelId: labelA,
|
|
514
|
+
});
|
|
515
|
+
await link("DatabaseServerLabel", {
|
|
516
|
+
databaseServerId: mixedLabels,
|
|
517
|
+
labelId: labelB,
|
|
518
|
+
});
|
|
519
|
+
kept.set("a rule's label plus a person's", mixedLabels);
|
|
520
|
+
const foreignLabelled: ObjectID = await insertDatabase({});
|
|
521
|
+
await link("DatabaseServerLabel", {
|
|
522
|
+
databaseServerId: foreignLabelled,
|
|
523
|
+
labelId: foreignLabel,
|
|
524
|
+
});
|
|
525
|
+
archived.set("labelled from another project", foreignLabelled);
|
|
526
|
+
const deletedLabelled: ObjectID = await insertDatabase({});
|
|
527
|
+
await link("DatabaseServerLabel", {
|
|
528
|
+
databaseServerId: deletedLabelled,
|
|
529
|
+
labelId: deletedLabel,
|
|
530
|
+
});
|
|
531
|
+
archived.set("labelled with a deleted label", deletedLabelled);
|
|
532
|
+
|
|
533
|
+
// Owners: the same rule.
|
|
534
|
+
const humanOwned: ObjectID = await insertDatabase({});
|
|
535
|
+
await insertRow("DatabaseServerOwnerUser", {
|
|
536
|
+
projectId: projectId,
|
|
537
|
+
databaseServerId: humanOwned,
|
|
538
|
+
userId: userId,
|
|
539
|
+
});
|
|
540
|
+
kept.set("owned by a person-added user", humanOwned);
|
|
541
|
+
const ruleOwned: ObjectID = await insertDatabase({
|
|
542
|
+
automaticAssignments: {
|
|
543
|
+
ownerUserIds: [userId.toString()],
|
|
544
|
+
ownerTeamIds: [teamId.toString()],
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
await insertRow("DatabaseServerOwnerUser", {
|
|
548
|
+
projectId: projectId,
|
|
549
|
+
databaseServerId: ruleOwned,
|
|
550
|
+
userId: userId,
|
|
551
|
+
});
|
|
552
|
+
await insertRow("DatabaseServerOwnerTeam", {
|
|
553
|
+
projectId: projectId,
|
|
554
|
+
databaseServerId: ruleOwned,
|
|
555
|
+
teamId: teamId,
|
|
556
|
+
});
|
|
557
|
+
archived.set("owned only through owner rules", ruleOwned);
|
|
558
|
+
const teamOwned: ObjectID = await insertDatabase({});
|
|
559
|
+
await insertRow("DatabaseServerOwnerTeam", {
|
|
560
|
+
projectId: projectId,
|
|
561
|
+
databaseServerId: teamOwned,
|
|
562
|
+
teamId: teamId,
|
|
563
|
+
});
|
|
564
|
+
kept.set("owned by a person-added team", teamOwned);
|
|
565
|
+
|
|
566
|
+
// Incidents, alerts, maintenance.
|
|
567
|
+
const incident: ObjectID = await insertRow("Incident", {
|
|
568
|
+
projectId: projectId,
|
|
569
|
+
});
|
|
570
|
+
const deletedIncident: ObjectID = await insertRow("Incident", {
|
|
571
|
+
projectId: projectId,
|
|
572
|
+
deletedAt: new Date(),
|
|
573
|
+
});
|
|
574
|
+
const alert: ObjectID = await insertRow("Alert", { projectId: projectId });
|
|
575
|
+
const maintenance: ObjectID = await insertRow("ScheduledMaintenance", {
|
|
576
|
+
projectId: projectId,
|
|
577
|
+
});
|
|
578
|
+
const incidentLinked: ObjectID = await insertDatabase({});
|
|
579
|
+
await link("IncidentDatabaseServer", {
|
|
580
|
+
incidentId: incident,
|
|
581
|
+
databaseServerId: incidentLinked,
|
|
582
|
+
});
|
|
583
|
+
kept.set("linked to an incident", incidentLinked);
|
|
584
|
+
const deletedIncidentLinked: ObjectID = await insertDatabase({});
|
|
585
|
+
await link("IncidentDatabaseServer", {
|
|
586
|
+
incidentId: deletedIncident,
|
|
587
|
+
databaseServerId: deletedIncidentLinked,
|
|
588
|
+
});
|
|
589
|
+
archived.set("linked only to a deleted incident", deletedIncidentLinked);
|
|
590
|
+
const alertLinked: ObjectID = await insertDatabase({});
|
|
591
|
+
await link("AlertDatabaseServer", {
|
|
592
|
+
alertId: alert,
|
|
593
|
+
databaseServerId: alertLinked,
|
|
594
|
+
});
|
|
595
|
+
kept.set("linked to an alert", alertLinked);
|
|
596
|
+
const maintenanceLinked: ObjectID = await insertDatabase({});
|
|
597
|
+
await link("ScheduledMaintenanceDatabaseServer", {
|
|
598
|
+
scheduledMaintenanceId: maintenance,
|
|
599
|
+
databaseServerId: maintenanceLinked,
|
|
600
|
+
});
|
|
601
|
+
kept.set("linked to scheduled maintenance", maintenanceLinked);
|
|
602
|
+
|
|
603
|
+
// Endpoints and retention.
|
|
604
|
+
const userEndpoint: ObjectID = await insertDatabase({});
|
|
605
|
+
await insertEndpoint({
|
|
606
|
+
databaseServerId: userEndpoint,
|
|
607
|
+
endpoint: `${userEndpoint.toString()}.example.com:5432`,
|
|
608
|
+
source: "user",
|
|
609
|
+
});
|
|
610
|
+
kept.set("with a person-added endpoint", userEndpoint);
|
|
611
|
+
const autoEndpoint: ObjectID = await insertDatabase({});
|
|
612
|
+
await insertEndpoint({
|
|
613
|
+
databaseServerId: autoEndpoint,
|
|
614
|
+
endpoint: `${autoEndpoint.toString()}.example.com:5432`,
|
|
615
|
+
source: "workload",
|
|
616
|
+
});
|
|
617
|
+
archived.set("with discovered endpoints only", autoEndpoint);
|
|
618
|
+
kept.set(
|
|
619
|
+
"with a retention override",
|
|
620
|
+
await insertDatabase({ retainDays: 30 }),
|
|
621
|
+
);
|
|
622
|
+
archived.set(
|
|
623
|
+
"with an empty retention config",
|
|
624
|
+
await insertDatabase({ retentionConfig: {} }),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
// A person's restore.
|
|
628
|
+
kept.set(
|
|
629
|
+
"restored yesterday, unseen since",
|
|
630
|
+
await insertDatabase({ manuallyRestoredAt: ago(DAY_MS) }),
|
|
631
|
+
);
|
|
632
|
+
archived.set(
|
|
633
|
+
"restored beyond the grace period",
|
|
634
|
+
await insertDatabase({
|
|
635
|
+
manuallyRestoredAt: ago(40 * DAY_MS),
|
|
636
|
+
lastSeenAt: ago(50 * DAY_MS),
|
|
637
|
+
}),
|
|
638
|
+
);
|
|
639
|
+
archived.set(
|
|
640
|
+
"restored, seen again, then stale",
|
|
641
|
+
await insertDatabase({
|
|
642
|
+
manuallyRestoredAt: ago(20 * DAY_MS),
|
|
643
|
+
lastSeenAt: ago(10 * DAY_MS),
|
|
644
|
+
}),
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
// Parents: a dark parent keeps its databases' last-known state.
|
|
648
|
+
const darkCluster: ObjectID = await insertRow("KubernetesCluster", {
|
|
649
|
+
projectId: projectId,
|
|
650
|
+
lastSeenAt: ago(9 * DAY_MS),
|
|
651
|
+
});
|
|
652
|
+
const liveCluster: ObjectID = await insertRow("KubernetesCluster", {
|
|
653
|
+
projectId: projectId,
|
|
654
|
+
lastSeenAt: new Date(),
|
|
655
|
+
});
|
|
656
|
+
const deletedCluster: ObjectID = await insertRow("KubernetesCluster", {
|
|
657
|
+
projectId: projectId,
|
|
658
|
+
lastSeenAt: ago(9 * DAY_MS),
|
|
659
|
+
deletedAt: new Date(),
|
|
660
|
+
});
|
|
661
|
+
const darkDocker: ObjectID = await insertRow("DockerHost", {
|
|
662
|
+
projectId: projectId,
|
|
663
|
+
lastSeenAt: ago(9 * DAY_MS),
|
|
664
|
+
});
|
|
665
|
+
const darkPodman: ObjectID = await insertRow("PodmanHost", {
|
|
666
|
+
projectId: projectId,
|
|
667
|
+
lastSeenAt: ago(9 * DAY_MS),
|
|
668
|
+
});
|
|
669
|
+
kept.set(
|
|
670
|
+
"workload of a dark cluster",
|
|
671
|
+
await insertDatabase({
|
|
672
|
+
source: DatabaseServerDiscoverySource.Kubernetes,
|
|
673
|
+
kubernetesClusterId: darkCluster,
|
|
674
|
+
lastSeenAt: ago(9 * DAY_MS - 5 * 60 * 1000),
|
|
675
|
+
}),
|
|
676
|
+
);
|
|
677
|
+
archived.set(
|
|
678
|
+
"workload gone well before its cluster went dark",
|
|
679
|
+
await insertDatabase({
|
|
680
|
+
source: DatabaseServerDiscoverySource.Kubernetes,
|
|
681
|
+
kubernetesClusterId: darkCluster,
|
|
682
|
+
lastSeenAt: ago(12 * DAY_MS),
|
|
683
|
+
}),
|
|
684
|
+
);
|
|
685
|
+
archived.set(
|
|
686
|
+
"workload gone from a live cluster",
|
|
687
|
+
await insertDatabase({
|
|
688
|
+
source: DatabaseServerDiscoverySource.Kubernetes,
|
|
689
|
+
kubernetesClusterId: liveCluster,
|
|
690
|
+
lastSeenAt: ago(8 * DAY_MS),
|
|
691
|
+
}),
|
|
692
|
+
);
|
|
693
|
+
archived.set(
|
|
694
|
+
"workload of a deleted cluster",
|
|
695
|
+
await insertDatabase({
|
|
696
|
+
source: DatabaseServerDiscoverySource.Kubernetes,
|
|
697
|
+
kubernetesClusterId: deletedCluster,
|
|
698
|
+
lastSeenAt: ago(9 * DAY_MS),
|
|
699
|
+
}),
|
|
700
|
+
);
|
|
701
|
+
kept.set(
|
|
702
|
+
"container of a dark Docker host",
|
|
703
|
+
await insertDatabase({
|
|
704
|
+
source: DatabaseServerDiscoverySource.Docker,
|
|
705
|
+
dockerHostId: darkDocker,
|
|
706
|
+
lastSeenAt: ago(9 * DAY_MS),
|
|
707
|
+
}),
|
|
708
|
+
);
|
|
709
|
+
kept.set(
|
|
710
|
+
"container of a dark Podman host",
|
|
711
|
+
await insertDatabase({
|
|
712
|
+
source: DatabaseServerDiscoverySource.Podman,
|
|
713
|
+
podmanHostId: darkPodman,
|
|
714
|
+
lastSeenAt: ago(9 * DAY_MS),
|
|
715
|
+
}),
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
const personArchived: ObjectID = await insertDatabase({ isArchived: true });
|
|
719
|
+
|
|
720
|
+
const count: number =
|
|
721
|
+
await DatabaseServerService.autoArchiveStaleDatabaseServers();
|
|
722
|
+
|
|
723
|
+
const unexpected: Array<string> = [];
|
|
724
|
+
for (const [label, id] of archived) {
|
|
725
|
+
const state: any = await databaseState(id);
|
|
726
|
+
if (!state.isArchived || !state.autoArchivedAt) {
|
|
727
|
+
unexpected.push(`${label}: should have been archived`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
for (const [label, id] of kept) {
|
|
731
|
+
const state: any = await databaseState(id);
|
|
732
|
+
if (state.isArchived) {
|
|
733
|
+
unexpected.push(`${label}: should have been kept`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
expect(unexpected).toEqual([]);
|
|
737
|
+
expect(count).toBe(archived.size);
|
|
738
|
+
expect(feed).toHaveBeenCalledTimes(archived.size);
|
|
739
|
+
|
|
740
|
+
const untouched: any = await databaseState(personArchived);
|
|
741
|
+
expect(untouched.isArchived).toBe(true);
|
|
742
|
+
expect(untouched.autoArchivedAt).toBeNull();
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("the auto-create budget counts live, non-archived, non-manual rows of the project - collector rows included", async () => {
|
|
746
|
+
await insertDatabase({ source: DatabaseServerDiscoverySource.Collector });
|
|
747
|
+
await insertDatabase({ source: DatabaseServerDiscoverySource.ClientSpans });
|
|
748
|
+
await insertDatabase({ source: DatabaseServerDiscoverySource.Kubernetes });
|
|
749
|
+
await insertDatabase({ source: DatabaseServerDiscoverySource.Manual });
|
|
750
|
+
await insertDatabase({ isArchived: true });
|
|
751
|
+
await insertDatabase({ deletedAt: new Date() });
|
|
752
|
+
await insertDatabase({ project: otherProjectId });
|
|
753
|
+
|
|
754
|
+
await expect(
|
|
755
|
+
DatabaseServerService.countAutoCreatedDatabaseServers(projectId),
|
|
756
|
+
).resolves.toBe(3);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test("discovery restores only a row it archived itself", async () => {
|
|
760
|
+
const autoArchived: ObjectID = await insertDatabase({
|
|
761
|
+
isArchived: true,
|
|
762
|
+
autoArchivedAt: ago(DAY_MS),
|
|
763
|
+
});
|
|
764
|
+
const personArchived: ObjectID = await insertDatabase({ isArchived: true });
|
|
765
|
+
const service: any = DatabaseServerService;
|
|
766
|
+
|
|
767
|
+
for (const id of [autoArchived, personArchived]) {
|
|
768
|
+
const row: DatabaseServer =
|
|
769
|
+
(await DatabaseServerService.findByIdInProject(projectId, id))!;
|
|
770
|
+
await service.restoreIfAutoArchived(row);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const restored: any = await databaseState(autoArchived);
|
|
774
|
+
expect(restored.isArchived).toBe(false);
|
|
775
|
+
expect(restored.autoArchivedAt).toBeNull();
|
|
776
|
+
expect(restored.archivedAt).toBeNull();
|
|
777
|
+
expect((await databaseState(personArchived)).isArchived).toBe(true);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("automatic assignments are recorded deduped and forgotten precisely", async () => {
|
|
781
|
+
const id: ObjectID = await insertDatabase({});
|
|
782
|
+
const a: string = ObjectID.generate().toString();
|
|
783
|
+
const b: string = ObjectID.generate().toString();
|
|
784
|
+
|
|
785
|
+
await DatabaseServerService.recordAutomaticAssignments({
|
|
786
|
+
databaseServerId: id,
|
|
787
|
+
kind: "labelIds",
|
|
788
|
+
ids: [a],
|
|
789
|
+
});
|
|
790
|
+
await DatabaseServerService.recordAutomaticAssignments({
|
|
791
|
+
databaseServerId: id,
|
|
792
|
+
kind: "labelIds",
|
|
793
|
+
ids: [a, b],
|
|
794
|
+
});
|
|
795
|
+
await DatabaseServerService.recordAutomaticAssignments({
|
|
796
|
+
databaseServerId: id,
|
|
797
|
+
kind: "ownerTeamIds",
|
|
798
|
+
ids: [b],
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
let state: any = (await databaseState(id)).automaticAssignments;
|
|
802
|
+
expect([...state.labelIds].sort()).toEqual([a, b].sort());
|
|
803
|
+
expect(state.ownerTeamIds).toEqual([b]);
|
|
804
|
+
|
|
805
|
+
await DatabaseServerService.forgetAutomaticAssignments({
|
|
806
|
+
databaseServerId: id,
|
|
807
|
+
kind: "labelIds",
|
|
808
|
+
ids: [a],
|
|
809
|
+
});
|
|
810
|
+
await DatabaseServerService.forgetAutomaticAssignments({
|
|
811
|
+
databaseServerId: id,
|
|
812
|
+
kind: "ownerUserIds",
|
|
813
|
+
ids: [a],
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
state = (await databaseState(id)).automaticAssignments;
|
|
817
|
+
expect(state.labelIds).toEqual([b]);
|
|
818
|
+
expect(state.ownerTeamIds).toEqual([b]);
|
|
819
|
+
expect(state.ownerUserIds).toBeUndefined();
|
|
820
|
+
|
|
821
|
+
// A row that never had any stays NULL: a person's label counts as-is.
|
|
822
|
+
const plain: ObjectID = await insertDatabase({});
|
|
823
|
+
await DatabaseServerService.forgetAutomaticAssignments({
|
|
824
|
+
databaseServerId: plain,
|
|
825
|
+
kind: "labelIds",
|
|
826
|
+
ids: [a],
|
|
827
|
+
});
|
|
828
|
+
expect((await databaseState(plain)).automaticAssignments).toBeNull();
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("an untouched trace-discovered duplicate is found; an invested, renamed, described or recently restored one, or a workload row, is not", async () => {
|
|
832
|
+
// As discovery names a row it creates from traces.
|
|
833
|
+
function generated(host: string): {
|
|
834
|
+
name: string;
|
|
835
|
+
serverAddress: string;
|
|
836
|
+
serverPort: number;
|
|
837
|
+
} {
|
|
838
|
+
return {
|
|
839
|
+
name: `PostgreSQL ${host}:5432`,
|
|
840
|
+
serverAddress: host,
|
|
841
|
+
serverPort: 5432,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const untouched: ObjectID = await insertDatabase({
|
|
846
|
+
lastSeenAt: new Date(),
|
|
847
|
+
...generated("a.example.com"),
|
|
848
|
+
});
|
|
849
|
+
const invested: ObjectID = await insertDatabase({
|
|
850
|
+
lastSeenAt: new Date(),
|
|
851
|
+
...generated("b.example.com"),
|
|
852
|
+
});
|
|
853
|
+
const renamed: ObjectID = await insertDatabase({
|
|
854
|
+
...generated("c.example.com"),
|
|
855
|
+
name: "Orders (prod)",
|
|
856
|
+
});
|
|
857
|
+
const described: ObjectID = await insertDatabase({
|
|
858
|
+
...generated("d.example.com"),
|
|
859
|
+
description: "Checkout sessions",
|
|
860
|
+
});
|
|
861
|
+
const blankDescription: ObjectID = await insertDatabase({
|
|
862
|
+
...generated("e.example.com"),
|
|
863
|
+
description: " ",
|
|
864
|
+
});
|
|
865
|
+
const restoredLastWeek: ObjectID = await insertDatabase({
|
|
866
|
+
...generated("f.example.com"),
|
|
867
|
+
manuallyRestoredAt: ago(7 * DAY_MS),
|
|
868
|
+
lastSeenAt: new Date(),
|
|
869
|
+
});
|
|
870
|
+
const restoredLongAgo: ObjectID = await insertDatabase({
|
|
871
|
+
...generated("g.example.com"),
|
|
872
|
+
manuallyRestoredAt: ago(45 * DAY_MS),
|
|
873
|
+
});
|
|
874
|
+
const label: ObjectID = await insertRow("Label", {
|
|
875
|
+
projectId: projectId,
|
|
876
|
+
name: "keep",
|
|
877
|
+
});
|
|
878
|
+
await link("DatabaseServerLabel", {
|
|
879
|
+
databaseServerId: invested,
|
|
880
|
+
labelId: label,
|
|
881
|
+
});
|
|
882
|
+
const workload: ObjectID = await insertDatabase({
|
|
883
|
+
...generated("h.example.com"),
|
|
884
|
+
workloadIdentifier: "postgresql|kubernetes:c/ns/statefulset/pg",
|
|
885
|
+
});
|
|
886
|
+
const service: any = DatabaseServerService;
|
|
887
|
+
|
|
888
|
+
const found: Set<string> = await service.findUntouchedTraceRows(
|
|
889
|
+
projectId,
|
|
890
|
+
new Date(),
|
|
891
|
+
[
|
|
892
|
+
untouched,
|
|
893
|
+
invested,
|
|
894
|
+
renamed,
|
|
895
|
+
described,
|
|
896
|
+
blankDescription,
|
|
897
|
+
restoredLastWeek,
|
|
898
|
+
restoredLongAgo,
|
|
899
|
+
workload,
|
|
900
|
+
].map((id: ObjectID): string => {
|
|
901
|
+
return id.toString();
|
|
902
|
+
}),
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
expect(Array.from(found).sort()).toEqual(
|
|
906
|
+
[untouched, blankDescription, restoredLongAgo]
|
|
907
|
+
.map((id: ObjectID): string => {
|
|
908
|
+
return id.toString();
|
|
909
|
+
})
|
|
910
|
+
.sort(),
|
|
911
|
+
);
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
/*
|
|
915
|
+
* -------------------------------------------------------------------------
|
|
916
|
+
* Endpoint lifecycle statements
|
|
917
|
+
* -------------------------------------------------------------------------
|
|
918
|
+
*/
|
|
919
|
+
test("refresh, hand-over and release touch exactly the endpoints they name", async () => {
|
|
920
|
+
const oldRow: ObjectID = await insertDatabase({});
|
|
921
|
+
const newRow: ObjectID = await insertDatabase({});
|
|
922
|
+
const now: Date = new Date();
|
|
923
|
+
|
|
924
|
+
const stale: ObjectID = await insertEndpoint({
|
|
925
|
+
databaseServerId: oldRow,
|
|
926
|
+
endpoint: "a.example.com:5432",
|
|
927
|
+
source: "workload",
|
|
928
|
+
lastMatchedAt: ago(3 * HOUR_MS),
|
|
929
|
+
});
|
|
930
|
+
const person: ObjectID = await insertEndpoint({
|
|
931
|
+
databaseServerId: oldRow,
|
|
932
|
+
endpoint: "b.example.com:5432",
|
|
933
|
+
source: "user",
|
|
934
|
+
});
|
|
935
|
+
await insertEndpoint({
|
|
936
|
+
databaseServerId: oldRow,
|
|
937
|
+
endpoint: "c.example.com:5432",
|
|
938
|
+
source: "workload",
|
|
939
|
+
lastMatchedAt: ago(3 * HOUR_MS),
|
|
940
|
+
});
|
|
941
|
+
await insertEndpoint({
|
|
942
|
+
databaseServerId: oldRow,
|
|
943
|
+
endpoint: "primary.example.com:5432",
|
|
944
|
+
source: "workload",
|
|
945
|
+
isPrimary: true,
|
|
946
|
+
lastMatchedAt: ago(3 * HOUR_MS),
|
|
947
|
+
});
|
|
948
|
+
await insertEndpoint({
|
|
949
|
+
databaseServerId: oldRow,
|
|
950
|
+
endpoint: "auto.example.com:5432",
|
|
951
|
+
source: "auto",
|
|
952
|
+
lastMatchedAt: ago(3 * HOUR_MS),
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
await DatabaseServerEndpointService.refreshMatchedEndpoints({
|
|
956
|
+
projectId: projectId,
|
|
957
|
+
databaseServerId: oldRow,
|
|
958
|
+
endpoints: ["c.example.com:5432"],
|
|
959
|
+
now: now,
|
|
960
|
+
staleBefore: ago(HOUR_MS),
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// A person's endpoint never moves; a discovered one does, once.
|
|
964
|
+
await expect(
|
|
965
|
+
DatabaseServerEndpointService.transferEndpoint({
|
|
966
|
+
projectId: projectId,
|
|
967
|
+
endpointId: person,
|
|
968
|
+
fromDatabaseServerId: oldRow,
|
|
969
|
+
toDatabaseServerId: newRow,
|
|
970
|
+
isPrimary: false,
|
|
971
|
+
now: now,
|
|
972
|
+
}),
|
|
973
|
+
).resolves.toBe(false);
|
|
974
|
+
await expect(
|
|
975
|
+
DatabaseServerEndpointService.transferEndpoint({
|
|
976
|
+
projectId: projectId,
|
|
977
|
+
endpointId: stale,
|
|
978
|
+
fromDatabaseServerId: oldRow,
|
|
979
|
+
toDatabaseServerId: newRow,
|
|
980
|
+
isPrimary: true,
|
|
981
|
+
now: now,
|
|
982
|
+
}),
|
|
983
|
+
).resolves.toBe(true);
|
|
984
|
+
await expect(
|
|
985
|
+
DatabaseServerEndpointService.transferEndpoint({
|
|
986
|
+
projectId: projectId,
|
|
987
|
+
endpointId: stale,
|
|
988
|
+
fromDatabaseServerId: oldRow,
|
|
989
|
+
toDatabaseServerId: newRow,
|
|
990
|
+
isPrimary: true,
|
|
991
|
+
now: now,
|
|
992
|
+
}),
|
|
993
|
+
).resolves.toBe(false);
|
|
994
|
+
|
|
995
|
+
// Nothing kept: only the stale non-primary workload aliases go.
|
|
996
|
+
await expect(
|
|
997
|
+
DatabaseServerEndpointService.releaseUnproducedWorkloadEndpoints({
|
|
998
|
+
projectId: projectId,
|
|
999
|
+
databaseServerId: oldRow,
|
|
1000
|
+
keepEndpoints: [],
|
|
1001
|
+
staleBefore: ago(2 * HOUR_MS),
|
|
1002
|
+
}),
|
|
1003
|
+
).resolves.toEqual([]);
|
|
1004
|
+
|
|
1005
|
+
const oldEndpoints: Array<any> = await endpointsOf(oldRow);
|
|
1006
|
+
expect(
|
|
1007
|
+
oldEndpoints.map((row: any): string => {
|
|
1008
|
+
return row.endpoint;
|
|
1009
|
+
}),
|
|
1010
|
+
).toEqual([
|
|
1011
|
+
"auto.example.com:5432",
|
|
1012
|
+
"b.example.com:5432",
|
|
1013
|
+
"c.example.com:5432",
|
|
1014
|
+
"primary.example.com:5432",
|
|
1015
|
+
]);
|
|
1016
|
+
// "c" was re-stamped by the refresh, so it survived the release.
|
|
1017
|
+
const c: any = oldEndpoints.find((row: any) => {
|
|
1018
|
+
return row.endpoint === "c.example.com:5432";
|
|
1019
|
+
});
|
|
1020
|
+
expect(new Date(c.lastMatchedAt).getTime()).toBe(now.getTime());
|
|
1021
|
+
|
|
1022
|
+
const moved: Array<any> = await endpointsOf(newRow);
|
|
1023
|
+
expect(moved).toEqual([
|
|
1024
|
+
{
|
|
1025
|
+
endpoint: "a.example.com:5432",
|
|
1026
|
+
source: "workload",
|
|
1027
|
+
isPrimary: true,
|
|
1028
|
+
lastMatchedAt: now,
|
|
1029
|
+
},
|
|
1030
|
+
]);
|
|
1031
|
+
|
|
1032
|
+
// Once "c" is stale again and still not produced, it is released.
|
|
1033
|
+
await database.query(
|
|
1034
|
+
`UPDATE "${schema}"."DatabaseServerEndpoint" SET "lastMatchedAt" = $1 WHERE "endpoint" = 'c.example.com:5432'`,
|
|
1035
|
+
[ago(3 * HOUR_MS)],
|
|
1036
|
+
);
|
|
1037
|
+
await expect(
|
|
1038
|
+
DatabaseServerEndpointService.releaseUnproducedWorkloadEndpoints({
|
|
1039
|
+
projectId: projectId,
|
|
1040
|
+
databaseServerId: oldRow,
|
|
1041
|
+
keepEndpoints: ["primary.example.com:5432"],
|
|
1042
|
+
staleBefore: ago(2 * HOUR_MS),
|
|
1043
|
+
}),
|
|
1044
|
+
).resolves.toEqual(["c.example.com:5432"]);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
test("a workload identity change hands the Service endpoint to the new workload, end to end", async () => {
|
|
1048
|
+
const cluster: ObjectID = await insertRow("KubernetesCluster", {
|
|
1049
|
+
projectId: projectId,
|
|
1050
|
+
lastSeenAt: new Date(),
|
|
1051
|
+
});
|
|
1052
|
+
const service: string = "redis.cache.svc.cluster.local:6379@prod";
|
|
1053
|
+
const unqualified: string = "redis.cache.svc.cluster.local:6379";
|
|
1054
|
+
|
|
1055
|
+
const deployment: DatabaseServer | null =
|
|
1056
|
+
await DatabaseServerService.upsertWorkloadDatabase({
|
|
1057
|
+
projectId: projectId,
|
|
1058
|
+
workloadIdentifier: "redis|kubernetes:prod/cache/deployment/redis",
|
|
1059
|
+
dbSystem: "redis",
|
|
1060
|
+
displayName: "Redis cache/redis",
|
|
1061
|
+
discoverySource: DatabaseServerDiscoverySource.Kubernetes,
|
|
1062
|
+
aliases: [service, unqualified],
|
|
1063
|
+
memberKeysSeenNow: [],
|
|
1064
|
+
instanceCount: 1,
|
|
1065
|
+
kubernetesClusterId: cluster,
|
|
1066
|
+
kubernetesNamespace: "cache",
|
|
1067
|
+
workloadKind: "Deployment",
|
|
1068
|
+
workloadName: "redis",
|
|
1069
|
+
allowCreate: true,
|
|
1070
|
+
});
|
|
1071
|
+
expect(deployment).not.toBeNull();
|
|
1072
|
+
expect(await endpointsOf(deployment!.id!)).toEqual([
|
|
1073
|
+
expect.objectContaining({
|
|
1074
|
+
endpoint: unqualified,
|
|
1075
|
+
source: "workload",
|
|
1076
|
+
isPrimary: false,
|
|
1077
|
+
}),
|
|
1078
|
+
expect.objectContaining({
|
|
1079
|
+
endpoint: service,
|
|
1080
|
+
source: "workload",
|
|
1081
|
+
isPrimary: true,
|
|
1082
|
+
}),
|
|
1083
|
+
]);
|
|
1084
|
+
|
|
1085
|
+
/*
|
|
1086
|
+
* Two hours later: the Deployment is gone, and a second cluster means
|
|
1087
|
+
* the unqualified alias is no longer produced.
|
|
1088
|
+
*/
|
|
1089
|
+
await database.query(
|
|
1090
|
+
`UPDATE "${schema}"."DatabaseServer" SET "workloadLastSeenAt" = $1 WHERE "_id" = $2`,
|
|
1091
|
+
[ago(2 * HOUR_MS), deployment!.id!.toString()],
|
|
1092
|
+
);
|
|
1093
|
+
await database.query(
|
|
1094
|
+
`UPDATE "${schema}"."DatabaseServerEndpoint" SET "lastMatchedAt" = $1`,
|
|
1095
|
+
[ago(3 * HOUR_MS)],
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
const statefulSet: DatabaseServer | null =
|
|
1099
|
+
await DatabaseServerService.upsertWorkloadDatabase({
|
|
1100
|
+
projectId: projectId,
|
|
1101
|
+
workloadIdentifier: "redis|kubernetes:prod/cache/statefulset/redis",
|
|
1102
|
+
dbSystem: "redis",
|
|
1103
|
+
displayName: "Redis cache/redis",
|
|
1104
|
+
discoverySource: DatabaseServerDiscoverySource.Kubernetes,
|
|
1105
|
+
aliases: [service],
|
|
1106
|
+
memberKeysSeenNow: [],
|
|
1107
|
+
instanceCount: 1,
|
|
1108
|
+
kubernetesClusterId: cluster,
|
|
1109
|
+
kubernetesNamespace: "cache",
|
|
1110
|
+
workloadKind: "StatefulSet",
|
|
1111
|
+
workloadName: "redis",
|
|
1112
|
+
allowCreate: true,
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
expect(statefulSet).not.toBeNull();
|
|
1116
|
+
expect(statefulSet!.id!.toString()).not.toBe(deployment!.id!.toString());
|
|
1117
|
+
expect(await endpointsOf(statefulSet!.id!)).toEqual([
|
|
1118
|
+
expect.objectContaining({
|
|
1119
|
+
endpoint: service,
|
|
1120
|
+
source: "workload",
|
|
1121
|
+
isPrimary: true,
|
|
1122
|
+
}),
|
|
1123
|
+
]);
|
|
1124
|
+
/*
|
|
1125
|
+
* The old row keeps what nobody else serves - until its own workload
|
|
1126
|
+
* runs again, nothing releases its unqualified alias.
|
|
1127
|
+
*/
|
|
1128
|
+
expect(
|
|
1129
|
+
(await endpointsOf(deployment!.id!)).map((row: any): string => {
|
|
1130
|
+
return row.endpoint;
|
|
1131
|
+
}),
|
|
1132
|
+
).toEqual([unqualified]);
|
|
1133
|
+
const state: any = await databaseState(statefulSet!.id!);
|
|
1134
|
+
expect(state.workloadLastSeenAt).not.toBeNull();
|
|
1135
|
+
expect(state.dbSystem).toBe("redis");
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
test("the same workload under a fork of its engine stays the same database", async () => {
|
|
1139
|
+
const cluster: ObjectID = await insertRow("KubernetesCluster", {
|
|
1140
|
+
projectId: projectId,
|
|
1141
|
+
lastSeenAt: new Date(),
|
|
1142
|
+
});
|
|
1143
|
+
const input: any = {
|
|
1144
|
+
projectId: projectId,
|
|
1145
|
+
workloadIdentifier: "redis|kubernetes:prod/cache/statefulset/cache",
|
|
1146
|
+
dbSystem: "redis",
|
|
1147
|
+
displayName: "Redis cache/cache",
|
|
1148
|
+
discoverySource: DatabaseServerDiscoverySource.Kubernetes,
|
|
1149
|
+
aliases: ["cache.cache.svc.cluster.local:6379@prod"],
|
|
1150
|
+
memberKeysSeenNow: [],
|
|
1151
|
+
instanceCount: 1,
|
|
1152
|
+
kubernetesClusterId: cluster,
|
|
1153
|
+
kubernetesNamespace: "cache",
|
|
1154
|
+
workloadKind: "StatefulSet",
|
|
1155
|
+
workloadName: "cache",
|
|
1156
|
+
allowCreate: true,
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
const redis: DatabaseServer | null =
|
|
1160
|
+
await DatabaseServerService.upsertWorkloadDatabase(input);
|
|
1161
|
+
const valkey: DatabaseServer | null =
|
|
1162
|
+
await DatabaseServerService.upsertWorkloadDatabase({
|
|
1163
|
+
...input,
|
|
1164
|
+
workloadIdentifier: "valkey|kubernetes:prod/cache/statefulset/cache",
|
|
1165
|
+
dbSystem: "valkey",
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
expect(valkey!.id!.toString()).toBe(redis!.id!.toString());
|
|
1169
|
+
const state: any = await databaseState(redis!.id!);
|
|
1170
|
+
expect(state.workloadIdentifier).toBe(
|
|
1171
|
+
"valkey|kubernetes:prod/cache/statefulset/cache",
|
|
1172
|
+
);
|
|
1173
|
+
expect(state.dbSystem).toBe("valkey");
|
|
1174
|
+
expect(state.dbSystemSource).toBe("container");
|
|
1175
|
+
expect(state.name).toBe("Valkey cache/cache");
|
|
1176
|
+
const rows: Array<any> = await database.query(
|
|
1177
|
+
`SELECT "_id" FROM "${schema}"."DatabaseServer"`,
|
|
1178
|
+
);
|
|
1179
|
+
expect(rows).toHaveLength(1);
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
test("a workload that stops producing an alias releases it after the grace period", async () => {
|
|
1183
|
+
const cluster: ObjectID = await insertRow("KubernetesCluster", {
|
|
1184
|
+
projectId: projectId,
|
|
1185
|
+
lastSeenAt: new Date(),
|
|
1186
|
+
});
|
|
1187
|
+
const input: any = {
|
|
1188
|
+
projectId: projectId,
|
|
1189
|
+
workloadIdentifier: "postgresql|kubernetes:prod/data/statefulset/pg",
|
|
1190
|
+
dbSystem: "postgresql",
|
|
1191
|
+
displayName: "PostgreSQL data/pg",
|
|
1192
|
+
discoverySource: DatabaseServerDiscoverySource.Kubernetes,
|
|
1193
|
+
aliases: [
|
|
1194
|
+
"pg.data.svc.cluster.local:5432@prod",
|
|
1195
|
+
"pg.data.svc.cluster.local:5432",
|
|
1196
|
+
],
|
|
1197
|
+
memberKeysSeenNow: [],
|
|
1198
|
+
instanceCount: 1,
|
|
1199
|
+
kubernetesClusterId: cluster,
|
|
1200
|
+
kubernetesNamespace: "data",
|
|
1201
|
+
workloadKind: "StatefulSet",
|
|
1202
|
+
workloadName: "pg",
|
|
1203
|
+
allowCreate: true,
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
const row: DatabaseServer | null =
|
|
1207
|
+
await DatabaseServerService.upsertWorkloadDatabase(input);
|
|
1208
|
+
|
|
1209
|
+
// Not produced once, but produced recently: kept.
|
|
1210
|
+
await DatabaseServerService.upsertWorkloadDatabase({
|
|
1211
|
+
...input,
|
|
1212
|
+
aliases: ["pg.data.svc.cluster.local:5432@prod"],
|
|
1213
|
+
});
|
|
1214
|
+
expect((await endpointsOf(row!.id!)).length).toBe(2);
|
|
1215
|
+
|
|
1216
|
+
await database.query(
|
|
1217
|
+
`UPDATE "${schema}"."DatabaseServerEndpoint" SET "lastMatchedAt" = $1 WHERE "endpoint" = 'pg.data.svc.cluster.local:5432'`,
|
|
1218
|
+
[ago(3 * HOUR_MS)],
|
|
1219
|
+
);
|
|
1220
|
+
await DatabaseServerService.upsertWorkloadDatabase({
|
|
1221
|
+
...input,
|
|
1222
|
+
aliases: ["pg.data.svc.cluster.local:5432@prod"],
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
expect(
|
|
1226
|
+
(await endpointsOf(row!.id!)).map((endpoint: any): string => {
|
|
1227
|
+
return endpoint.endpoint;
|
|
1228
|
+
}),
|
|
1229
|
+
).toEqual(["pg.data.svc.cluster.local:5432@prod"]);
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
test("a workload seen again with nothing new is not rewritten; a change is written at once", async () => {
|
|
1233
|
+
const cluster: ObjectID = await insertRow("KubernetesCluster", {
|
|
1234
|
+
projectId: projectId,
|
|
1235
|
+
lastSeenAt: new Date(),
|
|
1236
|
+
});
|
|
1237
|
+
const input: any = {
|
|
1238
|
+
projectId: projectId,
|
|
1239
|
+
workloadIdentifier: "mysql|kubernetes:prod/shop/statefulset/orders",
|
|
1240
|
+
dbSystem: "mysql",
|
|
1241
|
+
displayName: "MySQL shop/orders",
|
|
1242
|
+
discoverySource: DatabaseServerDiscoverySource.Kubernetes,
|
|
1243
|
+
aliases: ["orders.shop.svc.cluster.local:3306@prod"],
|
|
1244
|
+
memberKeysSeenNow: ["0123456789abcdef"],
|
|
1245
|
+
instanceCount: 1,
|
|
1246
|
+
dbVersion: "8.0",
|
|
1247
|
+
kubernetesClusterId: cluster,
|
|
1248
|
+
kubernetesNamespace: "shop",
|
|
1249
|
+
workloadKind: "StatefulSet",
|
|
1250
|
+
workloadName: "orders",
|
|
1251
|
+
allowCreate: true,
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
const row: DatabaseServer | null =
|
|
1255
|
+
await DatabaseServerService.upsertWorkloadDatabase(input);
|
|
1256
|
+
const created: any = await databaseState(row!.id!);
|
|
1257
|
+
|
|
1258
|
+
// Every column the workload path reads back exists on the migrated table.
|
|
1259
|
+
const again: DatabaseServer | null =
|
|
1260
|
+
await DatabaseServerService.upsertWorkloadDatabase(input);
|
|
1261
|
+
expect(again!.id!.toString()).toBe(row!.id!.toString());
|
|
1262
|
+
const unchanged: any = await databaseState(row!.id!);
|
|
1263
|
+
expect(unchanged.updatedAt.toISOString()).toBe(
|
|
1264
|
+
created.updatedAt.toISOString(),
|
|
1265
|
+
);
|
|
1266
|
+
expect(unchanged.lastSeenAt.toISOString()).toBe(
|
|
1267
|
+
created.lastSeenAt.toISOString(),
|
|
1268
|
+
);
|
|
1269
|
+
expect(unchanged.version).toBe(created.version);
|
|
1270
|
+
|
|
1271
|
+
await DatabaseServerService.upsertWorkloadDatabase({
|
|
1272
|
+
...input,
|
|
1273
|
+
instanceCount: 3,
|
|
1274
|
+
dbSystem: "mariadb",
|
|
1275
|
+
});
|
|
1276
|
+
const changed: any = await databaseState(row!.id!);
|
|
1277
|
+
expect(changed.instanceCount).toBe(3);
|
|
1278
|
+
expect(changed.dbSystem).toBe("mariadb");
|
|
1279
|
+
expect(changed.name).toBe("MariaDB shop/orders");
|
|
1280
|
+
expect(changed.lastSeenAt.getTime()).toBeGreaterThanOrEqual(
|
|
1281
|
+
created.lastSeenAt.getTime(),
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
// A quarter of an hour on, an unchanged workload refreshes its liveness.
|
|
1285
|
+
await database.query(
|
|
1286
|
+
`UPDATE "${schema}"."DatabaseServer" SET "lastSeenAt" = $1, "workloadLastSeenAt" = $1 WHERE "_id" = $2`,
|
|
1287
|
+
[ago(20 * 60 * 1000), row!.id!.toString()],
|
|
1288
|
+
);
|
|
1289
|
+
const before: number = Date.now();
|
|
1290
|
+
await DatabaseServerService.upsertWorkloadDatabase({
|
|
1291
|
+
...input,
|
|
1292
|
+
instanceCount: 3,
|
|
1293
|
+
dbSystem: "mariadb",
|
|
1294
|
+
});
|
|
1295
|
+
const refreshed: any = await databaseState(row!.id!);
|
|
1296
|
+
expect(refreshed.workloadLastSeenAt.getTime()).toBeGreaterThanOrEqual(
|
|
1297
|
+
before - 1000,
|
|
1298
|
+
);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
/*
|
|
1302
|
+
* -------------------------------------------------------------------------
|
|
1303
|
+
* The collector status is only ever a collector's
|
|
1304
|
+
* -------------------------------------------------------------------------
|
|
1305
|
+
*/
|
|
1306
|
+
test("a database found from traces stores no collector status - and the sweep leaves it empty", async () => {
|
|
1307
|
+
const traces: DatabaseServer | null =
|
|
1308
|
+
await DatabaseServerService.findOrCreateByEndpoint({
|
|
1309
|
+
projectId: projectId,
|
|
1310
|
+
dbSystem: "postgresql",
|
|
1311
|
+
endpoint: { host: "orders-db.example.com", port: 5432 },
|
|
1312
|
+
discoverySource: DatabaseServerDiscoverySource.ClientSpans,
|
|
1313
|
+
allowCreate: true,
|
|
1314
|
+
});
|
|
1315
|
+
const collector: DatabaseServer | null =
|
|
1316
|
+
await DatabaseServerService.findOrCreateByEndpoint({
|
|
1317
|
+
projectId: projectId,
|
|
1318
|
+
dbSystem: "postgresql",
|
|
1319
|
+
endpoint: { host: "billing-db.example.com", port: 5432 },
|
|
1320
|
+
discoverySource: DatabaseServerDiscoverySource.Collector,
|
|
1321
|
+
allowCreate: true,
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
expect((await databaseState(traces!.id!)).otelCollectorStatus).toBeNull();
|
|
1325
|
+
expect((await databaseState(collector!.id!)).otelCollectorStatus).toBe(
|
|
1326
|
+
"connected",
|
|
1327
|
+
);
|
|
1328
|
+
|
|
1329
|
+
await DatabaseServerService.markDisconnectedDatabaseServers();
|
|
1330
|
+
|
|
1331
|
+
expect((await databaseState(traces!.id!)).otelCollectorStatus).toBeNull();
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
/*
|
|
1335
|
+
* -------------------------------------------------------------------------
|
|
1336
|
+
* The engine compare-and-set
|
|
1337
|
+
* -------------------------------------------------------------------------
|
|
1338
|
+
*/
|
|
1339
|
+
test("a collector corrects a trace-found engine in one compare-and-set; a writer that read the row before loses and announces nothing", async () => {
|
|
1340
|
+
const service: any = DatabaseServerService;
|
|
1341
|
+
const endpoint: { host: string; port: number } = {
|
|
1342
|
+
host: "orders-db.example.com",
|
|
1343
|
+
port: 5432,
|
|
1344
|
+
};
|
|
1345
|
+
const traces: DatabaseServer | null =
|
|
1346
|
+
await DatabaseServerService.findOrCreateByEndpoint({
|
|
1347
|
+
projectId: projectId,
|
|
1348
|
+
dbSystem: "postgresql",
|
|
1349
|
+
endpoint: endpoint,
|
|
1350
|
+
discoverySource: DatabaseServerDiscoverySource.ClientSpans,
|
|
1351
|
+
allowCreate: true,
|
|
1352
|
+
});
|
|
1353
|
+
// What a second writer read before the correction below.
|
|
1354
|
+
const staleRead: DatabaseServer | null =
|
|
1355
|
+
await DatabaseServerService.findByIdInProject(projectId, traces!.id!);
|
|
1356
|
+
feed.mockClear();
|
|
1357
|
+
|
|
1358
|
+
await DatabaseServerService.findOrCreateByEndpoint({
|
|
1359
|
+
projectId: projectId,
|
|
1360
|
+
dbSystem: "mysql",
|
|
1361
|
+
endpoint: endpoint,
|
|
1362
|
+
discoverySource: DatabaseServerDiscoverySource.Collector,
|
|
1363
|
+
allowCreate: false,
|
|
1364
|
+
});
|
|
1365
|
+
let state: any = await databaseState(traces!.id!);
|
|
1366
|
+
expect(state.dbSystem).toBe("mysql");
|
|
1367
|
+
expect(state.dbSystemSource).toBe("collector");
|
|
1368
|
+
expect(state.name).toBe("MySQL orders-db.example.com:5432");
|
|
1369
|
+
expect(feed).toHaveBeenCalledTimes(1);
|
|
1370
|
+
|
|
1371
|
+
// The stale writer weighs its own evidence against what it read: lost.
|
|
1372
|
+
await service.applyDatabaseSystemEvidence(staleRead, {
|
|
1373
|
+
system: "mongodb",
|
|
1374
|
+
evidence: "collector",
|
|
1375
|
+
});
|
|
1376
|
+
state = await databaseState(traces!.id!);
|
|
1377
|
+
expect(state.dbSystem).toBe("mysql");
|
|
1378
|
+
expect(feed).toHaveBeenCalledTimes(1);
|
|
1379
|
+
expect(staleRead!.dbSystem).toBe("postgresql");
|
|
1380
|
+
|
|
1381
|
+
// A person's name survives an engine change made after they renamed it.
|
|
1382
|
+
await database.query(
|
|
1383
|
+
`UPDATE "${schema}"."DatabaseServer" SET "name" = 'Orders (prod)' WHERE "_id" = $1`,
|
|
1384
|
+
[traces!.id!.toString()],
|
|
1385
|
+
);
|
|
1386
|
+
const beforeRename: DatabaseServer | null =
|
|
1387
|
+
await DatabaseServerService.findByIdInProject(projectId, traces!.id!);
|
|
1388
|
+
beforeRename!.name = "MySQL orders-db.example.com:5432";
|
|
1389
|
+
await service.applyDatabaseSystemEvidence(beforeRename, {
|
|
1390
|
+
system: "mariadb",
|
|
1391
|
+
evidence: "collector",
|
|
1392
|
+
});
|
|
1393
|
+
state = await databaseState(traces!.id!);
|
|
1394
|
+
expect(state.dbSystem).toBe("mariadb");
|
|
1395
|
+
expect(state.name).toBe("Orders (prod)");
|
|
1396
|
+
expect(beforeRename!.name).toBe("Orders (prod)");
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
/*
|
|
1400
|
+
* -------------------------------------------------------------------------
|
|
1401
|
+
* A workload's archived row and application traces
|
|
1402
|
+
* -------------------------------------------------------------------------
|
|
1403
|
+
*/
|
|
1404
|
+
test("a trace naming a retired workload row neither sights nor restores it; a collector still resolves to it", async () => {
|
|
1405
|
+
const lastSeenAt: Date = ago(8 * DAY_MS);
|
|
1406
|
+
const retired: ObjectID = await insertDatabase({
|
|
1407
|
+
source: DatabaseServerDiscoverySource.Kubernetes,
|
|
1408
|
+
workloadIdentifier: "postgresql|kubernetes:prod/data/statefulset/pg",
|
|
1409
|
+
workloadLastSeenAt: lastSeenAt,
|
|
1410
|
+
lastSeenAt: lastSeenAt,
|
|
1411
|
+
isArchived: true,
|
|
1412
|
+
autoArchivedAt: ago(DAY_MS),
|
|
1413
|
+
});
|
|
1414
|
+
await insertEndpoint({
|
|
1415
|
+
databaseServerId: retired,
|
|
1416
|
+
endpoint: "pg.data.svc.cluster.local:5432@prod",
|
|
1417
|
+
source: "workload",
|
|
1418
|
+
isPrimary: true,
|
|
1419
|
+
});
|
|
1420
|
+
const endpoint: {
|
|
1421
|
+
host: string;
|
|
1422
|
+
port: number;
|
|
1423
|
+
kubernetesClusterName: string;
|
|
1424
|
+
} = {
|
|
1425
|
+
host: "pg.data.svc.cluster.local",
|
|
1426
|
+
port: 5432,
|
|
1427
|
+
kubernetesClusterName: "prod",
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
await expect(
|
|
1431
|
+
DatabaseServerService.findOrCreateByEndpoint({
|
|
1432
|
+
projectId: projectId,
|
|
1433
|
+
dbSystem: "postgresql",
|
|
1434
|
+
endpoint: endpoint,
|
|
1435
|
+
discoverySource: DatabaseServerDiscoverySource.ClientSpans,
|
|
1436
|
+
allowCreate: true,
|
|
1437
|
+
}),
|
|
1438
|
+
).resolves.toBeNull();
|
|
1439
|
+
const state: any = await databaseState(retired);
|
|
1440
|
+
expect(state.isArchived).toBe(true);
|
|
1441
|
+
expect(state.lastSeenAt.getTime()).toBe(lastSeenAt.getTime());
|
|
1442
|
+
const rows: Array<any> = await database.query(
|
|
1443
|
+
`SELECT "_id" FROM "${schema}"."DatabaseServer"`,
|
|
1444
|
+
);
|
|
1445
|
+
expect(rows).toHaveLength(1);
|
|
1446
|
+
|
|
1447
|
+
const collector: DatabaseServer | null =
|
|
1448
|
+
await DatabaseServerService.findOrCreateByEndpoint({
|
|
1449
|
+
projectId: projectId,
|
|
1450
|
+
dbSystem: "postgresql",
|
|
1451
|
+
endpoint: endpoint,
|
|
1452
|
+
discoverySource: DatabaseServerDiscoverySource.Collector,
|
|
1453
|
+
allowCreate: false,
|
|
1454
|
+
});
|
|
1455
|
+
expect(collector!.id!.toString()).toBe(retired.toString());
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
test("the archive sweep tells a workload's row apart in its feed item", async () => {
|
|
1459
|
+
await insertDatabase({
|
|
1460
|
+
source: DatabaseServerDiscoverySource.Kubernetes,
|
|
1461
|
+
workloadIdentifier: "postgresql|kubernetes:prod/data/statefulset/pg",
|
|
1462
|
+
});
|
|
1463
|
+
await insertDatabase({});
|
|
1464
|
+
|
|
1465
|
+
await expect(
|
|
1466
|
+
DatabaseServerService.autoArchiveStaleDatabaseServers(),
|
|
1467
|
+
).resolves.toBe(2);
|
|
1468
|
+
|
|
1469
|
+
const texts: Array<string> = feed.mock.calls
|
|
1470
|
+
.map((call: Array<any>): string => {
|
|
1471
|
+
return call[0].moreInformationInMarkdown;
|
|
1472
|
+
})
|
|
1473
|
+
.sort();
|
|
1474
|
+
expect(texts).toHaveLength(2);
|
|
1475
|
+
expect(
|
|
1476
|
+
texts.filter((text: string): boolean => {
|
|
1477
|
+
return text.includes("application traces alone do not restore it");
|
|
1478
|
+
}),
|
|
1479
|
+
).toHaveLength(1);
|
|
1480
|
+
expect(
|
|
1481
|
+
texts.filter((text: string): boolean => {
|
|
1482
|
+
return text.includes(
|
|
1483
|
+
"restored automatically as soon as it is seen again",
|
|
1484
|
+
);
|
|
1485
|
+
}),
|
|
1486
|
+
).toHaveLength(1);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
/*
|
|
1490
|
+
* -------------------------------------------------------------------------
|
|
1491
|
+
* The cluster names a manual create's refusal lists
|
|
1492
|
+
* -------------------------------------------------------------------------
|
|
1493
|
+
*/
|
|
1494
|
+
function clusterReaderProps(
|
|
1495
|
+
labelIds: Array<ObjectID> = [],
|
|
1496
|
+
): DatabaseCommonInteractionProps {
|
|
1497
|
+
return {
|
|
1498
|
+
userId: userId,
|
|
1499
|
+
tenantId: projectId,
|
|
1500
|
+
userGlobalAccessPermission: {
|
|
1501
|
+
projectIds: [projectId],
|
|
1502
|
+
globalPermissions: [Permission.Public, Permission.User],
|
|
1503
|
+
_type: "UserGlobalAccessPermission",
|
|
1504
|
+
},
|
|
1505
|
+
userTenantAccessPermission: {
|
|
1506
|
+
[projectId.toString()]: {
|
|
1507
|
+
projectId: projectId,
|
|
1508
|
+
permissions: [
|
|
1509
|
+
{
|
|
1510
|
+
permission: Permission.ReadKubernetesCluster,
|
|
1511
|
+
labelIds: labelIds,
|
|
1512
|
+
scope:
|
|
1513
|
+
labelIds.length > 0
|
|
1514
|
+
? PermissionScope.Labels
|
|
1515
|
+
: PermissionScope.All,
|
|
1516
|
+
isBlockPermission: false,
|
|
1517
|
+
_type: "UserPermission",
|
|
1518
|
+
},
|
|
1519
|
+
],
|
|
1520
|
+
_type: "UserTenantAccessPermission",
|
|
1521
|
+
},
|
|
1522
|
+
},
|
|
1523
|
+
userTeamIds: [],
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
test("whether the project has clusters is read as root; their names only through the caller's own cluster permissions", async () => {
|
|
1528
|
+
const service: any = DatabaseServerService;
|
|
1529
|
+
const teamA: ObjectID = await insertRow("Label", {
|
|
1530
|
+
projectId: projectId,
|
|
1531
|
+
name: "team-a",
|
|
1532
|
+
});
|
|
1533
|
+
const clusters: Map<string, ObjectID> = new Map();
|
|
1534
|
+
for (const clusterIdentifier of [
|
|
1535
|
+
"staging",
|
|
1536
|
+
"prod-eu",
|
|
1537
|
+
"acme-internal-pci",
|
|
1538
|
+
]) {
|
|
1539
|
+
clusters.set(
|
|
1540
|
+
clusterIdentifier,
|
|
1541
|
+
await insertRow("KubernetesCluster", {
|
|
1542
|
+
projectId: projectId,
|
|
1543
|
+
clusterIdentifier: clusterIdentifier,
|
|
1544
|
+
}),
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
await link("KubernetesClusterLabel", {
|
|
1548
|
+
kubernetesClusterId: clusters.get("staging")!,
|
|
1549
|
+
labelId: teamA,
|
|
1550
|
+
});
|
|
1551
|
+
await insertRow("KubernetesCluster", {
|
|
1552
|
+
projectId: projectId,
|
|
1553
|
+
clusterIdentifier: "decommissioned",
|
|
1554
|
+
deletedAt: new Date(),
|
|
1555
|
+
});
|
|
1556
|
+
await insertRow("KubernetesCluster", {
|
|
1557
|
+
projectId: projectId,
|
|
1558
|
+
clusterIdentifier: "",
|
|
1559
|
+
});
|
|
1560
|
+
await insertRow("KubernetesCluster", {
|
|
1561
|
+
projectId: otherProjectId,
|
|
1562
|
+
clusterIdentifier: "theirs",
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
await expect(service.hasKubernetesClusters(projectId)).resolves.toBe(true);
|
|
1566
|
+
await expect(
|
|
1567
|
+
service.hasKubernetesClusters(ObjectID.generate()),
|
|
1568
|
+
).resolves.toBe(false);
|
|
1569
|
+
|
|
1570
|
+
// Project-wide read: every live, named cluster of the project, sorted.
|
|
1571
|
+
await expect(
|
|
1572
|
+
service.findReadableKubernetesClusterNames(
|
|
1573
|
+
projectId,
|
|
1574
|
+
clusterReaderProps(),
|
|
1575
|
+
),
|
|
1576
|
+
).resolves.toEqual(["acme-internal-pci", "prod-eu", "staging"]);
|
|
1577
|
+
// Label-scoped read: only the clusters with that label.
|
|
1578
|
+
await expect(
|
|
1579
|
+
service.findReadableKubernetesClusterNames(
|
|
1580
|
+
projectId,
|
|
1581
|
+
clusterReaderProps([teamA]),
|
|
1582
|
+
),
|
|
1583
|
+
).resolves.toEqual(["staging"]);
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
/*
|
|
1587
|
+
* -------------------------------------------------------------------------
|
|
1588
|
+
* A person adding an alias: label- and Owned-scoped edit permission
|
|
1589
|
+
* -------------------------------------------------------------------------
|
|
1590
|
+
*/
|
|
1591
|
+
describe("a person adding an alias", () => {
|
|
1592
|
+
function scopedProps(
|
|
1593
|
+
scope: PermissionScope,
|
|
1594
|
+
labelIds: Array<ObjectID> = [],
|
|
1595
|
+
): DatabaseCommonInteractionProps {
|
|
1596
|
+
const permissions: Array<UserPermission> = [
|
|
1597
|
+
Permission.EditDatabaseServer,
|
|
1598
|
+
Permission.ReadDatabaseServer,
|
|
1599
|
+
].map((permission: Permission): UserPermission => {
|
|
1600
|
+
return {
|
|
1601
|
+
permission,
|
|
1602
|
+
labelIds,
|
|
1603
|
+
scope,
|
|
1604
|
+
isBlockPermission: false,
|
|
1605
|
+
_type: "UserPermission",
|
|
1606
|
+
};
|
|
1607
|
+
});
|
|
1608
|
+
return {
|
|
1609
|
+
userId: userId,
|
|
1610
|
+
tenantId: projectId,
|
|
1611
|
+
userGlobalAccessPermission: {
|
|
1612
|
+
projectIds: [projectId],
|
|
1613
|
+
globalPermissions: [Permission.Public, Permission.User],
|
|
1614
|
+
_type: "UserGlobalAccessPermission",
|
|
1615
|
+
},
|
|
1616
|
+
userTenantAccessPermission: {
|
|
1617
|
+
[projectId.toString()]: {
|
|
1618
|
+
projectId: projectId,
|
|
1619
|
+
permissions: permissions,
|
|
1620
|
+
_type: "UserTenantAccessPermission",
|
|
1621
|
+
},
|
|
1622
|
+
},
|
|
1623
|
+
userTeamIds: [],
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function alias(databaseServerId: ObjectID, endpoint: string): any {
|
|
1628
|
+
const data: DatabaseServerEndpoint = new DatabaseServerEndpoint();
|
|
1629
|
+
data.databaseServerId = databaseServerId;
|
|
1630
|
+
data.endpoint = endpoint;
|
|
1631
|
+
return data;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
test("a label-scoped editor adds aliases to their label's databases only", async () => {
|
|
1635
|
+
const teamA: ObjectID = await insertRow("Label", {
|
|
1636
|
+
projectId: projectId,
|
|
1637
|
+
name: "team-a",
|
|
1638
|
+
});
|
|
1639
|
+
const teamB: ObjectID = await insertRow("Label", {
|
|
1640
|
+
projectId: projectId,
|
|
1641
|
+
name: "team-b",
|
|
1642
|
+
});
|
|
1643
|
+
const ours: ObjectID = await insertDatabase({ name: "orders (team A)" });
|
|
1644
|
+
const theirs: ObjectID = await insertDatabase({
|
|
1645
|
+
name: "payments (team B)",
|
|
1646
|
+
});
|
|
1647
|
+
await link("DatabaseServerLabel", {
|
|
1648
|
+
databaseServerId: ours,
|
|
1649
|
+
labelId: teamA,
|
|
1650
|
+
});
|
|
1651
|
+
await link("DatabaseServerLabel", {
|
|
1652
|
+
databaseServerId: theirs,
|
|
1653
|
+
labelId: teamB,
|
|
1654
|
+
});
|
|
1655
|
+
await insertEndpoint({
|
|
1656
|
+
databaseServerId: theirs,
|
|
1657
|
+
endpoint: "taken.example.com:5432",
|
|
1658
|
+
source: "auto",
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
await DatabaseServerEndpointService.create({
|
|
1662
|
+
data: alias(ours, "orders-replica.example.com"),
|
|
1663
|
+
props: scopedProps(PermissionScope.Labels, [teamA]),
|
|
1664
|
+
});
|
|
1665
|
+
expect(
|
|
1666
|
+
(await endpointsOf(ours)).map((row: any): string => {
|
|
1667
|
+
return row.endpoint;
|
|
1668
|
+
}),
|
|
1669
|
+
).toEqual(["orders-replica.example.com:5432"]);
|
|
1670
|
+
|
|
1671
|
+
await expect(
|
|
1672
|
+
DatabaseServerEndpointService.create({
|
|
1673
|
+
data: alias(theirs, "hijack.example.com"),
|
|
1674
|
+
props: scopedProps(PermissionScope.Labels, [teamA]),
|
|
1675
|
+
}),
|
|
1676
|
+
).rejects.toThrow("you do not have permission to edit it");
|
|
1677
|
+
expect(await endpointsOf(theirs)).toHaveLength(1);
|
|
1678
|
+
|
|
1679
|
+
// The owner of a colliding endpoint is not named to someone who cannot read it.
|
|
1680
|
+
const error: Error = (await DatabaseServerEndpointService.create({
|
|
1681
|
+
data: alias(ours, "taken.example.com"),
|
|
1682
|
+
props: scopedProps(PermissionScope.Labels, [teamA]),
|
|
1683
|
+
}).catch((e: unknown) => {
|
|
1684
|
+
return e;
|
|
1685
|
+
})) as Error;
|
|
1686
|
+
expect(error.message).toContain("already belongs to another database");
|
|
1687
|
+
expect(error.message).not.toContain("payments");
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
test("an Owned-scoped editor adds aliases to databases they own only", async () => {
|
|
1691
|
+
const owned: ObjectID = await insertDatabase({ name: "owned" });
|
|
1692
|
+
const notOwned: ObjectID = await insertDatabase({ name: "not owned" });
|
|
1693
|
+
await insertRow("DatabaseServerOwnerUser", {
|
|
1694
|
+
projectId: projectId,
|
|
1695
|
+
databaseServerId: owned,
|
|
1696
|
+
userId: userId,
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
await DatabaseServerEndpointService.create({
|
|
1700
|
+
data: alias(owned, "owned-replica.example.com"),
|
|
1701
|
+
props: scopedProps(PermissionScope.Owned),
|
|
1702
|
+
});
|
|
1703
|
+
expect(await endpointsOf(owned)).toHaveLength(1);
|
|
1704
|
+
|
|
1705
|
+
await expect(
|
|
1706
|
+
DatabaseServerEndpointService.create({
|
|
1707
|
+
data: alias(notOwned, "not-owned-replica.example.com"),
|
|
1708
|
+
props: scopedProps(PermissionScope.Owned),
|
|
1709
|
+
}),
|
|
1710
|
+
).rejects.toThrow("you do not have permission to edit it");
|
|
1711
|
+
expect(await endpointsOf(notOwned)).toHaveLength(0);
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
test("a relation object cannot point an alias at another project's database", async () => {
|
|
1715
|
+
const foreign: ObjectID = await insertDatabase({
|
|
1716
|
+
project: otherProjectId,
|
|
1717
|
+
});
|
|
1718
|
+
const ours: ObjectID = await insertDatabase({});
|
|
1719
|
+
const data: any = alias(ours, "cross.example.com");
|
|
1720
|
+
data.databaseServer = new DatabaseServer(foreign);
|
|
1721
|
+
|
|
1722
|
+
await expect(
|
|
1723
|
+
DatabaseServerEndpointService.create({
|
|
1724
|
+
data: data,
|
|
1725
|
+
props: scopedProps(PermissionScope.All),
|
|
1726
|
+
}),
|
|
1727
|
+
).rejects.toThrow("Conflicting database references");
|
|
1728
|
+
|
|
1729
|
+
const onlyRelation: any = new DatabaseServerEndpoint();
|
|
1730
|
+
onlyRelation.endpoint = "cross.example.com";
|
|
1731
|
+
onlyRelation.databaseServer = new DatabaseServer(foreign);
|
|
1732
|
+
await expect(
|
|
1733
|
+
DatabaseServerEndpointService.create({
|
|
1734
|
+
data: onlyRelation,
|
|
1735
|
+
props: scopedProps(PermissionScope.All),
|
|
1736
|
+
}),
|
|
1737
|
+
).rejects.toThrow("you do not have permission to edit it");
|
|
1738
|
+
|
|
1739
|
+
const rows: Array<any> = await database.query(
|
|
1740
|
+
`SELECT "_id" FROM "${schema}"."DatabaseServerEndpoint"`,
|
|
1741
|
+
);
|
|
1742
|
+
expect(rows).toHaveLength(0);
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
test("a label-scoped editor removes aliases of their label's databases only - never another team's", async () => {
|
|
1746
|
+
const teamA: ObjectID = await insertRow("Label", {
|
|
1747
|
+
projectId: projectId,
|
|
1748
|
+
name: "team-a",
|
|
1749
|
+
});
|
|
1750
|
+
const teamB: ObjectID = await insertRow("Label", {
|
|
1751
|
+
projectId: projectId,
|
|
1752
|
+
name: "team-b",
|
|
1753
|
+
});
|
|
1754
|
+
const ours: ObjectID = await insertDatabase({ name: "orders (team A)" });
|
|
1755
|
+
const theirs: ObjectID = await insertDatabase({
|
|
1756
|
+
name: "payments (team B)",
|
|
1757
|
+
});
|
|
1758
|
+
await link("DatabaseServerLabel", {
|
|
1759
|
+
databaseServerId: ours,
|
|
1760
|
+
labelId: teamA,
|
|
1761
|
+
});
|
|
1762
|
+
await link("DatabaseServerLabel", {
|
|
1763
|
+
databaseServerId: theirs,
|
|
1764
|
+
labelId: teamB,
|
|
1765
|
+
});
|
|
1766
|
+
const ourAlias: ObjectID = await insertEndpoint({
|
|
1767
|
+
databaseServerId: ours,
|
|
1768
|
+
endpoint: "orders-replica.example.com:5432",
|
|
1769
|
+
source: "user",
|
|
1770
|
+
});
|
|
1771
|
+
const theirAlias: ObjectID = await insertEndpoint({
|
|
1772
|
+
databaseServerId: theirs,
|
|
1773
|
+
endpoint: "payments-db.example.com:5432",
|
|
1774
|
+
source: "user",
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
await expect(
|
|
1778
|
+
DatabaseServerEndpointService.deleteOneById({
|
|
1779
|
+
id: theirAlias,
|
|
1780
|
+
props: scopedProps(PermissionScope.Labels, [teamA]),
|
|
1781
|
+
}),
|
|
1782
|
+
).rejects.toThrow("you do not have permission to edit it");
|
|
1783
|
+
expect(await endpointsOf(theirs)).toHaveLength(1);
|
|
1784
|
+
|
|
1785
|
+
// ...so the endpoint cannot be freed and re-added to their own database.
|
|
1786
|
+
await expect(
|
|
1787
|
+
DatabaseServerEndpointService.create({
|
|
1788
|
+
data: alias(ours, "payments-db.example.com"),
|
|
1789
|
+
props: scopedProps(PermissionScope.Labels, [teamA]),
|
|
1790
|
+
}),
|
|
1791
|
+
).rejects.toThrow("already belongs to another database");
|
|
1792
|
+
|
|
1793
|
+
await expect(
|
|
1794
|
+
DatabaseServerEndpointService.deleteOneById({
|
|
1795
|
+
id: ourAlias,
|
|
1796
|
+
props: scopedProps(PermissionScope.Labels, [teamA]),
|
|
1797
|
+
}),
|
|
1798
|
+
).resolves.toBe(1);
|
|
1799
|
+
expect(await endpointsOf(ours)).toHaveLength(0);
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
/*
|
|
1803
|
+
* e2e: aliases added and removed from the Endpoints tab never reached
|
|
1804
|
+
* the database's Feed. Through the real create and delete pipelines,
|
|
1805
|
+
* each lands there once, on its own database - and a refusal writes
|
|
1806
|
+
* nothing.
|
|
1807
|
+
*/
|
|
1808
|
+
test("an alias a person adds and removes each put one item on the database's Feed", async () => {
|
|
1809
|
+
const teamA: ObjectID = await insertRow("Label", {
|
|
1810
|
+
projectId: projectId,
|
|
1811
|
+
name: "team-a",
|
|
1812
|
+
});
|
|
1813
|
+
const ours: ObjectID = await insertDatabase({ name: "orders (team A)" });
|
|
1814
|
+
const primary: ObjectID = await insertEndpoint({
|
|
1815
|
+
databaseServerId: ours,
|
|
1816
|
+
endpoint: "orders-db.example.com:5432",
|
|
1817
|
+
source: "auto",
|
|
1818
|
+
isPrimary: true,
|
|
1819
|
+
});
|
|
1820
|
+
await link("DatabaseServerLabel", {
|
|
1821
|
+
databaseServerId: ours,
|
|
1822
|
+
labelId: teamA,
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
const created: any = await DatabaseServerEndpointService.create({
|
|
1826
|
+
data: alias(ours, "Orders-Replica.Example.com"),
|
|
1827
|
+
props: scopedProps(PermissionScope.Labels, [teamA]),
|
|
1828
|
+
});
|
|
1829
|
+
expect(feed).toHaveBeenCalledTimes(1);
|
|
1830
|
+
expect(feed.mock.calls[0]![0].feedInfoInMarkdown).toBe(
|
|
1831
|
+
"🔗 Added the endpoint `orders-replica.example.com:5432` to [Database x](/x).",
|
|
1832
|
+
);
|
|
1833
|
+
expect(feed.mock.calls[0]![0].databaseServerId.toString()).toBe(
|
|
1834
|
+
ours.toString(),
|
|
1835
|
+
);
|
|
1836
|
+
|
|
1837
|
+
// The primary cannot be removed, and the refusal writes nothing.
|
|
1838
|
+
await expect(
|
|
1839
|
+
DatabaseServerEndpointService.deleteOneById({
|
|
1840
|
+
id: primary,
|
|
1841
|
+
props: scopedProps(PermissionScope.Labels, [teamA]),
|
|
1842
|
+
}),
|
|
1843
|
+
).rejects.toThrow("primary endpoint");
|
|
1844
|
+
expect(feed).toHaveBeenCalledTimes(1);
|
|
1845
|
+
|
|
1846
|
+
await expect(
|
|
1847
|
+
DatabaseServerEndpointService.deleteOneById({
|
|
1848
|
+
id: new ObjectID(created._id.toString()),
|
|
1849
|
+
props: scopedProps(PermissionScope.Labels, [teamA]),
|
|
1850
|
+
}),
|
|
1851
|
+
).resolves.toBe(1);
|
|
1852
|
+
expect(feed).toHaveBeenCalledTimes(2);
|
|
1853
|
+
expect(feed.mock.calls[1]![0].feedInfoInMarkdown).toBe(
|
|
1854
|
+
"🔗 Removed the endpoint `orders-replica.example.com:5432` from [Database x](/x).",
|
|
1855
|
+
);
|
|
1856
|
+
expect(feed.mock.calls[1]![0].projectId.toString()).toBe(
|
|
1857
|
+
projectId.toString(),
|
|
1858
|
+
);
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
test("an Owned-scoped editor removes aliases of databases they own only", async () => {
|
|
1862
|
+
const owned: ObjectID = await insertDatabase({ name: "owned" });
|
|
1863
|
+
const notOwned: ObjectID = await insertDatabase({ name: "not owned" });
|
|
1864
|
+
await insertRow("DatabaseServerOwnerUser", {
|
|
1865
|
+
projectId: projectId,
|
|
1866
|
+
databaseServerId: owned,
|
|
1867
|
+
userId: userId,
|
|
1868
|
+
});
|
|
1869
|
+
const ownedAlias: ObjectID = await insertEndpoint({
|
|
1870
|
+
databaseServerId: owned,
|
|
1871
|
+
endpoint: "owned-replica.example.com:5432",
|
|
1872
|
+
source: "user",
|
|
1873
|
+
});
|
|
1874
|
+
const otherAlias: ObjectID = await insertEndpoint({
|
|
1875
|
+
databaseServerId: notOwned,
|
|
1876
|
+
endpoint: "not-owned-replica.example.com:5432",
|
|
1877
|
+
source: "user",
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
await expect(
|
|
1881
|
+
DatabaseServerEndpointService.deleteOneById({
|
|
1882
|
+
id: otherAlias,
|
|
1883
|
+
props: scopedProps(PermissionScope.Owned),
|
|
1884
|
+
}),
|
|
1885
|
+
).rejects.toThrow("you do not have permission to edit it");
|
|
1886
|
+
expect(await endpointsOf(notOwned)).toHaveLength(1);
|
|
1887
|
+
|
|
1888
|
+
await expect(
|
|
1889
|
+
DatabaseServerEndpointService.deleteOneById({
|
|
1890
|
+
id: ownedAlias,
|
|
1891
|
+
props: scopedProps(PermissionScope.Owned),
|
|
1892
|
+
}),
|
|
1893
|
+
).resolves.toBe(1);
|
|
1894
|
+
expect(await endpointsOf(owned)).toHaveLength(0);
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
test("an alias the form cannot use is refused with what to change, and nothing is written", async () => {
|
|
1898
|
+
const ours: ObjectID = await insertDatabase({});
|
|
1899
|
+
|
|
1900
|
+
await expect(
|
|
1901
|
+
DatabaseServerEndpointService.create({
|
|
1902
|
+
data: alias(ours, "admin@10.0.0.5:5432"),
|
|
1903
|
+
props: scopedProps(PermissionScope.All),
|
|
1904
|
+
}),
|
|
1905
|
+
).rejects.toThrow('Remove "admin@"');
|
|
1906
|
+
await expect(
|
|
1907
|
+
DatabaseServerEndpointService.create({
|
|
1908
|
+
data: alias(ours, "orders-db.example.com:70000"),
|
|
1909
|
+
props: scopedProps(PermissionScope.All),
|
|
1910
|
+
}),
|
|
1911
|
+
).rejects.toThrow("has a port outside 1-65535");
|
|
1912
|
+
expect(await endpointsOf(ours)).toHaveLength(0);
|
|
1913
|
+
});
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
/*
|
|
1917
|
+
* -------------------------------------------------------------------------
|
|
1918
|
+
* A person adding a database: the real create pipeline
|
|
1919
|
+
* -------------------------------------------------------------------------
|
|
1920
|
+
*/
|
|
1921
|
+
describe("a person adding a database", () => {
|
|
1922
|
+
beforeEach(() => {
|
|
1923
|
+
// The Created feed item names its creator from the User table, not cloned here.
|
|
1924
|
+
jest
|
|
1925
|
+
.spyOn(DatabaseServerService as any, "writeDatabaseServerCreatedFeed")
|
|
1926
|
+
.mockResolvedValue(undefined);
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
function creatorProps(
|
|
1930
|
+
extra: Array<Permission> = [],
|
|
1931
|
+
): DatabaseCommonInteractionProps {
|
|
1932
|
+
return {
|
|
1933
|
+
userId: userId,
|
|
1934
|
+
tenantId: projectId,
|
|
1935
|
+
userGlobalAccessPermission: {
|
|
1936
|
+
projectIds: [projectId],
|
|
1937
|
+
globalPermissions: [Permission.Public, Permission.User],
|
|
1938
|
+
_type: "UserGlobalAccessPermission",
|
|
1939
|
+
},
|
|
1940
|
+
userTenantAccessPermission: {
|
|
1941
|
+
[projectId.toString()]: {
|
|
1942
|
+
projectId: projectId,
|
|
1943
|
+
permissions: [
|
|
1944
|
+
Permission.CreateDatabaseServer,
|
|
1945
|
+
Permission.ReadDatabaseServer,
|
|
1946
|
+
...extra,
|
|
1947
|
+
].map((permission: Permission): UserPermission => {
|
|
1948
|
+
return {
|
|
1949
|
+
permission,
|
|
1950
|
+
labelIds: [],
|
|
1951
|
+
isBlockPermission: false,
|
|
1952
|
+
_type: "UserPermission",
|
|
1953
|
+
};
|
|
1954
|
+
}),
|
|
1955
|
+
_type: "UserTenantAccessPermission",
|
|
1956
|
+
},
|
|
1957
|
+
},
|
|
1958
|
+
userTeamIds: [],
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function manual(serverAddress: string): DatabaseServer {
|
|
1963
|
+
const data: DatabaseServer = new DatabaseServer();
|
|
1964
|
+
data.dbSystem = "postgresql";
|
|
1965
|
+
data.serverAddress = serverAddress;
|
|
1966
|
+
return data;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
test("an unqualified Kubernetes Service name is refused in a project with clusters, naming only the clusters the caller may read", async () => {
|
|
1970
|
+
await insertRow("KubernetesCluster", {
|
|
1971
|
+
projectId: projectId,
|
|
1972
|
+
clusterIdentifier: "prod-eu",
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
// May not read clusters: refused all the same, with no cluster named.
|
|
1976
|
+
const error: Error = (await DatabaseServerService.create({
|
|
1977
|
+
data: manual("pg.shop.svc.cluster.local"),
|
|
1978
|
+
props: creatorProps(),
|
|
1979
|
+
}).catch((e: unknown) => {
|
|
1980
|
+
return e;
|
|
1981
|
+
})) as Error;
|
|
1982
|
+
expect(error.message).toContain(
|
|
1983
|
+
"pg.shop.svc.cluster.local:5432@<cluster name> instead, so name the cluster the way they report it.",
|
|
1984
|
+
);
|
|
1985
|
+
expect(error.message).not.toContain("prod-eu");
|
|
1986
|
+
|
|
1987
|
+
await expect(
|
|
1988
|
+
DatabaseServerService.create({
|
|
1989
|
+
data: manual("pg.shop.svc.cluster.local"),
|
|
1990
|
+
props: creatorProps([Permission.ReadKubernetesCluster]),
|
|
1991
|
+
}),
|
|
1992
|
+
).rejects.toThrow(
|
|
1993
|
+
"for example pg.shop.svc.cluster.local:5432@prod-eu (this project's clusters: prod-eu)",
|
|
1994
|
+
);
|
|
1995
|
+
const rows: Array<any> = await database.query(
|
|
1996
|
+
`SELECT "_id" FROM "${schema}"."DatabaseServer"`,
|
|
1997
|
+
);
|
|
1998
|
+
expect(rows).toHaveLength(0);
|
|
1999
|
+
|
|
2000
|
+
const created: DatabaseServer = await DatabaseServerService.create({
|
|
2001
|
+
data: manual("pg.shop.svc.cluster.local@prod-eu"),
|
|
2002
|
+
props: creatorProps(),
|
|
2003
|
+
});
|
|
2004
|
+
expect((await databaseState(created.id!)).databaseIdentifier).toBe(
|
|
2005
|
+
"postgresql|pg.shop.svc.cluster.local:5432@prod-eu",
|
|
2006
|
+
);
|
|
2007
|
+
expect(await endpointsOf(created.id!)).toEqual([
|
|
2008
|
+
expect.objectContaining({
|
|
2009
|
+
endpoint: "pg.shop.svc.cluster.local:5432@prod-eu",
|
|
2010
|
+
source: "user",
|
|
2011
|
+
isPrimary: true,
|
|
2012
|
+
}),
|
|
2013
|
+
]);
|
|
2014
|
+
// ...and a database found from traces has no collector status.
|
|
2015
|
+
expect((await databaseState(created.id!)).otelCollectorStatus).toBeNull();
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
test("the same name is accepted as typed in a project without clusters, and a private IP always is", async () => {
|
|
2019
|
+
await insertRow("KubernetesCluster", {
|
|
2020
|
+
projectId: otherProjectId,
|
|
2021
|
+
clusterIdentifier: "theirs",
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
const service: DatabaseServer = await DatabaseServerService.create({
|
|
2025
|
+
data: manual("pg.shop.svc.cluster.local"),
|
|
2026
|
+
props: creatorProps(),
|
|
2027
|
+
});
|
|
2028
|
+
expect((await databaseState(service.id!)).databaseIdentifier).toBe(
|
|
2029
|
+
"postgresql|pg.shop.svc.cluster.local:5432",
|
|
2030
|
+
);
|
|
2031
|
+
|
|
2032
|
+
await insertRow("KubernetesCluster", {
|
|
2033
|
+
projectId: projectId,
|
|
2034
|
+
clusterIdentifier: "prod-eu",
|
|
2035
|
+
});
|
|
2036
|
+
const privateIp: DatabaseServer = await DatabaseServerService.create({
|
|
2037
|
+
data: manual("10.0.1.5"),
|
|
2038
|
+
props: creatorProps(),
|
|
2039
|
+
});
|
|
2040
|
+
expect((await databaseState(privateIp.id!)).databaseIdentifier).toBe(
|
|
2041
|
+
"postgresql|10.0.1.5:5432",
|
|
2042
|
+
);
|
|
2043
|
+
});
|
|
2044
|
+
});
|
|
2045
|
+
});
|