Compare commits

..

3 Commits

Author SHA1 Message Date
Rory 7709c60978 feat: added double asterisk wildcard selector to prevent uppercasing of keys before exporting envs (#545)
* feat: added double asterisk wildcard selector to prevent uppercasing of keys before exporting envs

* chore: update changelog

---------

Co-authored-by: John-Michael Faircloth <fairclothjm@users.noreply.github.com>
2025-03-03 15:31:00 -06:00
John-Michael Faircloth 4b1f32b395 Prepare for v3.2.0 release (#575)
* Prepare for v3.2.0 release

* changelog update
2025-03-03 14:18:41 -06:00
Andre Fuentes 5d06ce836f Retry core.getIDToken for JWT Auth Method (#574)
* adding generic async retry function and retry core.getIDToken

* adding test and build
2025-03-03 12:02:57 -06:00
13 changed files with 172 additions and 40 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
actionlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Lint workflow files"
uses: docker://docker.mirror.hashicorp.services/rhysd/actionlint:latest
with:
+15 -15
View File
@@ -6,14 +6,14 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20.9.0"
- name: Setup NPM Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -33,17 +33,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run docker compose
run: docker compose up -d vault
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20.9.0"
- name: Setup NPM Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -67,19 +67,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run docker compose
run: docker compose up -d vault-enterprise
env:
VAULT_LICENSE_CI: ${{ secrets.VAULT_LICENSE_CI }}
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20.9.0"
- name: Setup NPM Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -103,17 +103,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run docker compose
run: docker compose up -d vault
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20.9.0"
- name: Setup NPM Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -199,17 +199,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run docker compose
run: docker compose up -d vault-tls
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20.9.0"
- name: Setup NPM Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+2 -2
View File
@@ -18,9 +18,9 @@ jobs:
name: local-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: '20.9.0'
+11
View File
@@ -1,3 +1,14 @@
## 3.3.0 (March 3, 2025)
Features:
* Wildcard secret imports can use `**` to retain case of exported env keys [GH-545](https://github.com/hashicorp/vault-action/pull/545)
## 3.2.0 (March 3, 2025)
Improvements:
* Add retry for jwt auth login to fix intermittent login failures [GH-574](https://github.com/hashicorp/vault-action/pull/574)
## 3.1.0 (January 9, 2025)
Improvements:
+9
View File
@@ -403,6 +403,15 @@ with:
secret/data/ci/aws * | MYAPP_ ;
```
When using the `exportEnv` option all exported keys will be normalized to uppercase. For example, the key `SecretKey` would be exported as `MYAPP_SECRETKEY`.
You can disable uppercase normalization by specifying double asterisks `**` in the selector path:
```yaml
with:
secrets: |
secret/data/ci/aws ** | MYAPP_ ;
```
### KV secrets engine version 2
When accessing secrets from the KV secrets engine version 2, Vault Action
+44 -10
View File
@@ -18535,7 +18535,7 @@ const command = __nccwpck_require__(7351);
const got = (__nccwpck_require__(3061)["default"]);
const jsonata = __nccwpck_require__(4245);
const { normalizeOutputKey } = __nccwpck_require__(1608);
const { WILDCARD } = __nccwpck_require__(4438);
const { WILDCARD, WILDCARD_UPPERCASE } = __nccwpck_require__(4438);
const { auth: { retrieveToken }, secrets: { getSecrets }, pki: { getCertificates } } = __nccwpck_require__(4351);
@@ -18752,7 +18752,7 @@ function parseSecretsInput(secretsInput) {
const selectorAst = jsonata(selectorQuoted).ast();
const selector = selectorQuoted.replace(new RegExp('"', 'g'), '');
if (selector !== WILDCARD && (selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) {
if (selector !== WILDCARD && selector !== WILDCARD_UPPERCASE && (selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) {
throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`);
}
@@ -18817,6 +18817,8 @@ const fs = __nccwpck_require__(7147);
const { default: got } = __nccwpck_require__(3061);
const defaultKubernetesTokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token'
const retries = 5
const retries_delay = 3000
/***
* Authenticate with Vault and retrieve a Vault token that can be used for requests.
* @param {string} method
@@ -18847,7 +18849,10 @@ async function retrieveToken(method, client) {
const githubAudience = core.getInput('jwtGithubAudience', { required: false });
if (!privateKey) {
jwt = await core.getIDToken(githubAudience)
jwt = await retryAsyncFunction(retries, retries_delay, core.getIDToken, githubAudience)
.then((result) => {
return result;
});
} else {
jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl));
}
@@ -18954,6 +18959,30 @@ async function getClientToken(client, method, path, payload) {
}
}
/***
* Generic function for retrying an async function
* @param {number} retries
* @param {number} delay
* @param {Function} func
* @param {any[]} args
*/
async function retryAsyncFunction(retries, delay, func, ...args) {
let attempt = 0;
while (attempt < retries) {
try {
const result = await func(...args);
return result;
} catch (error) {
attempt++;
if (attempt < retries) {
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
}
/***
* @typedef {Object} VaultLoginResponse
* @property {{
@@ -18976,12 +19005,15 @@ module.exports = {
/***/ 4438:
/***/ ((module) => {
const WILDCARD = '*';
const WILDCARD_UPPERCASE = '*';
const WILDCARD = '**';
module.exports = {
WILDCARD
WILDCARD,
WILDCARD_UPPERCASE,
};
/***/ }),
/***/ 4351:
@@ -19085,7 +19117,7 @@ module.exports = {
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const jsonata = __nccwpck_require__(4245);
const { WILDCARD } = __nccwpck_require__(4438);
const { WILDCARD, WILDCARD_UPPERCASE} = __nccwpck_require__(4438);
const { normalizeOutputKey } = __nccwpck_require__(1608);
const core = __nccwpck_require__(2186);
@@ -19112,6 +19144,7 @@ const core = __nccwpck_require__(2186);
async function getSecrets(secretRequests, client, ignoreNotFound) {
const responseCache = new Map();
let results = [];
let upperCaseEnv = false;
for (const secretRequest of secretRequests) {
let { path, selector } = secretRequest;
@@ -19145,7 +19178,8 @@ async function getSecrets(secretRequests, client, ignoreNotFound) {
body = JSON.parse(body);
if (selector == WILDCARD) {
if (selector === WILDCARD || selector === WILDCARD_UPPERCASE) {
upperCaseEnv = selector === WILDCARD_UPPERCASE;
let keys = body.data;
if (body.data["data"] != undefined) {
keys = keys.data;
@@ -19164,7 +19198,7 @@ async function getSecrets(secretRequests, client, ignoreNotFound) {
}
newRequest.outputVarName = normalizeOutputKey(newRequest.outputVarName);
newRequest.envVarName = normalizeOutputKey(newRequest.envVarName,true);
newRequest.envVarName = normalizeOutputKey(newRequest.envVarName, upperCaseEnv);
// JSONata field references containing reserved tokens should
// be enclosed in backticks
@@ -19273,12 +19307,12 @@ module.exports = {
* @param {string} dataKey
* @param {boolean=} isEnvVar
*/
function normalizeOutputKey(dataKey, isEnvVar = false) {
function normalizeOutputKey(dataKey, upperCase = false) {
let outputKey = dataKey
.replace(".", "__")
.replace(new RegExp("-", "g"), "")
.replace(/[^\p{L}\p{N}_-]/gu, "");
if (isEnvVar) {
if (upperCase) {
outputKey = outputKey.toUpperCase();
}
return outputKey;
+27 -1
View File
@@ -395,7 +395,7 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('FOO', 'bar');
});
it('wildcard supports cubbyhole', async () => {
it('wildcard supports cubbyhole with uppercase transform', async () => {
mockInput('/cubbyhole/test *');
await exportSecrets();
@@ -405,6 +405,32 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('FOO', 'bar');
expect(core.exportVariable).toBeCalledWith('ZIP', 'zap');
});
it('wildcard supports cubbyhole with no change in case', async () => {
mockInput('/cubbyhole/test **');
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(2);
expect(core.exportVariable).toBeCalledWith('foo', 'bar');
expect(core.exportVariable).toBeCalledWith('zip', 'zap');
});
it('wildcard supports cubbyhole with mixed case change', async () => {
mockInput(`
/cubbyhole/test * ;
/cubbyhole/test **`);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(4);
expect(core.exportVariable).toBeCalledWith('FOO', 'bar');
expect(core.exportVariable).toBeCalledWith('ZIP', 'zap');
expect(core.exportVariable).toBeCalledWith('foo', 'bar');
expect(core.exportVariable).toBeCalledWith('zip', 'zap');
});
it('caches responses', async () => {
mockInput(`
+2 -2
View File
@@ -4,7 +4,7 @@ const command = require('@actions/core/lib/command');
const got = require('got').default;
const jsonata = require('jsonata');
const { normalizeOutputKey } = require('./utils');
const { WILDCARD } = require('./constants');
const { WILDCARD, WILDCARD_UPPERCASE } = require('./constants');
const { auth: { retrieveToken }, secrets: { getSecrets }, pki: { getCertificates } } = require('./index');
@@ -221,7 +221,7 @@ function parseSecretsInput(secretsInput) {
const selectorAst = jsonata(selectorQuoted).ast();
const selector = selectorQuoted.replace(new RegExp('"', 'g'), '');
if (selector !== WILDCARD && (selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) {
if (selector !== WILDCARD && selector !== WILDCARD_UPPERCASE && (selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) {
throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`);
}
+30 -1
View File
@@ -5,6 +5,8 @@ const fs = require('fs');
const { default: got } = require('got');
const defaultKubernetesTokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token'
const retries = 5
const retries_delay = 3000
/***
* Authenticate with Vault and retrieve a Vault token that can be used for requests.
* @param {string} method
@@ -35,7 +37,10 @@ async function retrieveToken(method, client) {
const githubAudience = core.getInput('jwtGithubAudience', { required: false });
if (!privateKey) {
jwt = await core.getIDToken(githubAudience)
jwt = await retryAsyncFunction(retries, retries_delay, core.getIDToken, githubAudience)
.then((result) => {
return result;
});
} else {
jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl));
}
@@ -142,6 +147,30 @@ async function getClientToken(client, method, path, payload) {
}
}
/***
* Generic function for retrying an async function
* @param {number} retries
* @param {number} delay
* @param {Function} func
* @param {any[]} args
*/
async function retryAsyncFunction(retries, delay, func, ...args) {
let attempt = 0;
while (attempt < retries) {
try {
const result = await func(...args);
return result;
} catch (error) {
attempt++;
if (attempt < retries) {
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
}
/***
* @typedef {Object} VaultLoginResponse
* @property {{
+19
View File
@@ -85,4 +85,23 @@ describe("test retrival for token", () => {
const url = got.post.mock.calls[0][0]
expect(url).toContain('differentK8sPath')
})
it("test retrieval with jwt", async () => {
const method = "jwt"
const jwtToken = "someTestToken"
const testRole = "testRole"
const privateKeyRaw = ""
mockApiResponse()
mockInput("role", testRole)
mockInput("jwtPrivateKey", privateKeyRaw)
core.getIDToken = jest.fn()
core.getIDToken.mockReturnValueOnce(jwtToken)
const token = await retrieveToken(method, got)
expect(token).toEqual(testToken)
const payload = got.post.mock.calls[0][1].json
expect(payload).toEqual({ jwt: jwtToken, role: testRole })
const url = got.post.mock.calls[0][0]
expect(url).toContain('jwt')
})
})
+5 -3
View File
@@ -1,5 +1,7 @@
const WILDCARD = '*';
const WILDCARD_UPPERCASE = '*';
const WILDCARD = '**';
module.exports = {
WILDCARD
};
WILDCARD,
WILDCARD_UPPERCASE,
};
+5 -3
View File
@@ -1,5 +1,5 @@
const jsonata = require("jsonata");
const { WILDCARD } = require("./constants");
const { WILDCARD, WILDCARD_UPPERCASE} = require("./constants");
const { normalizeOutputKey } = require("./utils");
const core = require('@actions/core');
@@ -26,6 +26,7 @@ const core = require('@actions/core');
async function getSecrets(secretRequests, client, ignoreNotFound) {
const responseCache = new Map();
let results = [];
let upperCaseEnv = false;
for (const secretRequest of secretRequests) {
let { path, selector } = secretRequest;
@@ -59,7 +60,8 @@ async function getSecrets(secretRequests, client, ignoreNotFound) {
body = JSON.parse(body);
if (selector == WILDCARD) {
if (selector === WILDCARD || selector === WILDCARD_UPPERCASE) {
upperCaseEnv = selector === WILDCARD_UPPERCASE;
let keys = body.data;
if (body.data["data"] != undefined) {
keys = keys.data;
@@ -78,7 +80,7 @@ async function getSecrets(secretRequests, client, ignoreNotFound) {
}
newRequest.outputVarName = normalizeOutputKey(newRequest.outputVarName);
newRequest.envVarName = normalizeOutputKey(newRequest.envVarName,true);
newRequest.envVarName = normalizeOutputKey(newRequest.envVarName, upperCaseEnv);
// JSONata field references containing reserved tokens should
// be enclosed in backticks
+2 -2
View File
@@ -3,12 +3,12 @@
* @param {string} dataKey
* @param {boolean=} isEnvVar
*/
function normalizeOutputKey(dataKey, isEnvVar = false) {
function normalizeOutputKey(dataKey, upperCase = false) {
let outputKey = dataKey
.replace(".", "__")
.replace(new RegExp("-", "g"), "")
.replace(/[^\p{L}\p{N}_-]/gu, "");
if (isEnvVar) {
if (upperCase) {
outputKey = outputKey.toUpperCase();
}
return outputKey;