UNPKG

portainer-backup

Version:

Utility for scripting or scheduling scheduled backups for Portainer

362 lines (314 loc) 13.5 kB
/** * ------------------------------------------------------------------- * ___ ___ ___ ___ ___ * / __| /_\ \ / /_\ / __| __| * \__ \/ _ \ V / _ \ (_ | _| * |___/_/_\_\_/_/_\_\___|___| ___ ___ ___ * / __|/ _ \| __|_ _\ \ / /_\ | _ \ __| * \__ \ (_) | _| | | \ \/\/ / _ \| / _| * |___/\___/|_| |_| \_/\_/_/ \_\_|_\___| * * ------------------------------------------------------------------- * COPYRIGHT SAVAGESOFTWARE,LLC, @ 2022, ALL RIGHTS RESERVED * https://github.com/SavageSoftware/portainer-backup * ------------------------------------------------------------------- */ // import libraries import Table from 'cli-table3'; import figlet from 'figlet'; import colors from 'colors'; import prettyBytes from 'pretty-bytes'; import gradient from 'gradient-string'; import symbols from 'log-symbols'; import figures from 'figures'; import Util from './util.js' import fs from 'node:fs'; import {Portainer} from './portainer.js'; /** * This class is used to display/render much of the * command line output used in this application. */ export class Render { /** * Default Constructor * @param {*} context the application context */ constructor(context) { this.context = context; } /** * Display program title & header */ title() { // display program title (using ASCII art) this.writeln(); this.writeln(); this.writeln(gradient.cristal(figlet.textSync('Portainer Backup', { font: 'Small', horizontalLayout: 'default', verticalLayout: 'default', width: 80, whitespaceBreak: true }))); // display header message var table = new Table({ head: [{hAlign:'center', content: `Made with ${colors.bold.red(figures.heart)} by SavageSoftware, LLC © 2022 (Version ${this.context.version})`}], style:{head:['dim'],border:['dim']}, wordWrap:false, colWidths:[66] }); this.writeln(table.toString()); this.writeln(); } /** * Display a table with current runtime configuration settings. * Different settings/option may be displayed based on the current * operation (i.e. 'backup', 'restore' , 'schedule', etc.) */ configuration(){ // create and format a table to contaion configuration settings var table = new Table({ //head: [{hAlign:'center', content: 'Configuration Setting'}, {hAlign:'center', content: 'Value'}], style:{head:['white'],border:[]}, wordWrap:true, colWidths:[null,null] }); // include standard configuration elements table.push( [{colSpan:2,hAlign:'center',content: `${symbols.info} -- CONFIGURATION PROPERTIES -- ${symbols.info}`}], ['PORTAINER_BACKUP_URL', this.context.config.portainer.baseUrl] ); if(this.context.operation != "info"){ table.push(['PORTAINER_BACKUP_TOKEN', this.context.config.portainer.token ? "*********************" : colors.red("!! MISSING !!")],); table.push(['PORTAINER_BACKUP_DIRECTORY', Util.wrapFilePath(this.context.config.backup.directory, 45)]); if(this.context.operation == "backup") table.push(['PORTAINER_BACKUP_FILENAME', this.context.config.backup.filename]); table.push(['PORTAINER_BACKUP_PASSWORD', this.context.config.backup.password ? "********" : colors.gray("(NONE)")]); if(this.context.operation == "backup"){ if(this.context.config.backup.stacks) table.push(['PORTAINER_BACKUP_STACKS', colors.green("ENABLED; stack files will be saved")]); } if(this.context.config.backup.overwrite) table.push(['PORTAINER_BACKUP_OVERWRITE', colors.green("ENABLED; files will be overwritten as needed.")]); if(this.context.operation == "schedule") table.push(['PORTAINER_BACKUP_SCHEDULE', this.context.config.backup.schedule]); } // include optional configuration elements if(this.context.config.debug) table.push(['PORTAINER_BACKUP_DEBUG', colors.green("ENABLED")]); if(this.context.config.dryRun) table.push(['PORTAINER_BACKUP_DRYRUN', colors.yellow("ENABLED; no files will be written.")]); // print the configuration table this.writeln(`The table below ${figures.triangleDown} lists the current runtime configuration settings:`) this.writeln(); this.writeln(table.toString()); this.writeln(); } /** * Display a table with portainer server status information. */ status(data){ var table = new Table({ style:{head:[],border:[]} }); table.push([{colSpan:2,hAlign:'center',content:`${symbols.info} -- PORTAINER SERVER -- ${symbols.info}`}]); table.push(['Portainer Version', data.Version],['Instance ID', data.InstanceID] ); this.writeln(table.toString()); this.writeln(); } /** * Display a backup completed successfully message. */ success(){ var table = new Table({ head: [symbols.success + ' PORTAINER BACKUP COMPLETED SUCCESSFULLY!'], style:{head:['brightGreen'],border:['brightGreen'], paddingLeft: 2, paddingRight: 2} }); this.writeln(table.toString()); this.writeln(); } /** * Display a termination signal message. * @param {*} signal the signal detected causing the application to terminate. */ terminate(signal){ var table = new Table({ head: [`${symbols.warning} TERMINATE SIGNAL DETECTED: ${colors.bgBlue.white.bold(" - " + signal + " - ")}`], style:{head:['brightYellow'],border:['brightYellow'], paddingLeft: 2, paddingRight: 2} }); this.writeln(); this.writeln(table.toString()); this.writeln(); } /** * Display an unsupported version message. * @param {*} version current version of connected portainer server. */ unsupportedVersion(version){ var table = new Table({ head: [{colSpan: 2, content: `${symbols.warning} -- UNSUPPORTED PORTAINER VERSION -- ${symbols.warning}`}], style:{head:['brightYellow'],border:['brightYellow'], paddingLeft: 2, paddingRight: 2} }); table.push([colors.yellow("PORTAINER VERSION"), colors.yellow(version)]) table.push([colors.yellow("MINIMUM SUPPORTED VERSION"), colors.yellow(Portainer.MIN_VERSION)]) this.writeln(); this.writeln(table.toString()); this.writeln(); } /** * Display detials about the current backup file. * @param {*} file current backup file */ backupFile(file){ // display configuration settings var table = new Table({ style:{head:[],border:[]}, wordWrap:true }); // get file stats let stats = fs.statSync(file); // add table elements from response data and backup file table.push( [{colSpan:2,hAlign:'center',content:`${symbols.info} -- BACKUP FILE -- ${symbols.info}`}], ['PATH', Util.wrapFilePath(`${file}`, 55)], ['SIZE', prettyBytes(stats.size)], ['CREATED', stats.ctime.toString()], ['PROTECTED', this.context.config.backup.password ? "PROTECTED (with password)" : "NO PASSWORD"] ); // print table to output stream this.writeln(table.toString()); this.writeln(); } /** * Display the number of stacks found in the stacks metadata/catalog. * @param {*} stacks an array of stack objects from the stacks metadata/catalog */ stacksCount(stacks){ // create table for stack count var table = new Table({ head: [symbols.info + ` Acquired [${stacks.length}] stacks from portainer server!`], style:{head:[],border:['blue'], paddingLeft: 2, paddingRight: 2} }); // print stacks count table this.writeln(table.toString()); this.writeln(); } /** * Display a table listing all the stack files. * @param {*} stacks an array of stack objects (including 'stack.file' references) */ stackFiles(stacks){ // create table for stack files listing let table = new Table({ style:{head:[],border:[]}, wordWrap:true }); table.push([{colSpan:3,hAlign:'center',content:`${symbols.info} -- STACK FILES -- ${symbols.info}`}], [ {hAlign:'center', content: "ID"}, {hAlign:'left', content: 'Name'}, {hAlign:'left', content: 'File'}]); // iterate stacks; append this stack info to the stack results table for(let index in stacks) { let stack = stacks[index]; // reference stack instance table.push([stack.Id, stack.Name, Util.wrapFilePath(stack.file, 55)]); } // print stack files table this.writeln(table.toString()); this.writeln(); } /** * Display a backup summary * @param {*} context */ summary(context){ // create a summary table var table = new Table({ style:{head:[],border:[]}, wordWrap:true }); table.push( [{colSpan:2,hAlign:'center',content:`${symbols.info} -- BACKUP SUMMARY -- ${symbols.info}`}], ['RESULT', (context.results.success)? colors.bold.green(`${symbols.success} SUCCCESS`) : colors.error(`${symbols.error} FAILED`) ] ); // add backup filename to summary table (if backup file exists) if(context.operation === "backup"){ table.push(['BACKUP FILE', context.results.backup.filename]); } // add stacks count to summary table (if any exist) if(context.cache.stacks && context.cache.stacks.length > 0){ table.push(['STACKS BACKED UP', `${context.cache.stacks.length} STACK FILES`]); } // add elapsed time to summary table table.push(['ELAPSED TIME', `${context.results.elapsed/1000} seconds`]); // print summary table to output stream this.writeln(table.toString()); this.writeln(); } /** * Display a "dry-run" warning message. */ dryRun(){ // create and display dry-run warning message table var table = new Table({ head: [symbols.warning + ' This was a DRY-RUN; no backup files were saved.'], style:{head:['yellow'],border:['yellow'], paddingLeft: 2, paddingRight: 2} }); this.writeln(table.toString()); this.writeln(); } /** * Display a goodbye message used with application is terminating. */ goodbye(){ this.writeln("-------- GOODBYE --------"); this.writeln(); } /** * Write/print the data parameter to output stream. * * @param data optional string or object to print */ write(data){ if(data) this.context.output.stream.write(data.toString()); } /** * Write/print the data parameter followed by a NEWLINE to output stream. * * @param data optional string or object to print */ writeln(data){ if(data) this.context.output.stream.write(data.toString()); this.context.output.stream.write("\n"); } /** * Display error message and any additional error details. * * @param {*} err Error error object * @param {*} message String error message * @param {*} note String optional note */ error(err, message, note){ // display error message (if exists) if(message){ var failTable = new Table({ head: [symbols.error + ' ' + message], style:{head:[],border:['red'], paddingLeft: 2, paddingRight: 2}, }); this.writeln(); this.writeln(failTable.toString()); this.writeln(); } // create error results table; err error object instance mesage var table = new Table({ //head: [{hAlign:'center', content: "Results"}, {hAlign:'center', content: 'Value'}], style:{head:['white'],border:['red']}, wordWrap:true, colWidths:[15,50] }); table.push([{colSpan:2,hAlign:'center', content: colors.bgRed.white(`${symbols.error} -------------------------- ERROR -------------------------- ${symbols.error}`)}]), table.push(['ERROR MESSAGE', err.message]); // display additional error context details if(err.response){ // ALL table.push(['API RESPONSE STATUS', colors.red(err.response.status + ` (${err.response.statusText})`)]); // 401 if(err.response.status == 401) table.push(['NOTES', "Check your 'PORTAINER_BACKUP_TOKEN' to make sure it is valid and is assigned to a user with 'ADMIN' privileges."]); } // ERROR=ECONNRESET if(err.errno == -54) table.push(['NOTES', "Check your 'PORTAINER_BACKUP_URL'; unable to access portainer server via this URL."]); // add any optional notes if(note) table.push(['NOTES', note]); // display error results this.writeln(table.toString()); this.writeln(); } }