mirror of
https://github.com/hashicorp/vault-action.git
synced 2025-11-08 15:46:56 +00:00
feat: add the ability to set extra headers (#27)
* feat: add the ability to set extra headers * switch to more generic map solution for headers
This commit is contained in:
parent
0ece1da433
commit
bef2eb0b90
4 changed files with 148 additions and 6 deletions
36
action.js
36
action.js
|
|
@ -12,6 +12,7 @@ const VALID_KV_VERSION = [-1, 1, 2];
|
||||||
async function exportSecrets() {
|
async function exportSecrets() {
|
||||||
const vaultUrl = core.getInput('url', { required: true });
|
const vaultUrl = core.getInput('url', { required: true });
|
||||||
const vaultNamespace = core.getInput('namespace', { required: false });
|
const vaultNamespace = core.getInput('namespace', { required: false });
|
||||||
|
const extraHeaders = parseHeadersInput('extraHeaders', { required: false });
|
||||||
|
|
||||||
let enginePath = core.getInput('path', { required: false });
|
let enginePath = core.getInput('path', { required: false });
|
||||||
let kvVersion = core.getInput('kv-version', { required: false });
|
let kvVersion = core.getInput('kv-version', { required: false });
|
||||||
|
|
@ -76,6 +77,10 @@ async function exportSecrets() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const [headerName, headerValue] of extraHeaders) {
|
||||||
|
requestOptions.headers[headerName] = headerValue;
|
||||||
|
}
|
||||||
|
|
||||||
if (vaultNamespace != null) {
|
if (vaultNamespace != null) {
|
||||||
requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
|
requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
|
||||||
}
|
}
|
||||||
|
|
@ -216,6 +221,9 @@ function normalizeOutputKey(dataKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
*/
|
||||||
function parseBoolInput(input) {
|
function parseBoolInput(input) {
|
||||||
if (input === null || input === undefined || input.trim() === '') {
|
if (input === null || input === undefined || input.trim() === '') {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -223,9 +231,35 @@ function parseBoolInput(input) {
|
||||||
return Boolean(input);
|
return Boolean(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} inputKey
|
||||||
|
* @param {any} inputOptions
|
||||||
|
*/
|
||||||
|
function parseHeadersInput(inputKey, inputOptions) {
|
||||||
|
/** @type {string}*/
|
||||||
|
const rawHeadersString = core.getInput(inputKey, inputOptions) || '';
|
||||||
|
const headerStrings = rawHeadersString
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line !== '');
|
||||||
|
return headerStrings
|
||||||
|
.reduce((map, line) => {
|
||||||
|
const seperator = line.indexOf(':');
|
||||||
|
const key = line.substring(0, seperator).trim().toLowerCase();
|
||||||
|
const value = line.substring(seperator + 1).trim();
|
||||||
|
if (map.has(key)) {
|
||||||
|
map.set(key, [map.get(key), value].join(', '));
|
||||||
|
} else {
|
||||||
|
map.set(key, value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
exportSecrets,
|
exportSecrets,
|
||||||
parseSecretsInput,
|
parseSecretsInput,
|
||||||
parseResponse: getResponseData,
|
parseResponse: getResponseData,
|
||||||
normalizeOutputKey
|
normalizeOutputKey,
|
||||||
|
parseHeadersInput
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ const got = require('got');
|
||||||
const {
|
const {
|
||||||
exportSecrets,
|
exportSecrets,
|
||||||
parseSecretsInput,
|
parseSecretsInput,
|
||||||
parseResponse
|
parseResponse,
|
||||||
|
parseHeadersInput
|
||||||
} = require('./action');
|
} = require('./action');
|
||||||
|
|
||||||
const { when } = require('jest-when');
|
const { when } = require('jest-when');
|
||||||
|
|
@ -90,6 +91,46 @@ describe('parseSecretsInput', () => {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseHeaders', () => {
|
||||||
|
it('parses simple header', () => {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('extraHeaders')
|
||||||
|
.mockReturnValueOnce('TEST: 1');
|
||||||
|
const result = parseHeadersInput('extraHeaders');
|
||||||
|
expect(Array.from(result)).toContainEqual(['test', '1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses simple header with whitespace', () => {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('extraHeaders')
|
||||||
|
.mockReturnValueOnce(`
|
||||||
|
TEST: 1
|
||||||
|
`);
|
||||||
|
const result = parseHeadersInput('extraHeaders');
|
||||||
|
expect(Array.from(result)).toContainEqual(['test', '1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses multiple headers', () => {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('extraHeaders')
|
||||||
|
.mockReturnValueOnce(`
|
||||||
|
TEST: 1
|
||||||
|
FOO: bAr
|
||||||
|
`);
|
||||||
|
const result = parseHeadersInput('extraHeaders');
|
||||||
|
expect(Array.from(result)).toContainEqual(['test', '1']);
|
||||||
|
expect(Array.from(result)).toContainEqual(['foo', 'bAr']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses null response', () => {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('extraHeaders')
|
||||||
|
.mockReturnValueOnce(null);
|
||||||
|
const result = parseHeadersInput('extraHeaders');
|
||||||
|
expect(Array.from(result)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
describe('parseResponse', () => {
|
describe('parseResponse', () => {
|
||||||
// https://www.vaultproject.io/api/secret/kv/kv-v1.html#sample-response
|
// https://www.vaultproject.io/api/secret/kv/kv-v1.html#sample-response
|
||||||
it('parses K/V version 1 response', () => {
|
it('parses K/V version 1 response', () => {
|
||||||
|
|
@ -148,6 +189,12 @@ describe('exportSecrets', () => {
|
||||||
.mockReturnValueOnce(version);
|
.mockReturnValueOnce(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockExtraHeaders(headerString) {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('extraHeaders')
|
||||||
|
.mockReturnValueOnce(headerString);
|
||||||
|
}
|
||||||
|
|
||||||
function mockVaultData(data, version='2') {
|
function mockVaultData(data, version='2') {
|
||||||
switch(version) {
|
switch(version) {
|
||||||
case '1':
|
case '1':
|
||||||
|
|
@ -196,6 +243,23 @@ describe('exportSecrets', () => {
|
||||||
it('simple secret retrieval from K/V v1', async () => {
|
it('simple secret retrieval from K/V v1', async () => {
|
||||||
const version = '1';
|
const version = '1';
|
||||||
|
|
||||||
|
mockInput('test key');
|
||||||
|
mockExtraHeaders(`
|
||||||
|
TEST: 1
|
||||||
|
`);
|
||||||
|
mockVaultData({
|
||||||
|
key: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
|
||||||
|
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||||
|
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('simple secret retrieval with extra headers', async () => {
|
||||||
|
const version = '1';
|
||||||
|
|
||||||
mockInput('test key');
|
mockInput('test key');
|
||||||
mockVersion(version);
|
mockVersion(version);
|
||||||
mockVaultData({
|
mockVaultData({
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ inputs:
|
||||||
namespace:
|
namespace:
|
||||||
description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default'
|
description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default'
|
||||||
required: false
|
required: false
|
||||||
|
extraHeaders:
|
||||||
|
description: 'A string of newline seperated extra headers to include on every request.'
|
||||||
|
required: false
|
||||||
runs:
|
runs:
|
||||||
using: 'node12'
|
using: 'node12'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
|
|
|
||||||
49
dist/index.js
vendored
49
dist/index.js
vendored
|
|
@ -648,6 +648,7 @@ const defaults = {
|
||||||
maxRedirects: 10,
|
maxRedirects: 10,
|
||||||
prefixUrl: '',
|
prefixUrl: '',
|
||||||
methodRewriting: true,
|
methodRewriting: true,
|
||||||
|
allowGetBody: false,
|
||||||
ignoreInvalidCookies: false,
|
ignoreInvalidCookies: false,
|
||||||
context: {},
|
context: {},
|
||||||
_pagination: {
|
_pagination: {
|
||||||
|
|
@ -1019,6 +1020,7 @@ exports.preNormalizeArguments = (options, defaults) => {
|
||||||
options.dnsCache = (_e = options.dnsCache, (_e !== null && _e !== void 0 ? _e : false));
|
options.dnsCache = (_e = options.dnsCache, (_e !== null && _e !== void 0 ? _e : false));
|
||||||
options.useElectronNet = Boolean(options.useElectronNet);
|
options.useElectronNet = Boolean(options.useElectronNet);
|
||||||
options.methodRewriting = Boolean(options.methodRewriting);
|
options.methodRewriting = Boolean(options.methodRewriting);
|
||||||
|
options.allowGetBody = Boolean(options.allowGetBody);
|
||||||
options.context = (_f = options.context, (_f !== null && _f !== void 0 ? _f : {}));
|
options.context = (_f = options.context, (_f !== null && _f !== void 0 ? _f : {}));
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
@ -1131,7 +1133,8 @@ exports.normalizeArguments = (url, options, defaults) => {
|
||||||
}
|
}
|
||||||
return normalizedOptions;
|
return normalizedOptions;
|
||||||
};
|
};
|
||||||
const withoutBody = new Set(['GET', 'HEAD']);
|
const withoutBody = new Set(['HEAD']);
|
||||||
|
const withoutBodyUnlessSpecified = 'GET';
|
||||||
exports.normalizeRequestArguments = async (options) => {
|
exports.normalizeRequestArguments = async (options) => {
|
||||||
var _a, _b, _c;
|
var _a, _b, _c;
|
||||||
options = exports.mergeOptions(options);
|
options = exports.mergeOptions(options);
|
||||||
|
|
@ -1146,6 +1149,9 @@ exports.normalizeRequestArguments = async (options) => {
|
||||||
if ((isBody || isForm || isJson) && withoutBody.has(options.method)) {
|
if ((isBody || isForm || isJson) && withoutBody.has(options.method)) {
|
||||||
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
|
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
|
||||||
}
|
}
|
||||||
|
if (!options.allowGetBody && (isBody || isForm || isJson) && withoutBodyUnlessSpecified === options.method) {
|
||||||
|
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
|
||||||
|
}
|
||||||
if ([isBody, isForm, isJson].filter(isTrue => isTrue).length > 1) {
|
if ([isBody, isForm, isJson].filter(isTrue => isTrue).length > 1) {
|
||||||
throw new TypeError('The `body`, `json` and `form` options are mutually exclusive');
|
throw new TypeError('The `body`, `json` and `form` options are mutually exclusive');
|
||||||
}
|
}
|
||||||
|
|
@ -1192,7 +1198,7 @@ exports.normalizeRequestArguments = async (options) => {
|
||||||
// a payload body and the method semantics do not anticipate such a
|
// a payload body and the method semantics do not anticipate such a
|
||||||
// body.
|
// body.
|
||||||
if (is_1.default.undefined(headers['content-length']) && is_1.default.undefined(headers['transfer-encoding'])) {
|
if (is_1.default.undefined(headers['content-length']) && is_1.default.undefined(headers['transfer-encoding'])) {
|
||||||
if ((options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || options.method === 'DELETE') &&
|
if ((options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || options.method === 'DELETE' || (options.allowGetBody && options.method === 'GET')) &&
|
||||||
!is_1.default.undefined(uploadBodySize)) {
|
!is_1.default.undefined(uploadBodySize)) {
|
||||||
// @ts-ignore We assign if it is undefined, so this IS correct
|
// @ts-ignore We assign if it is undefined, so this IS correct
|
||||||
headers['content-length'] = String(uploadBodySize);
|
headers['content-length'] = String(uploadBodySize);
|
||||||
|
|
@ -3146,7 +3152,7 @@ function asStream(options) {
|
||||||
throw new Error('Got\'s stream is not writable when the `body`, `json` or `form` option is used');
|
throw new Error('Got\'s stream is not writable when the `body`, `json` or `form` option is used');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') {
|
else if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || (options.allowGetBody && options.method === 'GET')) {
|
||||||
options.body = input;
|
options.body = input;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -3551,6 +3557,13 @@ exports.setFailed = setFailed;
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
// Logging Commands
|
// Logging Commands
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
|
/**
|
||||||
|
* Gets whether Actions Step Debug is on or not
|
||||||
|
*/
|
||||||
|
function isDebug() {
|
||||||
|
return process.env['RUNNER_DEBUG'] === '1';
|
||||||
|
}
|
||||||
|
exports.isDebug = isDebug;
|
||||||
/**
|
/**
|
||||||
* Writes debug message to user log
|
* Writes debug message to user log
|
||||||
* @param message debug message
|
* @param message debug message
|
||||||
|
|
@ -4853,6 +4866,7 @@ const VALID_KV_VERSION = [-1, 1, 2];
|
||||||
async function exportSecrets() {
|
async function exportSecrets() {
|
||||||
const vaultUrl = core.getInput('url', { required: true });
|
const vaultUrl = core.getInput('url', { required: true });
|
||||||
const vaultNamespace = core.getInput('namespace', { required: false });
|
const vaultNamespace = core.getInput('namespace', { required: false });
|
||||||
|
const extraHeaders = parseHeadersInput('extraHeaders', { required: false });
|
||||||
|
|
||||||
let enginePath = core.getInput('path', { required: false });
|
let enginePath = core.getInput('path', { required: false });
|
||||||
let kvVersion = core.getInput('kv-version', { required: false });
|
let kvVersion = core.getInput('kv-version', { required: false });
|
||||||
|
|
@ -4917,6 +4931,10 @@ async function exportSecrets() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const [headerName, headerValue] of extraHeaders) {
|
||||||
|
requestOptions.headers[headerName] = headerValue;
|
||||||
|
}
|
||||||
|
|
||||||
if (vaultNamespace != null) {
|
if (vaultNamespace != null) {
|
||||||
requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
|
requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
|
||||||
}
|
}
|
||||||
|
|
@ -5057,6 +5075,9 @@ function normalizeOutputKey(dataKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
*/
|
||||||
function parseBoolInput(input) {
|
function parseBoolInput(input) {
|
||||||
if (input === null || input === undefined || input.trim() === '') {
|
if (input === null || input === undefined || input.trim() === '') {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -5064,11 +5085,31 @@ function parseBoolInput(input) {
|
||||||
return Boolean(input);
|
return Boolean(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} inputKey
|
||||||
|
* @param {any} inputOptions
|
||||||
|
*/
|
||||||
|
function parseHeadersInput(inputKey, inputOptions) {
|
||||||
|
/** @type {string}*/
|
||||||
|
const rawHeadersString = core.getInput(inputKey, inputOptions) || '';
|
||||||
|
const headerStrings = rawHeadersString
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line !== '');
|
||||||
|
const pairs = headerStrings
|
||||||
|
.map(line => {
|
||||||
|
const seperator = line.indexOf(':');
|
||||||
|
return [line.substring(0, seperator), line.substring(seperator + 1)];
|
||||||
|
});
|
||||||
|
return new Headers(pairs);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
exportSecrets,
|
exportSecrets,
|
||||||
parseSecretsInput,
|
parseSecretsInput,
|
||||||
parseResponse: getResponseData,
|
parseResponse: getResponseData,
|
||||||
normalizeOutputKey
|
normalizeOutputKey,
|
||||||
|
parseHeadersInput
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue