@oneuptime/common 12.0.9 → 12.0.11
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/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
- package/Models/DatabaseModels/RunnerJob.ts +17 -2
- package/Server/API/LlmProviderAPI.ts +8 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Services/NetworkDeviceService.ts +265 -12
- package/Server/Services/RunnerJobService.ts +24 -1
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Utils/LLM/LLMService.ts +108 -19
- package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
- package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
- package/Server/Utils/SSRFProtection.ts +5 -4
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
- package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
- package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
- package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
- package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
- package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
- package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
- package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
- package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
- package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
- package/Tests/Types/API/URLTelScheme.test.ts +261 -0
- package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
- package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
- package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
- package/Tests/UI/Components/Button.test.tsx +279 -0
- package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
- package/Tests/UI/Components/Icon.test.tsx +57 -0
- package/Tests/UI/Components/List.test.tsx +20 -3
- package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
- package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/UI/Components/Pagination.test.tsx +721 -146
- package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
- package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
- package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
- package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
- package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/API/Protocol.ts +9 -0
- package/Types/API/URL.ts +286 -20
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
- package/Types/Runbook/RunbookStepType.ts +23 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
- package/UI/Components/Button/Button.tsx +14 -22
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +4 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
- package/UI/Components/Dictionary/Dictionary.tsx +4 -39
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
- package/UI/Components/Pagination/Pagination.tsx +373 -328
- package/UI/Components/Pagination/PaginationUtil.ts +263 -0
- package/UI/Components/Pagination/Screenshots/README.md +15 -0
- package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
- package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
- package/UI/Components/TextArea/TextArea.tsx +3 -0
- package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
- package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
- package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
- package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
- package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
- package/Utils/API.ts +243 -36
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +9 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +20 -2
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
- package/build/dist/Server/Utils/SSRFProtection.js +5 -4
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Types/API/Protocol.js +9 -0
- package/build/dist/Types/API/Protocol.js.map +1 -1
- package/build/dist/Types/API/URL.js +241 -18
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +10 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +10 -16
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
- package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
- package/build/dist/Utils/API.js +175 -34
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PasswordHash carries a pre-existing TS5.9 diagnostic that fails any suite
|
|
3
|
+
* whose runtime require graph reaches it (DatabaseService, the base class of
|
|
4
|
+
* every concrete service, imports it). Nothing password-related is under
|
|
5
|
+
* test here, so the module is replaced WITH A FACTORY — an automock would
|
|
6
|
+
* still require (and type-check) the real file.
|
|
7
|
+
*/
|
|
8
|
+
jest.mock("../../../../Server/Utils/PasswordHash", () => {
|
|
9
|
+
return {
|
|
10
|
+
__esModule: true,
|
|
11
|
+
default: {
|
|
12
|
+
hash: jest.fn(),
|
|
13
|
+
verify: jest.fn(),
|
|
14
|
+
generateSalt: jest.fn(),
|
|
15
|
+
needsUpgrade: jest.fn(),
|
|
16
|
+
applyPepper: jest.fn(),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
import Alert from "../../../../Models/DatabaseModels/Alert";
|
|
22
|
+
import Host from "../../../../Models/DatabaseModels/Host";
|
|
23
|
+
import Incident from "../../../../Models/DatabaseModels/Incident";
|
|
24
|
+
import CephClusterService from "../../../../Server/Services/CephClusterService";
|
|
25
|
+
import DockerHostService from "../../../../Server/Services/DockerHostService";
|
|
26
|
+
import DockerSwarmClusterService from "../../../../Server/Services/DockerSwarmClusterService";
|
|
27
|
+
import HostService from "../../../../Server/Services/HostService";
|
|
28
|
+
import IoTFleetService from "../../../../Server/Services/IoTFleetService";
|
|
29
|
+
import KubernetesClusterService from "../../../../Server/Services/KubernetesClusterService";
|
|
30
|
+
import PodmanHostService from "../../../../Server/Services/PodmanHostService";
|
|
31
|
+
import ProxmoxClusterService from "../../../../Server/Services/ProxmoxClusterService";
|
|
32
|
+
import ServiceService from "../../../../Server/Services/ServiceService";
|
|
33
|
+
import SeriesResourceLinker, {
|
|
34
|
+
SeriesLinkableModel,
|
|
35
|
+
SeriesResolvedResourceIds,
|
|
36
|
+
} from "../../../../Server/Utils/Monitor/SeriesResourceLinker";
|
|
37
|
+
import Includes from "../../../../Types/BaseDatabase/Includes";
|
|
38
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
39
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
40
|
+
import { MonitorClusterContext } from "../../../../Server/Utils/Monitor/MonitorClusterContext";
|
|
41
|
+
import {
|
|
42
|
+
afterEach,
|
|
43
|
+
beforeEach,
|
|
44
|
+
describe,
|
|
45
|
+
expect,
|
|
46
|
+
jest,
|
|
47
|
+
test,
|
|
48
|
+
} from "@jest/globals";
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* SeriesResourceLinker turns a breaching series' labels into the real
|
|
52
|
+
* resources an alert/incident is about. It is the module behind the
|
|
53
|
+
* "Affected Resources" card, the per-resource Activity tabs, the sidebar
|
|
54
|
+
* badge counts, and resource-owner/label inheritance.
|
|
55
|
+
*
|
|
56
|
+
* The regression that motivated it: a metric monitor grouped by
|
|
57
|
+
* `oneuptime.host.name` (the stamp ingest puts on EVERY host metric row)
|
|
58
|
+
* opened alerts that linked no host at all, because the alert path only
|
|
59
|
+
* ever read the two `host.name` spellings. Test 1 below is that bug.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
63
|
+
"11111111-1111-4111-8111-111111111111",
|
|
64
|
+
);
|
|
65
|
+
const OTHER_PROJECT_ID: ObjectID = new ObjectID(
|
|
66
|
+
"99999999-9999-4999-8999-999999999999",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
* Every findBy the linker can issue, keyed by the relation it feeds, so a
|
|
71
|
+
* test can say "this one returns a row" and assert every other one was
|
|
72
|
+
* never called.
|
|
73
|
+
*/
|
|
74
|
+
interface ResourceServiceUnderTest {
|
|
75
|
+
relation: keyof SeriesLinkableModel;
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
service: any;
|
|
78
|
+
nameColumn: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const RESOURCE_SERVICES: Array<ResourceServiceUnderTest> = [
|
|
82
|
+
{ relation: "hosts", service: HostService, nameColumn: "hostIdentifier" },
|
|
83
|
+
{
|
|
84
|
+
relation: "dockerHosts",
|
|
85
|
+
service: DockerHostService,
|
|
86
|
+
nameColumn: "hostIdentifier",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
relation: "podmanHosts",
|
|
90
|
+
service: PodmanHostService,
|
|
91
|
+
nameColumn: "hostIdentifier",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
relation: "kubernetesClusters",
|
|
95
|
+
service: KubernetesClusterService,
|
|
96
|
+
nameColumn: "clusterIdentifier",
|
|
97
|
+
},
|
|
98
|
+
{ relation: "services", service: ServiceService, nameColumn: "name" },
|
|
99
|
+
{
|
|
100
|
+
relation: "proxmoxClusters",
|
|
101
|
+
service: ProxmoxClusterService,
|
|
102
|
+
nameColumn: "name",
|
|
103
|
+
},
|
|
104
|
+
{ relation: "cephClusters", service: CephClusterService, nameColumn: "name" },
|
|
105
|
+
{
|
|
106
|
+
relation: "dockerSwarmClusters",
|
|
107
|
+
service: DockerSwarmClusterService,
|
|
108
|
+
nameColumn: "name",
|
|
109
|
+
},
|
|
110
|
+
{ relation: "iotFleets", service: IoTFleetService, nameColumn: "name" },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* Jest's spy typing fights the loosely-typed findBy signature the linker
|
|
115
|
+
* deliberately abstracts over, so the captured spies are held loosely and
|
|
116
|
+
* asserted through the helpers below.
|
|
117
|
+
*/
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
+
type SpyLike = any;
|
|
120
|
+
|
|
121
|
+
const spies: Map<keyof SeriesLinkableModel, SpyLike> = new Map<
|
|
122
|
+
keyof SeriesLinkableModel,
|
|
123
|
+
SpyLike
|
|
124
|
+
>();
|
|
125
|
+
|
|
126
|
+
/*
|
|
127
|
+
* Rows the fake findBy should return, per relation. A relation with no
|
|
128
|
+
* entry resolves to [] — i.e. "the project has no such resource".
|
|
129
|
+
*/
|
|
130
|
+
let rowsByRelation: Map<keyof SeriesLinkableModel, Array<{ _id: string }>>;
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* Every query object the linker passed, per relation, so tests can assert
|
|
134
|
+
* on project scoping, the name column, and Includes contents.
|
|
135
|
+
*/
|
|
136
|
+
let queriesByRelation: Map<keyof SeriesLinkableModel, Array<JSONObject>>;
|
|
137
|
+
|
|
138
|
+
function relationQueries(
|
|
139
|
+
relation: keyof SeriesLinkableModel,
|
|
140
|
+
): Array<JSONObject> {
|
|
141
|
+
return queriesByRelation.get(relation) || [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function callCount(relation: keyof SeriesLinkableModel): number {
|
|
145
|
+
return spies.get(relation).mock.calls.length;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function idsOn(
|
|
149
|
+
model: SeriesLinkableModel,
|
|
150
|
+
relation: keyof SeriesLinkableModel,
|
|
151
|
+
): Array<string> {
|
|
152
|
+
const items: Array<{ _id?: string | undefined }> | undefined = model[
|
|
153
|
+
relation
|
|
154
|
+
] as Array<{ _id?: string | undefined }> | undefined;
|
|
155
|
+
|
|
156
|
+
if (!items) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return items.map((item: { _id?: string | undefined }): string => {
|
|
161
|
+
return String(item._id);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
* Pull the values out of the Includes the linker built for a query, so a
|
|
167
|
+
* test can assert on the exact identifiers it looked up.
|
|
168
|
+
*/
|
|
169
|
+
function includesValues(value: unknown): Array<string> {
|
|
170
|
+
const includes: Includes = value as Includes;
|
|
171
|
+
const values: Array<string | ObjectID | number> = (includes.values ||
|
|
172
|
+
[]) as Array<string | ObjectID | number>;
|
|
173
|
+
|
|
174
|
+
return values.map((v: string | ObjectID | number): string => {
|
|
175
|
+
return v.toString();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function emptyClusterContext(): MonitorClusterContext {
|
|
180
|
+
return {
|
|
181
|
+
proxmoxClusterIds: [],
|
|
182
|
+
cephClusterIds: [],
|
|
183
|
+
dockerSwarmClusterIds: [],
|
|
184
|
+
iotFleetIds: [],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/*
|
|
189
|
+
* Stand one resource service's findBy up as a fake that records the query
|
|
190
|
+
* it was handed and answers with whatever the running test staged.
|
|
191
|
+
*/
|
|
192
|
+
function installFindBySpy(resource: ResourceServiceUnderTest): void {
|
|
193
|
+
const spy: SpyLike = jest
|
|
194
|
+
.spyOn(resource.service, "findBy")
|
|
195
|
+
.mockImplementation((args: unknown): Promise<Array<{ _id: string }>> => {
|
|
196
|
+
const call: { query?: JSONObject } = args as { query?: JSONObject };
|
|
197
|
+
const recorded: Array<JSONObject> =
|
|
198
|
+
queriesByRelation.get(resource.relation) || [];
|
|
199
|
+
recorded.push(call.query || {});
|
|
200
|
+
queriesByRelation.set(resource.relation, recorded);
|
|
201
|
+
|
|
202
|
+
return Promise.resolve(rowsByRelation.get(resource.relation) || []);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
spies.set(resource.relation, spy);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
beforeEach(() => {
|
|
209
|
+
rowsByRelation = new Map<keyof SeriesLinkableModel, Array<{ _id: string }>>();
|
|
210
|
+
queriesByRelation = new Map<keyof SeriesLinkableModel, Array<JSONObject>>();
|
|
211
|
+
spies.clear();
|
|
212
|
+
|
|
213
|
+
for (const resource of RESOURCE_SERVICES) {
|
|
214
|
+
installFindBySpy(resource);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
afterEach(() => {
|
|
219
|
+
jest.restoreAllMocks();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("SeriesResourceLinker.linkSeriesResourcesToModel", () => {
|
|
223
|
+
test("links the host a series identifies by the oneuptime.host.name stamp", async () => {
|
|
224
|
+
/*
|
|
225
|
+
* THE REGRESSION. Ingest stamps `oneuptime.host.name` on every host
|
|
226
|
+
* metric row, and the group-by dropdown offers it, so this is the
|
|
227
|
+
* most common way a grouped host monitor identifies its series.
|
|
228
|
+
*/
|
|
229
|
+
rowsByRelation.set("hosts", [{ _id: "host-1" }]);
|
|
230
|
+
|
|
231
|
+
const alert: Alert = new Alert();
|
|
232
|
+
|
|
233
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
234
|
+
model: alert,
|
|
235
|
+
seriesLabels: { "oneuptime.host.name": "prodgreen000002" },
|
|
236
|
+
projectId: PROJECT_ID,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
|
|
240
|
+
expect(callCount("hosts")).toBe(1);
|
|
241
|
+
|
|
242
|
+
const query: JSONObject = relationQueries("hosts")[0]!;
|
|
243
|
+
expect(includesValues(query["hostIdentifier"])).toEqual([
|
|
244
|
+
"prodgreen000002",
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test.each([
|
|
249
|
+
["resource.oneuptime.host.name"],
|
|
250
|
+
["oneuptime.host.name"],
|
|
251
|
+
["resource.host.name"],
|
|
252
|
+
["host.name"],
|
|
253
|
+
])("resolves the host from the %s spelling", async (key: string) => {
|
|
254
|
+
rowsByRelation.set("hosts", [{ _id: "host-1" }]);
|
|
255
|
+
|
|
256
|
+
const alert: Alert = new Alert();
|
|
257
|
+
|
|
258
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
259
|
+
model: alert,
|
|
260
|
+
seriesLabels: { [key]: "web-1" },
|
|
261
|
+
projectId: PROJECT_ID,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
|
|
265
|
+
expect(
|
|
266
|
+
includesValues(relationQueries("hosts")[0]!["hostIdentifier"]),
|
|
267
|
+
).toEqual(["web-1"]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("resolves a host id stamp through the _id column, not the name column", async () => {
|
|
271
|
+
rowsByRelation.set("hosts", [{ _id: "host-1" }]);
|
|
272
|
+
|
|
273
|
+
const alert: Alert = new Alert();
|
|
274
|
+
|
|
275
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
276
|
+
model: alert,
|
|
277
|
+
seriesLabels: { "oneuptime.host.id": "host-1" },
|
|
278
|
+
projectId: PROJECT_ID,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(callCount("hosts")).toBe(1);
|
|
282
|
+
|
|
283
|
+
const query: JSONObject = relationQueries("hosts")[0]!;
|
|
284
|
+
expect(includesValues(query["_id"])).toEqual(["host-1"]);
|
|
285
|
+
expect(query["hostIdentifier"]).toBeUndefined();
|
|
286
|
+
expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("queries by id AND name when the series carries both, deduping the result", async () => {
|
|
290
|
+
// Both lookups find the same row; the alert must link it once.
|
|
291
|
+
rowsByRelation.set("hosts", [{ _id: "host-1" }]);
|
|
292
|
+
|
|
293
|
+
const alert: Alert = new Alert();
|
|
294
|
+
|
|
295
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
296
|
+
model: alert,
|
|
297
|
+
seriesLabels: {
|
|
298
|
+
"oneuptime.host.id": "host-1",
|
|
299
|
+
"oneuptime.host.name": "web-1",
|
|
300
|
+
},
|
|
301
|
+
projectId: PROJECT_ID,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(callCount("hosts")).toBe(2);
|
|
305
|
+
expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("looks up every value of a multi-valued label in one query", async () => {
|
|
309
|
+
rowsByRelation.set("hosts", [{ _id: "host-1" }, { _id: "host-2" }]);
|
|
310
|
+
|
|
311
|
+
const alert: Alert = new Alert();
|
|
312
|
+
|
|
313
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
314
|
+
model: alert,
|
|
315
|
+
seriesLabels: { "host.name": ["web-1", "web-2"] },
|
|
316
|
+
projectId: PROJECT_ID,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(callCount("hosts")).toBe(1);
|
|
320
|
+
expect(
|
|
321
|
+
includesValues(relationQueries("hosts")[0]!["hostIdentifier"]).sort(),
|
|
322
|
+
).toEqual(["web-1", "web-2"]);
|
|
323
|
+
expect(idsOn(alert, "hosts").sort()).toEqual(["host-1", "host-2"]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test.each([
|
|
327
|
+
["dockerHosts", "oneuptime.docker.host.name", "hostIdentifier"],
|
|
328
|
+
["podmanHosts", "oneuptime.podman.host.name", "hostIdentifier"],
|
|
329
|
+
["kubernetesClusters", "k8s.cluster.name", "clusterIdentifier"],
|
|
330
|
+
["services", "service.name", "name"],
|
|
331
|
+
["services", "oneuptime.service.name", "name"],
|
|
332
|
+
["proxmoxClusters", "proxmox.cluster.name", "name"],
|
|
333
|
+
["cephClusters", "ceph.cluster.name", "name"],
|
|
334
|
+
["dockerSwarmClusters", "docker.swarm.cluster.name", "name"],
|
|
335
|
+
["iotFleets", "iot.fleet.name", "name"],
|
|
336
|
+
])(
|
|
337
|
+
"resolves %s from %s against the %s column",
|
|
338
|
+
async (relation: string, labelKey: string, nameColumn: string) => {
|
|
339
|
+
const typedRelation: keyof SeriesLinkableModel =
|
|
340
|
+
relation as keyof SeriesLinkableModel;
|
|
341
|
+
|
|
342
|
+
rowsByRelation.set(typedRelation, [{ _id: "resource-1" }]);
|
|
343
|
+
|
|
344
|
+
const alert: Alert = new Alert();
|
|
345
|
+
|
|
346
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
347
|
+
model: alert,
|
|
348
|
+
seriesLabels: { [labelKey]: "the-resource" },
|
|
349
|
+
projectId: PROJECT_ID,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(idsOn(alert, typedRelation)).toEqual(["resource-1"]);
|
|
353
|
+
|
|
354
|
+
const query: JSONObject = relationQueries(typedRelation)[0]!;
|
|
355
|
+
expect(includesValues(query[nameColumn])).toEqual(["the-resource"]);
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
test("a bare host name never resolves a docker or podman host", async () => {
|
|
360
|
+
/*
|
|
361
|
+
* Docker/Podman hosts are deliberately addressed only by their own
|
|
362
|
+
* `oneuptime.*.host.name` stamps. Reading raw host.name for them
|
|
363
|
+
* would attach a container host to an alert about a bare-metal box
|
|
364
|
+
* that merely shares its name.
|
|
365
|
+
*/
|
|
366
|
+
rowsByRelation.set("hosts", [{ _id: "host-1" }]);
|
|
367
|
+
|
|
368
|
+
const alert: Alert = new Alert();
|
|
369
|
+
|
|
370
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
371
|
+
model: alert,
|
|
372
|
+
seriesLabels: { "host.name": "shared-name" },
|
|
373
|
+
projectId: PROJECT_ID,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
expect(callCount("dockerHosts")).toBe(0);
|
|
377
|
+
expect(callCount("podmanHosts")).toBe(0);
|
|
378
|
+
expect(alert.dockerHosts).toBeUndefined();
|
|
379
|
+
expect(alert.podmanHosts).toBeUndefined();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("links every resource type a single series identifies", async () => {
|
|
383
|
+
rowsByRelation.set("hosts", [{ _id: "host-1" }]);
|
|
384
|
+
rowsByRelation.set("kubernetesClusters", [{ _id: "cluster-1" }]);
|
|
385
|
+
rowsByRelation.set("services", [{ _id: "service-1" }]);
|
|
386
|
+
|
|
387
|
+
const alert: Alert = new Alert();
|
|
388
|
+
|
|
389
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
390
|
+
model: alert,
|
|
391
|
+
seriesLabels: {
|
|
392
|
+
"oneuptime.host.name": "web-1",
|
|
393
|
+
"k8s.cluster.name": "prod",
|
|
394
|
+
"service.name": "checkout",
|
|
395
|
+
},
|
|
396
|
+
projectId: PROJECT_ID,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
|
|
400
|
+
expect(idsOn(alert, "kubernetesClusters")).toEqual(["cluster-1"]);
|
|
401
|
+
expect(idsOn(alert, "services")).toEqual(["service-1"]);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("issues no query at all for labels that identify nothing", async () => {
|
|
405
|
+
const alert: Alert = new Alert();
|
|
406
|
+
|
|
407
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
408
|
+
model: alert,
|
|
409
|
+
seriesLabels: { "unrelated.label": "x", "http.status_code": "500" },
|
|
410
|
+
projectId: PROJECT_ID,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
for (const resource of RESOURCE_SERVICES) {
|
|
414
|
+
expect(callCount(resource.relation)).toBe(0);
|
|
415
|
+
expect(alert[resource.relation]).toBeUndefined();
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("issues no query for an empty label set", async () => {
|
|
420
|
+
const alert: Alert = new Alert();
|
|
421
|
+
|
|
422
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
423
|
+
model: alert,
|
|
424
|
+
seriesLabels: {},
|
|
425
|
+
projectId: PROJECT_ID,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
for (const resource of RESOURCE_SERVICES) {
|
|
429
|
+
expect(callCount(resource.relation)).toBe(0);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("scopes every lookup to the passed project", async () => {
|
|
434
|
+
/*
|
|
435
|
+
* Tenant isolation. These lookups run with isRoot, which bypasses
|
|
436
|
+
* row-level access control, so the project scope is the only thing
|
|
437
|
+
* stopping a stale or hostile id stamp from pulling in another
|
|
438
|
+
* tenant's row.
|
|
439
|
+
*/
|
|
440
|
+
const alert: Alert = new Alert();
|
|
441
|
+
|
|
442
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
443
|
+
model: alert,
|
|
444
|
+
seriesLabels: {
|
|
445
|
+
"oneuptime.host.id": "host-1",
|
|
446
|
+
"oneuptime.host.name": "web-1",
|
|
447
|
+
"oneuptime.docker.host.name": "docker-1",
|
|
448
|
+
"oneuptime.podman.host.name": "podman-1",
|
|
449
|
+
"k8s.cluster.name": "k8s-1",
|
|
450
|
+
"service.name": "svc-1",
|
|
451
|
+
"proxmox.cluster.name": "pve-1",
|
|
452
|
+
"ceph.cluster.name": "ceph-1",
|
|
453
|
+
"docker.swarm.cluster.name": "swarm-1",
|
|
454
|
+
"iot.fleet.name": "fleet-1",
|
|
455
|
+
},
|
|
456
|
+
projectId: PROJECT_ID,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
let assertedQueries: number = 0;
|
|
460
|
+
|
|
461
|
+
for (const resource of RESOURCE_SERVICES) {
|
|
462
|
+
const queries: Array<JSONObject> = relationQueries(resource.relation);
|
|
463
|
+
expect(queries.length).toBeGreaterThan(0);
|
|
464
|
+
|
|
465
|
+
for (const query of queries) {
|
|
466
|
+
expect(query["projectId"]).toBe(PROJECT_ID);
|
|
467
|
+
assertedQueries++;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 9 types, and hosts get an extra query for the id stamp.
|
|
472
|
+
expect(assertedQueries).toBe(10);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("does not link a resource that belongs to another project", async () => {
|
|
476
|
+
/*
|
|
477
|
+
* The service is queried with the alert's own project, so a row from
|
|
478
|
+
* elsewhere simply is not returned — the relation stays unset rather
|
|
479
|
+
* than linking across the tenant boundary.
|
|
480
|
+
*/
|
|
481
|
+
const alert: Alert = new Alert();
|
|
482
|
+
|
|
483
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
484
|
+
model: alert,
|
|
485
|
+
seriesLabels: { "oneuptime.host.name": "web-1" },
|
|
486
|
+
projectId: OTHER_PROJECT_ID,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
expect(relationQueries("hosts")[0]!["projectId"]).toBe(OTHER_PROJECT_ID);
|
|
490
|
+
expect(alert.hosts).toBeUndefined();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("leaves the relation unset when the resource no longer exists", async () => {
|
|
494
|
+
// findBy returns [] — the host was deleted, or never discovered.
|
|
495
|
+
const alert: Alert = new Alert();
|
|
496
|
+
|
|
497
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
498
|
+
model: alert,
|
|
499
|
+
seriesLabels: { "oneuptime.host.name": "ghost-host" },
|
|
500
|
+
projectId: PROJECT_ID,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
expect(callCount("hosts")).toBe(1);
|
|
504
|
+
expect(alert.hosts).toBeUndefined();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("preserves resources already attached to the model", async () => {
|
|
508
|
+
rowsByRelation.set("hosts", [{ _id: "host-2" }]);
|
|
509
|
+
|
|
510
|
+
const alert: Alert = new Alert();
|
|
511
|
+
const preExisting: Host = new Host();
|
|
512
|
+
preExisting._id = "host-1";
|
|
513
|
+
alert.hosts = [preExisting];
|
|
514
|
+
|
|
515
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
516
|
+
model: alert,
|
|
517
|
+
seriesLabels: { "oneuptime.host.name": "web-2" },
|
|
518
|
+
projectId: PROJECT_ID,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
expect(idsOn(alert, "hosts")).toEqual(["host-1", "host-2"]);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("does not attach the same resource twice", async () => {
|
|
525
|
+
rowsByRelation.set("hosts", [{ _id: "host-1" }]);
|
|
526
|
+
|
|
527
|
+
const alert: Alert = new Alert();
|
|
528
|
+
const preExisting: Host = new Host();
|
|
529
|
+
preExisting._id = "host-1";
|
|
530
|
+
alert.hosts = [preExisting];
|
|
531
|
+
|
|
532
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
533
|
+
model: alert,
|
|
534
|
+
seriesLabels: { "oneuptime.host.name": "web-1" },
|
|
535
|
+
projectId: PROJECT_ID,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("links an incident exactly as it links an alert", async () => {
|
|
542
|
+
/*
|
|
543
|
+
* The whole point of the shared util: the two creation paths must
|
|
544
|
+
* not drift apart again.
|
|
545
|
+
*/
|
|
546
|
+
rowsByRelation.set("hosts", [{ _id: "host-1" }]);
|
|
547
|
+
rowsByRelation.set("services", [{ _id: "service-1" }]);
|
|
548
|
+
|
|
549
|
+
const labels: JSONObject = {
|
|
550
|
+
"oneuptime.host.name": "web-1",
|
|
551
|
+
"service.name": "checkout",
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const alert: Alert = new Alert();
|
|
555
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
556
|
+
model: alert,
|
|
557
|
+
seriesLabels: labels,
|
|
558
|
+
projectId: PROJECT_ID,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const incident: Incident = new Incident();
|
|
562
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
563
|
+
model: incident,
|
|
564
|
+
seriesLabels: labels,
|
|
565
|
+
projectId: PROJECT_ID,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
expect(idsOn(incident, "hosts")).toEqual(idsOn(alert, "hosts"));
|
|
569
|
+
expect(idsOn(incident, "services")).toEqual(idsOn(alert, "services"));
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe("SeriesResourceLinker.resolveResourcesFromSeriesLabels", () => {
|
|
574
|
+
test("returns ids per type without touching a model", async () => {
|
|
575
|
+
rowsByRelation.set("hosts", [{ _id: "host-1" }]);
|
|
576
|
+
rowsByRelation.set("iotFleets", [{ _id: "fleet-1" }]);
|
|
577
|
+
|
|
578
|
+
const resolved: SeriesResolvedResourceIds =
|
|
579
|
+
await SeriesResourceLinker.resolveResourcesFromSeriesLabels({
|
|
580
|
+
seriesLabels: {
|
|
581
|
+
"oneuptime.host.name": "web-1",
|
|
582
|
+
"iot.fleet.name": "fleet-a",
|
|
583
|
+
},
|
|
584
|
+
projectId: PROJECT_ID,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
expect(resolved.hostIds).toEqual(["host-1"]);
|
|
588
|
+
expect(resolved.iotFleetIds).toEqual(["fleet-1"]);
|
|
589
|
+
expect(resolved.serviceIds).toEqual([]);
|
|
590
|
+
expect(resolved.dockerHostIds).toEqual([]);
|
|
591
|
+
expect(resolved.podmanHostIds).toEqual([]);
|
|
592
|
+
expect(resolved.kubernetesClusterIds).toEqual([]);
|
|
593
|
+
expect(resolved.proxmoxClusterIds).toEqual([]);
|
|
594
|
+
expect(resolved.cephClusterIds).toEqual([]);
|
|
595
|
+
expect(resolved.dockerSwarmClusterIds).toEqual([]);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
describe("SeriesResourceLinker.attachClusterContext", () => {
|
|
600
|
+
test("attaches the monitor-step clusters", () => {
|
|
601
|
+
const alert: Alert = new Alert();
|
|
602
|
+
|
|
603
|
+
SeriesResourceLinker.attachClusterContext({
|
|
604
|
+
model: alert,
|
|
605
|
+
clusterContext: {
|
|
606
|
+
proxmoxClusterIds: ["pve-1"],
|
|
607
|
+
cephClusterIds: ["ceph-1"],
|
|
608
|
+
dockerSwarmClusterIds: ["swarm-1"],
|
|
609
|
+
iotFleetIds: ["fleet-1"],
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
expect(idsOn(alert, "proxmoxClusters")).toEqual(["pve-1"]);
|
|
614
|
+
expect(idsOn(alert, "cephClusters")).toEqual(["ceph-1"]);
|
|
615
|
+
expect(idsOn(alert, "dockerSwarmClusters")).toEqual(["swarm-1"]);
|
|
616
|
+
expect(idsOn(alert, "iotFleets")).toEqual(["fleet-1"]);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test("merges with the label path instead of overwriting it", async () => {
|
|
620
|
+
/*
|
|
621
|
+
* The alert path used to ASSIGN the step-config clusters, which was
|
|
622
|
+
* only safe while its label path could not resolve them. Now that it
|
|
623
|
+
* can, an overwrite would silently drop the cluster the series
|
|
624
|
+
* itself named.
|
|
625
|
+
*/
|
|
626
|
+
rowsByRelation.set("proxmoxClusters", [{ _id: "pve-from-label" }]);
|
|
627
|
+
|
|
628
|
+
const alert: Alert = new Alert();
|
|
629
|
+
|
|
630
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
631
|
+
model: alert,
|
|
632
|
+
seriesLabels: { "proxmox.cluster.name": "pve-a" },
|
|
633
|
+
projectId: PROJECT_ID,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
SeriesResourceLinker.attachClusterContext({
|
|
637
|
+
model: alert,
|
|
638
|
+
clusterContext: {
|
|
639
|
+
...emptyClusterContext(),
|
|
640
|
+
proxmoxClusterIds: ["pve-from-step-config"],
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
expect(idsOn(alert, "proxmoxClusters")).toEqual([
|
|
645
|
+
"pve-from-label",
|
|
646
|
+
"pve-from-step-config",
|
|
647
|
+
]);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test("dedupes a cluster both paths resolved", async () => {
|
|
651
|
+
rowsByRelation.set("cephClusters", [{ _id: "ceph-1" }]);
|
|
652
|
+
|
|
653
|
+
const alert: Alert = new Alert();
|
|
654
|
+
|
|
655
|
+
await SeriesResourceLinker.linkSeriesResourcesToModel({
|
|
656
|
+
model: alert,
|
|
657
|
+
seriesLabels: { "ceph.cluster.name": "ceph-a" },
|
|
658
|
+
projectId: PROJECT_ID,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
SeriesResourceLinker.attachClusterContext({
|
|
662
|
+
model: alert,
|
|
663
|
+
clusterContext: {
|
|
664
|
+
...emptyClusterContext(),
|
|
665
|
+
cephClusterIds: ["ceph-1"],
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(idsOn(alert, "cephClusters")).toEqual(["ceph-1"]);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("leaves every relation untouched when the monitor has no clusters", () => {
|
|
673
|
+
const alert: Alert = new Alert();
|
|
674
|
+
|
|
675
|
+
SeriesResourceLinker.attachClusterContext({
|
|
676
|
+
model: alert,
|
|
677
|
+
clusterContext: emptyClusterContext(),
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
expect(alert.proxmoxClusters).toBeUndefined();
|
|
681
|
+
expect(alert.cephClusters).toBeUndefined();
|
|
682
|
+
expect(alert.dockerSwarmClusters).toBeUndefined();
|
|
683
|
+
expect(alert.iotFleets).toBeUndefined();
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("attaches to an incident the same way", () => {
|
|
687
|
+
const incident: Incident = new Incident();
|
|
688
|
+
|
|
689
|
+
SeriesResourceLinker.attachClusterContext({
|
|
690
|
+
model: incident,
|
|
691
|
+
clusterContext: {
|
|
692
|
+
...emptyClusterContext(),
|
|
693
|
+
dockerSwarmClusterIds: ["swarm-1"],
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
expect(idsOn(incident, "dockerSwarmClusters")).toEqual(["swarm-1"]);
|
|
698
|
+
});
|
|
699
|
+
});
|