Skip to content

Commit

Permalink
Configurable name transformation for environment variables (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
jirkafajfr authored May 10, 2024
1 parent 8e3f9d4 commit ff26a0a
Show file tree
Hide file tree
Showing 16 changed files with 1,140 additions and 1,189 deletions.
27 changes: 27 additions & 0 deletions .github/actions/build/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Builds
description: Builds the repository and assumes the AWS IAM role for testing
runs:
using: composite
steps:
- name: Install dependencies
run: npm ci
shell: bash
- name: Build the dist folder
run: npm run build
shell: bash
- name: Determine role to assume
id: role-to-assume
run: |
if [ "${{ github.repository_owner }}" == "aws-actions" ]; then
# Use prod role for the PRs running against the main repo
echo "arn=arn:aws:iam::339713045997:role/GithubActionsRole" >> "$GITHUB_OUTPUT"
else
# Use beta role for the PRs running against engineer forks
echo "arn=arn:aws:iam::654654453185:role/GithubActionsRole" >> "$GITHUB_OUTPUT"
fi
shell: bash
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ steps.role-to-assume.outputs.arn }}
aws-region: us-east-1
126 changes: 108 additions & 18 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,44 +1,134 @@
name: Tests

on:
pull_request:
branches:
- main
push:
branches:
- main

permissions:
id-token: write
contents: read

jobs:
tests:
unit-tests:
runs-on: ubuntu-latest
name: Run Tests
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run Unit Tests
run: |
npm ci
npm run test
- name: Codecov
uses: codecov/codecov-action@v4.3.0
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

uppercase-transformation-integration-test:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
run: npm run build
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
uses: ./.github/actions/build
- name: Act
uses: ./
with:
role-to-assume: arn:aws:iam::339713045997:role/GithubActionsRole
aws-region: us-east-1
- name: Integration Tests Act
name-transformation: uppercase
parse-json-secrets: true
secret-ids: |
SampleSecret1
/special/chars/secret
0/special/chars/secret
PrefixSecret*
JsonSecret
SAMPLESECRET1_ALIAS, SampleSecret1
- name: Assert
run: npm run integration-test __integration_tests__/name_transformation/uppercase.test.ts

lowercase-transformation-integration-test:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
uses: ./.github/actions/build
- name: Act
uses: ./
with:
name-transformation: lowercase
parse-json-secrets: true
secret-ids: |
SampleSecret1
SAMPLESECRET1_ALIAS, SampleSecret1
/special/chars/secret
0/special/chars/secret
PrefixSecret*
JsonSecret
SampleSecret1
/special/chars/secret
0/special/chars/secret
PrefixSecret*
JsonSecret
samplesecret1_alias, SampleSecret1
- name: Assert
run: npm run integration-test __integration_tests__/name_transformation/lowercase.test.ts

none-transformation-integration-test:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
uses: ./.github/actions/build
- name: Act
uses: ./
with:
name-transformation: none
parse-json-secrets: true
- name: Integration Tests Assert
run: npm run integration-test
- name: Codecov
uses: codecov/codecov-action@v4
secret-ids: |
SampleSecret1
/special/chars/secret
0/special/chars/secret
PrefixSecret*
JsonSecret
SampleSecret1_Alias, SampleSecret1
- name: Assert
run: npm run integration-test __integration_tests__/name_transformation/none.test.ts

default-name-transformation-param-integration-test:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
uses: ./.github/actions/build
- name: Act
uses: ./
with:
parse-json-secrets: true
secret-ids: |
SampleSecret1
/special/chars/secret
0/special/chars/secret
PrefixSecret*
JsonSecret
SAMPLESECRET1_ALIAS, SampleSecret1
- name: Assert
run: npm run integration-test __integration_tests__/name_transformation/uppercase.test.ts

default-parse-json-secrets-integration-test:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
uses: ./.github/actions/build
- name: Act
uses: ./
with:
secret-ids: JsonSecret
- name: Assert Default Is No Json Secrets
run: npm run integration-test __integration_tests__/parse_json_secrets.test.ts
22 changes: 0 additions & 22 deletions __integration_tests__/env_variables.test.ts

This file was deleted.

24 changes: 24 additions & 0 deletions __integration_tests__/name_transformation.base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export function nameTransformationTest(transform: (secretName: string) => string) {
const dataset = [
// Standard name qualified test
['SampleSecret1', 'SomeSampleSecret1'],
// Special characters escaping test
['_special_chars_secret', 'SomeSampleSecret2'],
// Secret starting with numerical character escape test
['_0_special_chars_secret', 'SomeSampleSecret3'],
// Prefix matching test
['PrefixSecret1', 'PrefixSecret1Value'],
['PrefixSecret2', 'PrefixSecret2Value'],
// Json value expansion
['JsonSecret_api_user', 'user'],
['JsonSecret_api_key', 'key'],
['JsonSecret_config_active', 'true'],
// Alias test
['SampleSecret1_Alias', 'SomeSampleSecret1']
].map(([secretName, expectedValue]) => [transform(secretName), expectedValue]);

test.each(dataset)('Secret with name %s test', (secretName, expectedValue) => {
const secretValue = process.env[secretName];
expect(secretValue).toBe(expectedValue);
});
}
5 changes: 5 additions & 0 deletions __integration_tests__/name_transformation/lowercase.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { nameTransformationTest } from "../name_transformation.base";

describe('Lowercased Transformation Variables Assert', () => {
nameTransformationTest(secretName => secretName.toLowerCase());
});
5 changes: 5 additions & 0 deletions __integration_tests__/name_transformation/none.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { nameTransformationTest } from "../name_transformation.base";

describe('No Transformation Variables Assert', () => {
nameTransformationTest(secretName => secretName);
});
5 changes: 5 additions & 0 deletions __integration_tests__/name_transformation/uppercase.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { nameTransformationTest } from "../name_transformation.base";

describe('Uppercased Transformation Variables Assert', () => {
nameTransformationTest(secretName => secretName.toUpperCase());
});
8 changes: 8 additions & 0 deletions __integration_tests__/parse_json_secrets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
describe('parse-json-secrets: false Variables Assert', () => {
it('Has secret name, does not have json keys ', () => {
expect(process.env.JSONSECRET).not.toBeUndefined();
expect(process.env.JSONSECRET_API_USER).toBeUndefined();
expect(process.env.JSONSECRET_API_KEY).toBeUndefined();
expect(process.env.JSONSECRET_CONFIG_ACTIVE).toBeUndefined();
});
});
9 changes: 7 additions & 2 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jest.mock('@actions/core', () => {
return {
getMultilineInput: jest.fn(),
getBooleanInput: jest.fn(),
getInput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
Expand All @@ -75,6 +76,7 @@ describe('Test main action', () => {
const multilineInputSpy = jest.spyOn(core, "getMultilineInput").mockReturnValue(
[TEST_NAME, TEST_INPUT_3, TEST_ARN_INPUT, BLANK_ALIAS_INPUT]
);
const nameTransformationSpy = jest.spyOn(core, 'getInput').mockReturnValue('uppercase');

// Mock all Secrets Manager calls
smMockClient
Expand Down Expand Up @@ -106,8 +108,8 @@ describe('Test main action', () => {
.resolves({ Name: BLANK_NAME, SecretString: SECRET_FOR_BLANK });

await run();
expect(core.exportVariable).toHaveBeenCalledTimes(10);
expect(core.setFailed).not.toHaveBeenCalled();
expect(core.exportVariable).toHaveBeenCalledTimes(10);

// JSON secrets should be parsed
expect(core.exportVariable).toHaveBeenCalledWith('TEST_ONE_USER', 'admin');
Expand Down Expand Up @@ -137,6 +139,7 @@ describe('Test main action', () => {

booleanSpy.mockClear();
multilineInputSpy.mockClear();
nameTransformationSpy.mockClear();
});

test('Defaults to correct behavior with empty string alias', async () => {
Expand All @@ -152,8 +155,8 @@ describe('Test main action', () => {
.resolves({ Name: BLANK_NAME_3, SecretString: SECRET_FOR_BLANK_3 });

await run();
expect(core.exportVariable).toHaveBeenCalledTimes(3);
expect(core.setFailed).not.toHaveBeenCalled();
expect(core.exportVariable).toHaveBeenCalledTimes(3);

// Case when alias is blank, but still comma delimited in workflow and no json is parsed
// ex: ,test/blank2
Expand Down Expand Up @@ -192,6 +195,7 @@ describe('Test main action', () => {
const multilineInputSpy = jest.spyOn(core, "getMultilineInput").mockReturnValue(
[TEST_NAME, TEST_INPUT_3, TEST_ARN_INPUT]
);
const nameTransformationSpy = jest.spyOn(core, 'getInput').mockReturnValue('uppercase');

smMockClient
.on(GetSecretValueCommand, { SecretId: TEST_NAME_1})
Expand Down Expand Up @@ -226,5 +230,6 @@ describe('Test main action', () => {

booleanSpy.mockClear();
multilineInputSpy.mockClear();
nameTransformationSpy.mockClear();
});
});
23 changes: 20 additions & 3 deletions __tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
injectSecret,
isSecretArn,
extractAliasAndSecretIdFromInput,
transformToValidEnvName
transformToValidEnvName,
parseTransformationFunction,
TransformationFunc
} from "../src/utils";

import { CLEANUP_NAME, LIST_SECRETS_MAX_RESULTS } from "../src/constants";
Expand Down Expand Up @@ -367,8 +369,8 @@ describe('Test secret parsing and handling', () => {
expect(transformToValidEnvName('0Admin')).toBe('_0ADMIN')
});

test('Transforms to uppercase for environment name', () => {
expect(transformToValidEnvName('secret3')).toBe('SECRET3')
test('Transformation function is applied', () => {
expect(transformToValidEnvName('secret3', (x) => x.toUpperCase())).toBe('SECRET3')
});

/*
Expand Down Expand Up @@ -401,4 +403,19 @@ describe('Test secret parsing and handling', () => {
test('Test valid nested JSON { "a": "yes", "options": { "opt_a": "yes", "opt_b": "no"} } ', () => {
expect(isJSONString('{ "a": "yes", "options": { "opt_a": "yes", "opt_b": "no"} }')).toBe(true)
});

test.each([
[ 'Uppercase', (x: string) => x.toUpperCase() ],
[ 'uppercase', (x: string) => x.toUpperCase() ],
[ 'lowErCase', (x: string) => x.toLowerCase() ],
[ 'none', (x: string) => x ]
])('NameTransformation parsing of string %s should pass.', (name: string, transformation: TransformationFunc) => {
const sampleString = '$abcdEFGijk_';
const parsedTransformation = parseTransformationFunction(name);
expect(parsedTransformation(sampleString)).toEqual(transformation(sampleString));
});

test.each([ 'something', '' ])('NameTransformation parsing of string %s should fail.', (input) => {
expect(() => parseTransformationFunction(input)).toThrow();
});
});
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ inputs:
description: '(Optional) If true, JSON secrets will be deserialized, creating a secret environment variable for each key-value pair.'
required: false
default: 'false'
name-transformation:
description: '(Optional) Transforms environment variable name. Options: uppercase, lowercase, none. Default value: uppercase.'
required: false
default: 'uppercase'
runs:
using: 'node20'
main: 'dist/index.js'
Expand Down
Loading

0 comments on commit ff26a0a

Please sign in to comment.