mirror of
https://github.com/hashicorp/vault-action.git
synced 2026-04-16 08:45:44 +00:00
Merge remote-tracking branch 'upstream/main' into feat/wildcard-all-secrets
# Conflicts: # package-lock.json # src/action.js
This commit is contained in:
commit
0add1c8a81
19 changed files with 16503 additions and 11290 deletions
|
|
@ -6,6 +6,7 @@ const jsonata = require('jsonata');
|
|||
const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index');
|
||||
|
||||
const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes'];
|
||||
const ENCODING_TYPES = ['base64', 'hex', 'utf8'];
|
||||
|
||||
function addMask(value) {
|
||||
for (const line of value.replace(/\r/g, '').split('\n')) {
|
||||
|
|
@ -22,9 +23,11 @@ async function exportSecrets() {
|
|||
const exportEnv = core.getInput('exportEnv', { required: false }) != 'false';
|
||||
const exportToken = (core.getInput('exportToken', { required: false }) || 'false').toLowerCase() != 'false';
|
||||
|
||||
const secretsInput = core.getInput('secrets', { required: true });
|
||||
const secretsInput = core.getInput('secrets', { required: false });
|
||||
const secretRequests = parseSecretsInput(secretsInput);
|
||||
|
||||
const secretEncodingType = core.getInput('secretEncodingType', { required: false });
|
||||
|
||||
const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
|
||||
const authPayload = core.getInput('authPayload', { required: false });
|
||||
if (!AUTH_METHODS.includes(vaultMethod) && !authPayload) {
|
||||
|
|
@ -34,7 +37,15 @@ async function exportSecrets() {
|
|||
const defaultOptions = {
|
||||
prefixUrl: vaultUrl,
|
||||
headers: {},
|
||||
https: {}
|
||||
https: {},
|
||||
retry: {
|
||||
statusCodes: [
|
||||
...got.defaults.options.retry.statusCodes,
|
||||
// Vault returns 412 when the token in use hasn't yet been replicated
|
||||
// to the performance replica queried. See issue #332.
|
||||
412,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const tlsSkipVerify = (core.getInput('tlsSkipVerify', { required: false }) || 'false').toLowerCase() != 'false';
|
||||
|
|
@ -81,12 +92,28 @@ async function exportSecrets() {
|
|||
|
||||
const results = await getSecrets(requests, client);
|
||||
|
||||
|
||||
for (const result of results) {
|
||||
const { value, request, cachedResponse } = result;
|
||||
// Output the result
|
||||
|
||||
var value = result.value;
|
||||
const request = result.request;
|
||||
const cachedResponse = result.cachedResponse;
|
||||
|
||||
if (cachedResponse) {
|
||||
core.debug('ℹ using cached response');
|
||||
}
|
||||
|
||||
// if a secret is encoded, decode it
|
||||
if (ENCODING_TYPES.includes(secretEncodingType)) {
|
||||
value = Buffer.from(value, secretEncodingType).toString();
|
||||
}
|
||||
|
||||
for (const line of value.replace(/\r/g, '').split('\n')) {
|
||||
if (line.length > 0) {
|
||||
command.issue('add-mask', line);
|
||||
}
|
||||
}
|
||||
|
||||
if (exportEnv && typeof value === "object") {
|
||||
Object.entries(value).forEach(([envKey, envValue]) => {
|
||||
|
|
@ -102,7 +129,7 @@ async function exportSecrets() {
|
|||
}
|
||||
};
|
||||
|
||||
/** @typedef {Object} SecretRequest
|
||||
/** @typedef {Object} SecretRequest
|
||||
* @property {string} path
|
||||
* @property {string} envVarName
|
||||
* @property {string} outputVarName
|
||||
|
|
@ -114,6 +141,10 @@ async function exportSecrets() {
|
|||
* @param {string} secretsInput
|
||||
*/
|
||||
function parseSecretsInput(secretsInput) {
|
||||
if (!secretsInput) {
|
||||
return []
|
||||
}
|
||||
|
||||
const secrets = secretsInput
|
||||
.split(';')
|
||||
.filter(key => !!key)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ const got = require('got');
|
|||
const {
|
||||
exportSecrets,
|
||||
parseSecretsInput,
|
||||
parseResponse,
|
||||
parseHeadersInput
|
||||
} = require('./action');
|
||||
|
||||
|
|
@ -104,7 +103,7 @@ describe('parseSecretsInput', () => {
|
|||
describe('parseHeaders', () => {
|
||||
it('parses simple header', () => {
|
||||
when(core.getInput)
|
||||
.calledWith('extraHeaders')
|
||||
.calledWith('extraHeaders', undefined)
|
||||
.mockReturnValueOnce('TEST: 1');
|
||||
const result = parseHeadersInput('extraHeaders');
|
||||
expect(Array.from(result)).toContainEqual(['test', '1']);
|
||||
|
|
@ -112,7 +111,7 @@ describe('parseHeaders', () => {
|
|||
|
||||
it('parses simple header with whitespace', () => {
|
||||
when(core.getInput)
|
||||
.calledWith('extraHeaders')
|
||||
.calledWith('extraHeaders', undefined)
|
||||
.mockReturnValueOnce(`
|
||||
TEST: 1
|
||||
`);
|
||||
|
|
@ -122,7 +121,7 @@ describe('parseHeaders', () => {
|
|||
|
||||
it('parses multiple headers', () => {
|
||||
when(core.getInput)
|
||||
.calledWith('extraHeaders')
|
||||
.calledWith('extraHeaders', undefined)
|
||||
.mockReturnValueOnce(`
|
||||
TEST: 1
|
||||
FOO: bAr
|
||||
|
|
@ -134,7 +133,7 @@ describe('parseHeaders', () => {
|
|||
|
||||
it('parses null response', () => {
|
||||
when(core.getInput)
|
||||
.calledWith('extraHeaders')
|
||||
.calledWith('extraHeaders', undefined)
|
||||
.mockReturnValueOnce(null);
|
||||
const result = parseHeadersInput('extraHeaders');
|
||||
expect(Array.from(result)).toHaveLength(0);
|
||||
|
|
@ -146,29 +145,29 @@ describe('exportSecrets', () => {
|
|||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('url')
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce('http://vault:8200');
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('token')
|
||||
.calledWith('token', expect.anything())
|
||||
.mockReturnValueOnce('EXAMPLE');
|
||||
});
|
||||
|
||||
function mockInput(key) {
|
||||
when(core.getInput)
|
||||
.calledWith('secrets')
|
||||
.calledWith('secrets', expect.anything())
|
||||
.mockReturnValueOnce(key);
|
||||
}
|
||||
|
||||
function mockVersion(version) {
|
||||
when(core.getInput)
|
||||
.calledWith('kv-version')
|
||||
.calledWith('kv-version', expect.anything())
|
||||
.mockReturnValueOnce(version);
|
||||
}
|
||||
|
||||
function mockExtraHeaders(headerString) {
|
||||
when(core.getInput)
|
||||
.calledWith('extraHeaders')
|
||||
.calledWith('extraHeaders', expect.anything())
|
||||
.mockReturnValueOnce(headerString);
|
||||
}
|
||||
|
||||
|
|
@ -191,10 +190,16 @@ describe('exportSecrets', () => {
|
|||
|
||||
function mockExportToken(doExport) {
|
||||
when(core.getInput)
|
||||
.calledWith('exportToken')
|
||||
.calledWith('exportToken', expect.anything())
|
||||
.mockReturnValueOnce(doExport);
|
||||
}
|
||||
|
||||
function mockEncodeType(doEncode) {
|
||||
when(core.getInput)
|
||||
.calledWith('secretEncodingType', expect.anything())
|
||||
.mockReturnValueOnce(doEncode);
|
||||
}
|
||||
|
||||
it('simple secret retrieval', async () => {
|
||||
mockInput('test key');
|
||||
mockVaultData({
|
||||
|
|
@ -207,6 +212,19 @@ describe('exportSecrets', () => {
|
|||
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||
});
|
||||
|
||||
it('encoded secret retrieval', async () => {
|
||||
mockInput('test key');
|
||||
mockVaultData({
|
||||
key: 'MQ=='
|
||||
});
|
||||
mockEncodeType('base64');
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||
});
|
||||
|
||||
it('intl secret retrieval', async () => {
|
||||
mockInput('测试 测试');
|
||||
mockVaultData({
|
||||
|
|
@ -341,4 +359,13 @@ with blank lines
|
|||
expect(command.issue).toBeCalledWith('add-mask', 'with blank lines');
|
||||
expect(core.setOutput).toBeCalledWith('key', multiLineString);
|
||||
})
|
||||
|
||||
it('export only Vault token, no secrets', async () => {
|
||||
mockExportToken("true")
|
||||
|
||||
await exportSecrets();
|
||||
|
||||
expect(core.exportVariable).toBeCalledTimes(1);
|
||||
expect(core.exportVariable).toBeCalledWith('VAULT_TOKEN', 'EXAMPLE');
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ async function retrieveToken(method, client) {
|
|||
case 'jwt': {
|
||||
/** @type {string} */
|
||||
let jwt;
|
||||
const role = core.getInput('role', { required: true });
|
||||
const role = core.getInput('role', { required: false });
|
||||
const privateKeyRaw = core.getInput('jwtPrivateKey', { required: false });
|
||||
const privateKey = Buffer.from(privateKeyRaw, 'base64').toString();
|
||||
const keyPassword = core.getInput('jwtKeyPassword', { required: false });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
jest.mock('got');
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('@actions/core/lib/command');
|
||||
jest.mock("fs")
|
||||
jest.mock('fs', () => ({
|
||||
stat: jest.fn().mockResolvedValue(null),
|
||||
promises: {
|
||||
access: jest.fn().mockResolvedValue(null),
|
||||
}
|
||||
}));
|
||||
|
||||
const core = require('@actions/core');
|
||||
const got = require('got');
|
||||
|
|
@ -16,7 +21,7 @@ const {
|
|||
|
||||
function mockInput(name, key) {
|
||||
when(core.getInput)
|
||||
.calledWith(name)
|
||||
.calledWith(name, expect.anything())
|
||||
.mockReturnValueOnce(key);
|
||||
}
|
||||
|
||||
|
|
|
|||
69
src/retries.test.js
Normal file
69
src/retries.test.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
jest.mock('@actions/core');
|
||||
|
||||
const core = require('@actions/core');
|
||||
const ServerMock = require("mock-http-server");
|
||||
const { exportSecrets } = require("./action");
|
||||
const { when } = require('jest-when');
|
||||
|
||||
describe('exportSecrets retries', () => {
|
||||
var server = new ServerMock({ host: "127.0.0.1", port: 0 });
|
||||
var calls = 0;
|
||||
|
||||
beforeEach((done) => {
|
||||
calls = 0;
|
||||
jest.resetAllMocks();
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('token', expect.anything())
|
||||
.mockReturnValueOnce('EXAMPLE');
|
||||
|
||||
when(core.getInput)
|
||||
.calledWith('secrets', expect.anything())
|
||||
.mockReturnValueOnce("kv/mysecret key");
|
||||
|
||||
server.start(() => {
|
||||
expect(server.getHttpPort()).not.toBeNull();
|
||||
when(core.getInput)
|
||||
.calledWith('url', expect.anything())
|
||||
.mockReturnValueOnce('http://127.0.0.1:' + server.getHttpPort());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
server.stop(done);
|
||||
});
|
||||
|
||||
function mockStatusCodes(statusCodes) {
|
||||
server.on({
|
||||
path: '/v1/kv/mysecret',
|
||||
reply: {
|
||||
status: function() {
|
||||
let status = statusCodes[calls];
|
||||
calls += 1;
|
||||
return status;
|
||||
},
|
||||
headers: { "content-type": "application/json" },
|
||||
body: function() {
|
||||
return JSON.stringify({ data: {"key": "value"} })
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it('retries on 412 status code', (done) => {
|
||||
mockStatusCodes([412, 200])
|
||||
exportSecrets().then(() => {
|
||||
expect(calls).toEqual(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('retries on 500 status code', (done) => {
|
||||
mockStatusCodes([500, 200])
|
||||
exportSecrets().then(() => {
|
||||
expect(calls).toEqual(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -34,9 +34,17 @@ async function getSecrets(secretRequests, client) {
|
|||
body = responseCache.get(requestPath);
|
||||
cachedResponse = true;
|
||||
} else {
|
||||
const result = await client.get(requestPath);
|
||||
body = result.body;
|
||||
responseCache.set(requestPath, body);
|
||||
try {
|
||||
const result = await client.get(requestPath);
|
||||
body = result.body;
|
||||
responseCache.set(requestPath, body);
|
||||
} catch (error) {
|
||||
const {response} = error;
|
||||
if (response.statusCode === 404) {
|
||||
throw Error(`Unable to retrieve result for "${path}" because it was not found: ${response.body.trim()}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
let value;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue