-
Notifications
You must be signed in to change notification settings - Fork 103
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add backup and rebuild feature, suiting interactive and CI processes
- Loading branch information
Showing
12 changed files
with
337 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { createCommand } from 'commander' | ||
import * as inquirer from 'inquirer' | ||
import * as path from 'path' | ||
import * as fs from 'fs-extra' | ||
import { DockerComposeStack } from '../docker' | ||
import { serviceChoices, serviceFromName, servicesFromNames } from './util' | ||
|
||
export function backup (stack: DockerComposeStack) { | ||
|
||
const backupCmd = createCommand('backup') | ||
.description('Make and restore backups of the stateful services') | ||
|
||
backupCmd | ||
.command('make') | ||
.description('Backup a service. Omit options to be prompted') | ||
.option('--services [services]', 'Comma separated list of service names', (names) => servicesFromNames(stack, names)) | ||
.action(async (cmd) => { | ||
if (cmd.services === undefined) { | ||
return inquirer | ||
.prompt([{ | ||
name: 'services', | ||
message: 'Select services to backup', | ||
choices: serviceChoices(stack), | ||
type: 'checkbox', | ||
validate (input) { | ||
if (input.length === 0) { | ||
throw new Error('At least one service must be selected') | ||
} | ||
return true | ||
} | ||
}]) | ||
.then(({ services }) => stack.makeBackup(services)) | ||
} else { | ||
await stack.makeBackup(cmd.services) | ||
process.exit(0) | ||
} | ||
}) | ||
|
||
backupCmd | ||
.command('restore') | ||
.description('Restore a backup. Omit options to be prompted') | ||
.option('--backup-file [backupFile]', 'Filename of the backup') | ||
.option('--service [service]', 'Service name', (name) => serviceFromName(stack, name)) | ||
.action(async (cmd) => { | ||
const { backupFile, service } = cmd | ||
if (service === undefined && backupFile === undefined) { | ||
return inquirer | ||
.prompt([{ | ||
name: 'service', | ||
message: 'Select service', | ||
choices: serviceChoices(stack), | ||
type: 'list', | ||
validate (input) { | ||
if (input.length === 0) { | ||
throw new Error('At least one service must be selected') | ||
} | ||
return true | ||
} | ||
}, { | ||
name: 'backupFile', | ||
message: 'Select backup file to restore', | ||
choices: ({ service }) => fs.readdir(path.join(stack.backupDir, service.name)), | ||
type: 'list', | ||
validate (input) { | ||
if (input.length === 0) { | ||
throw new Error('At least one backup must be selected') | ||
} | ||
return true | ||
} | ||
}]) | ||
.then(({ service, backupFile }) => stack.restoreBackup(service, backupFile)) | ||
} else if (service !== undefined && backupFile !== undefined) { | ||
await stack.restoreBackup(service, backupFile) | ||
process.exit(0) | ||
} else { | ||
cmd.help() | ||
} | ||
}) | ||
return backupCmd | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
export * from './backup' | ||
export * from './init' | ||
export * from './rebuildService' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { createCommand } from 'commander' | ||
import * as inquirer from 'inquirer' | ||
import { DockerComposeStack } from '../docker' | ||
import { serviceChoices, servicesFromNames } from './util' | ||
|
||
export function rebuildService (stack: DockerComposeStack) { | ||
return createCommand('rebuild-service') | ||
.description('Drop and rebuild data volumes for the specified service') | ||
.option('--services [services]', 'Service name', (names) => servicesFromNames(stack, names)) | ||
.option('--no-backup', 'Rebuild without making a backup') | ||
.action(async (cmd) => { | ||
const { backup, service } = cmd | ||
if (backup === false) { | ||
await stack.rebuildService(service, backup) | ||
process.exit(0) | ||
} else { | ||
return inquirer | ||
.prompt([{ | ||
name: 'services', | ||
message: 'Select services', | ||
choices: serviceChoices(stack), | ||
type: 'checkbox', | ||
validate (input) { | ||
if (input.length === 0) { | ||
throw new Error('At least one service must be selected') | ||
} | ||
return true | ||
} | ||
}, { | ||
name: 'makeBackup', | ||
default: true, | ||
message: 'Make a backup first?', | ||
type: 'confirm' | ||
}]) | ||
.then(async (answers) => { | ||
const { makeBackup, services } = answers | ||
await stack.rebuildService(services, makeBackup) | ||
}) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { DockerComposeStack } from '../docker' | ||
|
||
export function serviceChoices (stack: DockerComposeStack) { | ||
return [...stack.statefulServices.values()].map(service => { | ||
return { name: service.name, value: service } | ||
}) | ||
} | ||
|
||
export function serviceFromName (stack: DockerComposeStack, name: string) { | ||
if (!stack.statefulServices.has(name)) { | ||
throw new Error(`Unknown service ${name}`) | ||
} | ||
return stack.statefulServices.get(name) | ||
} | ||
|
||
export function servicesFromNames (stack: DockerComposeStack, names: string) { | ||
return names.split(',').map((name: string) => serviceFromName(stack, name)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import * as fs from 'fs-extra' | ||
import * as path from 'path' | ||
import * as dockerCompose from 'docker-compose' | ||
import * as Docker from 'dockerode' | ||
import { cardanoNode, postgres, StatefulService } from './' | ||
|
||
type logFunc = (message: string) => void | ||
|
||
export class DockerComposeStack { | ||
readonly docker: Docker | ||
|
||
readonly log: logFunc | ||
|
||
readonly backupDir: string | ||
|
||
readonly projectName: string | ||
|
||
public statefulServices: Map<string, StatefulService> | ||
|
||
constructor ({ | ||
log = console.log as logFunc, | ||
projectName = 'cardano-graphql', | ||
backupDir = path.join(process.cwd(), 'backups'), | ||
statefulServices = new Map([['postgres', postgres], ['cardano-node', cardanoNode]]) | ||
}) { | ||
this.log = log | ||
this.statefulServices = statefulServices | ||
this.projectName = projectName | ||
this.backupDir = backupDir | ||
this.docker = new Docker() | ||
} | ||
|
||
// ************* LIFECYCLE ************************ | ||
|
||
public up () { | ||
this.log('docker-compose up') | ||
return dockerCompose.upAll({ | ||
composeOptions: ['-p', this.projectName] | ||
}) | ||
} | ||
|
||
public down (commandOptions?: string[]) { | ||
this.log('docker-compose down') | ||
return dockerCompose.down({ | ||
commandOptions | ||
}) | ||
} | ||
|
||
public stop () { | ||
this.log('docker-compose stop') | ||
return dockerCompose.stop({ | ||
composeOptions: ['-p', this.projectName] | ||
}) | ||
} | ||
|
||
// ************************************************* | ||
|
||
public async makeBackup (statefulServices: StatefulService[]): Promise<void> { | ||
await this.stop() | ||
for (const service of statefulServices) { | ||
const hostBackupDir = this.serviceBackupDir(service) | ||
await fs.ensureDir(hostBackupDir) | ||
this.log(`Backing up service ${service.name}`) | ||
await this.docker.run( | ||
'ubuntu', | ||
['bash', '-c', `cd ${service.dataPath} && tar cvf /backup/${service.volumeName}_${Date.now()}.tar .`], | ||
process.stdout, | ||
{ | ||
HostConfig: { | ||
AutoRemove: true, | ||
Binds: [ | ||
`${hostBackupDir}:/backup`, | ||
`${this.volumeName(service)}:${service.dataPath}` | ||
] | ||
} | ||
} | ||
) | ||
} | ||
await this.up() | ||
} | ||
|
||
public async restoreBackup (service: StatefulService, backupFile: string): Promise<void> { | ||
await this.validateBackup(service, backupFile) | ||
await this.down() | ||
await this.removeServiceVolumes([service]) | ||
await this.createServiceVolumes([service]) | ||
await this.docker.run( | ||
'ubuntu', | ||
['bash', '-c', `cd /restore && tar xvf /backup.tar`], | ||
process.stdout, | ||
{ | ||
HostConfig: { | ||
AutoRemove: true, | ||
Binds: [ | ||
`${this.volumeName(service)}:/restore`, | ||
`${path.join(this.serviceBackupDir(service), backupFile)}:/backup.tar` | ||
] | ||
} | ||
} | ||
) | ||
await this.up() | ||
} | ||
|
||
public async rebuildService (services: StatefulService[], backup = true): Promise<void> { | ||
try { | ||
if (backup === true) { | ||
await this.makeBackup(services) | ||
} | ||
await this.down() | ||
await this.removeServiceVolumes(services) | ||
await this.up() | ||
} catch (error) { | ||
console.error(error) | ||
} | ||
} | ||
|
||
private async createServiceVolumes (services: StatefulService[]): Promise<void> { | ||
for (const service of services) { | ||
this.log(`Creating volume ${service.volumeName}...`) | ||
await this.docker.createVolume({ | ||
Name: this.volumeName(service) | ||
}) | ||
} | ||
} | ||
|
||
private async removeServiceVolumes (services: StatefulService[]): Promise<void> { | ||
for (const service of services) { | ||
this.log(`Removing volume ${service.volumeName}...`) | ||
await this.docker.getVolume(this.volumeName(service)).remove() | ||
} | ||
} | ||
|
||
public async validateBackup (service: StatefulService, backupFile: string): Promise<void> { | ||
const fileStat = await fs.stat(path.join(this.serviceBackupDir(service), backupFile)) | ||
if (!fileStat.isFile()) throw new Error('Invalid backup') | ||
} | ||
|
||
private serviceBackupDir (service: StatefulService) { | ||
return path.join(this.backupDir, service.name) | ||
} | ||
|
||
private volumeName (service: StatefulService) { | ||
return `${this.projectName}_${service.volumeName}` | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './DockerComposeStack' | ||
export * from './services' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
export interface StatefulService { | ||
name: string, | ||
dataPath: string | ||
volumeName: string | ||
} | ||
|
||
export const cardanoNode: StatefulService = { | ||
name: 'cardano-node', | ||
volumeName: 'node-db', | ||
dataPath: '/data/db' | ||
} | ||
|
||
export const postgres: StatefulService = { | ||
name: 'postgres', | ||
dataPath: '/var/lib/postgresql/data', | ||
volumeName: 'postgres-data' | ||
} |
Oops, something went wrong.