mirror of
https://github.com/hashicorp/vault-action.git
synced 2025-11-07 07:06:56 +00:00
feat: add ability to retrieve secrets via ouputs
This commit is contained in:
parent
7d1d7d26ad
commit
ec10b5e257
3 changed files with 118 additions and 35 deletions
47
README.md
47
README.md
|
|
@ -52,7 +52,7 @@ The `secrets` parameter is a set of multiple secret requests separated by the `;
|
||||||
Each secret request is comprised of the `path` and the `key` of the desired secret, and optionally the desired Env Var 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
|
||||||
{{ Secret Path }} {{ Secret Key }} | {{ Output Environment Variable Name }}
|
{{ Secret Path }} {{ Secret Key }} | {{ Output Variable Name }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Simple Key
|
### Simple Key
|
||||||
|
|
@ -64,15 +64,28 @@ with:
|
||||||
secrets: ci npmToken
|
secrets: ci npmToken
|
||||||
```
|
```
|
||||||
|
|
||||||
`vault-action` will automatically normalize the given data key, and output:
|
`vault-action` will automatically normalize the given secret selector key, and set the follow as environment variables for the following steps in the current job:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
NPMTOKEN=somelongtoken
|
NPMTOKEN=somelongtoken
|
||||||
```
|
```
|
||||||
|
|
||||||
### Set Environment Variable Name
|
You can also access the secret via ouputs:
|
||||||
|
|
||||||
However, if you want to set it to a specific environmental variable, say `NPM_TOKEN`, you could do this instead:
|
```yaml
|
||||||
|
steps:
|
||||||
|
# ...
|
||||||
|
- name: Import Secrets
|
||||||
|
id: secrets
|
||||||
|
# Import config...
|
||||||
|
- name: Sensitive Operation
|
||||||
|
run: "my-cli --token '${{ steps.secrets.outputs.npmToken }}'"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Output Variable Name
|
||||||
|
|
||||||
|
However, if you want to set it to a specific name, say `NPM_TOKEN`, you could do this instead:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
with:
|
with:
|
||||||
|
|
@ -85,6 +98,17 @@ With that, `vault-action` will now use your requested name and output:
|
||||||
NPM_TOKEN=somelongtoken
|
NPM_TOKEN=somelongtoken
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
# ...
|
||||||
|
- name: Import Secrets
|
||||||
|
id: secrets
|
||||||
|
# Import config...
|
||||||
|
- name: Sensitive Operation
|
||||||
|
run: "my-cli --token '${{ steps.secrets.outputs.NPM_TOKEN }}'"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
### Multiple Secrets
|
### 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:
|
||||||
|
|
@ -147,7 +171,20 @@ with:
|
||||||
Resulting in:
|
Resulting in:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
FOO=bar MY_KEY=zap
|
FOO=bar
|
||||||
|
MY_KEY=zap
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
# ...
|
||||||
|
- name: Import Secrets
|
||||||
|
id: secrets
|
||||||
|
# Import config...
|
||||||
|
- name: Sensitive Operation
|
||||||
|
run: "my-cli --token '${{ steps.secrets.outputs.foo }}'"
|
||||||
|
- name: Another Sensitive Operation
|
||||||
|
run: "my-cli --token '${{ steps.secrets.outputs.MY_KEY }}'"
|
||||||
```
|
```
|
||||||
|
|
||||||
Secrets pulled from the same `Secret Path` are cached by default. So if you, for example, are using the `aws` engine and retrieve a key, only a single key for a given path is returned.
|
Secrets pulled from the same `Secret Path` are cached by default. So if you, for example, are using the `aws` engine and retrieve a key, only a single key for a given path is returned.
|
||||||
|
|
|
||||||
53
action.js
53
action.js
|
|
@ -1,4 +1,8 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const core = require('@actions/core');
|
const core = require('@actions/core');
|
||||||
|
// @ts-ignore
|
||||||
const command = require('@actions/core/lib/command');
|
const command = require('@actions/core/lib/command');
|
||||||
const got = require('got');
|
const got = require('got');
|
||||||
|
|
||||||
|
|
@ -36,6 +40,7 @@ async function exportSecrets() {
|
||||||
options.headers["X-Vault-Namespace"] = vaultNamespace;
|
options.headers["X-Vault-Namespace"] = vaultNamespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {any} */
|
||||||
const result = await got.post(`${vaultUrl}/v1/auth/approle/login`, options);
|
const result = await got.post(`${vaultUrl}/v1/auth/approle/login`, options);
|
||||||
if (result && result.body && result.body.auth && result.body.auth.client_token) {
|
if (result && result.body && result.body.auth && result.body.auth.client_token) {
|
||||||
vaultToken = result.body.auth.client_token;
|
vaultToken = result.body.auth.client_token;
|
||||||
|
|
@ -64,7 +69,7 @@ async function exportSecrets() {
|
||||||
|
|
||||||
const responseCache = new Map();
|
const responseCache = new Map();
|
||||||
for (const secretRequest of secretRequests) {
|
for (const secretRequest of secretRequests) {
|
||||||
const { secretPath, outputName, secretSelector, isJSONPath } = secretRequest;
|
const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest;
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': vaultToken
|
'X-Vault-Token': vaultToken
|
||||||
|
|
@ -98,13 +103,22 @@ async function exportSecrets() {
|
||||||
let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion;
|
let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion;
|
||||||
|
|
||||||
const secretData = getResponseData(body, dataDepth);
|
const secretData = getResponseData(body, dataDepth);
|
||||||
const value = selectData(secretData, secretSelector);
|
const value = selectData(secretData, secretSelector, isJSONPath);
|
||||||
command.issue('add-mask', value);
|
command.issue('add-mask', value);
|
||||||
core.exportVariable(outputName, `${value}`);
|
core.exportVariable(envVarName, `${value}`);
|
||||||
core.debug(`✔ ${secretPath} => ${outputName}`);
|
core.setOutput(outputVarName, `${value}`);
|
||||||
|
core.debug(`✔ ${secretPath} => outputs.${outputVarName} | env.${envVarName}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @typedef {Object} SecretRequest
|
||||||
|
* @property {string} secretPath
|
||||||
|
* @property {string} envVarName
|
||||||
|
* @property {string} outputVarName
|
||||||
|
* @property {string} secretSelector
|
||||||
|
* @property {boolean} isJSONPath
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a secrets 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} secretsInput
|
* @param {string} secretsInput
|
||||||
|
|
@ -116,18 +130,18 @@ function parseSecretsInput(secretsInput) {
|
||||||
.map(key => key.trim())
|
.map(key => key.trim())
|
||||||
.filter(key => key.length !== 0);
|
.filter(key => key.length !== 0);
|
||||||
|
|
||||||
/** @type {{ secretPath: string; outputName: string; dataKey: string; }[]} */
|
/** @type {SecretRequest[]} */
|
||||||
const output = [];
|
const output = [];
|
||||||
for (const secret of secrets) {
|
for (const secret of secrets) {
|
||||||
let path = secret;
|
let path = secret;
|
||||||
let outputName = null;
|
let outputVarName = null;
|
||||||
|
|
||||||
const renameSigilIndex = secret.lastIndexOf('|');
|
const renameSigilIndex = secret.lastIndexOf('|');
|
||||||
if (renameSigilIndex > -1) {
|
if (renameSigilIndex > -1) {
|
||||||
path = secret.substring(0, renameSigilIndex).trim();
|
path = secret.substring(0, renameSigilIndex).trim();
|
||||||
outputName = secret.substring(renameSigilIndex + 1).trim();
|
outputVarName = secret.substring(renameSigilIndex + 1).trim();
|
||||||
|
|
||||||
if (outputName.length < 1) {
|
if (outputVarName.length < 1) {
|
||||||
throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`);
|
throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,21 +152,29 @@ function parseSecretsInput(secretsInput) {
|
||||||
.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: "${secret}"`)
|
throw Error(`You must provide a valid path and key. Input: "${secret}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [secretPath, secretSelector] = pathParts;
|
const [secretPath, secretSelector] = pathParts;
|
||||||
|
|
||||||
// If we're not using a mapped name, normalize the key path into a variable name.
|
const isJSONPath = secretSelector.includes('.');
|
||||||
if (!outputName) {
|
|
||||||
outputName = normalizeOutputKey(secretSelector);
|
if (isJSONPath && !outputVarName) {
|
||||||
|
throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let envVarName = outputVarName;
|
||||||
|
if (!outputVarName) {
|
||||||
|
outputVarName = secretSelector;
|
||||||
|
envVarName = normalizeOutputKey(outputVarName);
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push({
|
output.push({
|
||||||
secretPath,
|
secretPath,
|
||||||
outputName,
|
envVarName,
|
||||||
|
outputVarName,
|
||||||
secretSelector,
|
secretSelector,
|
||||||
isJSONPath: secretSelector.startsWith('$')
|
isJSONPath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
|
|
@ -161,7 +183,7 @@ function parseSecretsInput(secretsInput) {
|
||||||
/**
|
/**
|
||||||
* Parses a JSON response and returns the secret data
|
* Parses a JSON response and returns the secret data
|
||||||
* @param {string} responseBody
|
* @param {string} responseBody
|
||||||
* @param {number} kvVersion
|
* @param {number} dataLevel
|
||||||
*/
|
*/
|
||||||
function getResponseData(responseBody, dataLevel) {
|
function getResponseData(responseBody, dataLevel) {
|
||||||
let secretData = JSON.parse(responseBody);
|
let secretData = JSON.parse(responseBody);
|
||||||
|
|
@ -193,6 +215,7 @@ function normalizeOutputKey(dataKey) {
|
||||||
return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase();
|
return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
function parseBoolInput(input) {
|
function parseBoolInput(input) {
|
||||||
if (input === null || input === undefined || input.trim() === '') {
|
if (input === null || input === undefined || input.trim() === '') {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
53
dist/index.js
vendored
53
dist/index.js
vendored
|
|
@ -4839,7 +4839,11 @@ module.exports = require("fs");
|
||||||
/***/ 751:
|
/***/ 751:
|
||||||
/***/ (function(module, __unusedexports, __webpack_require__) {
|
/***/ (function(module, __unusedexports, __webpack_require__) {
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const core = __webpack_require__(470);
|
const core = __webpack_require__(470);
|
||||||
|
// @ts-ignore
|
||||||
const command = __webpack_require__(431);
|
const command = __webpack_require__(431);
|
||||||
const got = __webpack_require__(77);
|
const got = __webpack_require__(77);
|
||||||
|
|
||||||
|
|
@ -4877,6 +4881,7 @@ async function exportSecrets() {
|
||||||
options.headers["X-Vault-Namespace"] = vaultNamespace;
|
options.headers["X-Vault-Namespace"] = vaultNamespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {any} */
|
||||||
const result = await got.post(`${vaultUrl}/v1/auth/approle/login`, options);
|
const result = await got.post(`${vaultUrl}/v1/auth/approle/login`, options);
|
||||||
if (result && result.body && result.body.auth && result.body.auth.client_token) {
|
if (result && result.body && result.body.auth && result.body.auth.client_token) {
|
||||||
vaultToken = result.body.auth.client_token;
|
vaultToken = result.body.auth.client_token;
|
||||||
|
|
@ -4905,7 +4910,7 @@ async function exportSecrets() {
|
||||||
|
|
||||||
const responseCache = new Map();
|
const responseCache = new Map();
|
||||||
for (const secretRequest of secretRequests) {
|
for (const secretRequest of secretRequests) {
|
||||||
const { secretPath, outputName, secretSelector, isJSONPath } = secretRequest;
|
const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest;
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': vaultToken
|
'X-Vault-Token': vaultToken
|
||||||
|
|
@ -4939,13 +4944,22 @@ async function exportSecrets() {
|
||||||
let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion;
|
let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion;
|
||||||
|
|
||||||
const secretData = getResponseData(body, dataDepth);
|
const secretData = getResponseData(body, dataDepth);
|
||||||
const value = selectData(secretData, secretSelector);
|
const value = selectData(secretData, secretSelector, isJSONPath);
|
||||||
command.issue('add-mask', value);
|
command.issue('add-mask', value);
|
||||||
core.exportVariable(outputName, `${value}`);
|
core.exportVariable(envVarName, `${value}`);
|
||||||
core.debug(`✔ ${secretPath} => ${outputName}`);
|
core.setOutput(outputVarName, `${value}`);
|
||||||
|
core.debug(`✔ ${secretPath} => outputs.${outputVarName} | env.${envVarName}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @typedef {Object} SecretRequest
|
||||||
|
* @property {string} secretPath
|
||||||
|
* @property {string} envVarName
|
||||||
|
* @property {string} outputVarName
|
||||||
|
* @property {string} secretSelector
|
||||||
|
* @property {boolean} isJSONPath
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a secrets 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} secretsInput
|
* @param {string} secretsInput
|
||||||
|
|
@ -4957,18 +4971,18 @@ function parseSecretsInput(secretsInput) {
|
||||||
.map(key => key.trim())
|
.map(key => key.trim())
|
||||||
.filter(key => key.length !== 0);
|
.filter(key => key.length !== 0);
|
||||||
|
|
||||||
/** @type {{ secretPath: string; outputName: string; dataKey: string; }[]} */
|
/** @type {SecretRequest[]} */
|
||||||
const output = [];
|
const output = [];
|
||||||
for (const secret of secrets) {
|
for (const secret of secrets) {
|
||||||
let path = secret;
|
let path = secret;
|
||||||
let outputName = null;
|
let outputVarName = null;
|
||||||
|
|
||||||
const renameSigilIndex = secret.lastIndexOf('|');
|
const renameSigilIndex = secret.lastIndexOf('|');
|
||||||
if (renameSigilIndex > -1) {
|
if (renameSigilIndex > -1) {
|
||||||
path = secret.substring(0, renameSigilIndex).trim();
|
path = secret.substring(0, renameSigilIndex).trim();
|
||||||
outputName = secret.substring(renameSigilIndex + 1).trim();
|
outputVarName = secret.substring(renameSigilIndex + 1).trim();
|
||||||
|
|
||||||
if (outputName.length < 1) {
|
if (outputVarName.length < 1) {
|
||||||
throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`);
|
throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4979,21 +4993,29 @@ function parseSecretsInput(secretsInput) {
|
||||||
.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: "${secret}"`)
|
throw Error(`You must provide a valid path and key. Input: "${secret}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [secretPath, secretSelector] = pathParts;
|
const [secretPath, secretSelector] = pathParts;
|
||||||
|
|
||||||
// If we're not using a mapped name, normalize the key path into a variable name.
|
const isJSONPath = secretSelector.includes('.');
|
||||||
if (!outputName) {
|
|
||||||
outputName = normalizeOutputKey(secretSelector);
|
if (isJSONPath && !outputVarName) {
|
||||||
|
throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let envVarName = outputVarName;
|
||||||
|
if (!outputVarName) {
|
||||||
|
outputVarName = secretSelector;
|
||||||
|
envVarName = normalizeOutputKey(outputVarName);
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push({
|
output.push({
|
||||||
secretPath,
|
secretPath,
|
||||||
outputName,
|
envVarName,
|
||||||
|
outputVarName,
|
||||||
secretSelector,
|
secretSelector,
|
||||||
isJSONPath: secretSelector.startsWith('$')
|
isJSONPath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
|
|
@ -5002,7 +5024,7 @@ function parseSecretsInput(secretsInput) {
|
||||||
/**
|
/**
|
||||||
* Parses a JSON response and returns the secret data
|
* Parses a JSON response and returns the secret data
|
||||||
* @param {string} responseBody
|
* @param {string} responseBody
|
||||||
* @param {number} kvVersion
|
* @param {number} dataLevel
|
||||||
*/
|
*/
|
||||||
function getResponseData(responseBody, dataLevel) {
|
function getResponseData(responseBody, dataLevel) {
|
||||||
let secretData = JSON.parse(responseBody);
|
let secretData = JSON.parse(responseBody);
|
||||||
|
|
@ -5034,6 +5056,7 @@ function normalizeOutputKey(dataKey) {
|
||||||
return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase();
|
return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
function parseBoolInput(input) {
|
function parseBoolInput(input) {
|
||||||
if (input === null || input === undefined || input.trim() === '') {
|
if (input === null || input === undefined || input.trim() === '') {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue