Improve the setup database for embedded MariaDB

This commit is contained in:
Louis Lam 2023-06-30 22:17:07 +08:00
parent 7975caf29e
commit d286c534bd
7 changed files with 160 additions and 91 deletions

View file

@ -23,6 +23,7 @@ RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyr
# Not working for armv7, so use the older version (10.5) of MariaDB from the debian repo # Not working for armv7, so use the older version (10.5) of MariaDB from the debian repo
# curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | bash -s -- --mariadb-server-version="mariadb-11.1" && \ # curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | bash -s -- --mariadb-server-version="mariadb-11.1" && \
FROM base2-slim AS base2 FROM base2-slim AS base2
ENV UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB=1
RUN apt update && \ RUN apt update && \
apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk mariadb-server && \ apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk mariadb-server && \
apt --yes remove curl && \ apt --yes remove curl && \
@ -30,4 +31,4 @@ RUN apt update && \
apt --yes autoremove && \ apt --yes autoremove && \
chown -R node:node /var/lib/mysql chown -R node:node /var/lib/mysql
ENV UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB=1

View file

@ -5,7 +5,7 @@ services:
container_name: uptime-kuma-dev container_name: uptime-kuma-dev
image: louislam/uptime-kuma:nightly2 image: louislam/uptime-kuma:nightly2
volumes: volumes:
- ./data:/app/data #- ./data:/app/data
- ../server:/app/server - ../server:/app/server
ports: ports:
- "3001:3001" # <Host Port>:<Container Port> - "3001:3001" # <Host Port>:<Container Port>

View file

@ -34,8 +34,8 @@
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push", "build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
"build-docker-slim": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim -t louislam/uptime-kuma:$VERSION-slim --target release --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push", "build-docker-slim": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim -t louislam/uptime-kuma:$VERSION-slim --target release --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push",
"build-docker-full": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2 -t louislam/uptime-kuma:$VERSION --target release . --push", "build-docker-full": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2 -t louislam/uptime-kuma:$VERSION --target release . --push",
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2 --target nightly --build-arg . --push", "build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2 --target nightly . --push",
"build-docker-nightly-local": "docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .", "build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push", "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.22.0 && npm ci --production && npm run download-dist", "setup": "git checkout 1.22.0 && npm ci --production && npm run download-dist",
@ -63,7 +63,7 @@
"deploy-demo-server": "node extra/deploy-demo-server.js", "deploy-demo-server": "node extra/deploy-demo-server.js",
"sort-contributors": "node extra/sort-contributors.js", "sort-contributors": "node extra/sort-contributors.js",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2", "quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up" "start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate"
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "~1.7.3", "@grpc/grpc-js": "~1.7.3",

View file

@ -76,7 +76,9 @@ log.info("server", "Importing this project modules");
log.debug("server", "Importing Monitor"); log.debug("server", "Importing Monitor");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
log.debug("server", "Importing Settings"); log.debug("server", "Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests } = require("./util-server"); const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests,
allowDevAllOrigin
} = require("./util-server");
log.debug("server", "Importing Notification"); log.debug("server", "Importing Notification");
const { Notification } = require("./notification"); const { Notification } = require("./notification");
@ -228,6 +230,14 @@ let needSetup = false;
} }
}); });
app.get("/setup-database-info", (request, response) => {
allowDevAllOrigin(response);
response.json({
runningSetup: false,
needSetup: false,
});
});
if (isDev) { if (isDev) {
app.post("/test-webhook", async (request, response) => { app.post("/test-webhook", async (request, response) => {
log.debug("test", request.headers); log.debug("test", request.headers);

View file

@ -18,6 +18,7 @@ class SetupDatabase {
* @type {boolean} * @type {boolean}
*/ */
needSetup = true; needSetup = true;
runningSetup = false;
server; server;
@ -80,6 +81,12 @@ class SetupDatabase {
let tempServer; let tempServer;
app.use(express.json()); app.use(express.json());
// Disable Keep Alive, otherwise the server will not shutdown, as the client will keep the connection alive
app.use(function (req, res, next) {
res.setHeader("Connection", "close");
next();
});
app.get("/", async (request, response) => { app.get("/", async (request, response) => {
response.redirect("/setup-database"); response.redirect("/setup-database");
}); });
@ -91,9 +98,12 @@ class SetupDatabase {
}); });
}); });
app.get("/info", (request, response) => { app.get("/setup-database-info", (request, response) => {
allowDevAllOrigin(response); allowDevAllOrigin(response);
console.log("Request /setup-database-info");
response.json({ response.json({
runningSetup: this.runningSetup,
needSetup: this.needSetup,
isEnabledEmbeddedMariaDB: this.isEnabledEmbeddedMariaDB(), isEnabledEmbeddedMariaDB: this.isEnabledEmbeddedMariaDB(),
}); });
}); });
@ -101,7 +111,12 @@ class SetupDatabase {
app.post("/setup-database", async (request, response) => { app.post("/setup-database", async (request, response) => {
allowDevAllOrigin(response); allowDevAllOrigin(response);
console.log(request); if (this.runningSetup) {
response.status(400).json("Setup is already running");
return;
}
this.runningSetup = true;
let dbConfig = request.body.dbConfig; let dbConfig = request.body.dbConfig;
@ -114,42 +129,50 @@ class SetupDatabase {
// Validate input // Validate input
if (typeof dbConfig !== "object") { if (typeof dbConfig !== "object") {
response.status(400).json("Invalid dbConfig"); response.status(400).json("Invalid dbConfig");
this.runningSetup = false;
return; return;
} }
if (!dbConfig.type) { if (!dbConfig.type) {
response.status(400).json("Database Type is required"); response.status(400).json("Database Type is required");
this.runningSetup = false;
return; return;
} }
if (!supportedDBTypes.includes(dbConfig.type)) { if (!supportedDBTypes.includes(dbConfig.type)) {
response.status(400).json("Unsupported Database Type"); response.status(400).json("Unsupported Database Type");
this.runningSetup = false;
return; return;
} }
if (dbConfig.type === "mariadb") { if (dbConfig.type === "mariadb") {
if (!dbConfig.hostname) { if (!dbConfig.hostname) {
response.status(400).json("Hostname is required"); response.status(400).json("Hostname is required");
this.runningSetup = false;
return; return;
} }
if (!dbConfig.port) { if (!dbConfig.port) {
response.status(400).json("Port is required"); response.status(400).json("Port is required");
this.runningSetup = false;
return; return;
} }
if (!dbConfig.dbName) { if (!dbConfig.dbName) {
response.status(400).json("Database name is required"); response.status(400).json("Database name is required");
this.runningSetup = false;
return; return;
} }
if (!dbConfig.username) { if (!dbConfig.username) {
response.status(400).json("Username is required"); response.status(400).json("Username is required");
this.runningSetup = false;
return; return;
} }
if (!dbConfig.password) { if (!dbConfig.password) {
response.status(400).json("Password is required"); response.status(400).json("Password is required");
this.runningSetup = false;
return; return;
} }
} }
@ -162,11 +185,16 @@ class SetupDatabase {
}); });
// Shutdown down this express and start the main server // Shutdown down this express and start the main server
log.info("setup-database", "Database is configured, close setup-database server and start the main server now."); log.info("setup-database", "Database is configured, close the setup-database server and start the main server now.");
if (tempServer) { if (tempServer) {
tempServer.close(); tempServer.close(() => {
log.info("setup-database", "The setup-database server is closed");
resolve();
});
} else {
resolve();
} }
resolve();
}); });
app.use("/", expressStaticGzip("dist", { app.use("/", expressStaticGzip("dist", {

View file

@ -62,6 +62,8 @@ export default {
}, },
mounted() { mounted() {
// TODO: Check if it is a database setup
this.$root.getSocket().emit("needSetup", (needSetup) => { this.$root.getSocket().emit("needSetup", (needSetup) => {
if (! needSetup) { if (! needSetup) {
this.$router.push("/"); this.$router.push("/");

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="form-container"> <div v-if="show" class="form-container">
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div> <div>
<object width="64" height="64" data="/icon.svg" /> <object width="64" height="64" data="/icon.svg" />
@ -8,83 +8,94 @@
</div> </div>
</div> </div>
<div class="form-floating short mt-3"> <div v-if="info.runningSetup" class="mt-5">
<select id="language" v-model="$root.language" class="form-select"> <div class="alert alert-success" role="alert">
<option v-for="(lang, i) in $i18n.availableLocales" :key="`Lang${i}`" :value="lang"> <div class="d-flex align-items-center">
{{ $i18n.messages[lang].languageName }} <strong>Setting up the database. It may take a while, please be patient.</strong>
</option> <div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
</select> </div>
<label for="language" class="form-label">{{ $t("Language") }}</label> </div>
</div> </div>
<p class="mt-5 short"> <template v-if="!info.runningSetup">
{{ $t("setupDatabaseChooseDatabase") }} <div class="form-floating short mt-3">
</p> <select id="language" v-model="$root.language" class="form-select">
<option v-for="(lang, i) in $i18n.availableLocales" :key="`Lang${i}`" :value="lang">
{{ $i18n.messages[lang].languageName }}
</option>
</select>
<label for="language" class="form-label">{{ $t("Language") }}</label>
</div>
<div class="btn-group" role="group" aria-label="Basic radio toggle button group"> <p class="mt-5 short">
<template v-if="isEnabledEmbeddedMariaDB"> {{ $t("setupDatabaseChooseDatabase") }}
<input id="btnradio3" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="embedded-mariadb"> </p>
<label class="btn btn-outline-primary" for="btnradio3"> <div class="btn-group" role="group" aria-label="Basic radio toggle button group">
Embedded MariaDB <template v-if="info.isEnabledEmbeddedMariaDB">
<input id="btnradio3" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="embedded-mariadb">
<label class="btn btn-outline-primary" for="btnradio3">
Embedded MariaDB
</label>
</template>
<input id="btnradio2" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="mariadb">
<label class="btn btn-outline-primary" for="btnradio2">
MariaDB/MySQL
</label> </label>
<input id="btnradio1" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="sqlite">
<label class="btn btn-outline-primary" for="btnradio1">
SQLite
</label>
</div>
<p v-if="dbConfig.type === 'embedded-mariadb'" class="mt-3">
{{ $t("setupDatabaseEmbeddedMariaDB") }}
</p>
<p v-if="dbConfig.type === 'mariadb'" class="mt-3">
{{ $t("setupDatabaseMariaDB") }}
</p>
<p v-if="dbConfig.type === 'sqlite'" class="mt-3">
{{ $t("setupDatabaseSQLite") }}
</p>
<template v-if="dbConfig.type === 'mariadb'">
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.hostname" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("Hostname") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.port" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("Port") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.username" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("Username") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.password" type="passwrod" class="form-control" required>
<label for="floatingInput">{{ $t("Password") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.dbName" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("dbName") }}</label>
</div>
</template> </template>
<input id="btnradio2" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="mariadb"> <button v-if="dbConfig.type === 'mariadb'" class="btn btn-warning mt-3" @submit.prevent="test">{{ $t("Test") }}</button>
<label class="btn btn-outline-primary" for="btnradio2">
MariaDB/MySQL
</label>
<input id="btnradio1" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="sqlite"> <button class="btn btn-primary mt-4 short" type="submit" :disabled="disabledButton">
<label class="btn btn-outline-primary" for="btnradio1"> {{ $t("Next") }}
SQLite </button>
</label>
</div>
<p v-if="dbConfig.type === 'embedded-mariadb'" class="mt-3">
{{ $t("setupDatabaseEmbeddedMariaDB") }}
</p>
<p v-if="dbConfig.type === 'mariadb'" class="mt-3">
{{ $t("setupDatabaseMariaDB") }}
</p>
<p v-if="dbConfig.type === 'sqlite'" class="mt-3">
{{ $t("setupDatabaseSQLite") }}
</p>
<template v-if="dbConfig.type === 'mariadb'">
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.hostname" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("Hostname") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.port" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("Port") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.username" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("Username") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.password" type="passwrod" class="form-control" required>
<label for="floatingInput">{{ $t("Password") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.dbName" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("dbName") }}</label>
</div>
</template> </template>
<button v-if="dbConfig.type === 'mariadb'" class="btn btn-warning mt-3" @submit.prevent="test">{{ $t("Test") }}</button>
<button class="btn btn-primary mt-4 short" type="submit" :disabled="disabledButton">
{{ $t("Next") }}
</button>
</form> </form>
</div> </div>
</template> </template>
@ -98,8 +109,7 @@ const toast = useToast();
export default { export default {
data() { data() {
return { return {
processing: false, show: false,
isEnabledEmbeddedMariaDB: false,
dbConfig: { dbConfig: {
type: undefined, type: undefined,
port: 3306, port: 3306,
@ -108,20 +118,31 @@ export default {
password: "", password: "",
dbName: "kuma", dbName: "kuma",
}, },
info: {
needSetup: false,
runningSetup: false,
isEnabledEmbeddedMariaDB: false,
},
}; };
}, },
computed: { computed: {
disabledButton() { disabledButton() {
return this.dbConfig.type === undefined || this.processing; return this.dbConfig.type === undefined || this.info.runningSetup;
}, },
}, },
async mounted() { async mounted() {
let res = await axios.get("/info"); let res = await axios.get("/setup-database-info");
this.isEnabledEmbeddedMariaDB = res.data.isEnabledEmbeddedMariaDB; this.info = res.data;
if (this.info && this.info.needSetup === false) {
location.href = "/setup";
} else {
this.show = true;
}
}, },
methods: { methods: {
async submit() { async submit() {
this.processing = true; this.info.runningSetup = true;
try { try {
let res = await axios.post("/setup-database", { let res = await axios.post("/setup-database", {
@ -129,12 +150,11 @@ export default {
}); });
await sleep(2000); await sleep(2000);
// TODO: an interval to check if the main server is ready, it is ready, go to "/" again to continue the setup of admin account
await this.goToMainServerWhenReady(); await this.goToMainServerWhenReady();
} catch (e) { } catch (e) {
toast.error(e.response.data); toast.error(e.response.data);
} finally { } finally {
this.processing = false; this.info.runningSetup = false;
} }
}, },
@ -142,10 +162,14 @@ export default {
async goToMainServerWhenReady() { async goToMainServerWhenReady() {
try { try {
console.log("Trying..."); console.log("Trying...");
let res = await axios.get("/api/entry-page"); let res = await axios.get("/setup-database-info");
if (res.data && res.data.type === "entryPage") { if (res.data && res.data.needSetup === false) {
location.href = "/"; this.show = false;
location.href = "/setup";
} else { } else {
if (res.data) {
this.info = res.data;
}
throw new Error("not ready"); throw new Error("not ready");
} }
} catch (e) { } catch (e) {
@ -153,6 +177,10 @@ export default {
await sleep(2000); await sleep(2000);
await this.goToMainServerWhenReady(); await this.goToMainServerWhenReady();
} }
},
test() {
toast.error("not implemented");
} }
}, },
}; };