diff --git a/packages/admin-ui/src/__mock-backend__/index.ts b/packages/admin-ui/src/__mock-backend__/index.ts index b547e360dc..f47b444496 100644 --- a/packages/admin-ui/src/__mock-backend__/index.ts +++ b/packages/admin-ui/src/__mock-backend__/index.ts @@ -386,7 +386,9 @@ export const otherCalls: Omit = { dockerLatestVersion: "20.10.8" }), getIsConnectedToInternet: async () => false, - getCoreVersion: async () => "0.2.92" + getCoreVersion: async () => "0.2.92", + gatusGetEndpoints: async () => new Map(), + gatusUpdateEndpoint: async () => {} }; export const calls: Routes = { diff --git a/packages/dappmanager/src/api/routes/packageManifest.ts b/packages/dappmanager/src/api/routes/packageManifest.ts index d8b38b9d59..bbc63afb0c 100644 --- a/packages/dappmanager/src/api/routes/packageManifest.ts +++ b/packages/dappmanager/src/api/routes/packageManifest.ts @@ -54,7 +54,8 @@ export const packageManifest = wrapHandler(async (req, res) => { "links", "repository", "bugs", - "license" + "license", + "notifications" ]); res.status(200).send(filteredManifest); diff --git a/packages/dappmanager/src/calls/gatusConfig.ts b/packages/dappmanager/src/calls/gatusConfig.ts new file mode 100644 index 0000000000..4cb22a8df4 --- /dev/null +++ b/packages/dappmanager/src/calls/gatusConfig.ts @@ -0,0 +1,56 @@ +import { listPackages } from "@dappnode/dockerapi"; +import { GatusConfig, Endpoint, Manifest } from "@dappnode/types"; +import { getManifestPath } from "@dappnode/utils"; +import fs from "fs"; + +/** + * Get gatus endpoints indexed by dnpName + */ +export async function gatusGetEndpoints(): Promise> { + const packages = await listPackages(); + + // Read all manifests files and retrieve the gatus config + const endpoints = new Map(); + for (const pkg of packages) { + const manifestPath = getManifestPath(pkg.dnpName, pkg.isCore); + const manifest: Manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + if (manifest.notifications) endpoints.set(pkg.dnpName, manifest.notifications); + } + + return endpoints; +} + +/** + * Update endpoint properties + * @param dnpName + * @param updatedEndpoint + */ +export async function gatusUpdateEndpoint({ + dnpName, + updatedEndpoint +}: { + dnpName: string; + updatedEndpoint: Endpoint; +}): Promise { + // Get current endpoint status + const manifest: Manifest = JSON.parse(fs.readFileSync(getManifestPath(dnpName, false), "utf8")); + if (!manifest.notifications) throw new Error("No notifications found in manifest"); + + const endpoint = manifest.notifications.endpoints.find((e) => e.name === updatedEndpoint.name); + if (!endpoint) throw new Error(`Endpoint ${updatedEndpoint.name} not found in manifest`); + + // Update endpoint + Object.assign(endpoint, updatedEndpoint); + + // Save manifest + fs.writeFileSync(getManifestPath(dnpName, false), JSON.stringify(manifest, null, 2)); + + // Update endpoint in gatus + // await fetch(`http://notifier.notifications.dappnode:8082/gatus/endpoints`, { + // method: "POST", + // headers: { + // "Content-Type": "application/json" + // }, + // body: JSON.stringify(endpoint) + // }); +} diff --git a/packages/dappmanager/src/calls/index.ts b/packages/dappmanager/src/calls/index.ts index a61851ced7..e2b1b1f658 100644 --- a/packages/dappmanager/src/calls/index.ts +++ b/packages/dappmanager/src/calls/index.ts @@ -22,6 +22,7 @@ export { getCoreVersion } from "./getCoreVersion.js"; export { getUserActionLogs } from "./getUserActionLogs.js"; export { getHostUptime } from "./getHostUptime.js"; export { getIsConnectedToInternet } from "./getIsConnectedToInternet.js"; +export { gatusGetEndpoints, gatusUpdateEndpoint } from "./gatusConfig.js"; export * from "./httpsPortal.js"; export { ipfsTest } from "./ipfsTest.js"; export { ipfsClientTargetSet } from "./ipfsClientTargetSet.js"; diff --git a/packages/installer/src/dappnodeInstaller.ts b/packages/installer/src/dappnodeInstaller.ts index 40aa987a35..42786a7a4b 100644 --- a/packages/installer/src/dappnodeInstaller.ts +++ b/packages/installer/src/dappnodeInstaller.ts @@ -11,7 +11,8 @@ import { PackageRequest, SetupWizard, GrafanaDashboard, - PrometheusTarget + PrometheusTarget, + GatusConfig } from "@dappnode/types"; import { DappGetState, DappgetOptions, dappGet } from "./dappGet/index.js"; import { validateDappnodeCompose, validateManifestSchema } from "@dappnode/schemas"; @@ -72,7 +73,8 @@ export class DappnodeInstaller extends DappnodeRepository { disclaimer: pkgRelease.disclaimer, gettingStarted: pkgRelease.gettingStarted, grafanaDashboards: pkgRelease.grafanaDashboards, - prometheusTargets: pkgRelease.prometheusTargets + prometheusTargets: pkgRelease.prometheusTargets, + notifications: pkgRelease.notifications }); // set compose to custom dappnode compose in release @@ -107,7 +109,8 @@ export class DappnodeInstaller extends DappnodeRepository { disclaimer: pkgRelease.disclaimer, gettingStarted: pkgRelease.gettingStarted, grafanaDashboards: pkgRelease.grafanaDashboards, - prometheusTargets: pkgRelease.prometheusTargets + prometheusTargets: pkgRelease.prometheusTargets, + notifications: pkgRelease.notifications }); }); @@ -151,7 +154,8 @@ export class DappnodeInstaller extends DappnodeRepository { disclaimer, gettingStarted, prometheusTargets, - grafanaDashboards + grafanaDashboards, + notifications }: { manifest: Manifest; SetupWizard?: SetupWizard; @@ -159,12 +163,14 @@ export class DappnodeInstaller extends DappnodeRepository { gettingStarted?: string; prometheusTargets?: PrometheusTarget[]; grafanaDashboards?: GrafanaDashboard[]; + notifications?: GatusConfig; }): Manifest { if (SetupWizard) manifest.setupWizard = SetupWizard; if (disclaimer) manifest.disclaimer = { message: disclaimer }; if (gettingStarted) manifest.gettingStarted = gettingStarted; if (prometheusTargets) manifest.prometheusTargets = prometheusTargets; if (grafanaDashboards && grafanaDashboards.length > 0) manifest.grafanaDashboards = grafanaDashboards; + if (notifications) manifest.notifications = notifications; return manifest; } diff --git a/packages/installer/src/installer/writeAndValidateFiles.ts b/packages/installer/src/installer/writeAndValidateFiles.ts index 8ad61238a9..a69c6688c0 100644 --- a/packages/installer/src/installer/writeAndValidateFiles.ts +++ b/packages/installer/src/installer/writeAndValidateFiles.ts @@ -1,10 +1,10 @@ import fs from "fs"; import { Log } from "@dappnode/logger"; import { validatePath } from "@dappnode/utils"; -import { InstallPackageData } from "@dappnode/types"; +import { InstallPackageData, Manifest } from "@dappnode/types"; import { dockerComposeConfig } from "@dappnode/dockerapi"; import { ComposeEditor } from "@dappnode/dockercompose"; -import { isNotFoundError, writeManifest } from "@dappnode/utils"; +import { isNotFoundError } from "@dappnode/utils"; /** * Write the new compose and test it with config @@ -47,3 +47,12 @@ function copyIfExists(src: string, dest: string): void { if (!isNotFoundError(e)) throw e; } } + +/** + * Util: Write manifest to file + * @param manfiestPath + * @param manifest + */ +function writeManifest(manfiestPath: string, manifest: Manifest): void { + fs.writeFileSync(manfiestPath, JSON.stringify(manifest, null, 2)); +} diff --git a/packages/installer/test/unit/release/findEntries.test.ts b/packages/installer/test/unit/release/findEntries.test.ts index c8a27f3b19..649e374acf 100644 --- a/packages/installer/test/unit/release/findEntries.test.ts +++ b/packages/installer/test/unit/release/findEntries.test.ts @@ -54,7 +54,8 @@ describe("validateTarImage", () => { "host-grafana-dashboard.json", "prometheus-targets.json", "setup-wizard.json", - "signature.json" + "signature.json", + "notifications.yaml" ].map((name) => ({ name, path: `Qm-root/${name}`, @@ -70,6 +71,7 @@ describe("validateTarImage", () => { disclaimer: "disclaimer.md", gettingStarted: "getting-started.md", prometheusTargets: "prometheus-targets.json", + notifications: "notifications.yaml", grafanaDashboards: ["docker-grafana-dashboard.json", "host-grafana-dashboard.json"] }; diff --git a/packages/toolkit/src/repository/repository.ts b/packages/toolkit/src/repository/repository.ts index c86992a794..56e9c87f07 100644 --- a/packages/toolkit/src/repository/repository.ts +++ b/packages/toolkit/src/repository/repository.ts @@ -205,7 +205,8 @@ export class DappnodeRepository extends ApmRepository { disclaimer: await this.getPkgAsset(releaseFilesToDownload.disclaimer, ipfsEntries), gettingStarted: await this.getPkgAsset(releaseFilesToDownload.gettingStarted, ipfsEntries), prometheusTargets: await this.getPkgAsset(releaseFilesToDownload.prometheusTargets, ipfsEntries), - grafanaDashboards: await this.getPkgAsset(releaseFilesToDownload.grafanaDashboards, ipfsEntries) + grafanaDashboards: await this.getPkgAsset(releaseFilesToDownload.grafanaDashboards, ipfsEntries), + notifications: await this.getPkgAsset(releaseFilesToDownload.notifications, ipfsEntries) }; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 61b5aa63dd..15c5bf572b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -10,6 +10,7 @@ export * from "./releaseFiles.js"; export * from "./errors.js"; export * from "./routes.js"; export * from "./subscriptions.js"; +export * from "./notifications.js"; // utils export * from "./utils/index.js"; diff --git a/packages/types/src/manifest.ts b/packages/types/src/manifest.ts index f81ed8acdb..74e9042299 100644 --- a/packages/types/src/manifest.ts +++ b/packages/types/src/manifest.ts @@ -1,3 +1,4 @@ +import { GatusConfig } from "./notifications.js"; import { SetupSchema, SetupTarget, SetupUiJson, SetupWizard } from "./setupWizard.js"; export interface Manifest { @@ -98,6 +99,9 @@ export interface Manifest { // setupWizard for compacted manifests in core packages setupWizard?: SetupWizard; + + // notifications + notifications?: GatusConfig; } export interface UpstreamItem { diff --git a/packages/types/src/notifications.ts b/packages/types/src/notifications.ts new file mode 100644 index 0000000000..440570aaf4 --- /dev/null +++ b/packages/types/src/notifications.ts @@ -0,0 +1,29 @@ +export interface GatusConfig { + endpoints: Endpoint[]; +} + +export interface Endpoint { + name: string; + enabled: boolean; + url: string; + method: string; + conditions: string[]; + interval: string; // e.g., "1m" + group: string; + alerts: Alert[]; + description: string; // dappnode specific + metric?: { + // dappnode specific + min: number; + max: number; + unit: string; // e.g ÂșC + }; +} + +interface Alert { + type: string; + "failure-threshold": number; + "success-threshold": number; + "send-on-resolved": boolean; + description: string; +} diff --git a/packages/types/src/pkg.ts b/packages/types/src/pkg.ts index dd74b29166..16d9d6a973 100644 --- a/packages/types/src/pkg.ts +++ b/packages/types/src/pkg.ts @@ -1,5 +1,6 @@ import { Compose } from "./compose.js"; import { Manifest, PrometheusTarget, GrafanaDashboard } from "./manifest.js"; +import { GatusConfig } from "./notifications.js"; import { SetupWizard } from "./setupWizard.js"; /** @@ -97,6 +98,7 @@ export type DirectoryFiles = { gettingStarted?: string; prometheusTargets?: PrometheusTarget[]; grafanaDashboards?: GrafanaDashboard[]; + notifications?: GatusConfig; }; export interface FileConfig { diff --git a/packages/types/src/releaseFiles.ts b/packages/types/src/releaseFiles.ts index 2c4d6df4b5..af7df593fd 100644 --- a/packages/types/src/releaseFiles.ts +++ b/packages/types/src/releaseFiles.ts @@ -84,6 +84,13 @@ export const releaseFiles = Object.freeze({ maxSize: 10e6, // ~ 10MB required: false as const, multiple: true as const + }), + notifications: Object.freeze({ + regex: /^.*notifications\.yaml$/, + format: FileFormat.YAML, + maxSize: 10e3, + required: false as const, + multiple: false as const }) } as const); @@ -95,5 +102,6 @@ export const releaseFilesToDownload = { disclaimer: releaseFiles.disclaimer, gettingStarted: releaseFiles.gettingStarted, prometheusTargets: releaseFiles.prometheusTargets, - grafanaDashboards: releaseFiles.grafanaDashboards + grafanaDashboards: releaseFiles.grafanaDashboards, + notifications: releaseFiles.notifications }; diff --git a/packages/types/src/routes.ts b/packages/types/src/routes.ts index 5173890dc1..cb15f79aef 100644 --- a/packages/types/src/routes.ts +++ b/packages/types/src/routes.ts @@ -45,6 +45,7 @@ import { } from "./calls.js"; import { PackageEnvs } from "./compose.js"; import { PackageBackup } from "./manifest.js"; +import { Endpoint, GatusConfig } from "./notifications.js"; import { TrustedReleaseKey } from "./pkg.js"; import { OptimismConfigSet, OptimismConfigGet } from "./rollups.js"; import { Network, StakerConfigGet, StakerConfigSet } from "./stakers.js"; @@ -261,6 +262,16 @@ export interface Routes { */ fetchDnpRequest: (kwargs: { id: string; version?: string }) => Promise; + /** + * Gatus get endpoints + */ + gatusGetEndpoints(): Promise>; + + /** + * Gatus update endpoint + */ + gatusUpdateEndpoint: (kwargs: { dnpName: string; updatedEndpoint: Endpoint }) => Promise; + /** * Returns the user action logs. This logs are stored in a different * file and format, and are meant to ease user support @@ -690,6 +701,8 @@ export const routesData: { [P in keyof Routes]: RouteData } = { fetchDirectory: {}, fetchRegistry: {}, fetchDnpRequest: {}, + gatusGetEndpoints: { log: true }, + gatusUpdateEndpoint: { log: true }, getUserActionLogs: {}, getHostUptime: {}, httpsPortalMappingAdd: { log: true }, diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index b2625f7596..62361276b9 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -37,6 +37,5 @@ export { shouldUpdate } from "./shouldUpdate.js"; export { getPublicIpFromUrls } from "./getPublicIpFromUrls.js"; export { computeSemverUpdateType } from "./computeSemverUpdateType.js"; export * from "./coreVersionId.js"; -export { writeManifest } from "./writeManifest.js"; export { readManifestIfExists } from "./readManifestIfExists.js"; export { removeCidrSuffix } from "./removeCidrSuffix.js"; diff --git a/packages/utils/src/writeManifest.ts b/packages/utils/src/writeManifest.ts deleted file mode 100644 index 00cc398a1c..0000000000 --- a/packages/utils/src/writeManifest.ts +++ /dev/null @@ -1,6 +0,0 @@ -import fs from "fs"; -import { Manifest } from "@dappnode/types"; - -export function writeManifest(manfiestPath: string, manifest: Manifest): void { - fs.writeFileSync(manfiestPath, JSON.stringify(manifest, null, 2)); -}