Skip to content

Commit

Permalink
feat: add secret creation flag and flow
Browse files Browse the repository at this point in the history
  • Loading branch information
danteay committed Nov 2, 2023
1 parent 7539ffe commit cb73bc6
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 40 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Github action that sync an AWS Secrets Manager and it's values from a provided j

- `secretsmanager:UpdateSecret`
- `secretsmanager:GetSecretValue`
- `secretsmanager:ListSecrets`
- `secretsmanager:CreateSecret`

### Configuration

Expand All @@ -28,6 +30,7 @@ jobs:
aws_region: <region>
secret_name: <secret-name>
json_file_path: path/to/json/secrets.json
create_secret: false # If true it will check if the secret exists or not to create it before execute sync (default false)
dry_run: true # Default false
show_values: false # If true secret values will be displayed on action logs (default false)
exclude: '^_' # Regular expression that excludes the matching keys to be synced (default '^_')
Expand Down
8 changes: 4 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ inputs:
json_file_path:
description: "Path to the JSON file containing secret data"
required: true
create_secret:
description: "Flag to create the given secret if not exist before execute sync"
required: false
default: "false"
dry_run:
description: "Dry run mode (preview changes without modifying the secret)"
required: false
Expand All @@ -33,10 +37,6 @@ inputs:
description: "List of regular expressions that determines if a secret key should be excluded from sync"
required: false

outputs:
changes:
description: "List of changes made to the secret"

runs:
using: "docker"
image: "Dockerfile"
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const getAction = () => {
core.getInput("json_file_path"),
core.getInput("exclude"),
core.getBooleanInput("show_values"),
core.getBooleanInput("create_secret"),
);
};

Expand Down
37 changes: 36 additions & 1 deletion src/action/Action.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export default class Action {
*/
#smClient;

/**
* Flag to create the secret before sync if not exists
* @type {boolean}
*/
#createSecretFlag;

/**
* Creates a new Action instance.
*
Expand All @@ -44,8 +50,9 @@ export default class Action {
* @param {string} jsonFile The path to the JSON file containing the new secret values.
* @param {string} skipPattern A regular expression that eval keys of the json file and if match,
* that key should be omitted
* @param {string} showValues If this flag is set to true all secret values will be displayed on logs,
* @param {boolean} showValues If this flag is set to true all secret values will be displayed on logs,
* if false, a place holder will be displayed.
* @param {boolean} createSecretFlag Flag to create the secret before sync if not exists
*
* @throws {Error} Throws an error if any required parameter is missing or if the JSON file doesn't exist.
*/
Expand All @@ -57,12 +64,14 @@ export default class Action {
jsonFile,
skipPattern,
showValues = false,
createSecret = false,
) {
this.#validateData(keyId, secretKey, region, secretName, jsonFile);

this.#jsonFile = jsonFile;
this.#skipPattern = skipPattern || defaultSkipPattern;
this.#showValues = showValues;
this.#createSecretFlag = createSecret;

this.#smClient = new SecretsManager(keyId, secretKey, region, secretName);
}
Expand All @@ -76,12 +85,23 @@ export default class Action {
this.#smClient = client;
}

/**
* Set the flag value after constructor.
*
* @param {boolean} flag value of the flag
*/
setCreateSecretFlag(flag) {
this.#createSecretFlag = flag;
}

/**
* Runs the action to synchronize secrets by fetching existing secrets and creating a change set.
*
* @returns {Promise<ChangeSet>} A promise that resolves to a ChangeSet instance representing the changes to be applied.
*/
async run() {
await this.#createSecret();

const existingSecretData = await this.#smClient.getValues();
const newSecretData = JSON.parse(fs.readFileSync(this.#jsonFile, "utf8"));

Expand All @@ -94,6 +114,21 @@ export default class Action {
);
}

/**
* Execute secret creation if needed
*/
async #createSecret() {
if (!this.#createSecretFlag) {
return;
}

if (await this.#smClient.exists()) {
return;
}

await this.#smClient.create();
}

/**
* Validates input data, ensuring that required parameters are provided and the JSON file exists.
*
Expand Down
38 changes: 38 additions & 0 deletions src/secrets-manager/SecretsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
SecretsManagerClient,
GetSecretValueCommand,
UpdateSecretCommand,
CreateSecretCommand,
ListSecretsCommand,
} from "@aws-sdk/client-secrets-manager";

import lodash from "lodash";
Expand Down Expand Up @@ -54,4 +56,40 @@ export default class SecretsManager {
const updateSecretCommand = new UpdateSecretCommand(updateSecretParams);
await this.client.send(updateSecretCommand);
}

/**
* Assert if the given secrets name exists.
*
* @returns {boolean}
*/
async exists() {
const listCommand = new ListSecretsCommand({
Filters: [
{
Key: "name",
Values: [this.secretName],
},
],
});

const res = await this.client.send(listCommand);

if (res.SecretList.length > 0) {
return true;
}

return false;
}

/**
* Create the given secret name with a default value
*/
async create() {
const createCommand = new CreateSecretCommand({
Name: this.secretName,
SecretString: JSON.stringify({ generated: true }),
});

await this.client.send(createCommand);
}
}
44 changes: 44 additions & 0 deletions tests/action/Action.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,48 @@ describe("Action", () => {
// Clean up the fs.readFileSync stub
fs.readFileSync.restore();
});

it("should create secret if not exists with flag create_secret", async () => {
const existingSecretData = { generated: true };
const newSecretData = { key1: "new-value1", key2: "new-value2" };

action.setCreateSecretFlag(true);

secretsManagerStub.exists.resolves(false);
secretsManagerStub.create.resolves();
secretsManagerStub.getValues.resolves(existingSecretData);

sinon.stub(fs, "readFileSync").returns(JSON.stringify(newSecretData));

const changeSet = await action.run();

expect(changeSet).to.be.an.instanceOf(ChangeSet);
expect(secretsManagerStub.exists.calledOnce).to.be.true;
expect(secretsManagerStub.create.calledOnce).to.be.true;
expect(secretsManagerStub.getValues.calledOnce).to.be.true;
expect(fs.readFileSync.calledOnce).to.be.true;

fs.readFileSync.restore();
});

it("should omit secret creation if exists with flag create_secret", async () => {
const existingSecretData = { key1: "value1", key2: "value2" };
const newSecretData = { key1: "new-value1", key2: "new-value2" };

action.setCreateSecretFlag(true);

secretsManagerStub.exists.resolves(true);
secretsManagerStub.getValues.resolves(existingSecretData);

sinon.stub(fs, "readFileSync").returns(JSON.stringify(newSecretData));

const changeSet = await action.run();

expect(changeSet).to.be.an.instanceOf(ChangeSet);
expect(secretsManagerStub.exists.calledOnce).to.be.true;
expect(secretsManagerStub.getValues.calledOnce).to.be.true;
expect(fs.readFileSync.calledOnce).to.be.true;

fs.readFileSync.restore();
});
});
Loading

0 comments on commit cb73bc6

Please sign in to comment.