hrms-api-log/src/controllers/backup-controller.ts

420 lines
16 KiB
TypeScript
Raw Normal View History

2024-07-17 14:02:52 +07:00
import { Body, Controller, Delete, Get, Path, Post, Put, Route, Security } from "tsoa";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
2024-07-17 14:02:52 +07:00
import { randomUUID } from "crypto";
2024-07-10 17:53:27 +07:00
function getEnvVar(environmentName: string) {
const environmentValue = process.env[environmentName];
if (!environmentValue) throw new Error(`${environmentName} is required.`);
return environmentValue;
2024-07-10 17:53:27 +07:00
}
const WINDMILL_URL = getEnvVar("WINDMILL_URL");
const WINDMILL_WORKSPACE = getEnvVar("WINDMILL_WORKSPACE");
const WINDMILL_BACKUP_FLOW_PATH = getEnvVar("WINDMILL_BACKUP_FLOW_PATH");
const WINDMILL_RESTORE_FLOW_PATH = getEnvVar("WINDMILL_RESTORE_FLOW_PATH");
2024-07-16 13:24:27 +07:00
const WINDMILL_BACKUP_DELETE_SCRIPT_PATH = getEnvVar("WINDMILL_BACKUP_DELETE_SCRIPT_PATH");
const WINDMILL_BACKUP_LIST_SCRIPT_PATH = getEnvVar("WINDMILL_BACKUP_LIST_SCRIPT_PATH");
2024-07-10 17:53:27 +07:00
const WINDMILL_API_KEY = getEnvVar("WINDMILL_API_KEY");
const DB_HOST = getEnvVar("DB_HOST");
const DB_PORT = process.env.DB_PORT;
const DB_USERNAME = getEnvVar("DB_USERNAME");
const DB_PASSWORD = getEnvVar("DB_PASSWORD");
const DB_LIST = process.env.DB_LIST;
const MAIN_MINIO_USE_SSL = getEnvVar("MAIN_MINIO_USE_SSL");
const MAIN_MINIO_HOST = getEnvVar("MAIN_MINIO_HOST");
const MAIN_MINIO_PORT = process.env.MAIN_MINIO_PORT;
const MAIN_MINIO_ACCESS_KEY = getEnvVar("MAIN_MINIO_ACCESS_KEY");
const MAIN_MINIO_SECRET_KEY = getEnvVar("MAIN_MINIO_SECRET_KEY");
const MAIN_MINIO_BUCKET = getEnvVar("MAIN_MINIO_BUCKET");
const BACKUP_MINIO_USE_SSL = getEnvVar("BACKUP_MINIO_USE_SSL");
const BACKUP_MINIO_HOST = getEnvVar("BACKUP_MINIO_HOST");
const BACKUP_MINIO_PORT = process.env.BACKUP_MINIO_PORT;
const BACKUP_MINIO_ACCESS_KEY = getEnvVar("BACKUP_MINIO_ACCESS_KEY");
const BACKUP_MINIO_SECRET_KEY = getEnvVar("BACKUP_MINIO_SECRET_KEY");
const BACKUP_MINIO_BUCKET = getEnvVar("BACKUP_MINIO_BUCKET");
2024-07-10 17:53:27 +07:00
2024-07-18 09:29:00 +07:00
function jsonParseOrPlainText(str: string) {
try {
return JSON.parse(str);
} catch (_) {
return str;
}
}
2024-07-10 17:53:27 +07:00
@Route("/api/v1/backup")
2024-07-11 10:41:36 +07:00
@Security("keycloak")
2024-07-10 17:53:27 +07:00
export class BackupController extends Controller {
@Get()
async listBackup() {
const data = await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run_wait_result/p/${WINDMILL_BACKUP_LIST_SCRIPT_PATH}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
s3_backup_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
s3_backup_access: BACKUP_MINIO_ACCESS_KEY,
s3_backup_secret: BACKUP_MINIO_SECRET_KEY,
s3_backup_bucket: BACKUP_MINIO_BUCKET,
}),
},
).then(async (r) => {
const data = await r.json();
if (typeof data === "object" && "error" in data) {
console.error(data);
throw new Error("Cannot get status.");
}
return JSON.parse(data) as { database: Record<string, any>[]; bucket: Record<string, any>[] };
2024-07-10 17:53:27 +07:00
});
return data.database.flatMap((a) =>
a.type === "file"
? {
name: a.key.replace(".sql.gz", "") as string,
databaseSize: a.size as number,
storageSize: data.bucket.find((b) => a.key.replace(".sql.gz", "") === b.prefix)
?.size as number,
timestamp: a.lastModified as string,
2024-07-19 10:52:05 +07:00
startAt: a.paused_until as string,
}
: [],
);
2024-07-10 17:53:27 +07:00
}
2024-07-11 11:45:23 +07:00
@Get("backup-running-list")
async runningBackupStatus() {
return await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/list?running=true&script_path_exact=${WINDMILL_BACKUP_FLOW_PATH}`,
2024-07-11 11:45:23 +07:00
{
headers: { Authorization: `Bearer ${WINDMILL_API_KEY}` },
},
).then(async (r) => {
const data = await r.json();
if (typeof data === "object" && "error" in data) {
console.error(data);
throw new Error("Cannot get status.");
2024-07-11 11:45:23 +07:00
}
return data as Record<string, any>[];
2024-07-11 11:45:23 +07:00
});
}
@Get("restore-running-list")
async runningRestoreStatus() {
return await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/list?running=true&script_path_exact=${WINDMILL_RESTORE_FLOW_PATH}`,
2024-07-11 11:45:23 +07:00
{
headers: { Authorization: `Bearer ${WINDMILL_API_KEY}` },
},
2024-07-18 09:29:00 +07:00
).then(async (r) => jsonParseOrPlainText(await r.text()));
2024-07-11 11:45:23 +07:00
}
2024-07-10 17:53:27 +07:00
@Post("create")
async runBackup(@Body() body?: { name?: string }) {
const timestamp = Math.round(Date.now() / 1000);
const name =
body?.name && body.name !== "auto-backup"
? `${timestamp}-${body.name}`
: `${timestamp}-manual`;
const listRunning = await this.runningBackupStatus();
if (!listRunning || listRunning.length > 0) {
throw new HttpError(HttpStatus.NOT_ACCEPTABLE, "Cannot create two backup at the same time.");
}
2024-07-10 17:53:27 +07:00
return await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run/f/${WINDMILL_BACKUP_FLOW_PATH}`,
2024-07-10 17:53:27 +07:00
{
method: "POST",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
backup_name: name,
storage: {
s3_source_endpoint: `${MAIN_MINIO_USE_SSL === "true" ? "https://" : "http://"}${MAIN_MINIO_HOST}${(MAIN_MINIO_PORT && ":" + MAIN_MINIO_PORT) || ""}`,
s3_source_access: MAIN_MINIO_ACCESS_KEY,
s3_source_secret: MAIN_MINIO_SECRET_KEY,
s3_source_bucket: MAIN_MINIO_BUCKET,
s3_dest_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
s3_dest_access: BACKUP_MINIO_ACCESS_KEY,
s3_dest_secret: BACKUP_MINIO_SECRET_KEY,
},
database: {
s3_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
s3_access: BACKUP_MINIO_ACCESS_KEY,
s3_secret: BACKUP_MINIO_SECRET_KEY,
s3_bucket: BACKUP_MINIO_BUCKET,
db_host: DB_HOST,
db_port: DB_PORT,
db_user: DB_USERNAME,
db_password: DB_PASSWORD,
db_list: DB_LIST?.replace(",", " "),
},
2024-07-10 17:53:27 +07:00
}),
},
2024-07-18 09:29:00 +07:00
).then(async (r) => jsonParseOrPlainText(await r.text()));
2024-07-10 17:53:27 +07:00
}
@Post("restore")
async restoreBackup(@Body() body: { name: string }) {
const listRunning = await this.runningRestoreStatus();
if (!listRunning || listRunning.length > 0) {
throw new HttpError(HttpStatus.NOT_ACCEPTABLE, "Cannot restore two backup at the same time.");
}
2024-07-10 17:53:27 +07:00
return await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run/f/${WINDMILL_RESTORE_FLOW_PATH}`,
2024-07-10 17:53:27 +07:00
{
method: "POST",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
backup_name: body.name,
storage: {
s3_restore_endpoint: `${MAIN_MINIO_USE_SSL === "true" ? "https://" : "http://"}${MAIN_MINIO_HOST}${(MAIN_MINIO_PORT && ":" + MAIN_MINIO_PORT) || ""}`,
s3_restore_access: MAIN_MINIO_ACCESS_KEY,
s3_restore_secret: MAIN_MINIO_SECRET_KEY,
s3_restore_bucket: MAIN_MINIO_BUCKET,
s3_backup_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
s3_backup_access: BACKUP_MINIO_ACCESS_KEY,
s3_backup_secret: BACKUP_MINIO_SECRET_KEY,
},
database: {
s3_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
s3_access: BACKUP_MINIO_ACCESS_KEY,
s3_secret: BACKUP_MINIO_SECRET_KEY,
s3_bucket: BACKUP_MINIO_BUCKET,
db_host: DB_HOST,
db_port: DB_PORT,
db_user: DB_USERNAME,
db_password: DB_PASSWORD,
},
2024-07-10 17:53:27 +07:00
}),
},
2024-07-18 09:29:00 +07:00
).then(async (r) => jsonParseOrPlainText(await r.text()));
2024-07-10 17:53:27 +07:00
}
@Delete("delete")
async deleteBackup(@Body() body: { name: string }) {
await fetch(
2024-07-16 13:24:27 +07:00
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run_wait_result/p/${WINDMILL_BACKUP_DELETE_SCRIPT_PATH}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
backup_name: body.name,
s3_backup_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https://" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
s3_backup_access: BACKUP_MINIO_ACCESS_KEY,
s3_backup_secret: BACKUP_MINIO_SECRET_KEY,
s3_backup_bucket: BACKUP_MINIO_BUCKET,
}),
},
).then(async (r) => {
2024-07-18 09:29:00 +07:00
return jsonParseOrPlainText(await r.text());
2024-07-10 17:53:27 +07:00
});
}
2024-07-17 14:02:52 +07:00
@Get("schedule")
async listSchedule() {
const result = await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/list?path=${WINDMILL_BACKUP_FLOW_PATH}`,
{
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
},
},
).then(async (r) => {
const data = await r.json();
if (typeof data === "object" && "error" in data) {
console.error(data);
throw new Error("Cannot get status.");
}
return data as Record<string, any>[];
});
return result.map((v) => ({
id: v.path.replace("f/backup_schedule/", ""),
name: v.summary,
schedule: v.schedule,
2024-07-18 09:29:00 +07:00
enabled: v.enabled,
2024-07-17 14:02:52 +07:00
}));
}
@Post("schedule")
2024-07-18 17:16:13 +07:00
async createSchedule(
@Body() body: { name: string; schedule: string; timezone?: string; startAt?: Date },
) {
2024-07-17 14:02:52 +07:00
if (!/^[a-zA-Z0-9\-]+$/.test(body.name)) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid name.");
}
return await fetch(`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/create`, {
method: "POST",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
schedule: body.schedule,
summary: body.name,
path: `f/backup_schedule/${randomUUID()}`,
enabled: true,
is_flow: true,
script_path: WINDMILL_BACKUP_FLOW_PATH,
timezone: body.timezone || "Asia/Bangkok",
2024-07-18 17:16:13 +07:00
paused_until: body.startAt,
2024-07-17 14:02:52 +07:00
args: {
backup_name: body.name,
prefix_timestamp: true,
storage: {
s3_source_endpoint: `${MAIN_MINIO_USE_SSL === "true" ? "https://" : "http://"}${MAIN_MINIO_HOST}${(MAIN_MINIO_PORT && ":" + MAIN_MINIO_PORT) || ""}`,
s3_source_access: MAIN_MINIO_ACCESS_KEY,
s3_source_secret: MAIN_MINIO_SECRET_KEY,
s3_source_bucket: MAIN_MINIO_BUCKET,
s3_dest_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
s3_dest_access: BACKUP_MINIO_ACCESS_KEY,
s3_dest_secret: BACKUP_MINIO_SECRET_KEY,
},
database: {
s3_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
s3_access: BACKUP_MINIO_ACCESS_KEY,
s3_secret: BACKUP_MINIO_SECRET_KEY,
s3_bucket: BACKUP_MINIO_BUCKET,
db_host: DB_HOST,
db_port: DB_PORT,
db_user: DB_USERNAME,
db_password: DB_PASSWORD,
db_list: DB_LIST?.replace(",", " "),
},
},
}),
2024-07-18 09:29:00 +07:00
}).then(async (r) => jsonParseOrPlainText(await r.text()));
2024-07-17 14:02:52 +07:00
}
@Put("schedule/{id}")
async updateSchedule(
@Path() id: string,
2024-07-18 17:16:13 +07:00
@Body()
body: { name: string; schedule: string; enabled?: boolean; timezone?: string; startAt?: Date },
2024-07-17 14:02:52 +07:00
) {
if (!/^[a-zA-Z0-9\-]+$/.test(body.name)) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid name.");
}
return await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/update/f/backup_schedule/${id}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
summary: body.name,
schedule: body.schedule,
timezone: body.timezone || "Asia/Bangkok",
2024-07-18 17:16:13 +07:00
paused_until: body.startAt,
2024-07-17 14:02:52 +07:00
args: {
backup_name: body.name,
prefix_timestamp: true,
storage: {
s3_source_endpoint: `${MAIN_MINIO_USE_SSL === "true" ? "https://" : "http://"}${MAIN_MINIO_HOST}${(MAIN_MINIO_PORT && ":" + MAIN_MINIO_PORT) || ""}`,
s3_source_access: MAIN_MINIO_ACCESS_KEY,
s3_source_secret: MAIN_MINIO_SECRET_KEY,
s3_source_bucket: MAIN_MINIO_BUCKET,
s3_dest_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
s3_dest_access: BACKUP_MINIO_ACCESS_KEY,
s3_dest_secret: BACKUP_MINIO_SECRET_KEY,
},
database: {
s3_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
s3_access: BACKUP_MINIO_ACCESS_KEY,
s3_secret: BACKUP_MINIO_SECRET_KEY,
s3_bucket: BACKUP_MINIO_BUCKET,
db_host: DB_HOST,
db_port: DB_PORT,
db_user: DB_USERNAME,
db_password: DB_PASSWORD,
db_list: DB_LIST?.replace(",", " "),
},
},
}),
},
).then(async (r) => {
2024-07-18 09:29:00 +07:00
const body = jsonParseOrPlainText(await r.text());
if (r.status === 200) {
await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/setenabled/f/backup_schedule/${id}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ enabled: body.enabled }),
},
);
2024-07-17 14:02:52 +07:00
}
2024-07-18 09:29:00 +07:00
return body;
});
}
@Post("schedule/{id}/toggle")
async toggleSchedule(@Path() id: string) {
return await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/get/f/backup_schedule/${id}`,
{
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
},
).then(async (r) => {
const body = jsonParseOrPlainText(await r.text());
if (typeof body === "object") {
const result = await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/setenabled/f/backup_schedule/${id}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ enabled: !body.enabled }),
},
);
return jsonParseOrPlainText(await result.text());
2024-07-17 14:02:52 +07:00
}
});
}
@Delete("schedule/{id}")
async deleteSchedule(@Path() id: string) {
return await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/delete/f/backup_schedule/${id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
},
2024-07-18 09:29:00 +07:00
).then(async (r) => jsonParseOrPlainText(await r.text()));
2024-07-17 14:02:52 +07:00
}
2024-07-10 17:53:27 +07:00
}