diff --git a/bin/oprah b/bin/oprah index c25a8ca..bb30cc0 100755 --- a/bin/oprah +++ b/bin/oprah @@ -23,7 +23,8 @@ program ) .option('-i, --interactive', 'Run on interactive mode') .option('-m, --missing', 'Only prompt missing values in interactive mode') - .action(({ interactive, variables, missing }) => { + .option('-cu, --cleaning-up [cleanUp]', 'Clean up orphan configs or secrets') + .action(({ interactive, variables, missing, cleanUp = false }) => { let parsedVariables = {}; try { @@ -45,7 +46,7 @@ program variables: parsedVariables }; - oprahPromise = makeOprah(params).run(); + oprahPromise = makeOprah(params).run({ deleting: cleanUp }); }); program @@ -116,6 +117,17 @@ program }); }); +program + .command('clean-up') + .description('Cleaning up orphan configs or secrets') + .option('-d, --dry-run [dryRun]', 'Execute a dry run') + .action(({ dryRun }) => { + const { stage, config } = program.opts(); + oprahPromise = makeOprah({ stage, config }).cleanUp({ + dryRun + }); + }); + function displayHelpAndExit() { program.help(); process.exit(1); diff --git a/lib/commands/cleanup/make-cleanup.js b/lib/commands/cleanup/make-cleanup.js index 83eea7c..dc03aed 100644 --- a/lib/commands/cleanup/make-cleanup.js +++ b/lib/commands/cleanup/make-cleanup.js @@ -1,7 +1,15 @@ -const get = require('lodash/get'); -const property = require('lodash/property'); +const { get, property, isEmpty } = require('lodash'); +const chalk = require('chalk'); +const { log } = require('../../utils/logger'); -const makeCleanup = ({ parameterStore, settingsService }) => async () => { +const maskValue = value => { + const stringToMask = value || ''; + return stringToMask.replace(/\S(?=\S{4})/g, '*'); +}; + +const makeCleanup = ({ parameterStore, settingsService }) => async ( + { dryRun } = { dryRun: false } +) => { const settings = await settingsService.getSettings(); const configPath = get(settings, 'config.path'); const secretPath = get(settings, 'secret.path'); @@ -15,12 +23,28 @@ const makeCleanup = ({ parameterStore, settingsService }) => async () => { ({ Name }) => ![...configParameters, ...secretParameters].includes(Name) ); - return ( - unusedParameters.length && - parameterStore.deleteParameters({ - parameterNames: unusedParameters.map(property('Name')) - }) - ); + if (isEmpty(unusedParameters)) { + log(chalk.gray('Cleanup --> No unused parameters')); + return Promise.resolve(); + } + + if (dryRun) { + log(chalk.gray('Cleanup --> Parameters to be deleted: ')); + return unusedParameters.map(({ Name, Value, Type }) => { + const shouldMask = Type === 'SecureString'; + return log( + chalk.gray( + `Cleanup --> Name: ${Name} | Value: [${ + shouldMask ? maskValue(Value) : Value + }]` + ) + ); + }); + } + + return parameterStore.deleteParameters({ + parameterNames: unusedParameters.map(property('Name')) + }); }; module.exports = { makeCleanup }; diff --git a/lib/commands/cleanup/make-cleanup.test.js b/lib/commands/cleanup/make-cleanup.test.js index d1f1ba6..25f593f 100644 --- a/lib/commands/cleanup/make-cleanup.test.js +++ b/lib/commands/cleanup/make-cleanup.test.js @@ -34,6 +34,9 @@ describe('cleanup', () => { config: path.resolve(__dirname, '../../../mocks/ssm-provider.yml') }); + beforeEach(() => { + jest.clearAllMocks(); + }); it('should delete unused parameters', async () => { mockGetAllParameters.mockResolvedValueOnce([ { @@ -69,7 +72,7 @@ describe('cleanup', () => { ]); mockDeleteParameters.mockResolvedValue({}); - await oprah.cleanup(); + await oprah.cleanUp({ dryRun: false }); expect.assertions(4); expect(mockGetAllParameters.mock.calls.length).toEqual(2); expect(mockGetAllParameters.mock.calls[0][0].path).toEqual('/test/config'); @@ -78,4 +81,82 @@ describe('cleanup', () => { '/test/config/THREE' ]); }); + + it('should not call delete parameters if no unused parameters', async () => { + mockGetAllParameters.mockResolvedValueOnce([ + { + Name: '/test/config/ONE', + Type: 'String', + Value: '3200', + Version: 1, + DataType: 'text' + }, + { + Name: '/test/config/TWO', + Type: 'String', + Value: 'my-database', + Version: 1, + DataType: 'text' + } + ]); + mockGetAllParameters.mockResolvedValueOnce([ + { + Name: '/test/secret/FOUR', + Type: 'String', + Value: 'disabled', + Version: 1, + DataType: 'text' + } + ]); + mockDeleteParameters.mockResolvedValue({}); + + await oprah.cleanUp({ dryRun: false }); + expect.assertions(4); + expect(mockGetAllParameters.mock.calls.length).toEqual(2); + expect(mockGetAllParameters.mock.calls[0][0].path).toEqual('/test/config'); + expect(mockGetAllParameters.mock.calls[1][0].path).toEqual('/test/secret'); + expect(mockDeleteParameters.mock.calls.length).toEqual(0); + }); + it("should not delete unused parameters if it's a dry run", async () => { + mockGetAllParameters.mockResolvedValueOnce([ + { + Name: '/test/config/ONE', + Type: 'String', + Value: '3200', + Version: 1, + DataType: 'text' + }, + { + Name: '/test/config/TWO', + Type: 'String', + Value: 'my-database', + Version: 1, + DataType: 'text' + }, + { + Name: '/test/config/THREE', + Type: 'String', + Value: 'test-table', + Version: 1, + DataType: 'text' + } + ]); + mockGetAllParameters.mockResolvedValueOnce([ + { + Name: '/test/secret/FOUR', + Type: 'String', + Value: 'disabled', + Version: 1, + DataType: 'text' + } + ]); + mockDeleteParameters.mockResolvedValue({}); + + await oprah.cleanUp({ dryRun: true }); + expect.assertions(4); + expect(mockGetAllParameters.mock.calls.length).toEqual(2); + expect(mockGetAllParameters.mock.calls[0][0].path).toEqual('/test/config'); + expect(mockGetAllParameters.mock.calls[1][0].path).toEqual('/test/secret'); + expect(mockDeleteParameters.mock.calls.length).toEqual(0); + }); }); diff --git a/lib/commands/run/make-run.js b/lib/commands/run/make-run.js index 7a5dadd..7a0e036 100644 --- a/lib/commands/run/make-run.js +++ b/lib/commands/run/make-run.js @@ -1,3 +1,8 @@ -const makeRun = ({ init, configure }) => () => init().then(configure); +const makeRun = ({ init, configure, cleanUp }) => ( + { deleting } = { deleting: false } +) => + init() + .then(configure) + .then(() => deleting && cleanUp()); module.exports = { makeRun }; diff --git a/lib/commands/run/make-run.test.js b/lib/commands/run/make-run.test.js index 8ae3193..6f84591 100644 --- a/lib/commands/run/make-run.test.js +++ b/lib/commands/run/make-run.test.js @@ -2,14 +2,30 @@ const { makeRun } = require('./make-run'); const mockInit = jest.fn(() => Promise.resolve({})); const mockConfigure = jest.fn(() => Promise.resolve({})); +const mockCleanUp = jest.fn(() => Promise.resolve({})); describe('#makeRun', () => { - it('should call both init and configure method', () => { - const run = makeRun({ init: mockInit, configure: mockConfigure }); + const run = makeRun({ + init: mockInit, + configure: mockConfigure, + cleanUp: mockCleanUp + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); - return run().then(() => { + it('should call init, configuration but not cleanUp if deleting is not required', () => + run({ deleting: false }).then(() => { expect(mockInit).toHaveBeenCalledTimes(1); expect(mockConfigure).toHaveBeenCalledTimes(1); - }); - }); + expect(mockCleanUp).not.toHaveBeenCalled(); + })); + + it('should call init, configuration and cleanUp if deleting is required', () => + run({ deleting: true }).then(() => { + expect(mockInit).toHaveBeenCalledTimes(1); + expect(mockConfigure).toHaveBeenCalledTimes(1); + expect(mockCleanUp).toHaveBeenCalledTimes(1); + })); }); diff --git a/lib/make-oprah.js b/lib/make-oprah.js index 0dffd73..84a7c51 100644 --- a/lib/make-oprah.js +++ b/lib/make-oprah.js @@ -52,15 +52,17 @@ const makeOprah = ({ const init = makeInit({ settingsService, cfService }); + const cleanUp = makeCleanup({ parameterStore, settingsService }); + return { configure, init, - run: makeRun({ init, configure }), + run: makeRun({ init, configure, cleanUp }), list: makeList({ settingsService, parameterStore }), export: makeExport({ settingsService, parameterStore }), import: makeImport({ settingsService, parameterStore }), fetch: makeFetch({ parameterStore, settingsService }), - cleanup: makeCleanup({ parameterStore, settingsService }) + cleanUp }; }; diff --git a/lib/services/parameter-store/make-delete-parameters.js b/lib/services/parameter-store/make-delete-parameters.js index 80b6f7b..bcb885d 100644 --- a/lib/services/parameter-store/make-delete-parameters.js +++ b/lib/services/parameter-store/make-delete-parameters.js @@ -4,12 +4,12 @@ const { log } = require('../../utils/logger'); const makeDeleteParameters = ({ getProviderStore }) => async ({ parameterNames }) => { - log(chalk.gray(`Deleting unused parameters...`)); + log(chalk.gray(`Cleanup --> Deleting unused parameters...`)); const providerStore = await getProviderStore(); return providerStore .deleteParameters({ parameterNames }) - .then(() => log(chalk.gray('Parameters deleted'))); + .then(() => log(chalk.gray('Cleanup --> All orphan parameters deleted'))); }; module.exports = { makeDeleteParameters }; diff --git a/lib/services/parameter-store/make-get-all-parameters.js b/lib/services/parameter-store/make-get-all-parameters.js index b47a88c..4e61694 100644 --- a/lib/services/parameter-store/make-get-all-parameters.js +++ b/lib/services/parameter-store/make-get-all-parameters.js @@ -1,5 +1,3 @@ -const { sortParameters } = require('./sort-parameters'); - const makeGetAllParameters = ({ getProviderStore }) => async ({ path }) => { if (!path) { throw new Error('Missing path!'); diff --git a/lib/services/parameter-store/stores/ssm/get-all-parameters-by-path.js b/lib/services/parameter-store/stores/ssm/get-all-parameters-by-path.js index b9b2d78..cfb3e7f 100644 --- a/lib/services/parameter-store/stores/ssm/get-all-parameters-by-path.js +++ b/lib/services/parameter-store/stores/ssm/get-all-parameters-by-path.js @@ -21,7 +21,8 @@ const getParametersByPathRecursively = async params => { const getAllParametersByPath = async ({ path }) => { const allParameters = await getParametersByPathRecursively({ Path: path, - Recursive: true + Recursive: true, + WithDecryption: true }); return allParameters;