diff --git a/db/knex_migrations/2024-10-31-0000-fix-snmp-monitor.js b/db/knex_migrations/2024-10-31-0000-fix-snmp-monitor.js new file mode 100644 index 000000000..0a605d38f --- /dev/null +++ b/db/knex_migrations/2024-10-31-0000-fix-snmp-monitor.js @@ -0,0 +1,7 @@ +exports.up = function (knex) { + return knex("monitor").whereNull("json_path_operator").update("json_path_operator", "=="); +}; +exports.down = function (knex) { + // changing the json_path_operator back to null for all "==" is not possible anymore + // we have lost the context which fields have been set explicitely in >= v2.0 and which would need to be reverted +}; diff --git a/extra/release/beta.mjs b/extra/release/beta.mjs index ec3be7c68..7ea13b9b4 100644 --- a/extra/release/beta.mjs +++ b/extra/release/beta.mjs @@ -6,14 +6,13 @@ import { checkDocker, checkTagExists, checkVersionFormat, - dryRun, - getRepoName, + getRepoNames, pressAnyKey, execSync, uploadArtifacts, } from "./lib.mjs"; import semver from "semver"; -const repoName = getRepoName(); +const repoNames = getRepoNames(); const version = process.env.RELEASE_BETA_VERSION; const githubToken = process.env.RELEASE_GITHUB_TOKEN; @@ -39,7 +38,7 @@ if (semverIdentifier[0] !== "beta") { checkDocker(); // Check if the tag exists -await checkTagExists(repoName, version); +await checkTagExists(repoNames, version); // node extra/beta/update-version.js execSync("node ./extra/beta/update-version.js"); @@ -48,16 +47,16 @@ execSync("node ./extra/beta/update-version.js"); buildDist(); // Build slim image (rootless) -buildImage(repoName, [ "beta-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); +buildImage(repoNames, [ "beta-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); // Build full image (rootless) -buildImage(repoName, [ "beta-rootless", ver(version, "rootless") ], "rootless"); +buildImage(repoNames, [ "beta-rootless", ver(version, "rootless") ], "rootless"); // Build slim image -buildImage(repoName, [ "beta-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); +buildImage(repoNames, [ "beta-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); // Build full image -buildImage(repoName, [ "beta", version ], "release"); +buildImage(repoNames, [ "beta", version ], "release"); await pressAnyKey(); diff --git a/extra/release/final.mjs b/extra/release/final.mjs index b4435064f..bf0ec2b0c 100644 --- a/extra/release/final.mjs +++ b/extra/release/final.mjs @@ -6,11 +6,11 @@ import { checkDocker, checkTagExists, checkVersionFormat, - getRepoName, + getRepoNames, pressAnyKey, execSync, uploadArtifacts } from "./lib.mjs"; -const repoName = getRepoName(); +const repoNames = getRepoNames(); const version = process.env.RELEASE_VERSION; const githubToken = process.env.RELEASE_GITHUB_TOKEN; @@ -28,7 +28,7 @@ checkVersionFormat(version); checkDocker(); // Check if the tag exists -await checkTagExists(repoName, version); +await checkTagExists(repoNames, version); // node extra/beta/update-version.js execSync("node extra/update-version.js"); @@ -37,16 +37,16 @@ execSync("node extra/update-version.js"); buildDist(); // Build slim image (rootless) -buildImage(repoName, [ "2-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); +buildImage(repoNames, [ "2-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); // Build full image (rootless) -buildImage(repoName, [ "2-rootless", ver(version, "rootless") ], "rootless"); +buildImage(repoNames, [ "2-rootless", ver(version, "rootless") ], "rootless"); // Build slim image -buildImage(repoName, [ "next-slim", "2-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); +buildImage(repoNames, [ "next-slim", "2-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); // Build full image -buildImage(repoName, [ "next", "2", version ], "release"); +buildImage(repoNames, [ "next", "2", version ], "release"); await pressAnyKey(); diff --git a/extra/release/lib.mjs b/extra/release/lib.mjs index 49391900a..cd06cd370 100644 --- a/extra/release/lib.mjs +++ b/extra/release/lib.mjs @@ -24,8 +24,15 @@ export function checkDocker() { /** * Get Docker Hub repository name */ -export function getRepoName() { - return process.env.RELEASE_REPO_NAME || "louislam/uptime-kuma"; +export function getRepoNames() { + if (process.env.RELEASE_REPO_NAMES) { + // Split by comma + return process.env.RELEASE_REPO_NAMES.split(",").map((name) => name.trim()); + } + return [ + "louislam/uptime-kuma", + "ghcr.io/louislam/uptime-kuma", + ]; } /** @@ -42,7 +49,7 @@ export function buildDist() { /** * Build docker image and push to Docker Hub - * @param {string} repoName Docker Hub repository name + * @param {string[]} repoNames Docker Hub repository names * @param {string[]} tags Docker image tags * @param {string} target Dockerfile's target name * @param {string} buildArgs Docker build args @@ -50,7 +57,7 @@ export function buildDist() { * @param {string} platform Build platform * @returns {void} */ -export function buildImage(repoName, tags, target, buildArgs = "", dockerfile = "docker/dockerfile", platform = "linux/amd64,linux/arm64,linux/arm/v7") { +export function buildImage(repoNames, tags, target, buildArgs = "", dockerfile = "docker/dockerfile", platform = "linux/amd64,linux/arm64,linux/arm/v7") { let args = [ "buildx", "build", @@ -60,9 +67,11 @@ export function buildImage(repoName, tags, target, buildArgs = "", dockerfile = platform, ]; - // Add tags - for (let tag of tags) { - args.push("-t", `${repoName}:${tag}`); + for (let repoName of repoNames) { + // Add tags + for (let tag of tags) { + args.push("-t", `${repoName}:${tag}`); + } } args = [ diff --git a/extra/release/nightly.mjs b/extra/release/nightly.mjs index c6641bad7..54951ac42 100644 --- a/extra/release/nightly.mjs +++ b/extra/release/nightly.mjs @@ -1,7 +1,7 @@ -import { buildDist, buildImage, checkDocker, getRepoName } from "./lib.mjs"; +import { buildDist, buildImage, checkDocker, getRepoNames } from "./lib.mjs"; // Docker Hub repository name -const repoName = getRepoName(); +const repoNames = getRepoNames(); // Check if docker is running checkDocker(); @@ -10,7 +10,7 @@ checkDocker(); buildDist(); // Build full image (rootless) -buildImage(repoName, [ "nightly2-rootless" ], "nightly-rootless"); +buildImage(repoNames, [ "nightly2-rootless" ], "nightly-rootless"); // Build full image -buildImage(repoName, [ "nightly2" ], "nightly"); +buildImage(repoNames, [ "nightly2" ], "nightly"); diff --git a/server/database.js b/server/database.js index 3927d6db8..3b7646de8 100644 --- a/server/database.js +++ b/server/database.js @@ -296,7 +296,7 @@ class Database { client: "mysql2", connection: { socketPath: embeddedMariaDB.socketPath, - user: "node", + user: embeddedMariaDB.username, database: "kuma", timezone: "Z", typeCast: function (field, next) { diff --git a/server/embedded-mariadb.js b/server/embedded-mariadb.js index 8aa7134bf..7c9630dd7 100644 --- a/server/embedded-mariadb.js +++ b/server/embedded-mariadb.js @@ -14,9 +14,15 @@ class EmbeddedMariaDB { mariadbDataDir = "/app/data/mariadb"; - runDir = "/app/data/run/mariadb"; + runDir = "/app/data/run"; - socketPath = this.runDir + "/mysqld.sock"; + socketPath = this.runDir + "/mariadb.sock"; + + /** + * The username to connect to the MariaDB + * @type {string} + */ + username = null; /** * @type {ChildProcessWithoutNullStreams} @@ -46,16 +52,42 @@ class EmbeddedMariaDB { /** * Start the embedded MariaDB + * @throws {Error} If the current user is not "node" or "root" * @returns {Promise|void} A promise that resolves when the MariaDB is started or void if it is already started */ start() { + // Check if the current user is "node" or "root" + this.username = require("os").userInfo().username; + if (this.username !== "node" && this.username !== "root") { + throw new Error("Embedded Mariadb supports only 'node' or 'root' user, but the current user is: " + this.username); + } + + this.initDB(); + + this.startChildProcess(); + + return new Promise((resolve) => { + let interval = setInterval(() => { + if (this.started) { + clearInterval(interval); + resolve(); + } else { + log.info("mariadb", "Waiting for Embedded MariaDB to start..."); + } + }, 1000); + }); + } + + /** + * Start the child process + * @returns {void} + */ + startChildProcess() { if (this.childProcess) { log.info("mariadb", "Already started"); return; } - this.initDB(); - this.running = true; log.info("mariadb", "Starting Embedded MariaDB"); this.childProcess = childProcess.spawn(this.exec, [ @@ -63,6 +95,8 @@ class EmbeddedMariaDB { "--datadir=" + this.mariadbDataDir, `--socket=${this.socketPath}`, `--pid-file=${this.runDir}/mysqld.pid`, + // Don't add the following option, the mariadb will not report message to the console, which affects initDBAfterStarted() + // "--log-error=" + `${this.mariadbDataDir}/mariadb-error.log`, ]); this.childProcess.on("close", (code) => { @@ -72,8 +106,8 @@ class EmbeddedMariaDB { log.info("mariadb", "Stopped Embedded MariaDB: " + code); if (code !== 0) { - log.info("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user"); - this.start(); + log.error("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user"); + this.startChildProcess(); } }); @@ -86,7 +120,7 @@ class EmbeddedMariaDB { }); let handler = (data) => { - log.debug("mariadb", data.toString("utf-8")); + log.info("mariadb", data.toString("utf-8")); if (data.toString("utf-8").includes("ready for connections")) { this.initDBAfterStarted(); } @@ -94,17 +128,6 @@ class EmbeddedMariaDB { this.childProcess.stdout.on("data", handler); this.childProcess.stderr.on("data", handler); - - return new Promise((resolve) => { - let interval = setInterval(() => { - if (this.started) { - clearInterval(interval); - resolve(); - } else { - log.info("mariadb", "Waiting for Embedded MariaDB to start..."); - } - }, 1000); - }); } /** @@ -129,9 +152,11 @@ class EmbeddedMariaDB { recursive: true, }); - let result = childProcess.spawnSync("mysql_install_db", [ + let result = childProcess.spawnSync("mariadb-install-db", [ "--user=node", - "--ldata=" + this.mariadbDataDir, + "--auth-root-socket-user=node", + "--datadir=" + this.mariadbDataDir, + "--auth-root-authentication-method=socket", ]); if (result.status !== 0) { @@ -143,6 +168,17 @@ class EmbeddedMariaDB { } } + // Check the owner of the mariadb directory, and change it if necessary + let stat = fs.statSync(this.mariadbDataDir); + if (stat.uid !== 1000 || stat.gid !== 1000) { + fs.chownSync(this.mariadbDataDir, 1000, 1000); + } + + // Check the permission of the mariadb directory, and change it if it is not 755 + if (stat.mode !== 0o755) { + fs.chmodSync(this.mariadbDataDir, 0o755); + } + if (!fs.existsSync(this.runDir)) { log.info("mariadb", `Embedded MariaDB: ${this.runDir} is not found, create one now.`); fs.mkdirSync(this.runDir, { @@ -150,6 +186,13 @@ class EmbeddedMariaDB { }); } + stat = fs.statSync(this.runDir); + if (stat.uid !== 1000 || stat.gid !== 1000) { + fs.chownSync(this.runDir, 1000, 1000); + } + if (stat.mode !== 0o755) { + fs.chmodSync(this.runDir, 0o755); + } } /** @@ -159,7 +202,7 @@ class EmbeddedMariaDB { async initDBAfterStarted() { const connection = mysql.createConnection({ socketPath: this.socketPath, - user: "node", + user: this.username, }); let result = await connection.execute("CREATE DATABASE IF NOT EXISTS `kuma`"); diff --git a/server/model/monitor.js b/server/model/monitor.js index 9a30a6689..3ad8cfafc 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1522,7 +1522,7 @@ class Monitor extends BeanModel { */ static async getMonitorTag(monitorIDs) { return await R.getAll(` - SELECT monitor_tag.monitor_id, monitor_tag.tag_id, tag.name, tag.color + SELECT monitor_tag.monitor_id, monitor_tag.tag_id, monitor_tag.value, tag.name, tag.color FROM monitor_tag JOIN tag ON monitor_tag.tag_id = tag.id WHERE monitor_tag.monitor_id IN (${monitorIDs.map((_) => "?").join(",")}) @@ -1567,6 +1567,8 @@ class Monitor extends BeanModel { } tagsMap.get(row.monitor_id).push({ tag_id: row.tag_id, + monitor_id: row.monitor_id, + value: row.value, name: row.name, color: row.color }); diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js index 209c7c0c6..5e25a1fbc 100644 --- a/server/notification-providers/slack.js +++ b/server/notification-providers/slack.js @@ -1,7 +1,7 @@ const NotificationProvider = require("./notification-provider"); const axios = require("axios"); const { setSettings, setting } = require("../util-server"); -const { getMonitorRelativeURL, UP } = require("../../src/util"); +const { getMonitorRelativeURL, UP, log } = require("../../src/util"); class Slack extends NotificationProvider { name = "slack"; @@ -50,15 +50,20 @@ class Slack extends NotificationProvider { const address = this.extractAddress(monitorJSON); if (address) { - actions.push({ - "type": "button", - "text": { - "type": "plain_text", - "text": "Visit site", - }, - "value": "Site", - "url": address, - }); + try { + actions.push({ + "type": "button", + "text": { + "type": "plain_text", + "text": "Visit site", + }, + "value": "Site", + "url": new URL(address), + }); + + } catch (e) { + log.debug("slack", `Failed to parse address ${address} as URL`); + } } return actions; diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index 0804da15d..cbcc52b8f 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -220,13 +220,17 @@ module.exports.statusPageSocketHandler = (socket) => { // Delete groups that are not in the list log.debug("socket", "Delete groups that are not in the list"); - const slots = groupIDList.map(() => "?").join(","); + if (groupIDList.length === 0) { + await R.exec("DELETE FROM `group` WHERE status_page_id = ?", [ statusPage.id ]); + } else { + const slots = groupIDList.map(() => "?").join(","); - const data = [ - ...groupIDList, - statusPage.id - ]; - await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data); + const data = [ + ...groupIDList, + statusPage.id + ]; + await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data); + } const server = UptimeKumaServer.getInstance(); diff --git a/src/components/notifications/SendGrid.vue b/src/components/notifications/SendGrid.vue index 18118f469..53f1f1137 100644 --- a/src/components/notifications/SendGrid.vue +++ b/src/components/notifications/SendGrid.vue @@ -5,20 +5,20 @@
- +
- +
- +
{{ $t("Separate multiple email addresses with commas") }}
- + {{ $t("Separate multiple email addresses with commas") }}
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 4763f8722..a4f93bc2c 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -1065,7 +1065,7 @@ import { hostNameRegexPattern } from "../util-frontend"; import HiddenInput from "../components/HiddenInput.vue"; import EditMonitorConditions from "../components/EditMonitorConditions.vue"; -const toast = useToast; +const toast = useToast(); const pushTokenLength = 32;