import { Body, Controller, Delete, Get, Post, Route, Security } from "tsoa"; import { Client as MinioClient, BucketItem } from "minio"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; function getEnvVar(environmentName: string) { const environmentValue = process.env[environmentName]; if (!environmentValue) throw new Error(`${environmentName} is required.`); return environmentValue; } 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"); const WINDMILL_BACKUP_DELETE_SCRIPT_PATH = getEnvVar("WINDMILL_BACKUP_DELETE_SCRIPT_PATH"); 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"); const minio = new MinioClient({ useSSL: BACKUP_MINIO_USE_SSL === "true", endPoint: BACKUP_MINIO_HOST, port: +(BACKUP_MINIO_PORT || "9000"), accessKey: BACKUP_MINIO_ACCESS_KEY, secretKey: BACKUP_MINIO_SECRET_KEY, }); @Route("/api/v1/backup") @Security("keycloak") export class BackupController extends Controller { @Get() async listBackup() { return await new Promise((resolve, reject) => { const data: BucketItem[] = []; const stream = minio.listObjectsV2(BACKUP_MINIO_BUCKET); stream.on("data", (obj) => data.unshift(obj)); stream.on("end", () => resolve( data.flatMap((v) => "prefix" in v ? [] : { name: v.name.replace(".sql.gz", ""), timestamp: v.lastModified, }, ), ), ); stream.on("error", (err) => reject(err)); }); } @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}`, { 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[]; }); } @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}`, { 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; }); } @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."); } return await fetch( `${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run/f/${WINDMILL_BACKUP_FLOW_PATH}`, { 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(",", " "), }, }), }, ).then(async (r) => { const data = await r.text(); if (data.includes("error")) { console.error(data); throw new Error("Backup Error"); } return data; }); } @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."); } return await fetch( `${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run/f/${WINDMILL_RESTORE_FLOW_PATH}`, { 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, }, }), }, ).then(async (r) => { const data = await r.text(); if (data.includes("error")) { console.error(data); throw new Error("Backup Error"); } return data; }); } @Delete("delete") async deleteBackup(@Body() body: { name: string }) { await fetch( `${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) => { const data = await r.text(); if (data.includes("error")) { console.error(data); throw new Error("Error delete backup."); } return data; }); } }