5
0
Fork 0
mirror of https://github.com/hashicorp/vault-action.git synced 2025-11-08 23:56:55 +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:
matryxxx02 2023-01-19 01:40:57 +01:00
commit 0add1c8a81
19 changed files with 16503 additions and 11290 deletions

View file

@ -17,7 +17,7 @@ The yaml of the `vault-action` step, with any sensitive information masked or re
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
**Log Output** **Log Output**
For the most verbose logs, [add a secret called `ACTIONS_STEP_DEBUG` with the value `true`](https://github.com/actions/toolkit/blob/master/docs/action-debugging.md). Then, re-run the workflow if possible and post the *raw logs* for the step here with any sensitive information masked or removed. For the most verbose logs, [add a secret called `ACTIONS_STEP_DEBUG` with the value `true`](https://github.com/actions/toolkit/blob/main/docs/action-debugging.md). Then, re-run the workflow if possible and post the *raw logs* for the step here with any sensitive information masked or removed.
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View file

@ -1,19 +1,20 @@
on: on:
push: push:
branches: branches:
- master - main
pull_request_target: pull_request_target:
types: [opened, reopened, synchronize] types: [opened, reopened, synchronize]
workflow_dispatch:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- uses: actions/setup-node@v1 - uses: actions/setup-node@v3
with: with:
node-version: '' node-version: '16.14.0'
- name: Setup NPM Cache - name: Setup NPM Cache
uses: actions/cache@v1 uses: actions/cache@v1
@ -41,9 +42,9 @@ jobs:
- name: Run docker-compose - name: Run docker-compose
run: docker-compose up -d vault run: docker-compose up -d vault
- uses: actions/setup-node@v1 - uses: actions/setup-node@v3
with: with:
node-version: '' node-version: '16.14.0'
- name: Setup NPM Cache - name: Setup NPM Cache
uses: actions/cache@v1 uses: actions/cache@v1
@ -77,9 +78,9 @@ jobs:
env: env:
VAULT_LICENSE_CI: ${{ secrets.VAULT_LICENSE_CI }} VAULT_LICENSE_CI: ${{ secrets.VAULT_LICENSE_CI }}
- uses: actions/setup-node@v1 - uses: actions/setup-node@v3
with: with:
node-version: '' node-version: '16.14.0'
- name: Setup NPM Cache - name: Setup NPM Cache
uses: actions/cache@v1 uses: actions/cache@v1
@ -111,9 +112,9 @@ jobs:
- name: Run docker-compose - name: Run docker-compose
run: docker-compose up -d vault run: docker-compose up -d vault
- uses: actions/setup-node@v1 - uses: actions/setup-node@v3
with: with:
node-version: '' node-version: '16.14.0'
- name: Setup NPM Cache - name: Setup NPM Cache
uses: actions/cache@v1 uses: actions/cache@v1
@ -179,9 +180,9 @@ jobs:
- name: Run docker-compose - name: Run docker-compose
run: docker-compose up -d vault-tls run: docker-compose up -d vault-tls
- uses: actions/setup-node@v1 - uses: actions/setup-node@v3
with: with:
node-version: '' node-version: '16.14.0'
- name: Setup NPM Cache - name: Setup NPM Cache
uses: actions/cache@v1 uses: actions/cache@v1
@ -263,14 +264,14 @@ jobs:
# Removing publish step for now. # Removing publish step for now.
# publish: # publish:
# if: github.event_name == 'push' && contains(github.ref, 'master') # if: github.event_name == 'push' && contains(github.ref, 'main')
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
# needs: [build, integration, e2e] # needs: [build, integration, e2e]
# steps: # steps:
# - uses: actions/checkout@v1 # - uses: actions/checkout@v1
# - uses: actions/setup-node@v1 # - uses: actions/setup-node@v3
# with: # with:
# node-version: '' # node-version: '16.14.0'
# - name: setup npm cache # - name: setup npm cache
# uses: actions/cache@v1 # uses: actions/cache@v1
# with: # with:
@ -281,9 +282,8 @@ jobs:
# - name: npm install # - name: npm install
# run: npm ci # run: npm ci
# - name: release # - name: release
# if: success() && endsWith(github.ref, 'master') # if: success() && endsWith(github.ref, 'main')
# run: npx semantic-release # run: npx semantic-release
# env: # env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -40,7 +40,7 @@ jobs:
description: "${{ github.event.issue.body || github.event.pull_request.body }}\n\n_Created from GitHub Action for ${{ github.event.issue.html_url || github.event.pull_request.html_url }} from ${{ github.actor }}_" description: "${{ github.event.issue.body || github.event.pull_request.body }}\n\n_Created from GitHub Action for ${{ github.event.issue.html_url || github.event.pull_request.html_url }} from ${{ github.actor }}_"
# customfield_10089 is Issue Link custom field # customfield_10089 is Issue Link custom field
# customfield_10091 is team custom field # customfield_10091 is team custom field
extraFields: '{"fixVersions": [{"name": "TBD"}], "customfield_10091": ["ecosystem", "runtime"], "customfield_10089": "${{ github.event.issue.html_url || github.event.pull_request.html_url }}"}' extraFields: '{"fixVersions": [{"name": "TBD"}], "customfield_10091": ["ecosystem", "applications"], "customfield_10089": "${{ github.event.issue.html_url || github.event.pull_request.html_url }}"}'
- name: Search - name: Search
if: github.event.action != 'opened' if: github.event.action != 'opened'
@ -62,7 +62,7 @@ jobs:
uses: atlassian/gajira-transition@v2.0.1 uses: atlassian/gajira-transition@v2.0.1
with: with:
issue: ${{ steps.search.outputs.issue }} issue: ${{ steps.search.outputs.issue }}
transition: Close transition: Closed
- name: Reopen ticket - name: Reopen ticket
if: github.event.action == 'reopened' && steps.search.outputs.issue if: github.event.action == 'reopened' && steps.search.outputs.issue

View file

@ -1,5 +1,37 @@
## Unreleased ## Unreleased
## 2.4.3 (Nov 8th, 2022)
Improvements:
* bump jest-when from 3.5.1 to 3.5.2 [GH-388](https://github.com/hashicorp/vault-action/pull/388)
* bump semantic-release from 19.0.3 to 19.0.5 [GH-360](https://github.com/hashicorp/vault-action/pull/360)
* bump jsrsasign from 10.5.25 to 10.5.27 [GH-358](https://github.com/hashicorp/vault-action/pull/358)
* bump @actions/core from 1.9.0 to 1.10.0 [GH-371](https://github.com/hashicorp/vault-action/pull/371)
* update runtime to node16 for action [GH-375](https://github.com/hashicorp/vault-action/pull/375)
## 2.4.2 (Aug 15, 2022)
Bugs:
* Errors due to replication delay for tokens will now be retried [GH-333](https://github.com/hashicorp/vault-action/pull/333)
Improvements:
* bump got from 11.5.1 to 11.8.5 [GH-344](https://github.com/hashicorp/vault-action/pull/344)
## 2.4.1 (April 28th, 2022)
Improvements:
* Make secrets parameter optional [GH-299](https://github.com/hashicorp/vault-action/pull/299)
* auth/jwt: make "role" input optional [GH-291](https://github.com/hashicorp/vault-action/pull/291)
* Write a better error message when secret not found [GH-306](https://github.com/hashicorp/vault-action/pull/306)
* bump jest-when from 2.7.2 to 3.5.1 [GH-294](https://github.com/hashicorp/vault-action/pull/294)
* bump node-fetch from 2.6.1 to 2.6.7 [GH-308](https://github.com/hashicorp/vault-action/pull/308)
* bump @types/jest from 26.0.23 to 27.4.1 [GH-297](https://github.com/hashicorp/vault-action/pull/297)
* bump trim-off-newlines from 1.0.1 to 1.0.3 [GH-309](https://github.com/hashicorp/vault-action/pull/309)
* bump moment from 2.28.0 to 2.29.2 [GH-304](https://github.com/hashicorp/vault-action/pull/304)
* bump @types/got from 9.6.11 to 9.6.12 [GH-266](https://github.com/hashicorp/vault-action/pull/266)
## 2.4.0 (October 21st, 2021) ## 2.4.0 (October 21st, 2021)
Features: Features:

216
README.md
View file

@ -12,14 +12,21 @@ A helper action for easily pulling secrets from HashiCorp Vault™.
- [Vault GitHub Action](#vault-github-action) - [Vault GitHub Action](#vault-github-action)
- [Example Usage](#example-usage) - [Example Usage](#example-usage)
- [Authentication method](#authentication-method) - [Authentication Methods](#authentication-methods)
- [JWT with GitHub OIDC Tokens](#jwt-with-github-oidc-tokens)
- [AppRole](#approle)
- [Token](#token)
- [GitHub](#github)
- [JWT with OIDC Provider](#jwt-with-oidc-provider)
- [Kubernetes](#kubernetes)
- [Other Auth Methods](#other-auth-methods)
- [Key Syntax](#key-syntax) - [Key Syntax](#key-syntax)
- [Simple Key](#simple-key) - [Simple Key](#simple-key)
- [Set Output Variable Name](#set-output-variable-name) - [Set Output Variable Name](#set-output-variable-name)
- [Multiple Secrets](#multiple-secrets) - [Multiple Secrets](#multiple-secrets)
- [Other Secret Engines](#other-secret-engines) - [Other Secret Engines](#other-secret-engines)
- [Adding Extra Headers](#adding-extra-headers) - [Adding Extra Headers](#adding-extra-headers)
- [Vault Enterprise Features](#vault-enterprise-features) - [HashiCorp Cloud Platform or Vault Enterprise](#hashicorp-cloud-platform-or-vault-enterprise)
- [Namespace](#namespace) - [Namespace](#namespace)
- [Reference](#reference) - [Reference](#reference)
- [Masking - Hiding Secrets from Logs](#masking---hiding-secrets-from-logs) - [Masking - Hiding Secrets from Logs](#masking---hiding-secrets-from-logs)
@ -36,11 +43,11 @@ jobs:
steps: steps:
# ... # ...
- name: Import Secrets - name: Import Secrets
uses: hashicorp/vault-action@v2.3.1 uses: hashicorp/vault-action@v2
with: with:
url: https://vault.mycompany.com:8200 url: https://vault.mycompany.com:8200
token: ${{ secrets.VaultToken }} token: ${{ secrets.VAULT_TOKEN }}
caCertificate: ${{ secrets.VAULTCA }} caCertificate: ${{ secrets.VAULT_CA_CERT }}
secrets: | secrets: |
secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ; secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ; secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
@ -48,87 +55,176 @@ jobs:
# ... # ...
``` ```
## Authentication method ## Authentication Methods
While most workflows will likely use a vault token, you can also use an `approle` to authenticate with Vault. You can configure which by using the `method` parameter: Consider using a [Vault authentication method](https://www.vaultproject.io/docs/auth) such as the JWT auth method with
[GitHub OIDC tokens](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) or the AppRole auth method. You can configure which by using the `method` parameter.
### JWT with GitHub OIDC Tokens
You can configure trust between a GitHub Actions workflow
and Vault using the
[GitHub's OIDC provider](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect).
Each GitHub Actions workflow receives an auto-generated OIDC token with claims
to establish the identity of the workflow.
__Vault Configuration__
<details>
<summary>Click to toggle instructions for configuring Vault.</summary>
Set up Vault with the [JWT auth method](https://www.vaultproject.io/api/auth/jwt#configure).
Pass the following parameters to your auth method configuration:
- `oidc_discovery_url`: `https://token.actions.githubusercontent.com`
- `bound_issuer`: `https://token.actions.githubusercontent.com`
Configure a [Vault role](https://www.vaultproject.io/api/auth/jwt#create-role) for the auth method.
- `role_type`: `jwt`
- `bound_audiences`: `"https://github.com/<org>"`. Update this parameter if
you change the `aud` claim in the GitHub OIDC token via the
`jwtGithubAudience` parameter in the action config.
- `user_claim`: Set this to a claim name (e.g., `repository`) in the
[GitHub OIDC token](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token).
- `bound_claims` OR `bound_subject`: match on [GitHub subject claims](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims).
- For wildcard (non-exact) matches, use `bound_claims`.
- `bound_claims_type`: `glob`
- `bound_claims`: JSON object. Maps one or more claim names to corresponding wildcard values.
```json
{"sub": "repo:<orgName>/*"}
```
- For exact matches, use `bound_subject`.
- `bound_claims_type`: `string`
- `bound_subject`: Must exactly match the `sub` claim in the OIDC token.
```plaintext
repo:<orgName/repoName>:ref:refs/heads/branchName
```
</details>
__GitHub Actions Workflow__
In the GitHub Actions workflow, the workflow needs permissions to read contents
and write the ID token.
- **token**: (by default) you must provide a `token` parameter
```yaml ```yaml
... jobs:
with: retrieve-secret:
url: https://vault.mycompany.com:8200 permissions:
token: ${{ secrets.VaultToken }} contents: read
caCertificate: ${{ secrets.VAULTCA }} id-token: write
``` ```
- **approle**: you must provide a `roleId` & `secretId` parameter
In the action, provide the name of the Vault role you created to the `role` parameter.
You can optionally set the `jwtGithubAudience` parameter to change the `aud`
claim from its default.
```yaml ```yaml
...
with: with:
url: https://vault.mycompany.com:8200 url: https://vault.mycompany.com:8200
caCertificate: ${{ secrets.VAULT_CA_CERT }}
role: <Vault JWT Auth Role Name>
method: jwt
jwtGithubAudience: sigstore # set the GitHub token's aud claim
```
### AppRole
The [AppRole auth method](https://www.vaultproject.io/docs/auth/approle) allows
your GitHub Actions workflow to authenticate to Vault with a pre-defined role.
Set the role ID and secret ID as GitHub secrets and pass them to the
`roleId` and `secretId` parameters.
```yaml
with:
url: https://vault.mycompany.com:8200
caCertificate: ${{ secrets.VAULT_CA_CERT }}
method: approle method: approle
roleId: ${{ secrets.roleId }} roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.secretId }} secretId: ${{ secrets.VAULT_SECRET_ID }}
caCertificate: ${{ secrets.VAULTCA }}
``` ```
- **github**: you must provide the github token as `githubToken`
**Notice: [Vault GitHub authentication](https://www.vaultproject.io/docs/auth/github) ### Token
For the default method of authenticating to Vault,
use a [Vault token](https://www.vaultproject.io/docs/concepts/tokens).
Set the Vault token as a GitHub secret and pass
it to the `token` parameter.
```yaml
with:
url: https://vault.mycompany.com:8200
caCertificate: ${{ secrets.VAULT_CA_CERT }}
token: ${{ secrets.VAULT_TOKEN }}
```
### GitHub
The [GitHub auth method](https://www.vaultproject.io/docs/auth/github)
requires `read:org` permissions for authentication. The auto-generated `GITHUB_TOKEN` requires `read:org` permissions for authentication. The auto-generated `GITHUB_TOKEN`
created for projects does not have these permissions and GitHub does not allow this created for projects does not have these permissions and GitHub does not allow this
token's permissions to be modified. A new GitHub Token secret must be created with token's permissions to be modified. A new GitHub Token secret must be created with
`read:org` permissions to use this authentication method.** `read:org` permissions to use this authentication method.
Pass the GitHub token as a GitHub secret into the `githubToken` parameter.
```yaml ```yaml
...
with: with:
url: https://vault.mycompany.com:8200 url: https://vault.mycompany.com:8200
caCertificate: ${{ secrets.VAULT_CA_CERT }}
method: github method: github
githubToken: ${{ secrets.MY_GITHUB_TOKEN }} githubToken: ${{ secrets.GITHUB_TOKEN }}
caCertificate: ${{ secrets.VAULTCA }}
``` ```
- **jwt**: (Github OIDC) you must provide a `role` parameter, additionally you can pass `jwtGithubAudience` parameter.
### JWT with OIDC Provider
You can configure trust between your own OIDC Provider and Vault
with the JWT auth method. Provide a `role` & `jwtPrivateKey` parameters,
additionally you can pass `jwtKeyPassword` & `jwtTtl` parameters
```yaml ```yaml
...
with: with:
url: https://vault.mycompany.com:8200 url: https://vault.mycompany.com:8200
caCertificate: ${{ secrets.VAULT_CA_CERT }}
method: jwt method: jwt
role: github-action role: <Vault JWT Auth Role Name>
```
**Notice:** For Github provided OIDC token to work, the workflow should have `id-token: write` & `contents: read` specified in the `permissions` section of the workflow
```yaml
...
permissions:
id-token: write
contents: read
...
```
- **jwt**: you must provide a `role` & `jwtPrivateKey` parameters, additionally you can pass `jwtKeyPassword` & `jwtTtl` parameters
```yaml
...
with:
url: https://vault.mycompany.com:8200
method: jwt
role: github-action
jwtPrivateKey: ${{ secrets.JWT_PRIVATE_KEY }} jwtPrivateKey: ${{ secrets.JWT_PRIVATE_KEY }}
jwtKeyPassword: ${{ secrets.JWT_KEY_PASS }} jwtKeyPassword: ${{ secrets.JWT_KEY_PASS }}
jwtTtl: 3600 # 1 hour, default value jwtTtl: 3600 # 1 hour, default value
``` ```
- **kubernetes**: you must provide the `role` paramaters. You can optionally override the `kubernetesTokenPath` paramater for custom mounted serviceAccounts. Consider [kubernetes auth](https://www.vaultproject.io/docs/auth/kubernetes) when using self-hosted runners on Kubernetes: ### Kubernetes
Consider the [Kubernetes auth method](https://www.vaultproject.io/docs/auth/kubernetes)
when using self-hosted runners on Kubernetes. You must provide the `role` parameter
for the Vault role associated with the Kubernetes auth method.
You can optionally override the `kubernetesTokenPath` parameter for
custom-mounted serviceAccounts.
```yaml ```yaml
...
with: with:
url: https://vault.mycompany.com:8200 url: https://vault.mycompany.com:8200
caCertificate: ${{ secrets.VAULT_CA_CERT }}
method: kubernetes method: kubernetes
role: ${{ secrets.KUBE_ROLE }} role: <Vault Kubernetes Auth Role Name>
kubernetesTokenPath: /var/run/secrets/kubernetes.io/serviceaccount/token # default token path
``` ```
If any other method is specified and you provide an `authPayload`, the action will attempt to `POST` to `auth/${method}/login` with the provided payload and parse out the client token. ### Other Auth Methods
If any other method is specified and you provide an `authPayload`, the action will
attempt to `POST` to `auth/${method}/login` with the provided payload and parse out the client token.
## Key Syntax ## Key Syntax
@ -165,7 +261,6 @@ steps:
# Import config... # Import config...
- name: Sensitive Operation - name: Sensitive Operation
run: "my-cli --token '${{ steps.secrets.outputs.npmToken }}'" run: "my-cli --token '${{ steps.secrets.outputs.npmToken }}'"
``` ```
_**Note:** If you'd like to only use outputs and disable automatic environment variables, you can set the `exportEnv` option to `false`._ _**Note:** If you'd like to only use outputs and disable automatic environment variables, you can set the `exportEnv` option to `false`._
@ -258,11 +353,16 @@ with:
This will automatically add the `x-secure-id` and `x-secure-secret` headers to every request to Vault. This will automatically add the `x-secure-id` and `x-secure-secret` headers to every request to Vault.
## Vault Enterprise Features ## HashiCorp Cloud Platform or Vault Enterprise
If you are using [HCP Vault](https://cloud.hashicorp.com/products/vault)
or Vault Enterprise, you may need additional parameters in
your GitHub Actions workflow.
### Namespace ### Namespace
If you need to retrieve secrets from a specific Vault namespace, all that's required is an additional parameter specifying the namespace. If you need to retrieve secrets from a specific Vault namespace, set the `namespace`
parameter specifying the namespace. In HCP Vault, the namespace defaults to `admin`.
```yaml ```yaml
steps: steps:
@ -271,10 +371,10 @@ steps:
uses: hashicorp/vault-action uses: hashicorp/vault-action
with: with:
url: https://vault-enterprise.mycompany.com:8200 url: https://vault-enterprise.mycompany.com:8200
caCertificate: ${{ secrets.VAULT_CA_CERT }}
method: token method: token
caCertificate: ${{ secrets.VAULTCA }} token: ${{ secrets.VAULT_TOKEN }}
token: ${{ secrets.VaultToken }} namespace: admin
namespace: ns1
secrets: | secrets: |
secret/ci/aws accessKey | AWS_ACCESS_KEY_ID ; secret/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
secret/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ; secret/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
@ -288,7 +388,7 @@ Here are all the inputs available through `with`:
| Input | Description | Default | Required | | Input | Description | Default | Required |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| `url` | The URL for the vault endpoint | | ✔ | | `url` | The URL for the vault endpoint | | ✔ |
| `secrets` | A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details | | | | `secrets` | A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details | | |
| `namespace` | The Vault namespace from which to query secrets. Vault Enterprise only, unset by default | | | | `namespace` | The Vault namespace from which to query secrets. Vault Enterprise only, unset by default | | |
| `method` | The method to use to authenticate with Vault. | `token` | | | `method` | The method to use to authenticate with Vault. | `token` | |
| `role` | Vault role for specified auth method | | | | `role` | Vault role for specified auth method | | |

View file

@ -6,7 +6,7 @@ inputs:
required: true required: true
secrets: secrets:
description: 'A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details' description: 'A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details'
required: true required: false
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
@ -76,8 +76,11 @@ inputs:
description: 'Time in seconds, after which token expires' description: 'Time in seconds, after which token expires'
required: false required: false
default: 3600 default: 3600
secretEncodingType:
description: 'The encoding type of the secret to decode. If not specified, the secret will not be decoded. Supported values: base64, hex, utf8'
required: false
runs: runs:
using: 'node12' using: 'node16'
main: 'dist/index.js' main: 'dist/index.js'
branding: branding:
icon: 'unlock' icon: 'unlock'

3051
dist/index.js vendored

File diff suppressed because one or more lines are too long

View file

@ -109,20 +109,28 @@ describe('integration', () => {
jest.resetAllMocks(); jest.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('url') .calledWith('url', expect.anything())
.mockReturnValueOnce(`${vaultUrl}`); .mockReturnValueOnce(`${vaultUrl}`);
when(core.getInput) when(core.getInput)
.calledWith('token') .calledWith('token', expect.anything())
.mockReturnValueOnce('testtoken'); .mockReturnValueOnce('testtoken');
}); });
function mockInput(secrets) { function mockInput(secrets) {
when(core.getInput) when(core.getInput)
.calledWith('secrets') .calledWith('secrets', expect.anything())
.mockReturnValueOnce(secrets); .mockReturnValueOnce(secrets);
} }
it('prints a nice error message when secret not found', async () => {
mockInput(`secret/data/test secret ;
secret/data/test secret | NAMED_SECRET ;
secret/data/notFound kehe | NO_SIR ;`);
expect(exportSecrets()).rejects.toEqual(Error(`Unable to retrieve result for "secret/data/notFound" because it was not found: {"errors":[]}`));
})
it('get simple secret', async () => { it('get simple secret', async () => {
mockInput('secret/data/test secret'); mockInput('secret/data/test secret');

View file

@ -26,9 +26,9 @@ function mockGithubOIDCResponse(aud= "https://github.com/hashicorp/vault-action"
const now = rsasign.KJUR.jws.IntDate.getNow(); const now = rsasign.KJUR.jws.IntDate.getNow();
const payload = { const payload = {
jti: "unique-id", jti: "unique-id",
sub: "repo:hashicorp/vault-action:ref:refs/heads/master", sub: "repo:hashicorp/vault-action:ref:refs/heads/main",
aud, aud,
ref: "refs/heads/master", ref: "refs/heads/main",
sha: "commit-sha", sha: "commit-sha",
repository: "hashicorp/vault-action", repository: "hashicorp/vault-action",
repository_owner: "hashicorp", repository_owner: "hashicorp",
@ -41,7 +41,7 @@ function mockGithubOIDCResponse(aud= "https://github.com/hashicorp/vault-action"
base_ref: "", base_ref: "",
event_name: "push", event_name: "push",
ref_type: "branch", ref_type: "branch",
job_workflow_ref: "hashicorp/vault-action/.github/workflows/workflow.yml@refs/heads/master", job_workflow_ref: "hashicorp/vault-action/.github/workflows/workflow.yml@refs/heads/main",
iss: 'vault-action', iss: 'vault-action',
iat: now, iat: now,
nbf: now, nbf: now,
@ -51,6 +51,9 @@ function mockGithubOIDCResponse(aud= "https://github.com/hashicorp/vault-action"
return rsasign.KJUR.jws.JWS.sign(alg, JSON.stringify(header), JSON.stringify(payload), decryptedKey); return rsasign.KJUR.jws.JWS.sign(alg, JSON.stringify(header), JSON.stringify(payload), decryptedKey);
} }
// The sign call inside this function takes a while to run, so cache the default JWT in a constant.
const defaultGithubJwt = mockGithubOIDCResponse();
describe('jwt auth', () => { describe('jwt auth', () => {
beforeAll(async () => { beforeAll(async () => {
// Verify Connection // Verify Connection
@ -99,7 +102,8 @@ describe('jwt auth', () => {
'X-Vault-Token': 'testtoken', 'X-Vault-Token': 'testtoken',
}, },
json: { json: {
jwt_validation_pubkeys: publicRsaKey jwt_validation_pubkeys: publicRsaKey,
default_role: "default"
} }
}); });
@ -137,23 +141,23 @@ describe('jwt auth', () => {
jest.resetAllMocks(); jest.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('url') .calledWith('url', expect.anything())
.mockReturnValueOnce(`${vaultUrl}`); .mockReturnValueOnce(`${vaultUrl}`);
when(core.getInput) when(core.getInput)
.calledWith('method') .calledWith('method', expect.anything())
.mockReturnValueOnce('jwt'); .mockReturnValueOnce('jwt');
when(core.getInput) when(core.getInput)
.calledWith('jwtPrivateKey') .calledWith('jwtPrivateKey', expect.anything())
.mockReturnValueOnce(privateRsaKeyBase64); .mockReturnValueOnce(privateRsaKeyBase64);
when(core.getInput) when(core.getInput)
.calledWith('role') .calledWith('role', expect.anything())
.mockReturnValueOnce('default'); .mockReturnValueOnce('default');
when(core.getInput) when(core.getInput)
.calledWith('secrets') .calledWith('secrets', expect.anything())
.mockReturnValueOnce('secret/data/test secret'); .mockReturnValueOnce('secret/data/test secret');
}); });
@ -187,52 +191,65 @@ describe('jwt auth', () => {
jest.resetAllMocks(); jest.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('url') .calledWith('url', expect.anything())
.mockReturnValueOnce(`${vaultUrl}`); .mockReturnValueOnce(`${vaultUrl}`);
when(core.getInput) when(core.getInput)
.calledWith('method') .calledWith('method', expect.anything())
.mockReturnValueOnce('jwt'); .mockReturnValueOnce('jwt');
when(core.getInput) when(core.getInput)
.calledWith('jwtPrivateKey') .calledWith('jwtPrivateKey', expect.anything())
.mockReturnValueOnce(''); .mockReturnValueOnce('');
when(core.getInput) when(core.getInput)
.calledWith('role') .calledWith('secrets', expect.anything())
.mockReturnValueOnce('default');
when(core.getInput)
.calledWith('secrets')
.mockReturnValueOnce('secret/data/test secret'); .mockReturnValueOnce('secret/data/test secret');
when(core.getIDToken)
.calledWith()
.mockReturnValueOnce(mockGithubOIDCResponse());
}); });
it('successfully authenticates', async () => { it('successfully authenticates', async () => {
when(core.getInput)
.calledWith('role', expect.anything())
.mockReturnValueOnce('default');
when(core.getIDToken)
.calledWith(undefined)
.mockReturnValueOnce(defaultGithubJwt);
await exportSecrets(); await exportSecrets();
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET'); expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
}); });
it('successfully authenticates with `jwtGithubAudience` set to `sigstore`', async () => { it('successfully authenticates with `jwtGithubAudience` set to `sigstore`', async () => {
when(core.getInput) when(core.getInput)
.calledWith('role') .calledWith('role', expect.anything())
.mockReturnValueOnce('default-sigstore'); .mockReturnValueOnce('default-sigstore');
when(core.getInput) when(core.getInput)
.calledWith('jwtGithubAudience') .calledWith('jwtGithubAudience', expect.anything())
.mockReturnValueOnce('sigstore'); .mockReturnValueOnce('sigstore');
when(core.getIDToken) when(core.getIDToken)
.calledWith() .calledWith(expect.anything())
.mockReturnValueOnce(mockGithubOIDCResponse('sigstore')); .mockReturnValueOnce(mockGithubOIDCResponse('sigstore'));
await exportSecrets(); await exportSecrets();
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET'); expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
}) })
it('successfully authenticates as default role without specifying it', async () => {
when(core.getInput)
.calledWith('role', expect.anything())
.mockReturnValueOnce(null);
when(core.getIDToken)
.calledWith(undefined)
.mockReturnValueOnce(defaultGithubJwt);
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
})
}); });
}); });

View file

@ -1,54 +1,30 @@
const privateRsaKey = ` const privateRsaKey = `
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----
`; `;
@ -56,18 +32,13 @@ const privateRsaKeyBase64 = Buffer.from(privateRsaKey).toString('base64');
const publicRsaKey = ` const publicRsaKey = `
-----BEGIN PUBLIC KEY----- -----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwavSHLLo7bUSuKX2EKu3 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArcch89X6VuWj/CQtVfaC
YStrNTGdmhku7sAFaeDyi9His1oot+wzajWp1rqHaGVk4b5o5z6D7Xhm0zYPhpTT XUl0Pcv8IJRgICN8X+3zFNrbiTdhkTtrOhdkbEU5VaW6aQiCXX5+4C1T2sXXXT68
vEd3NyONc9sjVd7sf9rQaBY3QusPYAdF6j0ydTJGnTeG9N2zhHjdMLR+3F39F9Ry 2XJhIjKepyX3aY50Fh59pLCciwAKc5wPy3PVMOhup15u9reiQKxps1SNrqVLyZNj
6vddS9w3ibjVERucQtpqGhy5TIWhttk3gN3A7dk972+WCQeKUCC6wU6PvAEUPflT ha83qbN9IJvQcMnQghAjjPUeGPMaMMzG1GOnuPOWIiM2kxqRpbugwwTyuepPnakm
hQc1hSHldpjVUHQlZkQXl/XHWBzZESwMgiQcAmVL3lkvxmgIYscqWzf8cTHogNrP fkWqVtMIRprPLY6d3liDIUSSRZ7o6vbmgeF+9U4DyaimKVNngrmi+mW0OnyH1eJY
ml9Il89N+2XYcEfXgWLOyzGhQggigN9DRHDDE1UWT1foHnmeXAIZPCveKZc/Jp5S LrJEY9tZaRF8xraMZiOcBcyAt6S/TS29HttJ6+zlhcWx34fItEZ8jA5gzhTmspOY
ASIxaZ+r73mWjVt19GSvEsqtejDRmMC3jrYCdISrCVsHkbRQq7/yLCi5sJG9p2gD 8QIDAQAB
91jCgaBy2tw22nWN/KoHm9p/mhzWmlWwVJrFTNiP0qccyoa5h1a6WBPt0oCKQk3I
AMV5HhVw4DhPx8gvh7qwOZk9vXXAFyA8bWhOVIFIJ8i8Gq25y7/bukyzqsEPkQ2m
KgbQnh3VOUCSugJFodPC/Jf7VQK1yRpUZH2v7r9cBjHVnUnePGac3Zns7/iRYBvK
5cIFUg+X48VXIMKIvs4EA8ee8VacVh8tyR6EuP42BU5fGQlvLC+ZKT165maQv/Vl
f52E9W2iFNt3sxB0KFtOkbkCAwEAAQ==
-----END PUBLIC KEY----- -----END PUBLIC KEY-----
`; `;

View file

@ -43,15 +43,15 @@ describe('integration', () => {
jest.resetAllMocks(); jest.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('url') .calledWith('url', expect.anything())
.mockReturnValueOnce(`${vaultUrl}`); .mockReturnValueOnce(`${vaultUrl}`);
when(core.getInput) when(core.getInput)
.calledWith('token') .calledWith('token', expect.anything())
.mockReturnValueOnce('testtoken'); .mockReturnValueOnce('testtoken');
when(core.getInput) when(core.getInput)
.calledWith('namespace') .calledWith('namespace', expect.anything())
.mockReturnValueOnce('ns1'); .mockReturnValueOnce('ns1');
}); });
@ -211,16 +211,16 @@ describe('authenticate with approle', () => {
.calledWith('method', expect.anything()) .calledWith('method', expect.anything())
.mockReturnValueOnce('approle'); .mockReturnValueOnce('approle');
when(core.getInput) when(core.getInput)
.calledWith('roleId') .calledWith('roleId', expect.anything())
.mockReturnValueOnce(roleId); .mockReturnValueOnce(roleId);
when(core.getInput) when(core.getInput)
.calledWith('secretId') .calledWith('secretId', expect.anything())
.mockReturnValueOnce(secretId); .mockReturnValueOnce(secretId);
when(core.getInput) when(core.getInput)
.calledWith('url') .calledWith('url', expect.anything())
.mockReturnValueOnce(`${vaultUrl}`); .mockReturnValueOnce(`${vaultUrl}`);
when(core.getInput) when(core.getInput)
.calledWith('namespace') .calledWith('namespace', expect.anything())
.mockReturnValueOnce('ns2'); .mockReturnValueOnce('ns2');
}); });
@ -286,18 +286,6 @@ async function writeSecret(engine, path, namespace, version, data) {
function mockInput(secrets) { function mockInput(secrets) {
when(core.getInput) when(core.getInput)
.calledWith('secrets') .calledWith('secrets', expect.anything())
.mockReturnValueOnce(secrets); .mockReturnValueOnce(secrets);
} }
function mockEngineName(name) {
when(core.getInput)
.calledWith('path')
.mockReturnValueOnce(name);
}
function mockVersion(version) {
when(core.getInput)
.calledWith('kv-version')
.mockReturnValueOnce(version);
}

23914
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@
"dist/**/*" "dist/**/*"
], ],
"release": { "release": {
"branch": "master", "branch": "main",
"plugins": [ "plugins": [
"@semantic-release/commit-analyzer", "@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator", "@semantic-release/release-notes-generator",
@ -44,20 +44,21 @@
}, },
"homepage": "https://github.com/hashicorp/vault-action#readme", "homepage": "https://github.com/hashicorp/vault-action#readme",
"dependencies": { "dependencies": {
"got": "^11.5.1", "got": "^11.8.5",
"jsonata": "^1.8.2", "jsonata": "^1.8.6",
"jsrsasign": "^10.1.10" "jsrsasign": "^10.6.1"
}, },
"peerDependencies": { "peerDependencies": {
"@actions/core": ">=1 <2" "@actions/core": ">=1 <2"
}, },
"devDependencies": { "devDependencies": {
"@actions/core": "^1.6.0", "@actions/core": "^1.10.0",
"@types/got": "^9.6.11", "@types/got": "^9.6.11",
"@types/jest": "^26.0.13", "@types/jest": "^29.2.2",
"@zeit/ncc": "^0.22.3", "@zeit/ncc": "^0.22.3",
"jest": "^26.4.2", "jest": "^29.3.1",
"jest-when": "^2.7.2", "jest-when": "^3.5.2",
"semantic-release": "^17.1.1" "mock-http-server": "^1.4.5",
"semantic-release": "^19.0.5"
} }
} }

View file

@ -6,6 +6,7 @@ const jsonata = require('jsonata');
const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index'); const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index');
const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes']; const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes'];
const ENCODING_TYPES = ['base64', 'hex', 'utf8'];
function addMask(value) { function addMask(value) {
for (const line of value.replace(/\r/g, '').split('\n')) { 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 exportEnv = core.getInput('exportEnv', { required: false }) != 'false';
const exportToken = (core.getInput('exportToken', { required: false }) || 'false').toLowerCase() != '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 secretRequests = parseSecretsInput(secretsInput);
const secretEncodingType = core.getInput('secretEncodingType', { required: false });
const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase(); const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
const authPayload = core.getInput('authPayload', { required: false }); const authPayload = core.getInput('authPayload', { required: false });
if (!AUTH_METHODS.includes(vaultMethod) && !authPayload) { if (!AUTH_METHODS.includes(vaultMethod) && !authPayload) {
@ -34,7 +37,15 @@ async function exportSecrets() {
const defaultOptions = { const defaultOptions = {
prefixUrl: vaultUrl, prefixUrl: vaultUrl,
headers: {}, 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'; const tlsSkipVerify = (core.getInput('tlsSkipVerify', { required: false }) || 'false').toLowerCase() != 'false';
@ -81,12 +92,28 @@ async function exportSecrets() {
const results = await getSecrets(requests, client); const results = await getSecrets(requests, client);
for (const result of results) { 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) { if (cachedResponse) {
core.debug(' using cached response'); 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") { if (exportEnv && typeof value === "object") {
Object.entries(value).forEach(([envKey, envValue]) => { Object.entries(value).forEach(([envKey, envValue]) => {
@ -114,6 +141,10 @@ async function exportSecrets() {
* @param {string} secretsInput * @param {string} secretsInput
*/ */
function parseSecretsInput(secretsInput) { function parseSecretsInput(secretsInput) {
if (!secretsInput) {
return []
}
const secrets = secretsInput const secrets = secretsInput
.split(';') .split(';')
.filter(key => !!key) .filter(key => !!key)

View file

@ -8,7 +8,6 @@ const got = require('got');
const { const {
exportSecrets, exportSecrets,
parseSecretsInput, parseSecretsInput,
parseResponse,
parseHeadersInput parseHeadersInput
} = require('./action'); } = require('./action');
@ -104,7 +103,7 @@ describe('parseSecretsInput', () => {
describe('parseHeaders', () => { describe('parseHeaders', () => {
it('parses simple header', () => { it('parses simple header', () => {
when(core.getInput) when(core.getInput)
.calledWith('extraHeaders') .calledWith('extraHeaders', undefined)
.mockReturnValueOnce('TEST: 1'); .mockReturnValueOnce('TEST: 1');
const result = parseHeadersInput('extraHeaders'); const result = parseHeadersInput('extraHeaders');
expect(Array.from(result)).toContainEqual(['test', '1']); expect(Array.from(result)).toContainEqual(['test', '1']);
@ -112,7 +111,7 @@ describe('parseHeaders', () => {
it('parses simple header with whitespace', () => { it('parses simple header with whitespace', () => {
when(core.getInput) when(core.getInput)
.calledWith('extraHeaders') .calledWith('extraHeaders', undefined)
.mockReturnValueOnce(` .mockReturnValueOnce(`
TEST: 1 TEST: 1
`); `);
@ -122,7 +121,7 @@ describe('parseHeaders', () => {
it('parses multiple headers', () => { it('parses multiple headers', () => {
when(core.getInput) when(core.getInput)
.calledWith('extraHeaders') .calledWith('extraHeaders', undefined)
.mockReturnValueOnce(` .mockReturnValueOnce(`
TEST: 1 TEST: 1
FOO: bAr FOO: bAr
@ -134,7 +133,7 @@ describe('parseHeaders', () => {
it('parses null response', () => { it('parses null response', () => {
when(core.getInput) when(core.getInput)
.calledWith('extraHeaders') .calledWith('extraHeaders', undefined)
.mockReturnValueOnce(null); .mockReturnValueOnce(null);
const result = parseHeadersInput('extraHeaders'); const result = parseHeadersInput('extraHeaders');
expect(Array.from(result)).toHaveLength(0); expect(Array.from(result)).toHaveLength(0);
@ -146,29 +145,29 @@ describe('exportSecrets', () => {
jest.resetAllMocks(); jest.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('url') .calledWith('url', expect.anything())
.mockReturnValueOnce('http://vault:8200'); .mockReturnValueOnce('http://vault:8200');
when(core.getInput) when(core.getInput)
.calledWith('token') .calledWith('token', expect.anything())
.mockReturnValueOnce('EXAMPLE'); .mockReturnValueOnce('EXAMPLE');
}); });
function mockInput(key) { function mockInput(key) {
when(core.getInput) when(core.getInput)
.calledWith('secrets') .calledWith('secrets', expect.anything())
.mockReturnValueOnce(key); .mockReturnValueOnce(key);
} }
function mockVersion(version) { function mockVersion(version) {
when(core.getInput) when(core.getInput)
.calledWith('kv-version') .calledWith('kv-version', expect.anything())
.mockReturnValueOnce(version); .mockReturnValueOnce(version);
} }
function mockExtraHeaders(headerString) { function mockExtraHeaders(headerString) {
when(core.getInput) when(core.getInput)
.calledWith('extraHeaders') .calledWith('extraHeaders', expect.anything())
.mockReturnValueOnce(headerString); .mockReturnValueOnce(headerString);
} }
@ -191,10 +190,16 @@ describe('exportSecrets', () => {
function mockExportToken(doExport) { function mockExportToken(doExport) {
when(core.getInput) when(core.getInput)
.calledWith('exportToken') .calledWith('exportToken', expect.anything())
.mockReturnValueOnce(doExport); .mockReturnValueOnce(doExport);
} }
function mockEncodeType(doEncode) {
when(core.getInput)
.calledWith('secretEncodingType', expect.anything())
.mockReturnValueOnce(doEncode);
}
it('simple secret retrieval', async () => { it('simple secret retrieval', async () => {
mockInput('test key'); mockInput('test key');
mockVaultData({ mockVaultData({
@ -207,6 +212,19 @@ describe('exportSecrets', () => {
expect(core.setOutput).toBeCalledWith('key', '1'); 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 () => { it('intl secret retrieval', async () => {
mockInput('测试 测试'); mockInput('测试 测试');
mockVaultData({ mockVaultData({
@ -341,4 +359,13 @@ with blank lines
expect(command.issue).toBeCalledWith('add-mask', 'with blank lines'); expect(command.issue).toBeCalledWith('add-mask', 'with blank lines');
expect(core.setOutput).toBeCalledWith('key', multiLineString); 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');
})
}); });

View file

@ -25,7 +25,7 @@ async function retrieveToken(method, client) {
case 'jwt': { case 'jwt': {
/** @type {string} */ /** @type {string} */
let jwt; let jwt;
const role = core.getInput('role', { required: true }); const role = core.getInput('role', { required: false });
const privateKeyRaw = core.getInput('jwtPrivateKey', { required: false }); const privateKeyRaw = core.getInput('jwtPrivateKey', { required: false });
const privateKey = Buffer.from(privateKeyRaw, 'base64').toString(); const privateKey = Buffer.from(privateKeyRaw, 'base64').toString();
const keyPassword = core.getInput('jwtKeyPassword', { required: false }); const keyPassword = core.getInput('jwtKeyPassword', { required: false });

View file

@ -1,7 +1,12 @@
jest.mock('got'); jest.mock('got');
jest.mock('@actions/core'); jest.mock('@actions/core');
jest.mock('@actions/core/lib/command'); 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 core = require('@actions/core');
const got = require('got'); const got = require('got');
@ -16,7 +21,7 @@ const {
function mockInput(name, key) { function mockInput(name, key) {
when(core.getInput) when(core.getInput)
.calledWith(name) .calledWith(name, expect.anything())
.mockReturnValueOnce(key); .mockReturnValueOnce(key);
} }

69
src/retries.test.js Normal file
View 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();
});
});
});

View file

@ -34,9 +34,17 @@ async function getSecrets(secretRequests, client) {
body = responseCache.get(requestPath); body = responseCache.get(requestPath);
cachedResponse = true; cachedResponse = true;
} else { } else {
try {
const result = await client.get(requestPath); const result = await client.get(requestPath);
body = result.body; body = result.body;
responseCache.set(requestPath, 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; let value;