diff --git a/src/controllers/backup-controller.ts b/src/controllers/backup-controller.ts index ef21af0..4fe3c2f 100644 --- a/src/controllers/backup-controller.ts +++ b/src/controllers/backup-controller.ts @@ -1,6 +1,7 @@ -import { Body, Controller, Delete, Get, Post, Route, Security } from "tsoa"; +import { Body, Controller, Delete, Get, Path, Post, Put, Route, Security } from "tsoa"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; +import { randomUUID } from "crypto"; function getEnvVar(environmentName: string) { const environmentValue = process.env[environmentName]; @@ -241,4 +242,171 @@ export class BackupController extends Controller { return data; }); } + + @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[]; + }); + + return result.map((v) => ({ + id: v.path.replace("f/backup_schedule/", ""), + name: v.summary, + schedule: v.schedule, + })); + } + + @Post("schedule") + async createSchedule(@Body() body: { name: string; schedule: string; timezone?: string }) { + 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", + 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) => { + const data = await r.text(); + if (data.includes("error")) { + console.error(data); + throw new Error("Error create schedule."); + } + console.log(data); + return data; + }); + } + + @Put("schedule/{id}") + async updateSchedule( + @Path() id: string, + @Body() body: { name: string; schedule: string; timezone?: string }, + ) { + 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", + 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) => { + if (r.status >= 400) { + console.log(await r.json()); + throw new Error("Error trying to run script/flow."); + } + const data = await r.text(); + + if (data.includes("error")) { + console.error(data); + throw new Error("Error create schedule."); + } + return data; + }); + } + + @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", + }, + }, + ).then(async (r) => { + if (r.status === 404) { + throw new HttpError(HttpStatus.NOT_FOUND, "Schedule not found."); + } + + const data = await r.text(); + if (data.includes("error")) { + console.error(data); + throw new Error("Error create schedule."); + } + }); + } } diff --git a/windmill/scripts.md b/windmill/scripts.md index af83c82..515b7a2 100644 --- a/windmill/scripts.md +++ b/windmill/scripts.md @@ -174,6 +174,7 @@ value: modules: - id: c value: + tag: "" lock: |- { "dependencies": {} @@ -195,6 +196,32 @@ value: databaseBackupBucket: expr: "`${flow_input.database.s3_bucket}`" type: javascript + summary: Conflict Backup Database Bucket Check + - id: d + value: + tag: "" + lock: |- + { + "dependencies": {} + } + //bun.lockb + + type: rawscript + content: > + export async function main(prefixTimestamp: boolean, bucketName: + string) { + if (prefixTimestamp) return `${Math.round(Date.now() / 1000)}-${bucketName}`; + return bucketName; + } + language: bun + input_transforms: + bucketName: + expr: flow_input.backup_name + type: javascript + prefixTimestamp: + expr: flow_input.prefix_timestamp + type: javascript + summary: Prefix backup name with timestamp - id: a value: path: f/storage/backup_s3 @@ -204,7 +231,7 @@ value: expr: "`${flow_input.storage.s3_dest_access}`" type: javascript s3_dest_bucket: - expr: "`${flow_input.backup_name}`" + expr: "`${results.d}`" type: javascript s3_dest_secret: expr: "`${flow_input.storage.s3_dest_secret}`" @@ -261,7 +288,7 @@ value: expr: "`${flow_input.database.s3_endpoint}`" type: javascript backup_filename: - expr: "`${flow_input.backup_name}`" + expr: "`${results.d}`" type: javascript schema: $schema: https://json-schema.org/draft/2020-12/schema @@ -495,6 +522,9 @@ schema: - s3_dest_access - s3_dest_secret description: "" + prefix_timestamp: + default: false + type: boolean required: - database - storage @@ -504,6 +534,7 @@ schema: - backup_name - database - storage + - prefix_timestamp ``` ```yaml