mirror of
https://github.com/hashicorp/vault-action.git
synced 2025-11-07 15:16:56 +00:00
feat: simplify input parameters and docs
This commit is contained in:
parent
9c32e02d93
commit
19c0b21a1d
6 changed files with 80 additions and 80 deletions
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
|
|
@ -71,12 +71,12 @@ jobs:
|
||||||
env:
|
env:
|
||||||
VAULT_HOST: localhost
|
VAULT_HOST: localhost
|
||||||
VAULT_PORT: ${{ job.services.vault.ports[8200] }}
|
VAULT_PORT: ${{ job.services.vault.ports[8200] }}
|
||||||
- name: use vault actions
|
- name: use vault action
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
vaultUrl: http://localhost:${{ job.services.vault.ports[8200] }}
|
url: http://localhost:${{ job.services.vault.ports[8200] }}
|
||||||
vaultToken: testtoken
|
token: testtoken
|
||||||
keys: |
|
secrets: |
|
||||||
test secret ;
|
test secret ;
|
||||||
test secret | NAMED_SECRET ;
|
test secret | NAMED_SECRET ;
|
||||||
nested/test otherSecret ;
|
nested/test otherSecret ;
|
||||||
|
|
|
||||||
24
README.md
24
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# vault-action
|
# vault-action
|
||||||
|
|
||||||
A helper action for retrieving vault secrets as env vars.
|
A helper action for easily pulling secrets from the v2 K/V backend of vault.
|
||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
|
|
||||||
|
|
@ -11,25 +11,25 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
# ...
|
# ...
|
||||||
- name: Import Secrets
|
- name: Import Secrets
|
||||||
uses: richicoder1/vault-action
|
uses: RichiCoder1/vault-action
|
||||||
with:
|
with:
|
||||||
vaultUrl: https://vault.mycompany.com
|
url: https://vault.mycompany.com:8200
|
||||||
vaultToken: ${{ secrets.VaultToken }}
|
token: ${{ secrets.VaultToken }}
|
||||||
keys: |
|
secrets: |
|
||||||
ci/aws accessKey | AWS_ACCESS_KEY_ID ;
|
ci/aws accessKey | AWS_ACCESS_KEY_ID ;
|
||||||
ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
|
ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
|
||||||
ci/npm token | NPM_TOKEN
|
ci npm_token
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Syntax
|
## Key Syntax
|
||||||
|
|
||||||
The `keys` parameter is multiple keys separated by the `;` character.
|
The `secrets` parameter is a set of multiple secret requests separated by the `;` character.
|
||||||
|
|
||||||
Each key is comprised of the `path` of they key, and optionally a [`JSONPath`](https://www.npmjs.com/package/jsonpath) expression and an output name.
|
Each secret request is comprised of the `path` and the `key` of the desired secret, and optionally the desired Env Var output name.
|
||||||
|
|
||||||
```raw
|
```raw
|
||||||
{{ Key Path }} > {{ JSONPath Query }} | {{ Output Environment Variable Name }}
|
{{ Secret Path }} {{ Secret Key }} | {{ Output Environment Variable Name }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Simple Key
|
### Simple Key
|
||||||
|
|
@ -38,7 +38,7 @@ To retrieve a key `npmToken` from path `ci` that has value `somelongtoken` from
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
with:
|
with:
|
||||||
keys: ci npmToken
|
secrets: ci npmToken
|
||||||
```
|
```
|
||||||
|
|
||||||
`vault-action` will automatically normalize the given data key, and output:
|
`vault-action` will automatically normalize the given data key, and output:
|
||||||
|
|
@ -53,7 +53,7 @@ However, if you want to set it to a specific environmental variable, say `NPM_TO
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
with:
|
with:
|
||||||
keys: ci npmToken | NPM_TOKEN
|
secrets: ci npmToken | NPM_TOKEN
|
||||||
```
|
```
|
||||||
|
|
||||||
With that, `vault-action` will now use your requested name and output:
|
With that, `vault-action` will now use your requested name and output:
|
||||||
|
|
@ -62,7 +62,7 @@ With that, `vault-action` will now use your requested name and output:
|
||||||
NPM_TOKEN=somelongtoken
|
NPM_TOKEN=somelongtoken
|
||||||
```
|
```
|
||||||
|
|
||||||
### Multiple Keys
|
### Multiple Secrets
|
||||||
|
|
||||||
This action can take multi-line input, so say you had your AWS keys stored in a path and wanted to retrieve both of them. You can do:
|
This action can take multi-line input, so say you had your AWS keys stored in a path and wanted to retrieve both of them. You can do:
|
||||||
|
|
||||||
|
|
|
||||||
60
action.js
60
action.js
|
|
@ -3,15 +3,15 @@ const command = require('@actions/core/lib/command');
|
||||||
const got = require('got');
|
const got = require('got');
|
||||||
|
|
||||||
async function exportSecrets() {
|
async function exportSecrets() {
|
||||||
const vaultUrl = core.getInput('vaultUrl', { required: true });
|
const vaultUrl = core.getInput('url', { required: true });
|
||||||
const vaultToken = core.getInput('vaultToken', { required: true });
|
const vaultToken = core.getInput('token', { required: true });
|
||||||
|
|
||||||
const keysInput = core.getInput('keys', { required: true });
|
const secretsInput = core.getInput('secrets', { required: true });
|
||||||
const keys = parseKeysInput(keysInput);
|
const secrets = parseSecretsInput(secretsInput);
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const secret of secrets) {
|
||||||
const { keyPath, outputName, dataKey } = key;
|
const { secretPath, outputName, secretKey } = secret;
|
||||||
const result = await got(`${vaultUrl}/v1/secret/data/${keyPath}`, {
|
const result = await got(`${vaultUrl}/v1/secret/data/${secretPath}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': vaultToken
|
'X-Vault-Token': vaultToken
|
||||||
}
|
}
|
||||||
|
|
@ -20,37 +20,37 @@ async function exportSecrets() {
|
||||||
const parsedResponse = JSON.parse(result.body);
|
const parsedResponse = JSON.parse(result.body);
|
||||||
const vaultKeyData = parsedResponse.data;
|
const vaultKeyData = parsedResponse.data;
|
||||||
const versionData = vaultKeyData.data;
|
const versionData = vaultKeyData.data;
|
||||||
const value = versionData[dataKey];
|
const value = versionData[secretKey];
|
||||||
command.issue('add-mask', value);
|
command.issue('add-mask', value);
|
||||||
core.exportVariable(outputName, `${value}`);
|
core.exportVariable(outputName, `${value}`);
|
||||||
core.debug(`✔ ${keyPath} => ${outputName}`);
|
core.debug(`✔ ${secretPath} => ${outputName}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a keys input string into key paths and their resulting environment variable name.
|
* Parses a secrets input string into key paths and their resulting environment variable name.
|
||||||
* @param {string} keys
|
* @param {string} secretsInput
|
||||||
*/
|
*/
|
||||||
function parseKeysInput(keys) {
|
function parseSecretsInput(secretsInput) {
|
||||||
const keyPairs = keys
|
const secrets = secretsInput
|
||||||
.split(';')
|
.split(';')
|
||||||
.filter(key => !!key)
|
.filter(key => !!key)
|
||||||
.map(key => key.trim())
|
.map(key => key.trim())
|
||||||
.filter(key => key.length !== 0);
|
.filter(key => key.length !== 0);
|
||||||
|
|
||||||
/** @type {{ keyPath: string; outputName: string; dataKey: string; }[]} */
|
/** @type {{ secretPath: string; outputName: string; dataKey: string; }[]} */
|
||||||
const output = [];
|
const output = [];
|
||||||
for (const keyPair of keyPairs) {
|
for (const secret of secrets) {
|
||||||
let path = keyPair;
|
let path = secret;
|
||||||
let outputName = null;
|
let outputName = null;
|
||||||
|
|
||||||
const renameSigilIndex = keyPair.lastIndexOf('|');
|
const renameSigilIndex = secret.lastIndexOf('|');
|
||||||
if (renameSigilIndex > -1) {
|
if (renameSigilIndex > -1) {
|
||||||
path = keyPair.substring(0, renameSigilIndex).trim();
|
path = secret.substring(0, renameSigilIndex).trim();
|
||||||
outputName = keyPair.substring(renameSigilIndex + 1).trim();
|
outputName = secret.substring(renameSigilIndex + 1).trim();
|
||||||
|
|
||||||
if (outputName.length < 1) {
|
if (outputName.length < 1) {
|
||||||
throw Error(`You must provide a value when mapping a secret to a name. Input: "${keyPair}"`);
|
throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,20 +60,20 @@ function parseKeysInput(keys) {
|
||||||
.filter(part => part.length !== 0);
|
.filter(part => part.length !== 0);
|
||||||
|
|
||||||
if (pathParts.length !== 2) {
|
if (pathParts.length !== 2) {
|
||||||
throw Error(`You must provide a valid path and key. Input: "${keyPair}"`)
|
throw Error(`You must provide a valid path and key. Input: "${secret}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [keyPath, dataKey] = pathParts;
|
const [secretPath, secretKey] = pathParts;
|
||||||
|
|
||||||
// If we're not using a mapped name, normalize the key path into a variable name.
|
// If we're not using a mapped name, normalize the key path into a variable name.
|
||||||
if (!outputName) {
|
if (!outputName) {
|
||||||
outputName = normalizeKeyName(dataKey);
|
outputName = normalizeOutputKey(secretKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push({
|
output.push({
|
||||||
keyPath,
|
secretPath,
|
||||||
outputName,
|
outputName,
|
||||||
dataKey
|
secretKey
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
|
|
@ -81,14 +81,14 @@ function parseKeysInput(keys) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces any forward-slash characters to
|
* Replaces any forward-slash characters to
|
||||||
* @param {string} keyPath
|
* @param {string} dataKey
|
||||||
*/
|
*/
|
||||||
function normalizeKeyName(keyPath) {
|
function normalizeOutputKey(dataKey) {
|
||||||
return keyPath.replace('/', '__').replace(/[^\w-]/, '').toUpperCase();
|
return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
exportSecrets,
|
exportSecrets,
|
||||||
parseKeysInput,
|
parseSecretsInput,
|
||||||
normalizeKeyName
|
normalizeOutputKey
|
||||||
};
|
};
|
||||||
|
|
@ -6,23 +6,23 @@ const core = require('@actions/core');
|
||||||
const got = require('got');
|
const got = require('got');
|
||||||
const {
|
const {
|
||||||
exportSecrets,
|
exportSecrets,
|
||||||
parseKeysInput,
|
parseSecretsInput,
|
||||||
} = require('./action');
|
} = require('./action');
|
||||||
|
|
||||||
const { when } = require('jest-when');
|
const { when } = require('jest-when');
|
||||||
|
|
||||||
describe('parseKeysInput', () => {
|
describe('parseSecretsInput', () => {
|
||||||
it('parses simple key', () => {
|
it('parses simple secret', () => {
|
||||||
const output = parseKeysInput('test key');
|
const output = parseSecretsInput('test key');
|
||||||
expect(output).toContainEqual({
|
expect(output).toContainEqual({
|
||||||
keyPath: 'test',
|
secretPath: 'test',
|
||||||
dataKey: 'key',
|
secretKey: 'key',
|
||||||
outputName: 'KEY',
|
outputName: 'KEY',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses mapped key', () => {
|
it('parses mapped secret', () => {
|
||||||
const output = parseKeysInput('test key|testName');
|
const output = parseSecretsInput('test key|testName');
|
||||||
expect(output).toHaveLength(1);
|
expect(output).toHaveLength(1);
|
||||||
expect(output[0]).toMatchObject({
|
expect(output[0]).toMatchObject({
|
||||||
outputName: 'testName',
|
outputName: 'testName',
|
||||||
|
|
@ -30,29 +30,29 @@ describe('parseKeysInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails on invalid mapped name', () => {
|
it('fails on invalid mapped name', () => {
|
||||||
expect(() => parseKeysInput('test key|'))
|
expect(() => parseSecretsInput('test key|'))
|
||||||
.toThrowError(`You must provide a value when mapping a secret to a name. Input: "test key|"`)
|
.toThrowError(`You must provide a value when mapping a secret to a name. Input: "test key|"`)
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails on invalid path for mapped', () => {
|
it('fails on invalid path for mapped', () => {
|
||||||
expect(() => parseKeysInput('|testName'))
|
expect(() => parseSecretsInput('|testName'))
|
||||||
.toThrowError(`You must provide a valid path and key. Input: "|testName"`)
|
.toThrowError(`You must provide a valid path and key. Input: "|testName"`)
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses multiple keys', () => {
|
it('parses multiple secrets', () => {
|
||||||
const output = parseKeysInput('first a;second b;');
|
const output = parseSecretsInput('first a;second b;');
|
||||||
|
|
||||||
expect(output).toHaveLength(2);
|
expect(output).toHaveLength(2);
|
||||||
expect(output[0]).toMatchObject({
|
expect(output[0]).toMatchObject({
|
||||||
keyPath: 'first',
|
secretPath: 'first',
|
||||||
});
|
});
|
||||||
expect(output[1]).toMatchObject({
|
expect(output[1]).toMatchObject({
|
||||||
keyPath: 'second',
|
secretPath: 'second',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses multiple complex keys', () => {
|
it('parses multiple complex secret input', () => {
|
||||||
const output = parseKeysInput('first a;second b|secondName');
|
const output = parseSecretsInput('first a;second b|secondName');
|
||||||
|
|
||||||
expect(output).toHaveLength(2);
|
expect(output).toHaveLength(2);
|
||||||
expect(output[0]).toMatchObject({
|
expect(output[0]).toMatchObject({
|
||||||
|
|
@ -64,14 +64,14 @@ describe('parseKeysInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses multiline input', () => {
|
it('parses multiline input', () => {
|
||||||
const output = parseKeysInput(`
|
const output = parseSecretsInput(`
|
||||||
first a;
|
first a;
|
||||||
second b;
|
second b;
|
||||||
third c | SOME_C;`);
|
third c | SOME_C;`);
|
||||||
|
|
||||||
expect(output).toHaveLength(3);
|
expect(output).toHaveLength(3);
|
||||||
expect(output[0]).toMatchObject({
|
expect(output[0]).toMatchObject({
|
||||||
keyPath: 'first',
|
secretPath: 'first',
|
||||||
});
|
});
|
||||||
expect(output[1]).toMatchObject({
|
expect(output[1]).toMatchObject({
|
||||||
outputName: 'B',
|
outputName: 'B',
|
||||||
|
|
@ -88,17 +88,17 @@ describe('exportSecrets', () => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('vaultUrl')
|
.calledWith('url')
|
||||||
.mockReturnValue('http://vault:8200');
|
.mockReturnValue('http://vault:8200');
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('vaultToken')
|
.calledWith('token')
|
||||||
.mockReturnValue('EXAMPLE');
|
.mockReturnValue('EXAMPLE');
|
||||||
});
|
});
|
||||||
|
|
||||||
function mockInput(key) {
|
function mockInput(key) {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('keys')
|
.calledWith('secrets')
|
||||||
.mockReturnValue(key);
|
.mockReturnValue(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,7 +112,7 @@ describe('exportSecrets', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it('simple key retrieval', async () => {
|
it('simple secret retrieval', async () => {
|
||||||
mockInput('test key');
|
mockInput('test key');
|
||||||
mockVaultData({
|
mockVaultData({
|
||||||
key: 1
|
key: 1
|
||||||
|
|
@ -123,7 +123,7 @@ describe('exportSecrets', () => {
|
||||||
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mapped key retrieval', async () => {
|
it('mapped secret retrieval', async () => {
|
||||||
mockInput('test key|TEST_NAME');
|
mockInput('test key|TEST_NAME');
|
||||||
mockVaultData({
|
mockVaultData({
|
||||||
key: 1
|
key: 1
|
||||||
|
|
|
||||||
12
action.yml
12
action.yml
|
|
@ -1,14 +1,14 @@
|
||||||
name: 'Vault Secrets'
|
name: 'Vault'
|
||||||
description: 'A Github Action that allows you to consume vault secrets as secure environment variables'
|
description: 'A Github Action that allows you to consume the v2 K/V backend of HashiCorp Vault as secure environment variables'
|
||||||
inputs:
|
inputs:
|
||||||
vaultUrl:
|
url:
|
||||||
description: 'The URL for the vault endpoint'
|
description: 'The URL for the vault endpoint'
|
||||||
required: true
|
required: true
|
||||||
vaultToken:
|
token:
|
||||||
description: 'The Vault Token to be used to authenticate with Vault'
|
description: 'The Vault Token to be used to authenticate with Vault'
|
||||||
required: true
|
required: true
|
||||||
keys:
|
secrets:
|
||||||
description: 'A semicolon-separated list of key paths to retrieve. These will automatically be converted to environmental variable keys. See README for more details'
|
description: 'A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details'
|
||||||
required: true
|
required: true
|
||||||
runs:
|
runs:
|
||||||
using: 'node12'
|
using: 'node12'
|
||||||
|
|
|
||||||
|
|
@ -48,18 +48,18 @@ describe('integration', () => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('vaultUrl')
|
.calledWith('url')
|
||||||
.mockReturnValue(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`);
|
.mockReturnValue(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`);
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('vaultToken')
|
.calledWith('token')
|
||||||
.mockReturnValue('testtoken');
|
.mockReturnValue('testtoken');
|
||||||
});
|
});
|
||||||
|
|
||||||
function mockInput(key) {
|
function mockInput(secrets) {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('keys')
|
.calledWith('secrets')
|
||||||
.mockReturnValue(key);
|
.mockReturnValue(secrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
it('get simple secret', async () => {
|
it('get simple secret', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue