From cb545e445a4abb4a7780b512cf2e9d430ae1ffd2 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Mon, 19 May 2025 18:46:25 +0100 Subject: [PATCH] feat: Add monitorId to MonitorTest model and related migrations --- Common/Models/DatabaseModels/MonitorTest.ts | 67 +++++++ .../1747674762672-MigrationName.ts | 27 +++ .../Postgres/SchemaMigrations/Index.ts | 2 + .../Components/Form/Monitor/MonitorTest.tsx | 4 + Dashboard/src/Pages/Monitor/View/Criteria.tsx | 1 + ProbeIngest/API/Monitor.ts | 30 +++- ProbeIngest/Utils/Monitor.ts | 166 ++++++++++++------ 7 files changed, 244 insertions(+), 53 deletions(-) create mode 100644 Common/Server/Infrastructure/Postgres/SchemaMigrations/1747674762672-MigrationName.ts diff --git a/Common/Models/DatabaseModels/MonitorTest.ts b/Common/Models/DatabaseModels/MonitorTest.ts index cc0347f3e3..86851a2bde 100644 --- a/Common/Models/DatabaseModels/MonitorTest.ts +++ b/Common/Models/DatabaseModels/MonitorTest.ts @@ -21,6 +21,7 @@ import Permission from "../../Types/Permission"; import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"; import { MonitorStepProbeResponse } from "./MonitorProbe"; import Probe from "./Probe"; +import Monitor from "./Monitor"; @TenantColumn("projectId") @TableAccessControl({ @@ -475,4 +476,70 @@ export default class MonitorTest extends BaseModel { default: true, }) public isInQueue?: boolean = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectMonitor, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadProjectMonitor, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: false, + canReadOnRelationQuery: true, + title: "Monitor ID", + description: "ID of the Monitor this test is related to.", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public monitorId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateProjectMonitor, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadProjectMonitor, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "monitorId", + type: TableColumnType.Entity, + modelType: Monitor, + title: "Monitor", + description: "Relation to Monitor Resource in which this test belongs.", + }) + @ManyToOne( + () => { + return Monitor; + }, + { + eager: false, + nullable: true, + onDelete: "SET NULL", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "monitorId" }) + public monitor?: Monitor = undefined; } diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1747674762672-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1747674762672-MigrationName.ts new file mode 100644 index 0000000000..8e58f87765 --- /dev/null +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1747674762672-MigrationName.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MigrationName1747674762672 implements MigrationInterface { + public name = "MigrationName1747674762672"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "MonitorTest" ADD "monitorId" uuid`); + await queryRunner.query( + `CREATE INDEX "IDX_4650119024eca8c91608effb95" ON "MonitorTest" ("monitorId") `, + ); + await queryRunner.query( + `ALTER TABLE "MonitorTest" ADD CONSTRAINT "FK_4650119024eca8c91608effb959" FOREIGN KEY ("monitorId") REFERENCES "Monitor"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "MonitorTest" DROP CONSTRAINT "FK_4650119024eca8c91608effb959"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_4650119024eca8c91608effb95"`, + ); + await queryRunner.query( + `ALTER TABLE "MonitorTest" DROP COLUMN "monitorId"`, + ); + } +} diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts index b8ce839882..0b860578e6 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts @@ -132,6 +132,7 @@ import { MigrationName1743714801105 } from "./1743714801105-MigrationName"; import { MigrationName1744804990712 } from "./1744804990712-MigrationName"; import { MigrationName1744809770336 } from "./1744809770336-MigrationName"; import { MigrationName1747305098533 } from "./1747305098533-MigrationName"; +import { MigrationName1747674762672 } from "./1747674762672-MigrationName"; export default [ InitialMigration, @@ -268,4 +269,5 @@ export default [ MigrationName1744804990712, MigrationName1744809770336, MigrationName1747305098533, + MigrationName1747674762672, ]; diff --git a/Dashboard/src/Components/Form/Monitor/MonitorTest.tsx b/Dashboard/src/Components/Form/Monitor/MonitorTest.tsx index 15a568393e..72abde90f7 100644 --- a/Dashboard/src/Components/Form/Monitor/MonitorTest.tsx +++ b/Dashboard/src/Components/Form/Monitor/MonitorTest.tsx @@ -26,6 +26,7 @@ import { MonitorStepProbeResponse } from "Common/Models/DatabaseModels/MonitorPr import SummaryInfo from "../../Monitor/SummaryView/SummaryInfo"; export interface ComponentProps { + monitorId?: ObjectID | undefined; monitorSteps: MonitorSteps; monitorType: MonitorType; probes: Array; @@ -70,6 +71,9 @@ const MonitorTestForm: FunctionComponent = ( monitorTestObj.probeId = probeId; monitorTestObj.monitorType = props.monitorType; monitorTestObj.isInQueue = true; + if (props.monitorId) { + monitorTestObj.monitorId = props.monitorId; + } // save the monitor test to the database. diff --git a/Dashboard/src/Pages/Monitor/View/Criteria.tsx b/Dashboard/src/Pages/Monitor/View/Criteria.tsx index 1447783cab..fde1d1b7f6 100644 --- a/Dashboard/src/Pages/Monitor/View/Criteria.tsx +++ b/Dashboard/src/Pages/Monitor/View/Criteria.tsx @@ -124,6 +124,7 @@ const MonitorCriteria: FunctionComponent< description: "Here is the criteria we use to monitor this resource.", rightElement: monitorSteps ? ( = []; + const monitorTestsWithSecretsPopulatePromises: Array< + Promise + > = []; + + for (const monitorTest of monitorTests) { + monitorTestsWithSecretsPopulatePromises.push( + MonitorUtil.populateSecretsOnMonitorTest(monitorTest), + ); + } + + monitorTestsWithSecretPopulated = await Promise.all( + monitorTestsWithSecretsPopulatePromises, + ); + + logger.debug("Populated secrets"); + logger.debug(monitorTestsWithSecretPopulated); + + // return the list of monitors to be monitored + logger.debug("Sending response"); return Response.sendEntityArrayResponse( req, res, - monitorTests, - new PositiveNumber(monitorTests.length), + monitorTestsWithSecretPopulated, + new PositiveNumber(monitorTestsWithSecretPopulated.length), MonitorTest, ); } catch (err) { diff --git a/ProbeIngest/Utils/Monitor.ts b/ProbeIngest/Utils/Monitor.ts index cbbb6795da..c126d87027 100644 --- a/ProbeIngest/Utils/Monitor.ts +++ b/ProbeIngest/Utils/Monitor.ts @@ -2,7 +2,6 @@ import Hostname from "Common/Types/API/Hostname"; import URL from "Common/Types/API/URL"; import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; import Dictionary from "Common/Types/Dictionary"; -import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import IP from "Common/Types/IP/IP"; import { JSONObject } from "Common/Types/JSON"; import JSONFunctions from "Common/Types/JSONFunctions"; @@ -11,53 +10,56 @@ import MonitorSecretService from "Common/Server/Services/MonitorSecretService"; import VMUtil from "Common/Server/Utils/VM/VMAPI"; import Monitor from "Common/Models/DatabaseModels/Monitor"; import MonitorSecret from "Common/Models/DatabaseModels/MonitorSecret"; +import MonitorTest from "Common/Models/DatabaseModels/MonitorTest"; +import ObjectID from "Common/Types/ObjectID"; +import MonitorSteps from "Common/Types/Monitor/MonitorSteps"; export default class MonitorUtil { - public static async populateSecrets(monitor: Monitor): Promise { + public static async loadMonitorSecrets( + monitorId: ObjectID, + ): Promise { + const secrets: Array = await MonitorSecretService.findBy({ + query: { + monitors: [monitorId] as any, + }, + select: { + secretValue: true, + name: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + props: { + isRoot: true, + }, + }); + + return secrets; + } + + public static async populateSecretsInMonitorSteps(data: { + monitorSteps: MonitorSteps; + monitorType: MonitorType; + monitorId: ObjectID; + }): Promise { const isSecretsLoaded: boolean = false; let monitorSecrets: MonitorSecret[] = []; - const loadSecrets: PromiseVoidFunction = async (): Promise => { - if (isSecretsLoaded) { - return; - } + const monitorSteps: MonitorSteps = data.monitorSteps; + const monitorType: MonitorType = data.monitorType; + const monitorId: ObjectID = data.monitorId; - if (!monitor.id) { - return; - } - - const secrets: Array = await MonitorSecretService.findBy({ - query: { - monitors: [monitor.id] as any, - }, - select: { - secretValue: true, - name: true, - }, - limit: LIMIT_PER_PROJECT, - skip: 0, - props: { - isRoot: true, - }, - }); - - monitorSecrets = secrets; - }; - - if (!monitor.monitorSteps) { - return monitor; - } - - if (monitor.monitorType === MonitorType.API) { - for (const monitorStep of monitor.monitorSteps?.data - ?.monitorStepsInstanceArray || []) { + if (monitorType === MonitorType.API) { + for (const monitorStep of monitorSteps?.data?.monitorStepsInstanceArray || + []) { if ( monitorStep.data?.requestHeaders && this.hasSecrets( JSONFunctions.toString(monitorStep.data.requestHeaders), ) ) { - await loadSecrets(); + if (!isSecretsLoaded) { + monitorSecrets = await MonitorUtil.loadMonitorSecrets(monitorId); + } monitorStep.data.requestHeaders = (await MonitorUtil.fillSecretsInStringOrJSON({ @@ -68,7 +70,9 @@ export default class MonitorUtil { monitorStep.data?.requestBody && this.hasSecrets(JSONFunctions.toString(monitorStep.data.requestBody)) ) { - await loadSecrets(); + if (!isSecretsLoaded) { + monitorSecrets = await MonitorUtil.loadMonitorSecrets(monitorId); + } monitorStep.data.requestBody = (await MonitorUtil.fillSecretsInStringOrJSON({ @@ -80,15 +84,15 @@ export default class MonitorUtil { } if ( - monitor.monitorType === MonitorType.API || - monitor.monitorType === MonitorType.IP || - monitor.monitorType === MonitorType.Ping || - monitor.monitorType === MonitorType.Port || - monitor.monitorType === MonitorType.Website || - monitor.monitorType === MonitorType.SSLCertificate + monitorType === MonitorType.API || + monitorType === MonitorType.IP || + monitorType === MonitorType.Ping || + monitorType === MonitorType.Port || + monitorType === MonitorType.Website || + monitorType === MonitorType.SSLCertificate ) { - for (const monitorStep of monitor.monitorSteps?.data - ?.monitorStepsInstanceArray || []) { + for (const monitorStep of monitorSteps?.data?.monitorStepsInstanceArray || + []) { if ( monitorStep.data?.monitorDestination && this.hasSecrets( @@ -96,7 +100,9 @@ export default class MonitorUtil { ) ) { // replace secret in monitorDestination. - await loadSecrets(); + if (!isSecretsLoaded) { + monitorSecrets = await MonitorUtil.loadMonitorSecrets(monitorId); + } monitorStep.data.monitorDestination = (await MonitorUtil.fillSecretsInStringOrJSON({ @@ -108,17 +114,19 @@ export default class MonitorUtil { } if ( - monitor.monitorType === MonitorType.SyntheticMonitor || - monitor.monitorType === MonitorType.CustomJavaScriptCode + monitorType === MonitorType.SyntheticMonitor || + monitorType === MonitorType.CustomJavaScriptCode ) { - for (const monitorStep of monitor.monitorSteps?.data - ?.monitorStepsInstanceArray || []) { + for (const monitorStep of monitorSteps?.data?.monitorStepsInstanceArray || + []) { if ( monitorStep.data?.customCode && this.hasSecrets(JSONFunctions.toString(monitorStep.data.customCode)) ) { // replace secret in script - await loadSecrets(); + if (!isSecretsLoaded) { + monitorSecrets = await MonitorUtil.loadMonitorSecrets(monitorId); + } monitorStep.data.customCode = (await MonitorUtil.fillSecretsInStringOrJSON({ @@ -129,6 +137,62 @@ export default class MonitorUtil { } } + return monitorSteps; + } + + public static async populateSecretsOnMonitorTest( + monitorTest: MonitorTest, + ): Promise { + const monitorId: ObjectID | undefined = monitorTest.monitorId; + + if (!monitorId) { + return monitorTest; + } + + if (!monitorTest.monitorSteps) { + return monitorTest; + } + + if (!monitorTest.monitorSteps.data) { + return monitorTest; + } + + if (!monitorTest.monitorType) { + return monitorTest; + } + + monitorTest.monitorSteps = await MonitorUtil.populateSecretsInMonitorSteps({ + monitorSteps: monitorTest.monitorSteps, + monitorType: monitorTest.monitorType, + monitorId: monitorId, + }); + + return monitorTest; + } + + public static async populateSecrets(monitor: Monitor): Promise { + if (!monitor.id) { + return monitor; + } + + if (!monitor.monitorSteps) { + return monitor; + } + + if (!monitor.monitorSteps.data) { + return monitor; + } + + if (!monitor.monitorType) { + return monitor; + } + + monitor.monitorSteps = await MonitorUtil.populateSecretsInMonitorSteps({ + monitorSteps: monitor.monitorSteps, + monitorType: monitor.monitorType, + monitorId: monitor.id, + }); + return monitor; }