Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { frodo } from '@rockcarver/frodo-lib';
import { Option } from 'commander';

import { configManagerImportSecrets } from '../../../configManagerOps/FrConfigSecretOps';
import { getTokens } from '../../../ops/AuthenticateOps';
import { verboseMessage } from '../../../utils/Console';
import { FrodoCommand } from '../../FrodoCommand';

const { CLOUD_DEPLOYMENT_TYPE_KEY } = frodo.utils.constants;

const deploymentTypes = [CLOUD_DEPLOYMENT_TYPE_KEY];

export default function setup() {
const program = new FrodoCommand(
'frodo config-manager push secrets',
[],
deploymentTypes
);

program
.description('Import secrets.')
.addOption(
new Option(
'-n, --name <name>',
'Secret name; import only the specified secret'
)
)

.addOption(
new Option(
'-p, --prune',
'Prunes old versions that are still enabled before importing the new version(s)'
)
)

Comment thread
dallinjsevy marked this conversation as resolved.
.action(async (host, realm, user, password, options, command) => {
command.handleDefaultArgsAndOpts(
host,
realm,
user,
password,
options,
command
);

const getTokensIsSuccessful = await getTokens(
false,
true,
deploymentTypes
);
if (!getTokensIsSuccessful) process.exit(1);
verboseMessage('Importing secrets to cloud');
const outcome = await configManagerImportSecrets(
options.name,
options.prune
);
if (!outcome) process.exitCode = 1;
});

return program;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import RemoteServers from './config-manager-push-remote-servers';
import Restart from './config-manager-push-restart';
import Schedules from './config-manager-push-schedules';
import SecretMappings from './config-manager-push-secret-mappings';
import Secrets from './config-manager-push-secrets';
import ServiceObjects from './config-manager-push-service-objects';
import TermsAndConditions from './config-manager-push-terms-and-conditions';
import Themes from './config-manager-push-themes';
Expand Down Expand Up @@ -62,6 +63,7 @@ export default function setup() {
program.addCommand(Restart().name('restart'));
program.addCommand(Journeys().name('journeys'));
program.addCommand(Variables().name('variables'));
program.addCommand(Secrets().name('secrets'));

return program;
}
145 changes: 142 additions & 3 deletions src/configManagerOps/FrConfigSecretOps.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { frodo } from '@rockcarver/frodo-lib';
import { SecretSkeleton } from '@rockcarver/frodo-lib/types/api/cloud/SecretsApi';
import {
SecretSkeleton,
VersionOfSecretSkeleton,
} from '@rockcarver/frodo-lib/types/api/cloud/SecretsApi';
import { SecretsExportInterface } from '@rockcarver/frodo-lib/types/ops/cloud/SecretsOps';
import fs from 'fs';

import {
createProgressIndicator,
printError,
printMessage,
stopProgressIndicator,
updateProgressIndicator,
} from '../utils/Console';

const { getFilePath, saveJsonToFile } = frodo.utils;
const { readSecrets, exportSecret } = frodo.cloud.secret;
const { getFilePath, saveJsonToFile, readJsonFile } = frodo.utils;
const {
readSecrets,
exportSecret,
createSecret,
createVersionOfSecret,
pruneVersionsOfSecret,
importSecrets,
} = frodo.cloud.secret;

/**
* Export all secrets to individual files in fr-config-manager format
Expand Down Expand Up @@ -87,3 +99,130 @@ export async function configManagerExportSecrets(
}
return false;
}

/**
* Import secrets to cloud from fr-config-manager format
* @param {string} secretName optional name of secret to import
Comment thread
dallinjsevy marked this conversation as resolved.
* @param {boolean} prune if true, prunes old enabled versions before importing new versions.
* @returns {Promise<boolean>} true if successful, false otherwise
*/
export async function configManagerImportSecrets(
Comment thread
dallinjsevy marked this conversation as resolved.
secretName?: string,
prune?: boolean
): Promise<boolean> {
const spinnerId = createProgressIndicator(
'indeterminate',
0,
`Reading secrets...`
);
let indicatorId: string;
try {
const secretsDir = getFilePath(`esvs/secrets/`);
if (!fs.existsSync(secretsDir)) {
stopProgressIndicator(spinnerId, `No secrets found`, 'fail');
Comment thread
dallinjsevy marked this conversation as resolved.
return false;
}

const fileNames = fs
.readdirSync(secretsDir)
.filter((name) => name.toLowerCase().endsWith('.json'))
.filter((name) => !secretName || name === `${secretName}.json`);

if (fileNames.length === 0) {
stopProgressIndicator(
spinnerId,
secretName
? `No matching secret found for ${secretName}`
: 'No secrets found to import',
'fail'
);
return false;
}

stopProgressIndicator(
spinnerId,
`Successfully read ${fileNames.length} secrets.`,
'success'
);

indicatorId = createProgressIndicator(
'determinate',
fileNames.length,
'Importing secrets'
);
Comment thread
dallinjsevy marked this conversation as resolved.

const remoteSecrets = Object.fromEntries(
(await readSecrets()).map((s) => [s._id, s])
);
const secrets = {} as Record<string, SecretSkeleton>;

for (const fileName of fileNames) {
try {
const secret = readJsonFile(
`${secretsDir}/${fileName}`
) as SecretSkeleton & {
valueBase64?: string;
versions?: VersionOfSecretSkeleton[];
};
if (prune) await pruneVersionsOfSecret(secret._id, false, true);
if (secret.valueBase64 !== undefined) {
secret.activeValue = secret.valueBase64;
} else {
const versions = secret.versions.sort((a, b) =>
Number(a.version) > Number(b.version) ? 1 : -1
);
for (let i = 0; i < versions.length - 1; ++i) {
if (i === 0 && !remoteSecrets[secret._id]) {
await createSecret(
secret._id,
versions[i].valueBase64,
secret.description,
secret.encoding,
secret.useInPlaceholders
);
} else {
await createVersionOfSecret(secret._id, versions[i].valueBase64);
}
}
secret.activeValue = versions[versions.length - 1].valueBase64;
}
secrets[secret._id] = secret;
} catch (e) {
printError(e, `Error importing secret from "${fileName}"`);
}
Comment thread
dallinjsevy marked this conversation as resolved.
updateProgressIndicator(indicatorId, `Exported secret ${secrets._id}`);
}

const imported = await importSecrets({ secret: secrets }, true);

let unchanged = 0;
let updated = 0;

for (const s of imported) {
if (
remoteSecrets[s._id] &&
remoteSecrets[s._id].activeVersion === s.activeVersion
) {
printMessage(`Secret ${s._id} unchanged version ${s.activeVersion}`);
unchanged++;
} else {
printMessage(`Secret ${s._id} created version ${s.activeVersion}`);
updated++;
}
}

stopProgressIndicator(indicatorId, `${imported.length} secrets imported.`);

printMessage(
updated > 0
? `Changes made to secrets: ${updated} updated, ${unchanged} unchanged`
: `No changes, (${unchanged} secrets(s) already up to date)`
);

return true;
} catch (error) {
stopProgressIndicator(indicatorId, `Error importing secrets`, 'fail');
printError(error);
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`CLI help interface for 'config-manager push secrets' should be expected english 1`] = `
"Usage: frodo config-manager push secrets [options] [host] [realm] [username] [password]

[Experimental] Import secrets.

Arguments:
host AM base URL, e.g.: https://cdk.iam.example.com/am. To use a
connection profile, just specify a unique substring or
alias.
realm Realm. Specify realm as '/' for the root realm or 'realm'
or '/parent/child' otherwise. (default: "alpha" for
Identity Cloud tenants, "/" otherwise.)
username Username to login with. Must be an admin user with
appropriate rights to manage authentication journeys/trees.
If given without a password, and it matches the username
already stored in the connection profile for the target
host, frodo uses that profile's stored password instead of
requiring it on the command line.
password Password.

Deployment: Cloud-only

Options:
-n, --name <name> Secret name; import only the specified secret
-p, --prune Prunes old versions that are still enabled before importing
the new version(s)
-h, --help Help
-hh, --help-more Help with all options.
-hhh, --help-all Help with all options, environment variables, and usage
examples.
"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Commands:
iga-workflows [Experimental] Import iga-workflows.
restart [Experimental] Restart the environment.
secret-mappings [Experimental] Import secret mappings.
secrets [Experimental] Import secrets.
variables [Experimental] Import variables.
"
`;
10 changes: 10 additions & 0 deletions test/client_cli/en/config-manager-push-secrets.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import cp from 'child_process';
import { promisify } from 'util';

const exec = promisify(cp.exec);
const CMD = 'frodo config-manager push secrets --help';
const { stdout } = await exec(CMD);

test("CLI help interface for 'config-manager push secrets' should be expected english", async () => {
expect(stdout).toMatchSnapshot();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`frodo config-manager push secrets "frodo config-manager push secrets --env-file test/e2e/env/configManager1.env -D test/e2e/exports/fr-config-manager/cloud ": should import the secrets into cloud" 1`] = `""`;

exports[`frodo config-manager push secrets "frodo config-manager push secrets --env-file test/e2e/env/configManager1.env -D test/e2e/exports/fr-config-manager/cloud ": should import the secrets into cloud" 2`] = `
"Experimental feature in use: 'frodo config-manager push secrets'. This feature may change without notice.
✔ Successfully read 3 secrets.
Error importing secret from "esv-prune-secrets.json"
No value found for placeholder "ESV_PRUNE_SECRETS"
Secret esv-fr-test-secret created version 4
Secret esv-test-secret created version 8
• 2 secrets imported.
Changes made to secrets: 2 updated, 0 unchanged
"
`;

exports[`frodo config-manager push secrets "frodo config-manager push secrets -n esv-fr-test-secret -E ESV_FR_TEST_SECRET="new fr version" -n esv-test-secret -E ESV_TEST_SECRET="new test version" -D test/e2e/exports/fr-config-manager/cloud": should import multiple specified secrets into cloud 1`] = `""`;

exports[`frodo config-manager push secrets "frodo config-manager push secrets -n esv-fr-test-secret -E ESV_FR_TEST_SECRET="new fr version" -n esv-test-secret -E ESV_TEST_SECRET="new test version" -D test/e2e/exports/fr-config-manager/cloud": should import multiple specified secrets into cloud 2`] = `
"Experimental feature in use: 'frodo config-manager push secrets'. This feature may change without notice.
✔ Successfully read 1 secrets.
Error importing secret from "esv-test-secret.json"
No value found for placeholder "ESV_TEST_SECRET_1"
• 0 secrets imported.
No changes, (0 secrets(s) already up to date)
"
`;

exports[`frodo config-manager push secrets "frodo config-manager push secrets -n esv-fr-test-secret -E ESV_FR_TEST_SECRET="test1" test/e2e/exports/fr-config-manager/cloud": should import the specified secret into cloud 1`] = `""`;

exports[`frodo config-manager push secrets "frodo config-manager push secrets -n esv-fr-test-secret -E ESV_FR_TEST_SECRET="test1" test/e2e/exports/fr-config-manager/cloud": should import the specified secret into cloud 2`] = `
"Experimental feature in use: 'frodo config-manager push secrets'. This feature may change without notice.
✔ Successfully read 1 secrets.
Secret esv-fr-test-secret created version 3
• 1 secrets imported.
Changes made to secrets: 1 updated, 0 unchanged
"
`;

exports[`frodo config-manager push secrets "frodo config-manager push secrets -p -n esv-prune-secrets -E ESV_PRUNE_SECRETS=test3 -D test/e2e/exports/fr-config-manager/cloud": should import multiple specified secrets into cloud 1`] = `""`;

exports[`frodo config-manager push secrets "frodo config-manager push secrets -p -n esv-prune-secrets -E ESV_PRUNE_SECRETS=test3 -D test/e2e/exports/fr-config-manager/cloud": should import multiple specified secrets into cloud 2`] = `
"Experimental feature in use: 'frodo config-manager push secrets'. This feature may change without notice.
✔ Successfully read 1 secrets.
• Pruned 3 versions from secret esv-prune-secrets.
Secret esv-prune-secrets unchanged version 4
• 1 secrets imported.
No changes, (1 secrets(s) already up to date)
"
`;
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Should import variables into cloud "frodo config-manager push variables --env-file test/e2e/exports/fr-config-manager/cloud/fr-test-2.env --env-file test/e2e/exports/fr-config-manager/cloud/fr-test.env -D test/e2e/exports/fr-config-manager/cloud ": should import two variables into cloud" 1`] = `""`;
exports[`Should import variables into cloud "frodo config-manager push variables --env-file test/e2e/env/configManager2.env --env-file test/e2e/env/configManager1.env -D test/e2e/exports/fr-config-manager/cloud ": should import two variables into cloud" 1`] = `""`;

exports[`Should import variables into cloud "frodo config-manager push variables --env-file test/e2e/exports/fr-config-manager/cloud/fr-test-2.env --env-file test/e2e/exports/fr-config-manager/cloud/fr-test.env -D test/e2e/exports/fr-config-manager/cloud ": should import two variables into cloud" 2`] = `
exports[`Should import variables into cloud "frodo config-manager push variables --env-file test/e2e/env/configManager2.env --env-file test/e2e/env/configManager1.env -D test/e2e/exports/fr-config-manager/cloud ": should import two variables into cloud" 2`] = `
"Experimental feature in use: 'frodo config-manager push variables'. This feature may change without notice.
✔ Successfully read 2 variables.
Variable esv-fr-var-test-2 updated
Expand All @@ -12,9 +12,9 @@ Changes made to variables: 2 updated, 0 unchanged
"
`;

exports[`Should import variables into cloud "frodo config-manager push variables --name esv-fr-var-test --env ESV_FR_VAR_TEST="test2" --env-file test/e2e/exports/fr-config-manager/cloud/fr-test.env -D test/e2e/exports/fr-config-manager/cloud ": should import the specified variable into cloud" 1`] = `""`;
exports[`Should import variables into cloud "frodo config-manager push variables --name esv-fr-var-test --env ESV_FR_VAR_TEST="test2" --env-file test/e2e/env/configManager1.env -D test/e2e/exports/fr-config-manager/cloud ": should import the specified variable into cloud" 1`] = `""`;

exports[`Should import variables into cloud "frodo config-manager push variables --name esv-fr-var-test --env ESV_FR_VAR_TEST="test2" --env-file test/e2e/exports/fr-config-manager/cloud/fr-test.env -D test/e2e/exports/fr-config-manager/cloud ": should import the specified variable into cloud" 2`] = `
exports[`Should import variables into cloud "frodo config-manager push variables --name esv-fr-var-test --env ESV_FR_VAR_TEST="test2" --env-file test/e2e/env/configManager1.env -D test/e2e/exports/fr-config-manager/cloud ": should import the specified variable into cloud" 2`] = `
"Experimental feature in use: 'frodo config-manager push variables'. This feature may change without notice.
✔ Successfully read 1 variables.
Variable esv-fr-var-test updated
Expand Down
Loading