From fd7c4e87e7672accc192e1b3da165ce05011ea4b Mon Sep 17 00:00:00 2001 From: "Guillaume \"B.B.\" Van Hemmen" Date: Thu, 8 Aug 2024 06:51:27 +0200 Subject: [PATCH 1/2] #0000 - Add docker-volume-manager command and .editorconfig Introduces `docker-volume-manager` with functionalities to backup and restore Docker volumes. Also adds an .editorconfig file to ensure consistent coding styles across the project. --- .editorconfig | 11 ++ src/commands/docker-volume-manager.ts | 161 +++++++++++++++++++++++++- src/commands/generate.ts | 2 + 3 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5186fad --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# top-most EditorConfig file +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +max_line_length = 80 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/src/commands/docker-volume-manager.ts b/src/commands/docker-volume-manager.ts index 29dbf7a..1e2a55a 100644 --- a/src/commands/docker-volume-manager.ts +++ b/src/commands/docker-volume-manager.ts @@ -1,11 +1,166 @@ -import { GluegunCommand } from 'gluegun' +import {GluegunCommand, GluegunFilesystem} from 'gluegun' +import * as process from "process"; +import {Toolbox} from "gluegun/build/types/domain/toolbox"; + +const checkForDocker = async (toolbox: Toolbox) => { + const {system, print} = toolbox; + + print.divider() + print.info('Checking if the system has docker...'); + const spinner = print.spin('running docker --version'); + + try { + await system.run('docker --version'); + spinner.succeed('Docker is installed on your system!'); + print.info('Checking if current user can run docker...'); + spinner.start('running docker ps'); + try { + await system.run('docker ps'); + spinner.succeed('You can run docker!'); + } catch (dockerSudoError) { + spinner.fail('You are not allowed to run docker, please try again with an allowed user (did you forgot to run the command with sudo?).') + process.exit(2) + } + } catch (dockerVersionError) { + spinner.fail('Docker is not installed on your system!') + process.exit(1) + } +} + +const isValidPath = (path: string, filesystem: GluegunFilesystem) => filesystem.exists(path) && path !== '' && path !== '/' + +const getAndValidateBackupFolder = async (toolbox: Toolbox): Promise => { + const {prompt, print, filesystem} = toolbox; + + print.divider(); + + let backupPath: string = '' + do { + const {pathResponse} = await prompt.ask({ + type: 'input', + name: 'pathResponse', + message: 'Please, enter the absolute path to the folder containing the backups' + }) + backupPath = pathResponse + + if (!isValidPath(backupPath, filesystem)) { + print.error(`${backupPath} is not a valid path! Please try again.`) + print.newline() + } + } while (!isValidPath(backupPath, filesystem)) + + return backupPath; +} + +const selectAction = async (toolbox: Toolbox): Promise => { + const {prompt, print} = toolbox; + + print.divider(); + + const {action} = await prompt.ask({ + type: 'select', + name: 'action', + message: 'Select an action:', + choices: ['Backup volume', 'Restore volumes', 'exit'], + }); + + return action; +} + +const listAndSelectDockerVolumes = async (toolbox: Toolbox): Promise => { + const {system, prompt, print} = toolbox; + + print.divider(); + + const spinner = print.spin('running docker volume ls'); + + try { + const volumes = await system.run('docker volume ls --format "{{.Name}}"'); + spinner.succeed('Volumes listed!'); + const volumeList = volumes.split('\n').filter((volume: string) => volume !== ''); + const {selectedVolumes} = await prompt.ask({ + type: 'multiselect', + name: 'selectedVolumes', + message: 'Select the volumes you want to backup:', + choices: volumeList, + }) as { selectedVolumes: string[] }; + + return selectedVolumes; + } catch (dockerVolumeError) { + spinner.fail('Failed to list docker volumes!') + process.exit(3) + } +} + +const runVolumeBackup = async (toolbox: Toolbox, workingDir: string, volumes: string[]) => { + const {print, system} = toolbox; + + print.divider(); + print.info('starting backup process'); + print.newline(); + + const spinner = print.spin(); + + for (const volume of volumes) { + try { + spinner.start(`backing up volume ${volume}...`) + + // improvement: display progress + // see: https://superuser.com/questions/168749/is-there-a-way-to-see-any-tar-progress-per-file + await system.run(`docker run --rm -v ${volume}:/volume -v ${workingDir}:/tmp alpine sh -c "cd /volume && tar -z -c -p -f /tmp/${volume}.tar ."`); + + spinner.succeed(`volume ${volume} backed up!`) + } catch (error) { + spinner.fail(`volume ${volume} backup failed!`) + print.error(error); + print.newline(); + } + } +} const command: GluegunCommand = { name: 'docker-volume-manager', run: async (toolbox) => { - const { print } = toolbox + const {print} = toolbox - print.info('Welcome to your CLI') + print.info('####################################') + print.info('# Welcome to docker-volume-manager #') + print.info('####################################') + + // check if docker is present on your system + await checkForDocker(toolbox); + + // select action + const choice = await selectAction(toolbox); + + if (choice === 'Backup volume') { + // select the backup location folder + // @ts-ignore + const workingFolder = await getAndValidateBackupFolder(toolbox); + + // list and select the volumes + const volumes = await listAndSelectDockerVolumes(toolbox); + + // run the backup + await runVolumeBackup(toolbox, workingFolder, volumes); + + print.success('Your volume have been backed up!') + } else if (choice === 'Restore volumes') { + // select the backup location folder + // @ts-ignore + const workingFolder = await getAndValidateBackupFolder(toolbox); + + // list and select the archives to restore + + // run the restore + + print.success('your backup have been restored!'); + } else if (choice === 'exit') { + process.exit(0); + } + + // should never happen + process.exit(999); }, } diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 6ad2646..fce2258 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -1,5 +1,7 @@ import { GluegunToolbox } from 'gluegun' + +// Example command, to be rmeoved module.exports = { name: 'generate', alias: ['g'], -- 2.45.2 From 8767af34a63964717def66809408243987b541e4 Mon Sep 17 00:00:00 2001 From: "Guillaume \"B.B.\" Van Hemmen" Date: Wed, 21 Aug 2024 12:53:04 +0200 Subject: [PATCH 2/2] #000 - Remove redundant tar option in backup command The `-z` option in the tar command was removed since it is unnecessary for the current backup operation. This simplifies the command and avoids potential confusion regarding compression. --- src/commands/docker-volume-manager.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/commands/docker-volume-manager.ts b/src/commands/docker-volume-manager.ts index 1e2a55a..fbc9fdf 100644 --- a/src/commands/docker-volume-manager.ts +++ b/src/commands/docker-volume-manager.ts @@ -105,9 +105,7 @@ const runVolumeBackup = async (toolbox: Toolbox, workingDir: string, volumes: st try { spinner.start(`backing up volume ${volume}...`) - // improvement: display progress - // see: https://superuser.com/questions/168749/is-there-a-way-to-see-any-tar-progress-per-file - await system.run(`docker run --rm -v ${volume}:/volume -v ${workingDir}:/tmp alpine sh -c "cd /volume && tar -z -c -p -f /tmp/${volume}.tar ."`); + await system.run(`docker run --rm -v ${volume}:/volume -v ${workingDir}:/tmp alpine sh -c "cd /volume && tar -c -p -f /tmp/${volume}.tar ."`); spinner.succeed(`volume ${volume} backed up!`) } catch (error) { @@ -147,8 +145,6 @@ const command: GluegunCommand = { print.success('Your volume have been backed up!') } else if (choice === 'Restore volumes') { // select the backup location folder - // @ts-ignore - const workingFolder = await getAndValidateBackupFolder(toolbox); // list and select the archives to restore -- 2.45.2