Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add secret creation flag and flow #6

Merged
merged 1 commit into from
Nov 2, 2023
Merged
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
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