UNPKG

@wocker/pgsql-plugin

Version:
703 lines (702 loc) 29.7 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PgSqlService = void 0; const core_1 = require("@wocker/core"); const utils_1 = require("@wocker/utils"); const node_postgres_1 = require("drizzle-orm/node-postgres"); const pg_proxy_1 = require("drizzle-orm/pg-proxy"); const cli_table3_1 = __importDefault(require("cli-table3")); const csv_parser_1 = __importDefault(require("csv-parser")); const stream_1 = require("stream"); const format_1 = require("date-fns/format"); const Config_1 = require("../makes/Config"); const Service_1 = require("../makes/Service"); const PgDatabaseTable_1 = require("../table/PgDatabaseTable"); let PgSqlService = class PgSqlService { constructor(appConfigService, pluginConfigService, dockerService, proxyService, logService) { this.appConfigService = appConfigService; this.pluginConfigService = pluginConfigService; this.dockerService = dockerService; this.proxyService = proxyService; this.logService = logService; this.adminContainerName = "dbadmin-pgsql.workspace"; } get config() { if (!this._config) { this._config = Config_1.Config.make(this.fs); } return this._config; } get services() { return this.config.services; } get fs() { let fs = this.pluginConfigService.fs; if (!fs) { fs = new core_1.FileSystem(this.pluginConfigService.dataPath()); } return fs; } get dbFs() { return new core_1.FileSystem(this.appConfigService.dataPath("db/pgsql")); } query(service, query, headers) { return __awaiter(this, void 0, void 0, function* () { if (service.isExternal) { throw new Error("Unsupported for external service"); } const container = yield this.dockerService.getContainer(service.containerName); if (!container) { throw new Error("Service is not running"); } const exec = yield container.exec({ Cmd: ["psql", ...service.auth, "--csv", "-c", query], Env: [ `PGPASSWORD=${service.password}` ], AttachStdout: true, AttachStderr: true }); const stream = yield exec.start({ hijack: true, Tty: true }); return new Promise((resolve, reject) => { const results = []; stream .pipe((0, csv_parser_1.default)({ headers })) .on("data", (data) => { results.push(data); }) .on("end", () => { resolve(results); }) .on("error", reject); }); }); } getServiceDatabase(service) { if (service.isExternal && service.host) { const url = `postgresql://${service.user}:${service.password}@${service.host}:${service.port}`; this.logService.info(url); return (0, node_postgres_1.drizzle)(url); } return (0, pg_proxy_1.drizzle)((sql, params, method) => __awaiter(this, void 0, void 0, function* () { this.logService.debug("pgsql query", { sql, params, method }); return { rows: yield this.query(service, sql, false) }; })); } getTables(service) { return __awaiter(this, void 0, void 0, function* () { return this.getServiceDatabase(service) .select({ table: PgDatabaseTable_1.PgDatabaseTable.datname }) .from(PgDatabaseTable_1.PgDatabaseTable) .execute(); }); } dbPath(service) { return this.appConfigService.dataPath("db/pgsql", service); } init(admin) { return __awaiter(this, void 0, void 0, function* () { if (typeof admin.enabled === "undefined") { admin.enabled = yield (0, utils_1.promptConfirm)({ message: "Enable admin?" }); } else { this.config.admin.enabled = admin.enabled; } if (this.config.admin.enabled) { if (!admin.email) { this.config.admin.email = yield (0, utils_1.promptInput)({ message: "Email", type: "text", default: this.config.admin.email || "root@pgsql.ws" }); } else { this.config.admin.email = admin.email; } if (!admin.password) { this.config.admin.password = yield (0, utils_1.promptInput)({ message: "Password", type: "text", default: this.config.admin.password || "toor" }); } else { this.config.admin.password = admin.password; } if (typeof admin.skipPassword === "undefined") { this.config.admin.skipPassword = yield (0, utils_1.promptConfirm)({ message: "Skip password", default: this.config.admin.skipPassword }); } else { this.config.admin.skipPassword = admin.skipPassword; } } this.config.save(); }); } create() { return __awaiter(this, arguments, void 0, function* (serviceProps = {}) { if (!serviceProps.name || this.config.hasService(serviceProps.name)) { serviceProps.name = yield (0, utils_1.promptInput)({ message: "Service name", default: serviceProps.name || "default", validate: (name) => { if (!name) { return "Service name is required"; } if (this.config.hasService(name)) { return `Service "${name}" is already exists`; } return true; } }); } if (!serviceProps.user) { serviceProps.user = yield (0, utils_1.promptInput)({ message: "Database user", type: "text", required: true, default: "root" }); } while (!serviceProps.password) { serviceProps.password = yield (0, utils_1.promptInput)({ type: "password", required: true, message: "Database password", minLength: 4 }); const confirmPassword = yield (0, utils_1.promptInput)({ type: "password", required: true, message: "Confirm password" }); if (serviceProps.password !== confirmPassword) { console.error("Passwords do not match"); delete serviceProps.password; } } if (!serviceProps.host) { if (!serviceProps.storage || ![Service_1.STORAGE_VOLUME, Service_1.STORAGE_FILESYSTEM].includes(serviceProps.storage)) { serviceProps.storage = yield (0, utils_1.promptSelect)({ message: "Storage:", options: [Service_1.STORAGE_VOLUME, Service_1.STORAGE_FILESYSTEM] }); } if (!serviceProps.containerPort) { const needPort = yield (0, utils_1.promptConfirm)({ message: "Do you need to expose container port?", default: false }); if (needPort) { serviceProps.containerPort = yield (0, utils_1.promptInput)({ required: true, message: "Container port:", type: "number", min: 1, default: 5432 }); } } } this.config.setService(new Service_1.Service(serviceProps)); this.config.save(); }); } upgrade(serviceProps) { return __awaiter(this, void 0, void 0, function* () { const service = this.config.getServiceOrDefault(serviceProps.name); let changed = false; if (serviceProps.imageName) { service.imageName = serviceProps.imageName; changed = true; } if (serviceProps.imageVersion) { service.imageVersion = serviceProps.imageVersion; changed = true; } if (serviceProps.containerPort) { service.containerPort = serviceProps.containerPort; changed = true; } if (changed) { this.config.setService(service); this.config.save(); } }); } destroy(name, yes, force) { return __awaiter(this, void 0, void 0, function* () { const service = this.config.getServiceOrDefault(name); if (!force && service.name === this.config.default) { throw new Error(`Can't delete default service.`); } if (!yes) { const confirm = yield (0, utils_1.promptConfirm)({ message: `Are you sure you want to delete "${service.name}" service?`, default: false }); if (!confirm) { throw new Error("Aborted"); } } if (!service.host) { yield this.dockerService.removeContainer(service.containerName); switch (service.storage) { case Service_1.STORAGE_VOLUME: if (service.volume !== service.defaultVolume) { console.info(`Deletion of custom volume "${service.volume}" skipped.`); break; } if (!this.pluginConfigService.isVersionGTE("1.0.19")) { throw new Error("Please update wocker for using volume storage"); } if (yield this.dockerService.hasVolume(service.volume)) { yield this.dockerService.rmVolume(service.volume); } break; case Service_1.STORAGE_FILESYSTEM: this.dbFs.rm(service.name, { recursive: true, force: true }); break; default: throw new Error(`Unknown storage type "${service.storage}"`); } } this.config.unsetService(service.name); this.config.save(); }); } listTable() { return __awaiter(this, void 0, void 0, function* () { const table = new cli_table3_1.default({ head: ["Name", "Image", "Host/Container", "Expose port", "Volume"] }); for (const service of this.config.services) { table.push([ service.name + (this.config.default === service.name ? " (default)" : ""), service.image, service.host || service.containerName, service.containerPort, service.storage === "volume" ? service.volume : undefined ]); } return table.toString(); }); } start(name, restart) { return __awaiter(this, void 0, void 0, function* () { if (!name && !this.config.default) { yield this.create(); } const service = this.config.getServiceOrDefault(name); if (service.isExternal) { return; } if (restart) { yield this.dockerService.removeContainer(service.containerName); } let container = yield this.dockerService.getContainer(service.containerName); if (!container) { const { user = "root", password = "root" } = service; const volumes = []; switch (service.storage) { case Service_1.STORAGE_VOLUME: if (!this.pluginConfigService.isVersionGTE("1.0.19")) { throw new Error("Please update wocker for using volume storage"); } if (!(yield this.dockerService.hasVolume(service.volume))) { yield this.dockerService.createVolume(service.volume); } volumes.push(`${service.volume}:/var/lib/postgresql/data`); break; case Service_1.STORAGE_FILESYSTEM: volumes.push(`${this.dbPath(service.name)}:/var/lib/postgresql/data`); break; default: throw new Error(`Unknown storage type "${service.storage}"`); } container = yield this.dockerService.createContainer({ name: service.containerName, image: service.image, restart: "always", volumes: volumes, env: { POSTGRES_USER: user, POSTGRES_PASSWORD: password }, ports: service.containerPort ? [`${service.containerPort}:5432`] : undefined }); } const { State: { Running } } = yield container.inspect(); if (!Running) { yield container.start(); } console.info(`Started ${service.name} at ${service.containerName}`); }); } stop(name) { return __awaiter(this, void 0, void 0, function* () { const service = this.config.getServiceOrDefault(name); yield this.dockerService.removeContainer(service.containerName); }); } pgsql(name) { return __awaiter(this, void 0, void 0, function* () { const service = this.config.getServiceOrDefault(name); const container = yield this.dockerService.getContainer(service.containerName); if (!container) { throw new Error(`Service "${service.name}" isn't started`); } yield this.dockerService.exec(service.containerName, { tty: true, cmd: ["psql"] }); }); } dump(name, database) { return __awaiter(this, void 0, void 0, function* () { const service = this.config.getServiceOrDefault(name); const container = yield this.dockerService.getContainer(service.containerName); if (!container) { throw new Error(`Service "${service.name}" isn't started`); } if (!database) { const res = yield this.getTables(service); database = yield (0, utils_1.promptSelect)({ required: true, options: res.map((r) => r.table) }); } yield this.dockerService.exec(service.containerName, { tty: true, cmd: ["pg_dump", ...service.auth, "-d", database] }); }); } backup(name, database, filename) { return __awaiter(this, void 0, void 0, function* () { const service = this.config.getServiceOrDefault(name); if (!database) { const res = yield this.getTables(service); database = yield (0, utils_1.promptSelect)({ message: "Database", required: true, options: res.map((r) => r.table) }); } if (!filename) { const date = (0, format_1.format)(new Date(), "yyyy-MM-dd HH-mm"); filename = yield (0, utils_1.promptInput)({ message: "File name", required: true, suffix: ".sql", default: date }); filename += ".sql"; } if (!this.fs.exists(`dump/${service.name}/${database}`)) { this.fs.mkdir(`dump/${service.name}/${database}`, { recursive: true }); } const container = !service.isExternal ? yield this.dockerService.getContainer(service.containerName) : yield this.dockerService.createContainer({ name: service.name, image: service.image, tty: true, cmd: ["bash"], networkMode: "host" }); if (!container) { throw new Error(`Service "${service.name}" isn't started`); } try { if (service.isExternal) { yield container.start(); } const file = this.fs.createWriteStream(`dump/${service.name}/${database}/${filename}`); const exec = yield container.exec({ Cmd: ["pg_dump", ...service.auth, "--if-exists", "--no-comments", "-c", "-d", database], Env: [ `PGPASSWORD=${service.password}` ], AttachStdout: true, AttachStderr: true }); const stream = yield exec.start({ hijack: true }); yield new Promise((resolve, reject) => { container.modem.demuxStream(stream, file, process.stderr); stream .on("finish", resolve) .on("error", reject); }); console.info("Backup created"); } finally { if (service.isExternal) { yield container.stop(); yield container.remove(); } } }); } deleteBackup(name, database, filename) { return __awaiter(this, void 0, void 0, function* () { const service = this.config.getServiceOrDefault(name); if (!database) { database = yield (0, utils_1.promptSelect)({ required: true, options: this.fs.readdir(`dump/${service.name}`) }); } if (!filename) { filename = yield (0, utils_1.promptSelect)({ required: true, options: this.fs.readdir(`dump/${service.name}/${database}`) }); } this.fs.rm(`dump/${service.name}/${database}/${filename}`); }); } restore(name, database, filename) { return __awaiter(this, void 0, void 0, function* () { const service = this.config.getServiceOrDefault(name); if (!database) { database = yield (0, utils_1.promptSelect)({ message: "Database", required: true, options: this.fs.readdir(`dump/${service.name}`) }); } if (!filename) { filename = yield (0, utils_1.promptSelect)({ message: "File name", required: true, options: this.fs.readdir(`dump/${service.name}/${database}`) }); } const container = !service.isExternal ? yield this.dockerService.getContainer(service.containerName) : yield this.dockerService.createContainer({ name: service.containerName, image: service.image, tty: true, cmd: ["bash"], networkMode: "host" }); if (!container) { throw new Error(`Service "${service.name}" isn't started`); } try { if (service.isExternal) { yield container.start(); } const file = this.fs.createReadStream(`dump/${service.name}/${database}/${filename}`); const exec = yield container.exec({ Cmd: ["psql", "--set", "ON_ERROR_STOP=on", ...service.auth, "-d", database], Env: [ `PGPASSWORD=${service.password}` ], AttachStdin: true, AttachStdout: true, AttachStderr: true }); const stream = yield exec.start({ stdin: true, hijack: true }); yield new Promise((resolve, reject) => { container.modem.demuxStream(stream, new stream_1.Writable({ write: () => undefined }), process.stderr); file .pipe(stream) .on("error", reject); stream .on("end", resolve) .on("error", reject); }); const info = yield exec.inspect(); if (info.ExitCode !== 0) { throw new Error(`Restore failed with exit code ${info.ExitCode}`); } console.info("Restored"); } finally { if (service.isExternal) { yield container.stop().catch(() => undefined); yield container.remove().catch(() => undefined); } } }); } admin() { return __awaiter(this, void 0, void 0, function* () { const config = this.config; if (!config.admin.email || !config.admin.password) { console.info("Can't start admin credentials missed"); return; } const servers = []; const passwords = {}; for (const service of config.services || []) { let host; let port = 5432; if (service.host) { host = service.host; if (service.port) { port = service.port; } } else { const container = yield this.dockerService.getContainer(service.containerName); if (!container) { continue; } const { State: { Running } } = yield container.inspect(); if (!Running) { continue; } host = service.containerName; } passwords[service.name] = `${host}:${port}:postgres:${service.user || ""}:${service.password || ""}`; servers.push({ Group: "Servers", Name: service.name, Host: host, Port: 5432, MaintenanceDB: "postgres", Username: service.user, PassFile: `/var/lib/pgadmin/storage/passwords/${service.name}.pgpass`, SSLMode: "prefer" }); } yield this.dockerService.removeContainer(this.adminContainerName); if (!this.config.admin.enabled || servers.length === 0) { return; } this.fs.writeJSON("servers.json", { Servers: servers.reduce((res, server, index) => { return Object.assign(Object.assign({}, res), { [`${index}`]: server }); }, {}) }); let container = yield this.dockerService.getContainer(this.adminContainerName); if (!container) { container = yield this.dockerService.createContainer({ name: this.adminContainerName, image: "dpage/pgadmin4:latest", user: "root:root", restart: "always", entrypoint: [ "/bin/sh", "-c", [ "mkdir -p /var/lib/pgadmin/storage/passwords", ...Object.keys(passwords).map((name) => { return `echo '${passwords[name]}' > /var/lib/pgadmin/storage/passwords/${name}.pgpass`; }), "chmod -R 600 /var/lib/pgadmin/storage/passwords/", "chown -R root:root /var/lib/pgadmin/storage/passwords", "/entrypoint.sh" ].join(";") + ";" ], volumes: [ "wocker-pgadmin:/var/lib/pgadmin", `${this.fs.path("servers.json")}:/pgadmin4/servers.json` ], env: Object.assign({ VIRTUAL_HOST: this.adminContainerName, PGADMIN_DEFAULT_EMAIL: config.admin.email || "", PGADMIN_DEFAULT_PASSWORD: config.admin.password || "" }, config.admin.skipPassword ? { PGADMIN_CONFIG_SERVER_MODE: "False", PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False" } : {}) }); } const { State: { Running } } = yield container.inspect(); if (!Running) { yield container.start(); try { yield this.proxyService.start(); } catch (err) { // } } console.info(`Admin started at ${this.adminContainerName}`); if (!config.admin.skipPassword) { console.info(`Login: ${config.admin.email}`); console.info(`Password: ****`); } else { console.info("Password skipped"); } }); } setDefault(name) { return __awaiter(this, void 0, void 0, function* () { const config = this.config; if (!config.getService(name)) { throw new Error(`Service "${name}" not found`); } config.default = name; config.save(); }); } getServices() { return __awaiter(this, void 0, void 0, function* () { return this.services.map((service) => { return service.name; }); }); } }; exports.PgSqlService = PgSqlService; exports.PgSqlService = PgSqlService = __decorate([ (0, core_1.Injectable)(), __metadata("design:paramtypes", [core_1.AppConfigService, core_1.PluginConfigService, core_1.DockerService, core_1.ProxyService, core_1.LogService]) ], PgSqlService);