diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f7665d4..163fee94 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,7 +110,7 @@ jobs: env: VAULT_HOST: localhost VAULT_PORT: ${{ job.services.vault.ports[8200] }} - - name: use vault action + - name: use vault action (default K/V version 2) uses: ./ with: url: http://localhost:${{ job.services.vault.ports[8200] }} @@ -119,6 +119,17 @@ jobs: test secret ; test secret | NAMED_SECRET ; nested/test otherSecret ; + - name: use vault action (custom K/V version 1) + uses: ./ + with: + url: http://localhost:${{ job.services.vault.ports[8200] }} + token: testtoken + path: my-secret + kv-version: 1 + secrets: | + test altSecret ; + test altSecret | NAMED_ALTSECRET ; + nested/test otherAltSecret ; - name: verify run: npm run test:e2e diff --git a/README.md b/README.md index 7e4df7a5..a0ff1ccb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # vault-action -A helper action for easily pulling secrets from the default v2 K/V backend of vault. +A helper action for easily pulling secrets from the K/V backend of vault. + +Expects [Version 2](https://www.vaultproject.io/docs/secrets/kv/kv-v2/) of the KV Secrets Engine by default. ## Example Usage @@ -39,7 +41,7 @@ with: url: https://vault.mycompany.com:8200 method: approle roleId: ${{ secrets.roleId }} - secretId : ${{ secrets.secretId }} + secretId: ${{ secrets.secretId }} ``` ## Key Syntax @@ -93,9 +95,33 @@ with: ci/aws secretKey | AWS_SECRET_ACCESS_KEY ``` +### Using K/V version 1 + +By default, `vault-action` expects a K/V engine using [version 2](https://www.vaultproject.io/docs/secrets/kv/kv-v2.html). + +In order to work with a [v1 engine](https://www.vaultproject.io/docs/secrets/kv/kv-v1/), the `kv-version` parameter may be passed: + +```yaml +with: + kv-version: 1 +``` + +### Custom Engine Path + +When you enable the K/V Engine, by default it's placed at the path `secret`, so a secret named `ci` will be accessed from `secret/ci`. However, [if you enabled the secrets engine using a custom `path`](https://www.vaultproject.io/docs/commands/secrets/enable/#inlinecode--path-4), you +can pass it as follows: + +```yaml +with: + path: my-secrets + secrets: ci npmToken +``` + +This way, the `ci` secret in the example above will be retrieved from `my-secrets/ci`. + ### Namespace -This action could be use with namespace Vault Enterprise feature. You can specify namespace in request : +This action could be use with namespace Vault Enterprise feature. You can specify namespace in request : ```yaml steps: diff --git a/action.js b/action.js index 68104ced..226c18d2 100644 --- a/action.js +++ b/action.js @@ -7,6 +7,9 @@ async function exportSecrets() { const vaultUrl = core.getInput('url', { required: true }); const vaultNamespace = core.getInput('namespace', { required: false }); + let enginePath = core.getInput('path', { required: false }); + let kvVersion = core.getInput('kv-version', { required: false }); + const secretsInput = core.getInput('secrets', { required: true }); const secrets = parseSecretsInput(secretsInput); @@ -21,10 +24,10 @@ async function exportSecrets() { const vaultRoleId = core.getInput('roleId', { required: true }); const vaultSecretId = core.getInput('secretId', { required: true }); core.debug('Try to retrieve Vault Token from approle'); - var options = { - headers: {}, - json: { role_id: vaultRoleId, secret_id: vaultSecretId }, - responseType: 'json' + var options = { + headers: {}, + json: { role_id: vaultRoleId, secret_id: vaultSecretId }, + responseType: 'json' }; if (vaultNamespace != null) { @@ -44,6 +47,20 @@ async function exportSecrets() { break; } + if (!enginePath) { + enginePath = 'secret'; + } + + if (!kvVersion) { + kvVersion = '2'; + } + + if (kvVersion !== '1' && kvVersion !== '2') { + throw Error(`You must provide a valid K/V version (1 or 2). Input: "${kvVersion}"`); + } + + kvVersion = parseInt(kvVersion); + for (const secret of secrets) { const { secretPath, outputName, secretKey } = secret; const requestOptions = { @@ -56,12 +73,13 @@ async function exportSecrets() { requestOptions.headers["X-Vault-Namespace"] = vaultNamespace; } - const result = await got(`${vaultUrl}/v1/secret/data/${secretPath}`, requestOptions); + const requestPath = (kvVersion === 1) + ? `${vaultUrl}/v1/${enginePath}/${secretPath}` + : `${vaultUrl}/v1/${enginePath}/data/${secretPath}`; + const result = await got(requestPath, requestOptions); - const parsedResponse = JSON.parse(result.body); - const vaultKeyData = parsedResponse.data; - const versionData = vaultKeyData.data; - const value = versionData[secretKey]; + const secretData = parseResponse(result.body, kvVersion); + const value = secretData[secretKey]; command.issue('add-mask', value); core.exportVariable(outputName, `${value}`); core.debug(`✔ ${secretPath} => ${outputName}`); @@ -120,6 +138,29 @@ function parseSecretsInput(secretsInput) { return output; } +/** + * Parses a JSON response and returns the secret data + * @param {string} responseBody + * @param {number} kvVersion + */ +function parseResponse(responseBody, kvVersion) { + const parsedResponse = JSON.parse(responseBody); + let secretData; + + switch(kvVersion) { + case 1: { + secretData = parsedResponse.data; + } break; + + case 2: { + const vaultKeyData = parsedResponse.data; + secretData = vaultKeyData.data; + } break; + } + + return secretData; +} + /** * Replaces any forward-slash characters to * @param {string} dataKey @@ -131,5 +172,6 @@ function normalizeOutputKey(dataKey) { module.exports = { exportSecrets, parseSecretsInput, + parseResponse, normalizeOutputKey }; diff --git a/action.test.js b/action.test.js index 840722e7..9c6a30b2 100644 --- a/action.test.js +++ b/action.test.js @@ -7,6 +7,7 @@ const got = require('got'); const { exportSecrets, parseSecretsInput, + parseResponse } = require('./action'); const { when } = require('jest-when'); @@ -82,6 +83,38 @@ describe('parseSecretsInput', () => { }) }); +describe('parseResponse', () => { + // https://www.vaultproject.io/api/secret/kv/kv-v1.html#sample-response + it('parses K/V version 1 response', () => { + const response = JSON.stringify({ + data: { + foo: 'bar' + } + }) + const output = parseResponse(response, 1); + + expect(output).toEqual({ + foo: 'bar' + }); + }); + + // https://www.vaultproject.io/api/secret/kv/kv-v2.html#read-secret-version + it('parses K/V version 2 response', () => { + const response = JSON.stringify({ + data: { + data: { + foo: 'bar' + } + } + }) + const output = parseResponse(response, 2); + + expect(output).toEqual({ + foo: 'bar' + }); + }); +}); + describe('exportSecrets', () => { beforeEach(() => { @@ -89,27 +122,44 @@ describe('exportSecrets', () => { when(core.getInput) .calledWith('url') - .mockReturnValue('http://vault:8200'); + .mockReturnValueOnce('http://vault:8200'); when(core.getInput) .calledWith('token') - .mockReturnValue('EXAMPLE'); + .mockReturnValueOnce('EXAMPLE'); }); function mockInput(key) { when(core.getInput) .calledWith('secrets') - .mockReturnValue(key); + .mockReturnValueOnce(key); } - function mockVaultData(data) { - got.mockResolvedValue({ - body: JSON.stringify({ - data: { - data - } - }) - }); + function mockVersion(version) { + when(core.getInput) + .calledWith('kv-version') + .mockReturnValueOnce(version); + } + + function mockVaultData(data, version='2') { + switch(version) { + case '1': + got.mockResolvedValue({ + body: JSON.stringify({ + data + }) + }); + break; + case '2': + got.mockResolvedValue({ + body: JSON.stringify({ + data: { + data + } + }) + }); + break; + } } it('simple secret retrieval', async () => { @@ -133,4 +183,18 @@ describe('exportSecrets', () => { expect(core.exportVariable).toBeCalledWith('TEST_NAME', '1'); }); -}); \ No newline at end of file + + it('simple secret retrieval from K/V v1', async () => { + const version = '1'; + + mockInput('test key'); + mockVersion(version); + mockVaultData({ + key: 1 + }, version); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('KEY', '1'); + }); +}); diff --git a/dist/index.js b/dist/index.js index 74c01d65..d822679b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4070,6 +4070,9 @@ async function exportSecrets() { const vaultUrl = core.getInput('url', { required: true }); const vaultNamespace = core.getInput('namespace', { required: false }); + let enginePath = core.getInput('path', { required: false }); + let kvVersion = core.getInput('kv-version', { required: false }); + const secretsInput = core.getInput('secrets', { required: true }); const secrets = parseSecretsInput(secretsInput); @@ -4084,10 +4087,10 @@ async function exportSecrets() { const vaultRoleId = core.getInput('roleId', { required: true }); const vaultSecretId = core.getInput('secretId', { required: true }); core.debug('Try to retrieve Vault Token from approle'); - var options = { - headers: {}, - json: { role_id: vaultRoleId, secret_id: vaultSecretId }, - responseType: 'json' + var options = { + headers: {}, + json: { role_id: vaultRoleId, secret_id: vaultSecretId }, + responseType: 'json' }; if (vaultNamespace != null) { @@ -4107,6 +4110,20 @@ async function exportSecrets() { break; } + if (!enginePath) { + enginePath = 'secret'; + } + + if (!kvVersion) { + kvVersion = '2'; + } + + if (kvVersion !== '1' && kvVersion !== '2') { + throw Error(`You must provide a valid K/V version (1 or 2). Input: "${kvVersion}"`); + } + + kvVersion = parseInt(kvVersion); + for (const secret of secrets) { const { secretPath, outputName, secretKey } = secret; const requestOptions = { @@ -4119,12 +4136,13 @@ async function exportSecrets() { requestOptions.headers["X-Vault-Namespace"] = vaultNamespace; } - const result = await got(`${vaultUrl}/v1/secret/data/${secretPath}`, requestOptions); + const requestPath = (kvVersion === 1) + ? `${vaultUrl}/v1/${enginePath}/${secretPath}` + : `${vaultUrl}/v1/${enginePath}/data/${secretPath}`; + const result = await got(requestPath, requestOptions); - const parsedResponse = JSON.parse(result.body); - const vaultKeyData = parsedResponse.data; - const versionData = vaultKeyData.data; - const value = versionData[secretKey]; + const secretData = parseResponse(result.body, kvVersion); + const value = secretData[secretKey]; command.issue('add-mask', value); core.exportVariable(outputName, `${value}`); core.debug(`✔ ${secretPath} => ${outputName}`); @@ -4183,6 +4201,29 @@ function parseSecretsInput(secretsInput) { return output; } +/** + * Parses a JSON response and returns the secret data + * @param {string} responseBody + * @param {number} kvVersion + */ +function parseResponse(responseBody, kvVersion) { + const parsedResponse = JSON.parse(responseBody); + let secretData; + + switch(kvVersion) { + case 1: { + secretData = parsedResponse.data; + } break; + + case 2: { + const vaultKeyData = parsedResponse.data; + secretData = vaultKeyData.data; + } break; + } + + return secretData; +} + /** * Replaces any forward-slash characters to * @param {string} dataKey @@ -4194,6 +4235,7 @@ function normalizeOutputKey(dataKey) { module.exports = { exportSecrets, parseSecretsInput, + parseResponse, normalizeOutputKey }; diff --git a/integrationTests/basic/integration.test.js b/integrationTests/basic/integration.test.js index 70f9ce1b..22b52d69 100644 --- a/integrationTests/basic/integration.test.js +++ b/integrationTests/basic/integration.test.js @@ -39,6 +39,46 @@ describe('integration', () => { data: { otherSecret: 'OTHERSUPERSECRET', }, + } + }); + + // Enable custom secret engine + try { + await got(`${vaultUrl}/v1/sys/mounts/my-secret`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + }, + json: { + type: 'kv' + } + }); + } catch (error) { + const {response} = error; + if (response.statusCode === 400 && response.body.includes("path is already in use")) { + // Engine might already be enabled from previous test runs + } else { + throw error; + } + } + + await got(`${vaultUrl}/v1/my-secret/test`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + }, + json: { + secret: 'CUSTOMSECRET', + } + }); + + await got(`${vaultUrl}/v1/my-secret/nested/test`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + }, + json: { + otherSecret: 'OTHERCUSTOMSECRET', }, }); }); @@ -48,17 +88,29 @@ describe('integration', () => { when(core.getInput) .calledWith('url') - .mockReturnValue(`${vaultUrl}`); + .mockReturnValueOnce(`${vaultUrl}`); when(core.getInput) .calledWith('token') - .mockReturnValue('testtoken'); + .mockReturnValueOnce('testtoken'); }); function mockInput(secrets) { when(core.getInput) .calledWith('secrets') - .mockReturnValue(secrets); + .mockReturnValueOnce(secrets); + } + + function mockEngineName(name) { + when(core.getInput) + .calledWith('path') + .mockReturnValueOnce(name); + } + + function mockVersion(version) { + when(core.getInput) + .calledWith('kv-version') + .mockReturnValueOnce(version); } it('get simple secret', async () => { @@ -99,4 +151,24 @@ describe('integration', () => { expect(core.exportVariable).toBeCalledWith('NAMED_SECRET', 'SUPERSECRET'); expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERSUPERSECRET'); }); + + it('get secret from K/V v1', async () => { + mockInput('test secret'); + mockEngineName('my-secret'); + mockVersion('1'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'CUSTOMSECRET'); + }); + + it('get nested secret from K/V v1', async () => { + mockInput('nested/test otherSecret'); + mockEngineName('my-secret'); + mockVersion('1'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERCUSTOMSECRET'); + }); }); diff --git a/integrationTests/e2e/e2e.test.js b/integrationTests/e2e/e2e.test.js index f5bfd472..bfa4987c 100644 --- a/integrationTests/e2e/e2e.test.js +++ b/integrationTests/e2e/e2e.test.js @@ -3,5 +3,8 @@ describe('e2e', () => { expect(process.env.SECRET).toBe("SUPERSECRET"); expect(process.env.NAMED_SECRET).toBe("SUPERSECRET"); expect(process.env.OTHERSECRET).toBe("OTHERSUPERSECRET"); + expect(process.env.ALTSECRET).toBe("CUSTOMSECRET"); + expect(process.env.NAMED_ALTSECRET).toBe("CUSTOMSECRET"); + expect(process.env.OTHERALTSECRET).toBe("OTHERCUSTOMSECRET"); }); -}); \ No newline at end of file +}); diff --git a/integrationTests/e2e/setup.js b/integrationTests/e2e/setup.js index f81e4123..1cc73e21 100644 --- a/integrationTests/e2e/setup.js +++ b/integrationTests/e2e/setup.js @@ -1,15 +1,17 @@ const got = require('got'); +const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`; + (async () => { try { // Verify Connection - await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/secret/config`, { + await got(`http://${vaultUrl}/v1/secret/config`, { headers: { 'X-Vault-Token': 'testtoken', }, }); - await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/secret/data/test`, { + await got(`http://${vaultUrl}/v1/secret/data/test`, { method: 'POST', headers: { 'X-Vault-Token': 'testtoken', @@ -21,7 +23,7 @@ const got = require('got'); }, }); - await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/secret/data/nested/test`, { + await got(`http://${vaultUrl}/v1/secret/data/nested/test`, { method: 'POST', headers: { 'X-Vault-Token': 'testtoken', @@ -30,6 +32,36 @@ const got = require('got'); data: { otherSecret: 'OTHERSUPERSECRET', }, + } + }); + + await got(`http://${vaultUrl}/v1/sys/mounts/my-secret`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + }, + json: { + type: 'kv' + } + }); + + await got(`http://${vaultUrl}/v1/my-secret/test`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + }, + json: { + altSecret: 'CUSTOMSECRET', + } + }); + + await got(`http://${vaultUrl}/v1/my-secret/nested/test`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + }, + json: { + otherAltSecret: 'OTHERCUSTOMSECRET', }, }); } catch (error) { diff --git a/integrationTests/enterprise/enterprise.test.js b/integrationTests/enterprise/enterprise.test.js index 9c69d089..6c137bb7 100644 --- a/integrationTests/enterprise/enterprise.test.js +++ b/integrationTests/enterprise/enterprise.test.js @@ -20,48 +20,19 @@ describe('integration', () => { }); // Create namespace - await got(`${vaultUrl}/v1/sys/namespaces/ns1`, { - method: 'POST', - headers: { - 'X-Vault-Token': 'testtoken', - } - }); + await enableNamespace('ns1'); - // Enable secret engine - await got(`${vaultUrl}/v1/sys/mounts/secret`, { - method: 'POST', - headers: { - 'X-Vault-Token': 'testtoken', - 'X-Vault-Namespace': 'ns1', - }, - json: { path: 'secret', type: 'kv', config: {}, options: { version: 2 }, generate_signing_key: true }, - }); + // Enable K/V v2 secret engine at 'secret/' + await enableEngine('secret', 'ns1', 2); - await got(`${vaultUrl}/v1/secret/data/test`, { - method: 'POST', - headers: { - 'X-Vault-Token': 'testtoken', - 'X-Vault-Namespace': 'ns1', - }, - json: { - data: { - secret: 'SUPERSECRET_IN_NAMESPACE', - }, - }, - }); + await writeSecret('secret', 'test', 'ns1', 2, {secret: 'SUPERSECRET_IN_NAMESPACE'}) + await writeSecret('secret', 'nested/test', 'ns1', 2, {otherSecret: 'OTHERSUPERSECRET_IN_NAMESPACE'}) - await got(`${vaultUrl}/v1/secret/data/nested/test`, { - method: 'POST', - headers: { - 'X-Vault-Token': 'testtoken', - 'X-Vault-Namespace': 'ns1', - }, - json: { - data: { - otherSecret: 'OTHERSUPERSECRET_IN_NAMESPACE', - }, - }, - }); + // Enable K/V v1 secret engine at 'my-secret/' + await enableEngine('my-secret', 'ns1', 1); + + await writeSecret('my-secret', 'test', 'ns1', 1, {secret: 'CUSTOMSECRET_IN_NAMESPACE'}) + await writeSecret('my-secret', 'nested/test', 'ns1', 1, {otherSecret: 'OTHERCUSTOMSECRET_IN_NAMESPACE'}) } catch (e) { console.error('Failed to setup test', e); throw e; @@ -73,23 +44,17 @@ describe('integration', () => { when(core.getInput) .calledWith('url') - .mockReturnValue(`${vaultUrl}`); + .mockReturnValueOnce(`${vaultUrl}`); when(core.getInput) .calledWith('token') - .mockReturnValue('testtoken'); + .mockReturnValueOnce('testtoken'); when(core.getInput) .calledWith('namespace') - .mockReturnValue('ns1'); + .mockReturnValueOnce('ns1'); }); - function mockInput(secrets) { - when(core.getInput) - .calledWith('secrets') - .mockReturnValue(secrets); - } - it('get simple secret', async () => { mockInput('test secret'); @@ -128,6 +93,26 @@ describe('integration', () => { expect(core.exportVariable).toBeCalledWith('NAMED_SECRET', 'SUPERSECRET_IN_NAMESPACE'); expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERSUPERSECRET_IN_NAMESPACE'); }); + + it('get secret from K/V v1', async () => { + mockInput('test secret'); + mockEngineName('my-secret'); + mockVersion('1'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'CUSTOMSECRET_IN_NAMESPACE'); + }); + + it('get nested secret from K/V v1', async () => { + mockInput('nested/test otherSecret'); + mockEngineName('my-secret'); + mockVersion('1'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERCUSTOMSECRET_IN_NAMESPACE'); + }); }); describe('authenticate with approle', () => { @@ -143,48 +128,34 @@ describe('authenticate with approle', () => { }); // Create namespace - await got(`${vaultUrl}/v1/sys/namespaces/ns2`, { - method: 'POST', - headers: { - 'X-Vault-Token': 'testtoken', - }, - }); + await enableNamespace("ns2"); - // Enable secret engine - await got(`${vaultUrl}/v1/sys/mounts/secret`, { - method: 'POST', - headers: { - 'X-Vault-Token': 'testtoken', - 'X-Vault-Namespace': 'ns2', - }, - json: { path: 'secret', type: 'kv', config: {}, options: { version: 2 }, generate_signing_key: true }, - }); + // Enable K/V v2 secret engine at 'secret/' + await enableEngine("secret", "ns2", 2); // Add secret - await got(`${vaultUrl}/v1/secret/data/test`, { - method: 'POST', - headers: { - 'X-Vault-Token': 'testtoken', - 'X-Vault-Namespace': 'ns2', - }, - json: { - data: { - secret: 'SUPERSECRET_WITH_APPROLE', - }, - }, - }); + await writeSecret('secret', 'test', 'ns2', 2, {secret: 'SUPERSECRET_WITH_APPROLE'}) // Enable approle - await got(`${vaultUrl}/v1/sys/auth/approle`, { - method: 'POST', - headers: { - 'X-Vault-Token': 'testtoken', - 'X-Vault-Namespace': 'ns2', - }, - json: { - type: 'approle' - }, - }); + try { + await got(`${vaultUrl}/v1/sys/auth/approle`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': 'ns2', + }, + json: { + type: 'approle' + }, + }); + } catch (error) { + const {response} = error; + if (response.statusCode === 400 && response.body.includes("path is already in use")) { + // Approle might already be enabled from previous test runs + } else { + throw error; + } + } // Create policies await got(`${vaultUrl}/v1/sys/policies/acl/test`, { @@ -232,7 +203,7 @@ describe('authenticate with approle', () => { }); secretId = secretIdResponse.body.data.secret_id; } catch(err) { - console.warn('Create approle', err); + console.warn('Create approle', err.response.body); throw err; } }); @@ -242,27 +213,21 @@ describe('authenticate with approle', () => { when(core.getInput) .calledWith('method') - .mockReturnValue('approle'); + .mockReturnValueOnce('approle'); when(core.getInput) .calledWith('roleId') - .mockReturnValue(roleId); + .mockReturnValueOnce(roleId); when(core.getInput) .calledWith('secretId') - .mockReturnValue(secretId); + .mockReturnValueOnce(secretId); when(core.getInput) .calledWith('url') - .mockReturnValue(`${vaultUrl}`); + .mockReturnValueOnce(`${vaultUrl}`); when(core.getInput) .calledWith('namespace') - .mockReturnValue('ns2'); + .mockReturnValueOnce('ns2'); }); - function mockInput(secrets) { - when(core.getInput) - .calledWith('secrets') - .mockReturnValue(secrets); - } - it('authenticate with approle', async()=> { mockInput('test secret'); @@ -270,4 +235,73 @@ describe('authenticate with approle', () => { expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_WITH_APPROLE'); }) -}); \ No newline at end of file +}); + +async function enableNamespace(name) { + try { + await got(`${vaultUrl}/v1/sys/namespaces/${name}`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + } + }); + } catch (error) { + const {response} = error; + if (response.statusCode === 400 && response.body.includes("already exists")) { + // Namespace might already be enabled from previous test runs + } else { + throw error; + } + } +} + +async function enableEngine(path, namespace, version) { + try { + await got(`${vaultUrl}/v1/sys/mounts/${path}`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': namespace, + }, + json: { type: 'kv', config: {}, options: { version }, generate_signing_key: true }, + }); + } catch (error) { + const {response} = error; + if (response.statusCode === 400 && response.body.includes("path is already in use")) { + // Engine might already be enabled from previous test runs + } else { + throw error; + } + } +} + +async function writeSecret(engine, path, namespace, version, data) { + const secretPath = (version == 1) ? (`${engine}/${path}`) : (`${engine}/data/${path}`); + const secretPayload = (version == 1) ? (data) : ({data}); + await got(`${vaultUrl}/v1/${secretPath}`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': namespace, + }, + json: secretPayload + }); +} + +function mockInput(secrets) { + when(core.getInput) + .calledWith('secrets') + .mockReturnValueOnce(secrets); +} + +function mockEngineName(name) { + when(core.getInput) + .calledWith('path') + .mockReturnValueOnce(name); +} + +function mockVersion(version) { + when(core.getInput) + .calledWith('kv-version') + .mockReturnValueOnce(version); +}