Compare commits

..

4 Commits

Author SHA1 Message Date
JM Faircloth 867290158a use lts node 2024-04-15 12:57:33 -05:00
JM Faircloth 790b5eac34 try to test against main 2024-04-15 12:39:23 -05:00
JM Faircloth 074f580c7d export default 2024-04-15 12:27:27 -05:00
JM Faircloth 14c5ac9697 use node:module ref on import 2024-04-15 12:15:05 -05:00
45 changed files with 12687 additions and 15685 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
actionlint: actionlint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: "Lint workflow files" - name: "Lint workflow files"
uses: docker://docker.mirror.hashicorp.services/rhysd/actionlint:latest uses: docker://docker.mirror.hashicorp.services/rhysd/actionlint:latest
with: with:
+39 -38
View File
@@ -6,14 +6,14 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
with: with:
node-version: "20.9.0" node-version: '20.12.2'
- name: Setup NPM Cache - name: Setup NPM Cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
with: with:
path: ~/.npm path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -33,17 +33,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Run docker compose - name: Run docker-compose
run: docker compose up -d vault run: docker-compose up -d vault
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
with: with:
node-version: "20.9.0" node-version: '20.12.2'
- name: Setup NPM Cache - name: Setup NPM Cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
with: with:
path: ~/.npm path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -67,19 +67,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Run docker compose - name: Run docker-compose
run: docker compose up -d vault-enterprise run: docker-compose up -d vault-enterprise
env: env:
VAULT_LICENSE_CI: ${{ secrets.VAULT_LICENSE_CI }} VAULT_LICENSE_CI: ${{ secrets.VAULT_LICENSE_CI }}
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
with: with:
node-version: "20.9.0" node-version: '20.12.2'
- name: Setup NPM Cache - name: Setup NPM Cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
with: with:
path: ~/.npm path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -103,17 +103,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Run docker compose - name: Run docker-compose
run: docker compose up -d vault run: docker-compose up -d vault
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
with: with:
node-version: "20.9.0" node-version: '20.12.2'
- name: Setup NPM Cache - name: Setup NPM Cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
with: with:
path: ~/.npm path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -133,7 +133,7 @@ jobs:
VAULT_PORT: 8200 VAULT_PORT: 8200
- name: Test Vault Action (default KV V2) - name: Test Vault Action (default KV V2)
uses: ./ uses: hashicorp/vault-action@main
id: kv-secrets id: kv-secrets
with: with:
url: http://localhost:8200 url: http://localhost:8200
@@ -144,7 +144,7 @@ jobs:
secret/data/nested/test otherSecret ; secret/data/nested/test otherSecret ;
- name: Test Vault Action (default KV V1) - name: Test Vault Action (default KV V1)
uses: ./ uses: hashicorp/vault-action@main
with: with:
url: http://localhost:8200 url: http://localhost:8200
token: testtoken token: testtoken
@@ -154,7 +154,7 @@ jobs:
my-secret/nested/test otherAltSecret ; my-secret/nested/test otherAltSecret ;
- name: Test Vault Action (cubbyhole) - name: Test Vault Action (cubbyhole)
uses: ./ uses: hashicorp/vault-action@main
with: with:
url: http://localhost:8200 url: http://localhost:8200
token: testtoken token: testtoken
@@ -165,7 +165,7 @@ jobs:
# The ordering of these two Test Vault Action Overwrites Env Vars In Subsequent Action steps matters # The ordering of these two Test Vault Action Overwrites Env Vars In Subsequent Action steps matters
# They should come before the Verify Vault Action Outputs step # They should come before the Verify Vault Action Outputs step
- name: Test Vault Action Overwrites Env Vars In Subsequent Action (part 1/2) - name: Test Vault Action Overwrites Env Vars In Subsequent Action (part 1/2)
uses: ./ uses: hashicorp/vault-action@main
with: with:
url: http://localhost:8200/ url: http://localhost:8200/
token: testtoken token: testtoken
@@ -173,7 +173,7 @@ jobs:
secret/data/test secret | SUBSEQUENT_TEST_SECRET; secret/data/test secret | SUBSEQUENT_TEST_SECRET;
- name: Test Vault Action Overwrites Env Vars In Subsequent Action (part 2/2) - name: Test Vault Action Overwrites Env Vars In Subsequent Action (part 2/2)
uses: ./ uses: hashicorp/vault-action@main
with: with:
url: http://localhost:8200/ url: http://localhost:8200/
token: testtoken token: testtoken
@@ -181,7 +181,7 @@ jobs:
secret/data/subsequent-test secret | SUBSEQUENT_TEST_SECRET; secret/data/subsequent-test secret | SUBSEQUENT_TEST_SECRET;
- name: Test JSON Secrets - name: Test JSON Secrets
uses: ./ uses: hashicorp/vault-action@main
with: with:
url: http://localhost:8200 url: http://localhost:8200
token: testtoken token: testtoken
@@ -195,21 +195,22 @@ jobs:
env: env:
OTHER_SECRET_OUTPUT: ${{ steps.kv-secrets.outputs.otherSecret }} OTHER_SECRET_OUTPUT: ${{ steps.kv-secrets.outputs.otherSecret }}
e2e-tls: e2e-tls:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- 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@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
with: with:
node-version: "20.9.0" node-version: '20.12.2'
- name: Setup NPM Cache - name: Setup NPM Cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
with: with:
path: ~/.npm path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -232,7 +233,7 @@ jobs:
VAULT_CLIENT_KEY: ${{ secrets.VAULT_CLIENT_KEY }} VAULT_CLIENT_KEY: ${{ secrets.VAULT_CLIENT_KEY }}
- name: Test Vault Action (default KV V2) - name: Test Vault Action (default KV V2)
uses: ./ uses: hashicorp/vault-action@main
id: kv-secrets-tls id: kv-secrets-tls
with: with:
url: https://localhost:8200 url: https://localhost:8200
@@ -246,7 +247,7 @@ jobs:
secret/data/nested/test otherSecret ; secret/data/nested/test otherSecret ;
- name: Test Vault Action (tlsSkipVerify) - name: Test Vault Action (tlsSkipVerify)
uses: ./ uses: hashicorp/vault-action@main
with: with:
url: https://localhost:8200 url: https://localhost:8200
token: ${{ env.VAULT_TOKEN }} token: ${{ env.VAULT_TOKEN }}
@@ -257,7 +258,7 @@ jobs:
secret/data/tlsSkipVerify skip ; secret/data/tlsSkipVerify skip ;
- name: Test Vault Action (default KV V1) - name: Test Vault Action (default KV V1)
uses: ./ uses: hashicorp/vault-action@main
with: with:
url: https://localhost:8200 url: https://localhost:8200
token: ${{ env.VAULT_TOKEN }} token: ${{ env.VAULT_TOKEN }}
@@ -270,7 +271,7 @@ jobs:
my-secret/nested/test otherAltSecret ; my-secret/nested/test otherAltSecret ;
- name: Test Vault Action (cubbyhole) - name: Test Vault Action (cubbyhole)
uses: ./ uses: hashicorp/vault-action@main
with: with:
url: https://localhost:8200 url: https://localhost:8200
token: ${{ env.VAULT_TOKEN }} token: ${{ env.VAULT_TOKEN }}
+3 -3
View File
@@ -18,9 +18,9 @@ jobs:
name: local-test name: local-test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
with: with:
node-version: '20.9.0' node-version: '20.9.0'
@@ -50,7 +50,7 @@ jobs:
secret/data/test-json-string jsonString; secret/data/test-json-string jsonString;
secret/data/test-json-data jsonData; secret/data/test-json-data jsonData;
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - uses: actions/github-script@v7
with: with:
github-token: "foobar" github-token: "foobar"
script: | script: |
+4 -18
View File
@@ -1,28 +1,14 @@
## 3.3.0 (March 3, 2025) ## Unreleased
Features:
* Wildcard secret imports can use `**` to retain case of exported env keys [GH-545](https://github.com/hashicorp/vault-action/pull/545)
## 3.2.0 (March 3, 2025)
Improvements: Improvements:
* Add retry for jwt auth login to fix intermittent login failures [GH-574](https://github.com/hashicorp/vault-action/pull/574) * chore(deps): bump jsonata from 2.0.3 to 2.0.4 [GH-531](https://github.com/hashicorp/vault-action/pull/531)
* chore(deps): bump got from 11.8.6 to 14.2.1 [GH-533](https://github.com/hashicorp/vault-action/pull/533)
## 3.1.0 (January 9, 2025) * convert project from common js to an esm module [GH-533](https://github.com/hashicorp/vault-action/pull/533)
Improvements:
* fix wildcard handling when field contains dot [GH-542](https://github.com/hashicorp/vault-action/pull/542)
* bump body-parser from 1.20.0 to 1.20.3
* bump braces from 3.0.2 to 3.0.3
* bump cross-spawn from 7.0.3 to 7.0.6
* bump micromatch from 4.0.5 to 4.0.8
Features: Features:
* `secretId` is no longer required for approle to support advanced use cases like machine login when `bind_secret_id` is false. [GH-522](https://github.com/hashicorp/vault-action/pull/522) * `secretId` is no longer required for approle to support advanced use cases like machine login when `bind_secret_id` is false. [GH-522](https://github.com/hashicorp/vault-action/pull/522)
* Use `pki` configuration to generate certificates from Vault [GH-564](https://github.com/hashicorp/vault-action/pull/564)
## 3.0.0 (February 15, 2024) ## 3.0.0 (February 15, 2024)
+1 -1
View File
@@ -1,3 +1,3 @@
.PHONY: local-test .PHONY: local-test
local-test: local-test:
docker compose down; docker compose up -d vault && act workflow_dispatch -j local-test -W .github/workflows/local-test.yaml docker compose down; docker-compose up -d vault && act workflow_dispatch -j local-test
+17 -88
View File
@@ -30,7 +30,6 @@ is not meant to modify Vaults state.
- [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)
- [KV secrets engine version 2](#kv-secrets-engine-version-2)
- [Other Secret Engines](#other-secret-engines) - [Other Secret Engines](#other-secret-engines)
- [Adding Extra Headers](#adding-extra-headers) - [Adding Extra Headers](#adding-extra-headers)
- [HashiCorp Cloud Platform or Vault Enterprise](#hashicorp-cloud-platform-or-vault-enterprise) - [HashiCorp Cloud Platform or Vault Enterprise](#hashicorp-cloud-platform-or-vault-enterprise)
@@ -65,32 +64,27 @@ jobs:
``` ```
Retrieved secrets are available as environment variables or outputs for subsequent steps: Retrieved secrets are available as environment variables or outputs for subsequent steps:
```yaml ```yaml
#... #...
- name: Step following 'Import Secrets' - name: Step following 'Import Secrets'
run: | run: |
ACCESS_KEY_ID = "${{ env.AWS_ACCESS_KEY_ID }}" ACCESS_KEY_ID = "${{ env.AWS_ACCESS_KEY_ID }}"
SECRET_ACCESS_KEY = "${{ steps.import-secrets.outputs.AWS_SECRET_ACCESS_KEY }}" SECRET_ACCESS_KEY = "${{ steps.import-secrets.outputs.AWS_SECRET_ACCESS_KEY }}"
# ... # ...
``` ```
If your project needs a format other than env vars and step outputs, you can use additional steps to transform them into the desired format. If your project needs a format other than env vars and step outputs, you can use additional steps to transform them into the desired format.
For example, a common pattern is to save all the secrets in a JSON file: For example, a common pattern is to save all the secrets in a JSON file:
```yaml ```yaml
#... #...
- name: Step following 'Import Secrets' - name: Step following 'Import Secrets'
run: | run: |
touch secrets.json touch secrets.json
echo '${{ toJson(steps.import-secrets.outputs) }}' >> secrets.json echo '${{ toJson(steps.import-secrets.outputs) }}' >> secrets.json
# ... # ...
``` ```
Which with our example would yield a file containing: Which with our example would yield a file containing:
```json ```json
{ {
"ACCESS_KEY_ID": "MY_KEY_ID", "ACCESS_KEY_ID": "MY_KEY_ID",
@@ -101,6 +95,7 @@ Which with our example would yield a file containing:
Note that all secrets are masked so programs need to read the file themselves otherwise all values will be replaced with a `***` placeholder. Note that all secrets are masked so programs need to read the file themselves otherwise all values will be replaced with a `***` placeholder.
## Authentication Methods ## Authentication Methods
Consider using a [Vault authentication method](https://www.vaultproject.io/docs/auth) such as the JWT auth method with Consider using a [Vault authentication method](https://www.vaultproject.io/docs/auth) such as the JWT auth method with
@@ -114,7 +109,7 @@ and Vault using the
Each GitHub Actions workflow receives an auto-generated OIDC token with claims Each GitHub Actions workflow receives an auto-generated OIDC token with claims
to establish the identity of the workflow. to establish the identity of the workflow.
**Vault Configuration** __Vault Configuration__
<details> <details>
<summary>Click to toggle instructions for configuring Vault.</summary> <summary>Click to toggle instructions for configuring Vault.</summary>
@@ -125,6 +120,7 @@ Pass the following parameters to your auth method configuration:
- `oidc_discovery_url`: `https://token.actions.githubusercontent.com` - `oidc_discovery_url`: `https://token.actions.githubusercontent.com`
- `bound_issuer`: `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. Configure a [Vault role](https://www.vaultproject.io/api/auth/jwt#create-role) for the auth method.
- `role_type`: `jwt` - `role_type`: `jwt`
@@ -158,7 +154,7 @@ Configure a [Vault role](https://www.vaultproject.io/api/auth/jwt#create-role) f
</details> </details>
**GitHub Actions Workflow** __GitHub Actions Workflow__
In the GitHub Actions workflow, the workflow needs permissions to read contents In the GitHub Actions workflow, the workflow needs permissions to read contents
and write the ID token. and write the ID token.
@@ -382,6 +378,7 @@ steps:
# Import config... # Import config...
- name: Sensitive Operation - name: Sensitive Operation
run: "my-cli --token '${{ steps.secrets.outputs.NPM_TOKEN }}'" run: "my-cli --token '${{ steps.secrets.outputs.NPM_TOKEN }}'"
``` ```
### Multiple Secrets ### Multiple Secrets
@@ -394,8 +391,7 @@ with:
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
``` ```
You can specify a wildcard * for the key name to get all keys in the path. If you provide an output name with the wildcard, the name will be prepended to the key name:
You can specify a wildcard \* for the key name to get all keys in the path. If you provide an output name with the wildcard, the name will be prepended to the key name:
```yaml ```yaml
with: with:
@@ -403,60 +399,12 @@ with:
secret/data/ci/aws * | MYAPP_ ; secret/data/ci/aws * | MYAPP_ ;
``` ```
When using the `exportEnv` option all exported keys will be normalized to uppercase. For example, the key `SecretKey` would be exported as `MYAPP_SECRETKEY`.
You can disable uppercase normalization by specifying double asterisks `**` in the selector path:
```yaml
with:
secrets: |
secret/data/ci/aws ** | MYAPP_ ;
```
### KV secrets engine version 2
When accessing secrets from the KV secrets engine version 2, Vault Action
requires the full path to the secret. This is the same path that would be used
in a Vault policy for the secret. You can find the full path to your secret by
performing a `kv get` command like the following:
```bash
$ vault kv get secret/test
== Secret Path ==
secret/data/test
...
```
Note that the full path is not `secret/test`, but `secret/data/test`.
## PKI Certificate Requests
You can use the `pki` option to generate a certificate and private key for a given role.
````yaml
with:
pki: |
pki/issue/rolename {"common_name": "role.mydomain.com", "ttl": "1h"} ;
pki/issue/otherrole {"common_name": "otherrole.mydomain.com", "ttl": "1h"} ;
```
Resulting in:
```bash
ROLENAME_CA=-----BEGIN CERTIFICATE-----...
ROLENAME_CERT=-----BEGIN CERTIFICATE-----...
ROLENAME_KEY=-----BEGIN RSA PRIVATE KEY-----...
ROLENAME_CA_CHAIN=-----BEGIN CERTIFICATE-----...
OTHERROLE_CA=-----BEGIN CERTIFICATE-----...
OTHERROLE_CERT=-----BEGIN CERTIFICATE-----...
OTHERROLE_KEY=-----BEGIN RSA PRIVATE KEY-----...
OTHERROLE_CA_CHAIN=-----BEGIN CERTIFICATE-----...
````
## Other Secret Engines ## Other Secret Engines
Vault Action currently supports retrieving secrets from any engine where secrets Vault Action currently supports retrieving secrets from any engine where secrets
are retrieved via `GET` requests, except for the PKI engine as noted above. are retrieved via `GET` requests. This means secret engines such as PKI are currently
not supported due to their requirement of sending parameters along with the request
(such as `common_name`).
For example, to request a secret from the `cubbyhole` secret engine: For example, to request a secret from the `cubbyhole` secret engine:
@@ -493,8 +441,8 @@ If you ever need to add extra headers to the vault request, say if you need to a
```yaml ```yaml
with: with:
secrets: | secrets: |
secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ; secret/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY secret/ci/aws secretKey | AWS_SECRET_ACCESS_KEY
extraHeaders: | extraHeaders: |
X-Secure-Id: ${{ secrets.SECURE_ID }} X-Secure-Id: ${{ secrets.SECURE_ID }}
X-Secure-Secret: ${{ secrets.SECURE_SECRET }} X-Secure-Secret: ${{ secrets.SECURE_SECRET }}
@@ -520,32 +468,14 @@ 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
token: ${{ secrets.VAULT_TOKEN }} token: ${{ secrets.VAULT_TOKEN }}
namespace: admin namespace: admin
secrets: | secrets: |
secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ; secret/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ; secret/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
secret/data/ci npm_token secret/ci npm_token
```
Alternatively, you may need to authenticate to the root namespace and retrieve
a secret from a different namespace. To do this, do not set the `namespace`
parameter. Instead set the namespace in the secret path. For example, `<NAMESPACE>/secret/data/app`:
```yaml
steps:
# ...
- name: Import Secrets
uses: hashicorp/vault-action
with:
url: https://vault-enterprise.mycompany.com:8200
method: token
token: ${{ secrets.VAULT_TOKEN }}
secrets: |
namespace-1/secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
namespace-1/secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
namespace-1/secret/data/ci npm_token
``` ```
## Reference ## Reference
@@ -738,10 +668,9 @@ To make it simpler to consume certain secrets as env vars, if no Env/Output Var
## Contributing ## Contributing
If you wish to contribute to this project, the following dependencies are recommended for local development: If you wish to contribute to this project, the following dependencies are recommended for local development:
- [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) to install dependencies, build project and run tests - [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) to install dependencies, build project and run tests
- [docker](https://docs.docker.com/get-docker/) to run the pre-configured vault containers for acceptance tests - [docker](https://docs.docker.com/get-docker/) to run the pre-configured vault containers for acceptance tests
- [docker compose](https://docs.docker.com/compose/) to spin up the pre-configured vault containers for acceptance tests - [docker-compose](https://docs.docker.com/compose/) to spin up the pre-configured vault containers for acceptance tests
- [act](https://github.com/nektos/act) to run the vault-action locally - [act](https://github.com/nektos/act) to run the vault-action locally
### Build ### Build
@@ -758,7 +687,7 @@ The Github Action needs access to a working Vault instance to function.
Multiple docker configurations are available via the docker-compose.yml file to run containers compatible with the various acceptance test suites. Multiple docker configurations are available via the docker-compose.yml file to run containers compatible with the various acceptance test suites.
```sh ```sh
$ docker compose up -d vault # Choose one of: vault, vault-enterprise, vault-tls depending on which tests you would like to run $ docker-compose up -d vault # Choose one of: vault, vault-enterprise, vault-tls depending on which tests you would like to run
``` ```
Instead of using one of the dockerized instance, you can also use your own local or remote Vault instance by exporting these environment variables: Instead of using one of the dockerized instance, you can also use your own local or remote Vault instance by exporting these environment variables:
-3
View File
@@ -7,9 +7,6 @@ inputs:
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: false required: false
pki:
description: 'A semicolon-separated list of certificates to generate. These will automatically be converted to environment variable keys. Cannot be used with "secrets". See README for more details'
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
+8923 -13765
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+8 -7
View File
@@ -1,11 +1,12 @@
jest.mock('@actions/core'); import { vi, describe, test, expect } from 'vitest';
jest.mock('@actions/core/lib/command');
const core = require('@actions/core');
const got = require('got'); vi.mock('@actions/core');
const { when } = require('jest-when'); import core from '@actions/core';
const { exportSecrets } = require('../../src/action'); import got from 'got';
import { when } from 'jest-when'
import { exportSecrets } from '../../src/action.js';
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`; const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}` const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
@@ -102,7 +103,7 @@ describe('authenticate with approle', () => {
}); });
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); vi.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('method', expect.anything()) .calledWith('method', expect.anything())
+9 -134
View File
@@ -1,11 +1,12 @@
jest.mock('@actions/core'); import { vi, describe, test, expect } from 'vitest';
jest.mock('@actions/core/lib/command');
const core = require('@actions/core');
const got = require('got'); vi.mock('@actions/core');
const { when } = require('jest-when'); import core from '@actions/core';
const { exportSecrets } = require('../../src/action'); import got from 'got';
import { when } from 'jest-when'
import { exportSecrets } from '../../src/action.js';
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`; const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}` const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
@@ -31,14 +32,6 @@ describe('integration', () => {
}, },
}); });
await got(`${vaultUrl}/v1/secret/data/test-with-dot-char`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken,
},
body: `{"data":{"secret.foo":"SUPERSECRET"}}`
});
await got(`${vaultUrl}/v1/secret/data/nested/test`, { await got(`${vaultUrl}/v1/secret/data/nested/test`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -112,73 +105,10 @@ describe('integration', () => {
"other-Secret-dash": 'OTHERCUSTOMSECRET', "other-Secret-dash": 'OTHERCUSTOMSECRET',
}, },
}); });
// Enable pki engine
try {
await got(`${vaultUrl}/v1/sys/mounts/pki`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken,
},
json: {
type: 'pki'
}
});
} 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;
}
}
// Configure Root CA
try {
await got(`${vaultUrl}/v1/pki/root/generate/internal`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken,
},
json: {
common_name: 'test',
ttl: '24h',
},
});
} catch (error) {
const {response} = error;
if (response.statusCode === 400 && response.body.includes("already exists")) {
// Root CA might already be configured from previous test runs
} else {
throw error;
}
}
// Configure PKI Role
try {
await got(`${vaultUrl}/v1/pki/roles/Test`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken,
},
json: {
allowed_domains: ['test'],
allow_bare_domains: true,
max_ttl: '1h',
},
});
} catch (error) {
const {response} = error;
if (response.statusCode === 400 && response.body.includes("already exists")) {
// Role might already be configured from previous test runs
} else {
throw error;
}
}
}); });
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); vi.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('url', expect.anything()) .calledWith('url', expect.anything())
@@ -195,12 +125,6 @@ describe('integration', () => {
.mockReturnValueOnce(secrets); .mockReturnValueOnce(secrets);
} }
function mockPkiInput(pki) {
when(core.getInput)
.calledWith('pki', expect.anything())
.mockReturnValueOnce(pki);
}
function mockIgnoreNotFound(shouldIgnore) { function mockIgnoreNotFound(shouldIgnore) {
when(core.getInput) when(core.getInput)
.calledWith('ignoreNotFound', expect.anything()) .calledWith('ignoreNotFound', expect.anything())
@@ -231,19 +155,6 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('NAMED_SECRET', 'SUPERSECRET'); expect(core.exportVariable).toBeCalledWith('NAMED_SECRET', 'SUPERSECRET');
}) })
it('gets a pki certificate', async () => {
mockPkiInput('pki/issue/Test {"common_name":"test","ttl":"1h"}');
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(4);
expect(core.exportVariable).toBeCalledWith('TEST_KEY', expect.anything());
expect(core.exportVariable).toBeCalledWith('TEST_CERT', expect.anything());
expect(core.exportVariable).toBeCalledWith('TEST_CA', expect.anything());
expect(core.exportVariable).toBeCalledWith('TEST_CA_CHAIN', expect.anything());
});
it('get simple secret', async () => { it('get simple secret', async () => {
mockInput('secret/data/test secret'); mockInput('secret/data/test secret');
@@ -283,16 +194,6 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('OTHERSECRETDASH', 'OTHERSUPERSECRET'); expect(core.exportVariable).toBeCalledWith('OTHERSECRETDASH', 'OTHERSUPERSECRET');
}); });
it('get wildcard secrets with dot char', async () => {
mockInput(`secret/data/test-with-dot-char * ;`);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(1);
expect(core.exportVariable).toBeCalledWith('SECRET__FOO', 'SUPERSECRET');
});
it('get wildcard secrets', async () => { it('get wildcard secrets', async () => {
mockInput(`secret/data/test * ;`); mockInput(`secret/data/test * ;`);
@@ -395,7 +296,7 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('FOO', 'bar'); expect(core.exportVariable).toBeCalledWith('FOO', 'bar');
}); });
it('wildcard supports cubbyhole with uppercase transform', async () => { it('wildcard supports cubbyhole', async () => {
mockInput('/cubbyhole/test *'); mockInput('/cubbyhole/test *');
await exportSecrets(); await exportSecrets();
@@ -406,32 +307,6 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('ZIP', 'zap'); expect(core.exportVariable).toBeCalledWith('ZIP', 'zap');
}); });
it('wildcard supports cubbyhole with no change in case', async () => {
mockInput('/cubbyhole/test **');
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(2);
expect(core.exportVariable).toBeCalledWith('foo', 'bar');
expect(core.exportVariable).toBeCalledWith('zip', 'zap');
});
it('wildcard supports cubbyhole with mixed case change', async () => {
mockInput(`
/cubbyhole/test * ;
/cubbyhole/test **`);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(4);
expect(core.exportVariable).toBeCalledWith('FOO', 'bar');
expect(core.exportVariable).toBeCalledWith('ZIP', 'zap');
expect(core.exportVariable).toBeCalledWith('foo', 'bar');
expect(core.exportVariable).toBeCalledWith('zip', 'zap');
});
it('caches responses', async () => { it('caches responses', async () => {
mockInput(` mockInput(`
/cubbyhole/test foo ; /cubbyhole/test foo ;
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
verbose: true
};
+29 -48
View File
@@ -1,17 +1,18 @@
jest.mock('@actions/core'); import { vi, describe, test, expect } from 'vitest';
jest.mock('@actions/core/lib/command');
const core = require('@actions/core'); vi.mock('@actions/core');
const rsasign = require('jsrsasign'); import core from '@actions/core';
const { import rsasign from 'jsrsasign';
import {
privateRsaKey, privateRsaKey,
privateRsaKeyBase64, privateRsaKeyBase64,
publicRsaKey publicRsaKey
} = require('./rsa_keys'); } from './rsa_keys.js';
const got = require('got'); import got from 'got';
const { when } = require('jest-when'); import { when } from 'jest-when'
const { exportSecrets } = require('../../src/action'); import { exportSecrets } from '../../src/action.js';
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`; const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}` const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
@@ -97,8 +98,6 @@ describe('jwt auth', () => {
} }
}); });
// write the jwt config, the jwt role will be written on a per-test
// basis since the audience may vary
await got(`${vaultUrl}/v1/auth/jwt/config`, { await got(`${vaultUrl}/v1/auth/jwt/config`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -110,6 +109,22 @@ describe('jwt auth', () => {
} }
}); });
await got(`${vaultUrl}/v1/auth/jwt/role/default`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken,
},
json: {
role_type: 'jwt',
bound_audiences: null,
bound_claims: {
iss: 'vault-action'
},
user_claim: 'iss',
policies: ['reader']
}
});
await got(`${vaultUrl}/v1/secret/data/test`, { await got(`${vaultUrl}/v1/secret/data/test`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -124,26 +139,8 @@ describe('jwt auth', () => {
}); });
describe('authenticate with private key', () => { describe('authenticate with private key', () => {
beforeAll(async () => {
await got(`${vaultUrl}/v1/auth/jwt/role/default`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken,
},
json: {
role_type: 'jwt',
bound_audiences: null,
bound_claims: {
iss: 'vault-action'
},
user_claim: 'iss',
policies: ['reader']
}
});
});
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); vi.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('url', expect.anything()) .calledWith('url', expect.anything())
@@ -174,22 +171,6 @@ describe('jwt auth', () => {
describe('authenticate with Github OIDC', () => { describe('authenticate with Github OIDC', () => {
beforeAll(async () => { beforeAll(async () => {
await got(`${vaultUrl}/v1/auth/jwt/role/default`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken,
},
json: {
role_type: 'jwt',
bound_audiences: 'https://github.com/hashicorp/vault-action',
bound_claims: {
iss: 'vault-action'
},
user_claim: 'iss',
policies: ['reader']
}
});
await got(`${vaultUrl}/v1/auth/jwt/role/default-sigstore`, { await got(`${vaultUrl}/v1/auth/jwt/role/default-sigstore`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -197,7 +178,7 @@ describe('jwt auth', () => {
}, },
json: { json: {
role_type: 'jwt', role_type: 'jwt',
bound_audiences: 'sigstore', bound_audiences: null,
bound_claims: { bound_claims: {
iss: 'vault-action', iss: 'vault-action',
aud: 'sigstore', aud: 'sigstore',
@@ -209,7 +190,7 @@ describe('jwt auth', () => {
}) })
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); vi.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('url', expect.anything()) .calledWith('url', expect.anything())
+1 -1
View File
@@ -42,7 +42,7 @@ LrJEY9tZaRF8xraMZiOcBcyAt6S/TS29HttJ6+zlhcWx34fItEZ8jA5gzhTmspOY
-----END PUBLIC KEY----- -----END PUBLIC KEY-----
`; `;
module.exports = { export {
privateRsaKey, privateRsaKey,
privateRsaKeyBase64, privateRsaKeyBase64,
publicRsaKey publicRsaKey
+8 -7
View File
@@ -1,11 +1,12 @@
jest.mock('@actions/core'); import { vi, describe, test, expect } from 'vitest';
jest.mock('@actions/core/lib/command');
const core = require('@actions/core');
const got = require('got'); vi.mock('@actions/core');
const { when } = require('jest-when'); import core from '@actions/core';
const { exportSecrets } = require('../../src/action'); import got from 'got';
import { when } from 'jest-when'
import { exportSecrets } from '../../src/action.js';
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`; const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}` const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
@@ -84,7 +85,7 @@ describe('authenticate with userpass', () => {
}); });
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); vi.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('method', expect.anything()) .calledWith('method', expect.anything())
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig, configDefaults } from 'vitest/config'
export default defineConfig({
test: {
// required to make jest-when work with vitest
globals: true,
include: [
'**/integrationTests/basic/**.{test,spec}.?(c|m)[jt]s?(x)',
],
},
})
+2
View File
@@ -1,3 +1,5 @@
import { describe, test, expect } from 'vitest';
describe('e2e-tls', () => { describe('e2e-tls', () => {
it('verify', () => { it('verify', () => {
expect(process.env.SECRET).toBe("SUPERSECRET"); expect(process.env.SECRET).toBe("SUPERSECRET");
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
verbose: true
};
+2 -2
View File
@@ -1,5 +1,5 @@
const got = require('got'); import got from 'got';
const core = require('@actions/core'); import core from '@actions/core';
const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`; const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
const caCertificateRaw = `${process.env.VAULTCA}`; const caCertificateRaw = `${process.env.VAULTCA}`;
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig, configDefaults } from 'vitest/config'
export default defineConfig({
test: {
// required to make jest-when work with vitest
globals: true,
include: [
'**/integrationTests/e2e-tls/**.{test,spec}.?(c|m)[jt]s?(x)',
],
},
})
+2
View File
@@ -1,3 +1,5 @@
import { describe, test, expect } from 'vitest';
describe('e2e', () => { describe('e2e', () => {
it('verify', () => { it('verify', () => {
expect(process.env.SECRET).toBe("SUPERSECRET"); expect(process.env.SECRET).toBe("SUPERSECRET");
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
verbose: true
};
+1 -1
View File
@@ -1,4 +1,4 @@
const got = require('got'); import got from 'got';
const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`; const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
const vaultToken = `${process.env.VAULT_TOKEN}` === undefined ? `${process.env.VAULT_TOKEN}` : "testtoken"; const vaultToken = `${process.env.VAULT_TOKEN}` === undefined ? `${process.env.VAULT_TOKEN}` : "testtoken";
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig, configDefaults } from 'vitest/config'
export default defineConfig({
test: {
// required to make jest-when work with vitest
globals: true,
include: [
'**/integrationTests/e2e/**.{test,spec}.?(c|m)[jt]s?(x)',
],
},
})
@@ -1,11 +1,12 @@
jest.mock('@actions/core'); import { vi, describe, test, expect } from 'vitest';
jest.mock('@actions/core/lib/command');
const core = require('@actions/core');
const got = require('got'); vi.mock('@actions/core');
const { when } = require('jest-when'); import core from '@actions/core';
const { exportSecrets } = require('../../src/action'); import got from 'got';
import { when } from 'jest-when'
import { exportSecrets } from '../../src/action.js';
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8201'}`; const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8201'}`;
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}` const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
@@ -41,7 +42,7 @@ describe('integration', () => {
}); });
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); vi.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('url', expect.anything()) .calledWith('url', expect.anything())
@@ -238,7 +239,7 @@ describe('authenticate with approle', () => {
}); });
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); vi.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('method', expect.anything()) .calledWith('method', expect.anything())
@@ -1,3 +0,0 @@
module.exports = {
verbose: true
};
@@ -0,0 +1,11 @@
import { defineConfig, configDefaults } from 'vitest/config'
export default defineConfig({
test: {
// required to make jest-when work with vitest
globals: true,
include: [
'**/integrationTests/enterprise/**.{test,spec}.?(c|m)[jt]s?(x)',
],
},
})
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/integrationTests/'],
};
+3078 -883
View File
File diff suppressed because it is too large Load Diff
+11 -13
View File
@@ -2,14 +2,15 @@
"name": "vault-action", "name": "vault-action",
"version": "0.1.0", "version": "0.1.0",
"description": "A Github Action that allows you to consume vault secrets as secure environment variables.", "description": "A Github Action that allows you to consume vault secrets as secure environment variables.",
"type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"build": "ncc build src/entry.js -o dist", "build": "ncc build src/entry.js -o dist",
"test": "jest", "test": "vitest run",
"test:integration:basic": "jest -c integrationTests/basic/jest.config.js", "test:integration:basic": "vitest run -c integrationTests/basic/vitest.config.js",
"test:integration:enterprise": "jest -c integrationTests/enterprise/jest.config.js", "test:integration:enterprise": "vitest run -c integrationTests/enterprise/vitest.config.js",
"test:integration:e2e": "jest -c integrationTests/e2e/jest.config.js", "test:integration:e2e": "vitest run -c integrationTests/e2e/vitest.config.js",
"test:integration:e2e-tls": "jest -c integrationTests/e2e-tls/jest.config.js" "test:integration:e2e-tls": "vitest run -c integrationTests/e2e-tls/vitest.config.js"
}, },
"files": [ "files": [
"src/**/*", "src/**/*",
@@ -34,18 +35,15 @@
}, },
"homepage": "https://github.com/hashicorp/vault-action#readme", "homepage": "https://github.com/hashicorp/vault-action#readme",
"dependencies": { "dependencies": {
"got": "^11.8.6", "@actions/core": "^1.10.1",
"jsonata": "^2.0.3", "got": "^14.2.1",
"jsonata": "^2.0.4",
"jsrsasign": "^11.0.0" "jsrsasign": "^11.0.0"
}, },
"peerDependencies": {
"@actions/core": ">=1 <2"
},
"devDependencies": { "devDependencies": {
"@actions/core": "^1.10.1",
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.38.1",
"jest": "^29.7.0",
"jest-when": "^3.6.0", "jest-when": "^3.6.0",
"mock-http-server": "^1.4.5" "mock-http-server": "^1.4.5",
"vitest": "^1.3.1"
} }
} }
+19 -65
View File
@@ -1,12 +1,14 @@
// @ts-check import core from '@actions/core';
const core = require('@actions/core'); import got from 'got';
const command = require('@actions/core/lib/command');
const got = require('got').default;
const jsonata = require('jsonata');
const { normalizeOutputKey } = require('./utils');
const { WILDCARD, WILDCARD_UPPERCASE } = require('./constants');
const { auth: { retrieveToken }, secrets: { getSecrets }, pki: { getCertificates } } = require('./index'); import { normalizeOutputKey } from './utils.js';
import { WILDCARD } from './constants.js';
import { retrieveToken } from './auth.js';
import { getSecrets } from './secrets.js';
// ncc doesn't compile jsonata imports properly, so we must use our own custom require
import require from "./cjs-require.js";
const jsonata = require('jsonata');
const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass']; const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass'];
const ENCODING_TYPES = ['base64', 'hex', 'utf8']; const ENCODING_TYPES = ['base64', 'hex', 'utf8'];
@@ -22,16 +24,6 @@ async function exportSecrets() {
const secretsInput = core.getInput('secrets', { required: false }); const secretsInput = core.getInput('secrets', { required: false });
const secretRequests = parseSecretsInput(secretsInput); const secretRequests = parseSecretsInput(secretsInput);
const pkiInput = core.getInput('pki', { required: false });
let pkiRequests = [];
if (pkiInput) {
if (secretsInput) {
throw Error('You cannot provide both "secrets" and "pki" inputs.');
}
pkiRequests = parsePkiInput(pkiInput);
}
const secretEncodingType = core.getInput('secretEncodingType', { required: false }); 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();
@@ -94,12 +86,12 @@ async function exportSecrets() {
core.exportVariable('VAULT_TOKEN', `${vaultToken}`); core.exportVariable('VAULT_TOKEN', `${vaultToken}`);
} }
let results = []; const requests = secretRequests.map(request => {
if (pkiRequests.length > 0) { const { path, selector } = request;
results = await getCertificates(pkiRequests, client); return request;
} else { });
results = await getSecrets(secretRequests, client);
} const results = await getSecrets(requests, client);
for (const result of results) { for (const result of results) {
@@ -138,43 +130,6 @@ async function exportSecrets() {
* @property {string} selector * @property {string} selector
*/ */
/**
* Parses a pki input string into key paths and the request parameters.
* @param {string} pkiInput
*/
function parsePkiInput(pkiInput) {
if (!pkiInput) {
return []
}
const secrets = pkiInput
.split(';')
.filter(key => !!key)
.map(key => key.trim())
.filter(key => key.length !== 0);
return secrets.map(secret => {
const path = secret.substring(0, secret.indexOf(' '));
const parameters = secret.substring(secret.indexOf(' ') + 1);
core.debug(` Parsing PKI: ${path} with parameters: ${parameters}`);
if (!path || !parameters) {
throw Error(`You must provide a valid path and parameters. Input: "${secret}"`);
}
let outputVarName = path.split('/').pop();
let envVarName = normalizeOutputKey(outputVarName);
return {
path,
envVarName,
outputVarName,
parameters: JSON.parse(parameters),
};
});
}
/** /**
* Parses a secrets input string into key paths and their resulting environment variable name. * Parses a secrets input string into key paths and their resulting environment variable name.
* @param {string} secretsInput * @param {string} secretsInput
@@ -221,7 +176,7 @@ function parseSecretsInput(secretsInput) {
const selectorAst = jsonata(selectorQuoted).ast(); const selectorAst = jsonata(selectorQuoted).ast();
const selector = selectorQuoted.replace(new RegExp('"', 'g'), ''); const selector = selectorQuoted.replace(new RegExp('"', 'g'), '');
if (selector !== WILDCARD && selector !== WILDCARD_UPPERCASE && (selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) { if (selector !== WILDCARD && (selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) {
throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`); throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`);
} }
@@ -266,9 +221,8 @@ function parseHeadersInput(inputKey, inputOptions) {
}, new Map()); }, new Map());
} }
module.exports = { export {
exportSecrets, exportSecrets,
parseSecretsInput, parseSecretsInput,
parseHeadersInput, parseHeadersInput,
}; }
+10 -10
View File
@@ -1,17 +1,17 @@
jest.mock('got'); import { vi, describe, test, expect } from 'vitest';
jest.mock('@actions/core');
jest.mock('@actions/core/lib/command');
const command = require('@actions/core/lib/command'); vi.mock('got');
const core = require('@actions/core'); vi.mock('@actions/core');
const got = require('got');
const { import core from '@actions/core';
import got from 'got';
import {
exportSecrets, exportSecrets,
parseSecretsInput, parseSecretsInput,
parseHeadersInput parseHeadersInput
} = require('./action'); } from './action.js';
const { when } = require('jest-when'); import { when } from 'jest-when'
describe('parseSecretsInput', () => { describe('parseSecretsInput', () => {
it('parses simple secret', () => { it('parses simple secret', () => {
@@ -132,7 +132,7 @@ describe('parseHeaders', () => {
describe('exportSecrets', () => { describe('exportSecrets', () => {
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); vi.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('url', expect.anything()) .calledWith('url', expect.anything())
+6 -35
View File
@@ -1,12 +1,10 @@
// @ts-check // @ts-check
const core = require('@actions/core'); import core from '@actions/core';
const rsasign = require('jsrsasign'); import * as fs from 'fs';
const fs = require('fs'); import got from 'got';
const { default: got } = require('got'); import rsasign from 'jsrsasign';
const defaultKubernetesTokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token' const defaultKubernetesTokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token'
const retries = 5
const retries_delay = 3000
/*** /***
* Authenticate with Vault and retrieve a Vault token that can be used for requests. * Authenticate with Vault and retrieve a Vault token that can be used for requests.
* @param {string} method * @param {string} method
@@ -37,10 +35,7 @@ async function retrieveToken(method, client) {
const githubAudience = core.getInput('jwtGithubAudience', { required: false }); const githubAudience = core.getInput('jwtGithubAudience', { required: false });
if (!privateKey) { if (!privateKey) {
jwt = await retryAsyncFunction(retries, retries_delay, core.getIDToken, githubAudience) jwt = await core.getIDToken(githubAudience)
.then((result) => {
return result;
});
} else { } else {
jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl)); jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl));
} }
@@ -147,30 +142,6 @@ async function getClientToken(client, method, path, payload) {
} }
} }
/***
* Generic function for retrying an async function
* @param {number} retries
* @param {number} delay
* @param {Function} func
* @param {any[]} args
*/
async function retryAsyncFunction(retries, delay, func, ...args) {
let attempt = 0;
while (attempt < retries) {
try {
const result = await func(...args);
return result;
} catch (error) {
attempt++;
if (attempt < retries) {
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
}
/*** /***
* @typedef {Object} VaultLoginResponse * @typedef {Object} VaultLoginResponse
* @property {{ * @property {{
@@ -183,6 +154,6 @@ async function retryAsyncFunction(retries, delay, func, ...args) {
* }} auth * }} auth
*/ */
module.exports = { export {
retrieveToken, retrieveToken,
}; };
+15 -36
View File
@@ -1,22 +1,20 @@
jest.mock('got'); import { vi, describe, test, expect } from 'vitest';
jest.mock('@actions/core');
jest.mock('@actions/core/lib/command'); vi.mock('got');
jest.mock('fs', () => ({ vi.mock('@actions/core');
stat: jest.fn().mockResolvedValue(null), vi.mock('fs', () => ({
stat: vi.fn().mockResolvedValue(null),
promises: { promises: {
access: jest.fn().mockResolvedValue(null), access: vi.fn().mockResolvedValue(null),
} }
})); }));
const core = require('@actions/core'); import core from '@actions/core';
const got = require('got'); import got from 'got'
const fs = require("fs") import * as fs from 'fs';
const { when } = require('jest-when'); import { when } from 'jest-when'
import { retrieveToken } from './auth.js';
const {
retrieveToken
} = require('./auth');
function mockInput(name, key) { function mockInput(name, key) {
@@ -27,7 +25,7 @@ function mockInput(name, key) {
function mockApiResponse() { function mockApiResponse() {
const response = { body: { auth: { client_token: testToken, renewable: true, policies: [], accessor: "accessor" } } } const response = { body: { auth: { client_token: testToken, renewable: true, policies: [], accessor: "accessor" } } }
got.post = jest.fn() got.post = vi.fn()
got.post.mockReturnValue(response) got.post.mockReturnValue(response)
} }
const testToken = "testoken"; const testToken = "testoken";
@@ -35,7 +33,7 @@ const testToken = "testoken";
describe("test retrival for token", () => { describe("test retrival for token", () => {
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); vi.resetAllMocks();
}); });
it("test retrival with approle", async () => { it("test retrival with approle", async () => {
@@ -76,7 +74,7 @@ describe("test retrival for token", () => {
mockInput("kubernetesTokenPath", testTokenPath) mockInput("kubernetesTokenPath", testTokenPath)
mockInput("role", testRole) mockInput("role", testRole)
mockInput("path", testPath) mockInput("path", testPath)
fs.readFileSync = jest.fn() fs.readFileSync = vi.fn()
fs.readFileSync.mockReturnValueOnce(jwtToken) fs.readFileSync.mockReturnValueOnce(jwtToken)
const token = await retrieveToken(method, got) const token = await retrieveToken(method, got)
expect(token).toEqual(testToken) expect(token).toEqual(testToken)
@@ -85,23 +83,4 @@ describe("test retrival for token", () => {
const url = got.post.mock.calls[0][0] const url = got.post.mock.calls[0][0]
expect(url).toContain('differentK8sPath') expect(url).toContain('differentK8sPath')
}) })
it("test retrieval with jwt", async () => {
const method = "jwt"
const jwtToken = "someTestToken"
const testRole = "testRole"
const privateKeyRaw = ""
mockApiResponse()
mockInput("role", testRole)
mockInput("jwtPrivateKey", privateKeyRaw)
core.getIDToken = jest.fn()
core.getIDToken.mockReturnValueOnce(jwtToken)
const token = await retrieveToken(method, got)
expect(token).toEqual(testToken)
const payload = got.post.mock.calls[0][1].json
expect(payload).toEqual({ jwt: jwtToken, role: testRole })
const url = got.post.mock.calls[0][0]
expect(url).toContain('jwt')
})
}) })
+9
View File
@@ -0,0 +1,9 @@
// This allows us to use `require` in our ECMAScript module
// See: https://github.com/vercel/ncc/issues/791
import { createRequire } from 'node:module';
import url from 'node:url';
const __filename = url.fileURLToPath(import.meta.url);
globalThis.require = createRequire(__filename);
export default require;
+3 -5
View File
@@ -1,7 +1,5 @@
const WILDCARD_UPPERCASE = '*'; const WILDCARD = '*';
const WILDCARD = '**';
module.exports = { export {
WILDCARD, WILDCARD
WILDCARD_UPPERCASE,
}; };
+2 -2
View File
@@ -1,5 +1,5 @@
const core = require('@actions/core'); import core from '@actions/core';
const { exportSecrets } = require('./action'); import { exportSecrets } from './action.js';
(async () => { (async () => {
try { try {
+4 -6
View File
@@ -1,9 +1,7 @@
const auth = require('./auth'); import auth from './auth.js';
const secrets = require('./secrets'); import secrets from './secrets.js';
const pki = require('./pki');
module.exports = { export default {
auth, auth,
secrets, secrets
pki
}; };
-76
View File
@@ -1,76 +0,0 @@
const { normalizeOutputKey } = require('./utils');
const core = require('@actions/core');
/** A map of postfix values mapped to the key in the certificate response and a transformer function */
const outputMap = {
cert: { key: 'certificate', tx: (v) => v },
key: { key: 'private_key', tx: (v) => v },
ca: { key: 'issuing_ca', tx: (v) => v },
ca_chain: { key: 'ca_chain', tx: (v) => v.join('\n') },
};
/**
* @typedef PkiRequest
* @type {object}
* @property {string} path - The path to the PKI endpoint
* @property {Record<string, any>} parameters - The parameters to send to the PKI endpoint
* @property {string} envVarName - The name of the environment variable to set
* @property {string} outputVarName - The name of the output variable to set
*/
/**
* @typedef {Object} PkiResponse
* @property {PkiRequest} request
* @property {string} value
* @property {boolean} cachedResponse
*/
/**
* Generate and return the certificates from the PKI engine
* @param {Array<PkiRequest>} pkiRequests
* @param {import('got').Got} client
* @return {Promise<Array<PkiResponse>>}
*/
async function getCertificates(pkiRequests, client) {
/** @type Array<PkiResponse> */
let results = [];
for (const pkiRequest of pkiRequests) {
const { path, parameters } = pkiRequest;
const requestPath = `v1/${path}`;
let body;
try {
const result = await client.post(requestPath, {
body: JSON.stringify(parameters),
});
body = result.body;
} catch (error) {
core.error(`${error.response?.body ?? error.message}`);
throw error;
}
body = JSON.parse(body);
core.info(`✔ Successfully generated certificate (serial number ${body.data.serial_number})`);
Object.entries(outputMap).forEach(([key, value]) => {
const val = value.tx(body.data[value.key]);
results.push({
request: {
...pkiRequest,
envVarName: normalizeOutputKey(`${pkiRequest.envVarName}_${key}`, true),
outputVarName: normalizeOutputKey(`${pkiRequest.outputVarName}_${key}`),
},
value: val,
cachedResponse: false,
});
});
}
return results;
}
module.exports = {
getCertificates,
};
+16 -14
View File
@@ -1,17 +1,19 @@
jest.mock('@actions/core'); import { vi, describe, test, expect } from 'vitest';
const core = require('@actions/core'); vi.mock('@actions/core');
const ServerMock = require("mock-http-server");
const { exportSecrets } = require("./action"); import core from '@actions/core';
const { when } = require('jest-when'); import ServerMock from 'mock-http-server';
import { exportSecrets } from './action.js';
import { when } from 'jest-when';
describe('exportSecrets retries', () => { describe('exportSecrets retries', () => {
var server = new ServerMock({ host: "127.0.0.1", port: 0 }); var server = new ServerMock({ host: "127.0.0.1", port: 0 });
var calls = 0; var calls = 0;
beforeEach((done) => { beforeEach(() => new Promise(done => {
calls = 0; calls = 0;
jest.resetAllMocks(); vi.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('token', expect.anything()) .calledWith('token', expect.anything())
@@ -28,11 +30,11 @@ describe('exportSecrets retries', () => {
.mockReturnValueOnce('http://127.0.0.1:' + server.getHttpPort()); .mockReturnValueOnce('http://127.0.0.1:' + server.getHttpPort());
done(); done();
}); });
}); }));
afterEach((done) => { afterEach(() => new Promise(done => {
server.stop(done); server.stop(done);
}); }));
function mockStatusCodes(statusCodes) { function mockStatusCodes(statusCodes) {
server.on({ server.on({
@@ -51,19 +53,19 @@ describe('exportSecrets retries', () => {
}); });
} }
it('retries on 412 status code', (done) => { it('retries on 412 status code', () => new Promise(done => {
mockStatusCodes([412, 200]) mockStatusCodes([412, 200])
exportSecrets().then(() => { exportSecrets().then(() => {
expect(calls).toEqual(2); expect(calls).toEqual(2);
done(); done();
}); });
}); }));
it('retries on 500 status code', (done) => { it('retries on 500 status code', () => new Promise(done => {
mockStatusCodes([500, 200]) mockStatusCodes([500, 200])
exportSecrets().then(() => { exportSecrets().then(() => {
expect(calls).toEqual(2); expect(calls).toEqual(2);
done(); done();
}); });
}); }));
}); });
+14 -18
View File
@@ -1,7 +1,11 @@
const jsonata = require("jsonata"); import core from '@actions/core';
const { WILDCARD, WILDCARD_UPPERCASE} = require("./constants"); import { WILDCARD } from './constants.js';
const { normalizeOutputKey } = require("./utils"); import { normalizeOutputKey } from './utils.js';
const core = require('@actions/core');
// ncc doesn't compile jsonata imports properly, so we must use our own custom require
import require from "./cjs-require.js";
const jsonata = require('jsonata');
/** /**
* @typedef {Object} SecretRequest * @typedef {Object} SecretRequest
@@ -26,7 +30,6 @@ const core = require('@actions/core');
async function getSecrets(secretRequests, client, ignoreNotFound) { async function getSecrets(secretRequests, client, ignoreNotFound) {
const responseCache = new Map(); const responseCache = new Map();
let results = []; let results = [];
let upperCaseEnv = false;
for (const secretRequest of secretRequests) { for (const secretRequest of secretRequests) {
let { path, selector } = secretRequest; let { path, selector } = secretRequest;
@@ -45,7 +48,7 @@ async function getSecrets(secretRequests, client, ignoreNotFound) {
} catch (error) { } catch (error) {
const {response} = error; const {response} = error;
if (response?.statusCode === 404) { if (response?.statusCode === 404) {
notFoundMsg = `Unable to retrieve result for "${path}" because it was not found: ${response.body.trim()}`; let notFoundMsg = `Unable to retrieve result for "${path}" because it was not found: ${response.body.trim()}`;
const ignoreNotFound = (core.getInput('ignoreNotFound', { required: false }) || 'false').toLowerCase() != 'false'; const ignoreNotFound = (core.getInput('ignoreNotFound', { required: false }) || 'false').toLowerCase() != 'false';
if (ignoreNotFound) { if (ignoreNotFound) {
core.error(`${notFoundMsg}`); core.error(`${notFoundMsg}`);
@@ -60,8 +63,7 @@ async function getSecrets(secretRequests, client, ignoreNotFound) {
body = JSON.parse(body); body = JSON.parse(body);
if (selector === WILDCARD || selector === WILDCARD_UPPERCASE) { if (selector == WILDCARD) {
upperCaseEnv = selector === WILDCARD_UPPERCASE;
let keys = body.data; let keys = body.data;
if (body.data["data"] != undefined) { if (body.data["data"] != undefined) {
keys = keys.data; keys = keys.data;
@@ -74,21 +76,15 @@ async function getSecrets(secretRequests, client, ignoreNotFound) {
if (secretRequest.selector === secretRequest.outputVarName) { if (secretRequest.selector === secretRequest.outputVarName) {
newRequest.outputVarName = key; newRequest.outputVarName = key;
newRequest.envVarName = key; newRequest.envVarName = key;
} else { }
else {
newRequest.outputVarName = secretRequest.outputVarName+key; newRequest.outputVarName = secretRequest.outputVarName+key;
newRequest.envVarName = secretRequest.envVarName+key; newRequest.envVarName = secretRequest.envVarName+key;
} }
newRequest.outputVarName = normalizeOutputKey(newRequest.outputVarName); newRequest.outputVarName = normalizeOutputKey(newRequest.outputVarName);
newRequest.envVarName = normalizeOutputKey(newRequest.envVarName, upperCaseEnv); newRequest.envVarName = normalizeOutputKey(newRequest.envVarName,true);
// JSONata field references containing reserved tokens should
// be enclosed in backticks
// https://docs.jsonata.org/simple#examples
if (key.includes(".")) {
const backtick = '`';
key = backtick.concat(key, backtick);
}
selector = key; selector = key;
results = await selectAndAppendResults( results = await selectAndAppendResults(
@@ -173,7 +169,7 @@ const selectAndAppendResults = async (
]; ];
}; };
module.exports = { export {
getSecrets, getSecrets,
selectData selectData
} }
+3 -3
View File
@@ -3,17 +3,17 @@
* @param {string} dataKey * @param {string} dataKey
* @param {boolean=} isEnvVar * @param {boolean=} isEnvVar
*/ */
function normalizeOutputKey(dataKey, upperCase = false) { function normalizeOutputKey(dataKey, isEnvVar = false) {
let outputKey = dataKey let outputKey = dataKey
.replace(".", "__") .replace(".", "__")
.replace(new RegExp("-", "g"), "") .replace(new RegExp("-", "g"), "")
.replace(/[^\p{L}\p{N}_-]/gu, ""); .replace(/[^\p{L}\p{N}_-]/gu, "");
if (upperCase) { if (isEnvVar) {
outputKey = outputKey.toUpperCase(); outputKey = outputKey.toUpperCase();
} }
return outputKey; return outputKey;
} }
module.exports = { export {
normalizeOutputKey normalizeOutputKey
}; };
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig, configDefaults } from 'vitest/config'
export default defineConfig({
test: {
// required to make jest-when work with vitest
globals: true,
exclude:[
...configDefaults.exclude,
'**/integrationTests/**'
]
},
})