diff --git a/packages/dashmate/src/commands/ssl/cleanup.js b/packages/dashmate/src/commands/ssl/cleanup.js new file mode 100644 index 00000000000..eaef76cf49f --- /dev/null +++ b/packages/dashmate/src/commands/ssl/cleanup.js @@ -0,0 +1,58 @@ +import { Listr } from 'listr2'; +import { Flags } from '@oclif/core'; +import ConfigBaseCommand from '../../oclif/command/ConfigBaseCommand.js'; +import MuteOneLineError from '../../oclif/errors/MuteOneLineError.js'; + +export default class CleanupCommand extends ConfigBaseCommand { + static description = `Cleanup Zero SSL certificate + +Cancel all drafted or pending validation certificates on ZeroSSL +`; + + static flags = { + ...ConfigBaseCommand.flags, + verbose: Flags.boolean({ char: 'v', description: 'use verbose mode for output', default: false }), + }; + + /** + * @param {Object} args + * @param {Object} flags + * @param {boolean} flags.verbose + * @param {Config} config + * @param {cleanupZeroSSLCertificatesTask} cleanupZeroSSLCertificatesTask + * @return {Promise} + */ + async runWithDependencies( + args, + { + verbose: isVerbose, + }, + config, + cleanupZeroSSLCertificatesTask, + ) { + const tasks = new Listr( + [ + { + title: 'Cleanup ZeroSSL certificate', + task: () => cleanupZeroSSLCertificatesTask(config), + }, + ], + { + renderer: isVerbose ? 'verbose' : 'default', + rendererOptions: { + showTimer: isVerbose, + clearOutput: false, + collapse: false, + showSubtasks: true, + removeEmptyLines: false, + }, + }, + ); + + try { + await tasks.run(); + } catch (e) { + throw new MuteOneLineError(e); + } + } +} diff --git a/packages/dashmate/src/commands/ssl/obtain.js b/packages/dashmate/src/commands/ssl/obtain.js index cc39f1a47a9..4dbfcd35612 100644 --- a/packages/dashmate/src/commands/ssl/obtain.js +++ b/packages/dashmate/src/commands/ssl/obtain.js @@ -45,7 +45,7 @@ Certificate will be renewed if it is about to expire (see 'expiration-days' flag [ { title: 'Obtain ZeroSSL certificate', - task: async () => obtainZeroSSLCertificateTask(config), + task: () => obtainZeroSSLCertificateTask(config), }, ], { diff --git a/packages/dashmate/src/createDIContainer.js b/packages/dashmate/src/createDIContainer.js index 9f3b8616235..3b2e657d2b9 100644 --- a/packages/dashmate/src/createDIContainer.js +++ b/packages/dashmate/src/createDIContainer.js @@ -23,6 +23,9 @@ import analyseSystemResourcesFactory from './doctor/analyse/analyseSystemResourc import analyseSamplesFactory from './doctor/analyseSamplesFactory.js'; import archiveSamples from './doctor/archiveSamples.js'; import unarchiveSamplesFactory from './doctor/unarchiveSamplesFactory.js'; +import cleanupZeroSSLCertificatesTaskFactory + from './listr/tasks/ssl/zerossl/cleanupZeroSSLCertificatesTaskFactory.js'; +import cancelCertificate from './ssl/zerossl/cancelCertificate.js'; import renderTemplateFactory from './templates/renderTemplateFactory.js'; import renderServiceTemplatesFactory from './templates/renderServiceTemplatesFactory.js'; @@ -206,6 +209,7 @@ export default async function createDIContainer(options = {}) { downloadCertificate: asValue(downloadCertificate), getCertificate: asValue(getCertificate), listCertificates: asValue(listCertificates), + cancelCertificate: asValue(cancelCertificate), createSelfSignedCertificate: asValue(createSelfSignedCertificate), verificationServer: asClass(VerificationServer).singleton(), }); @@ -299,6 +303,7 @@ export default async function createDIContainer(options = {}) { enableCoreQuorumsTask: asFunction(enableCoreQuorumsTaskFactory).singleton(), registerMasternodeGuideTask: asFunction(registerMasternodeGuideTaskFactory).singleton(), obtainZeroSSLCertificateTask: asFunction(obtainZeroSSLCertificateTaskFactory).singleton(), + cleanupZeroSSLCertificatesTask: asFunction(cleanupZeroSSLCertificatesTaskFactory).singleton(), obtainSelfSignedCertificateTask: asFunction(obtainSelfSignedCertificateTaskFactory).singleton(), saveCertificateTask: asFunction(saveCertificateTaskFactory), reindexNodeTask: asFunction(reindexNodeTaskFactory).singleton(), diff --git a/packages/dashmate/src/helper/scheduleRenewZeroSslCertificateFactory.js b/packages/dashmate/src/helper/scheduleRenewZeroSslCertificateFactory.js index c403c2bef39..8d741511dd8 100644 --- a/packages/dashmate/src/helper/scheduleRenewZeroSslCertificateFactory.js +++ b/packages/dashmate/src/helper/scheduleRenewZeroSslCertificateFactory.js @@ -51,7 +51,7 @@ export default function scheduleRenewZeroSslCertificateFactory( } const job = new CronJob(expiresAt, async () => { - const tasks = await obtainZeroSSLCertificateTask(config); + const tasks = obtainZeroSSLCertificateTask(config); await tasks.run({ expirationDays: Certificate.EXPIRATION_LIMIT_DAYS, diff --git a/packages/dashmate/src/listr/tasks/ssl/zerossl/cleanupZeroSSLCertificatesTaskFactory.js b/packages/dashmate/src/listr/tasks/ssl/zerossl/cleanupZeroSSLCertificatesTaskFactory.js new file mode 100644 index 00000000000..f38d62562da --- /dev/null +++ b/packages/dashmate/src/listr/tasks/ssl/zerossl/cleanupZeroSSLCertificatesTaskFactory.js @@ -0,0 +1,98 @@ +import chalk from 'chalk'; +import { Listr } from 'listr2'; +import { Observable } from 'rxjs'; +import wait from '../../../../util/wait.js'; + +/** + * @param {listCertificates} listCertificates + * @param {cancelCertificate} cancelCertificate + * @return {cleanupZeroSSLCertificatesTask} + */ +export default function cleanupZeroSSLCertificatesTaskFactory( + listCertificates, + cancelCertificate, +) { + /** + * @typedef {cleanupZeroSSLCertificatesTask} + * @param {Config} config + * @return {Listr} + */ + function cleanupZeroSSLCertificatesTask(config) { + const apiKey = config.get('platform.gateway.ssl.providerConfigs.zerossl.apiKey', true); + + return new Listr([ + { + title: 'Collect drafted and pending validation certificates', + // Skips the check if force flag is set + task: async (ctx, task) => { + ctx.certificates = []; + + let certificatesPerRequest = []; + let page = 1; + + // Fetch all certificates in draft or pending validation status + // with pagination + do { + certificatesPerRequest = await listCertificates(apiKey, ['draft', 'pending_validation'], page); + + ctx.certificates = ctx.certificates.concat(certificatesPerRequest); + + page += 1; + + // eslint-disable-next-line no-param-reassign + task.output = `Found ${ctx.certificates.length} certificates`; + } while (certificatesPerRequest.length === 1000); + + ctx.total = ctx.certificates.length; + }, + }, + { + title: 'Cancel certificates', + skip: (ctx) => ctx.certificates.length === 0, + task: async (ctx, task) => { + // eslint-disable-next-line no-param-reassign + task.title = `Cancel ${ctx.certificates.length} certificates`; + ctx.canceled = 0; + ctx.errored = 0; + return new Observable(async (observer) => { + for (const certificate of ctx.certificates) { + try { + await cancelCertificate(apiKey, certificate.id); + + ctx.canceled += 1; + } catch (e) { + ctx.errored += 1; + + if (process.env.DEBUG) { + // eslint-disable-next-line no-console + console.warn(e); + } + } + + observer.next(chalk`{green ${ctx.canceled}} / {red ${ctx.errored}} / ${ctx.total}`); + + await wait(100); + } + + if (ctx.errored > 0) { + observer.error(new Error('Some certificates were not canceled. Please try again.')); + } else { + observer.complete(); + } + + return this; + }); + }, + options: { + persistentOutput: true, + }, + }, + ], { + rendererOptions: { + showErrorMessage: true, + }, + }); + } + + return cleanupZeroSSLCertificatesTask; +} diff --git a/packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js b/packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js index ca679233d03..95eb4b5e13a 100644 --- a/packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js +++ b/packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js @@ -36,9 +36,9 @@ export default function obtainZeroSSLCertificateTaskFactory( /** * @typedef {obtainZeroSSLCertificateTask} * @param {Config} config - * @return {Promise} + * @return {Listr} */ - async function obtainZeroSSLCertificateTask(config) { + function obtainZeroSSLCertificateTask(config) { return new Listr([ { title: 'Check if certificate already exists and not expiring soon', diff --git a/packages/dashmate/src/ssl/zerossl/cancelCertificate.js b/packages/dashmate/src/ssl/zerossl/cancelCertificate.js index 5a4e1316740..2d8f9dfa9b7 100644 --- a/packages/dashmate/src/ssl/zerossl/cancelCertificate.js +++ b/packages/dashmate/src/ssl/zerossl/cancelCertificate.js @@ -1,4 +1,4 @@ -import requestApi from './requestApi'; +import requestApi from './requestApi.js'; /** * Get ZeroSSL certificate diff --git a/packages/dashmate/src/ssl/zerossl/listCertificates.js b/packages/dashmate/src/ssl/zerossl/listCertificates.js index 63dad29283b..f7530cee4c9 100644 --- a/packages/dashmate/src/ssl/zerossl/listCertificates.js +++ b/packages/dashmate/src/ssl/zerossl/listCertificates.js @@ -8,12 +8,18 @@ import Certificate from './Certificate.js'; * @param {string} apiKey * @param {String[]} [statuses] - possible values: draft, pending_validation, issued, cancelled, * revoked, expired. + * @param {number} [page] * @param {string} [search] * @return {Promise} */ -export default async function listCertificates(apiKey, statuses = [], search = undefined) { - let url = `https://api.zerossl.com/certificates?access_key=${apiKey}&limit=1000`; +export default async function listCertificates( + apiKey, + statuses = [], + page = 1, + search = undefined, +) { + let url = `https://api.zerossl.com/certificates?access_key=${apiKey}&limit=1000&page=${page}`; if (statuses.length > 0) { url += `&statuses=${statuses.join(',')}`;