@wocker/pgsql-plugin
Version:
PostgreSQL plugin for wocker
703 lines (702 loc) • 29.7 kB
JavaScript
;
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);