feat: add retries (#181)
This commit is contained in:
parent
10d8e00a99
commit
95a6bc2a27
259
.github/workflows/test.yml
vendored
259
.github/workflows/test.yml
vendored
@ -3,10 +3,10 @@ name: 'test'
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@ -19,21 +19,20 @@ jobs:
|
|||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3'
|
- uses: 'actions/checkout@v3'
|
||||||
|
|
||||||
- uses: 'actions/setup-node@v2'
|
- uses: 'actions/setup-node@v2'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '16.x'
|
||||||
|
|
||||||
- name: 'npm build'
|
- name: 'npm build'
|
||||||
run: 'npm ci && npm run build'
|
run: 'npm ci && npm run build'
|
||||||
|
|
||||||
- name: 'npm lint'
|
- name: 'npm lint'
|
||||||
run: 'npm run lint'
|
run: 'npm run lint'
|
||||||
|
|
||||||
- name: 'npm test'
|
|
||||||
run: 'npm run test'
|
|
||||||
|
|
||||||
|
- name: 'npm test'
|
||||||
|
run: 'npm run test'
|
||||||
|
|
||||||
credentials_json:
|
credentials_json:
|
||||||
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
|
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
|
||||||
@ -43,62 +42,70 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- 'ubuntu-latest'
|
- 'ubuntu-latest'
|
||||||
- 'windows-latest'
|
- 'windows-latest'
|
||||||
- 'macos-latest'
|
- 'macos-latest'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3'
|
- uses: 'actions/checkout@v3'
|
||||||
|
|
||||||
- uses: 'actions/setup-node@v2'
|
- uses: 'actions/setup-node@v2'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '16.x'
|
||||||
|
|
||||||
- name: 'npm build'
|
- name: 'npm build'
|
||||||
run: 'npm ci && npm run build'
|
run: 'npm ci && npm run build'
|
||||||
|
|
||||||
- id: 'auth-default'
|
- id: 'auth-default'
|
||||||
name: 'auth-default'
|
name: 'auth-default'
|
||||||
uses: './'
|
uses: './'
|
||||||
with:
|
with:
|
||||||
credentials_json: '${{ secrets.AUTH_SA_KEY_JSON }}'
|
credentials_json: '${{ secrets.AUTH_SA_KEY_JSON }}'
|
||||||
|
|
||||||
- id: 'setup-gcloud'
|
- id: 'setup-gcloud'
|
||||||
name: 'setup-gcloud'
|
name: 'setup-gcloud'
|
||||||
uses: 'google-github-actions/setup-gcloud@main'
|
uses: 'google-github-actions/setup-gcloud@main'
|
||||||
|
|
||||||
- id: 'gcloud'
|
- id: 'gcloud'
|
||||||
name: 'gcloud'
|
name: 'gcloud'
|
||||||
shell: 'bash'
|
shell: 'bash'
|
||||||
run: |-
|
run: |-
|
||||||
gcloud secrets versions access "latest" --secret "${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}"
|
gcloud secrets versions access "latest" --secret "${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}"
|
||||||
|
|
||||||
- id: 'auth-access-token'
|
- id: 'auth-access-token'
|
||||||
name: 'auth-access-token'
|
name: 'auth-access-token'
|
||||||
uses: './'
|
uses: './'
|
||||||
with:
|
with:
|
||||||
credentials_json: '${{ secrets.AUTH_SA_KEY_B64 }}'
|
credentials_json: '${{ secrets.AUTH_SA_KEY_B64 }}'
|
||||||
token_format: 'access_token'
|
token_format: 'access_token'
|
||||||
|
|
||||||
- id: 'access-token'
|
- id: 'access-token'
|
||||||
name: 'access-token'
|
name: 'access-token'
|
||||||
shell: 'bash'
|
shell: 'bash'
|
||||||
run: |-
|
run: |-
|
||||||
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}/versions/latest:access \
|
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}/versions/latest:access \
|
||||||
--silent \
|
--silent \
|
||||||
--show-error \
|
--show-error \
|
||||||
--fail \
|
--fail \
|
||||||
--header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}"
|
--header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}"
|
||||||
|
|
||||||
- id: 'auth-id-token'
|
- id: 'auth-id-token'
|
||||||
name: 'auth-id-token'
|
name: 'auth-id-token'
|
||||||
uses: './'
|
uses: './'
|
||||||
with:
|
with:
|
||||||
credentials_json: '${{ secrets.AUTH_SA_KEY_JSON }}'
|
credentials_json: '${{ secrets.AUTH_SA_KEY_JSON }}'
|
||||||
token_format: 'id_token'
|
token_format: 'id_token'
|
||||||
id_token_audience: 'https://secretmanager.googleapis.com/'
|
id_token_audience: 'https://secretmanager.googleapis.com/'
|
||||||
id_token_include_email: true
|
id_token_include_email: true
|
||||||
|
|
||||||
|
- id: 'auth-sa-retries'
|
||||||
|
name: 'auth-sa-retries'
|
||||||
|
uses: './'
|
||||||
|
with:
|
||||||
|
retries: '2'
|
||||||
|
backoff: '200'
|
||||||
|
backoff_limit: '1000'
|
||||||
|
credentials_json: '${{ secrets.AUTH_SA_KEY_JSON }}'
|
||||||
|
|
||||||
workload_identity_federation:
|
workload_identity_federation:
|
||||||
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
|
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
|
||||||
@ -108,67 +115,77 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- 'ubuntu-latest'
|
- 'ubuntu-latest'
|
||||||
- 'windows-latest'
|
- 'windows-latest'
|
||||||
- 'macos-latest'
|
- 'macos-latest'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: 'write'
|
id-token: 'write'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3'
|
- uses: 'actions/checkout@v3'
|
||||||
|
|
||||||
- uses: 'actions/setup-node@v2'
|
- uses: 'actions/setup-node@v2'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '16.x'
|
||||||
|
|
||||||
- name: 'npm build'
|
- name: 'npm build'
|
||||||
run: 'npm ci && npm run build'
|
run: 'npm ci && npm run build'
|
||||||
|
|
||||||
- id: 'auth-default'
|
- id: 'auth-default'
|
||||||
name: 'auth-default'
|
name: 'auth-default'
|
||||||
uses: './'
|
uses: './'
|
||||||
with:
|
with:
|
||||||
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
||||||
service_account: '${{ secrets.OIDC_AUTH_SA_EMAIL }}'
|
service_account: '${{ secrets.OIDC_AUTH_SA_EMAIL }}'
|
||||||
|
|
||||||
- id: 'setup-gcloud'
|
- id: 'setup-gcloud'
|
||||||
name: 'setup-gcloud'
|
name: 'setup-gcloud'
|
||||||
uses: 'google-github-actions/setup-gcloud@main'
|
uses: 'google-github-actions/setup-gcloud@main'
|
||||||
|
|
||||||
- id: 'gcloud'
|
- id: 'gcloud'
|
||||||
name: 'gcloud'
|
name: 'gcloud'
|
||||||
shell: 'bash'
|
shell: 'bash'
|
||||||
run: |-
|
run: |-
|
||||||
gcloud secrets versions access "latest" --secret "${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}"
|
gcloud secrets versions access "latest" --secret "${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}"
|
||||||
|
|
||||||
- id: 'auth-access-token'
|
- id: 'auth-access-token'
|
||||||
name: 'auth-access-token'
|
name: 'auth-access-token'
|
||||||
uses: './'
|
uses: './'
|
||||||
with:
|
with:
|
||||||
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
||||||
service_account: '${{ secrets.OIDC_AUTH_SA_EMAIL }}'
|
service_account: '${{ secrets.OIDC_AUTH_SA_EMAIL }}'
|
||||||
token_format: 'access_token'
|
token_format: 'access_token'
|
||||||
|
|
||||||
- id: 'access-token'
|
- id: 'access-token'
|
||||||
name: 'access-token'
|
name: 'access-token'
|
||||||
shell: 'bash'
|
shell: 'bash'
|
||||||
run: |-
|
run: |-
|
||||||
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}/versions/latest:access \
|
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}/versions/latest:access \
|
||||||
--silent \
|
--silent \
|
||||||
--show-error \
|
--show-error \
|
||||||
--fail \
|
--fail \
|
||||||
--header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}"
|
--header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}"
|
||||||
|
|
||||||
- id: 'auth-id-token'
|
- id: 'auth-id-token'
|
||||||
name: 'auth-id-token'
|
name: 'auth-id-token'
|
||||||
uses: './'
|
uses: './'
|
||||||
with:
|
with:
|
||||||
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
||||||
service_account: '${{ secrets.OIDC_AUTH_SA_EMAIL }}'
|
service_account: '${{ secrets.OIDC_AUTH_SA_EMAIL }}'
|
||||||
token_format: 'id_token'
|
token_format: 'id_token'
|
||||||
id_token_audience: 'https://secretmanager.googleapis.com/'
|
id_token_audience: 'https://secretmanager.googleapis.com/'
|
||||||
id_token_include_email: true
|
id_token_include_email: true
|
||||||
|
|
||||||
|
- id: 'auth-wif-retries'
|
||||||
|
name: 'auth-wif-retries'
|
||||||
|
uses: './'
|
||||||
|
with:
|
||||||
|
retries: '2'
|
||||||
|
backoff: '200'
|
||||||
|
backoff_limit: '1000'
|
||||||
|
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
||||||
|
service_account: '${{ secrets.OIDC_AUTH_SA_EMAIL }}'
|
||||||
|
|
||||||
# This test ensures that the GOOGLE_APPLICATION_CREDENTIALS environment
|
# This test ensures that the GOOGLE_APPLICATION_CREDENTIALS environment
|
||||||
# variable is shared with the container and that the path of the file is on
|
# variable is shared with the container and that the path of the file is on
|
||||||
@ -181,22 +198,22 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3'
|
- uses: 'actions/checkout@v3'
|
||||||
|
|
||||||
- uses: 'actions/setup-node@v2'
|
- uses: 'actions/setup-node@v2'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '16.x'
|
||||||
|
|
||||||
- name: 'npm build'
|
- name: 'npm build'
|
||||||
run: 'npm ci && npm run build'
|
run: 'npm ci && npm run build'
|
||||||
|
|
||||||
- name: 'auth-default'
|
- name: 'auth-default'
|
||||||
uses: './'
|
uses: './'
|
||||||
with:
|
with:
|
||||||
credentials_json: '${{ secrets.AUTH_SA_KEY_JSON }}'
|
credentials_json: '${{ secrets.AUTH_SA_KEY_JSON }}'
|
||||||
|
|
||||||
- name: 'docker'
|
- name: 'docker'
|
||||||
uses: 'docker://alpine:3'
|
uses: 'docker://alpine:3'
|
||||||
with:
|
with:
|
||||||
entrypoint: '/bin/sh'
|
entrypoint: '/bin/sh'
|
||||||
args: '-euc "test -n "${GOOGLE_APPLICATION_CREDENTIALS}" && test -r "${GOOGLE_APPLICATION_CREDENTIALS}"'
|
args: '-euc "test -n "${GOOGLE_APPLICATION_CREDENTIALS}" && test -r "${GOOGLE_APPLICATION_CREDENTIALS}"'
|
||||||
|
18
action.yml
18
action.yml
@ -124,6 +124,24 @@ inputs:
|
|||||||
default: ''
|
default: ''
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
|
# retries
|
||||||
|
retries:
|
||||||
|
description: |-
|
||||||
|
Number of times to retry a failed authentication attempt. This is useful
|
||||||
|
for automated pipelines that may execute before IAM permissions are fully propogated.
|
||||||
|
default: '0'
|
||||||
|
required: false
|
||||||
|
backoff:
|
||||||
|
description: |-
|
||||||
|
Delay time before trying another authentication attempt. This
|
||||||
|
is implemented using a fibonacci backoff method (e.g. 1-1-2-3-5).
|
||||||
|
This value defaults to 100 milliseconds when retries are greater than 0.
|
||||||
|
required: false
|
||||||
|
backoff_limit:
|
||||||
|
description: |-
|
||||||
|
Limits the retry backoff to the specified value.
|
||||||
|
required: false
|
||||||
|
|
||||||
# id token params
|
# id token params
|
||||||
id_token_audience:
|
id_token_audience:
|
||||||
description: |-
|
description: |-
|
||||||
|
2
dist/main/index.js
vendored
2
dist/main/index.js
vendored
File diff suppressed because one or more lines are too long
2
dist/post/index.js
vendored
2
dist/post/index.js
vendored
File diff suppressed because one or more lines are too long
14
package-lock.json
generated
14
package-lock.json
generated
@ -10,7 +10,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.7.0",
|
"@actions/core": "^1.7.0",
|
||||||
"@google-github-actions/actions-utils": "^0.3.0"
|
"@google-github-actions/actions-utils": "^0.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.3.1",
|
"@types/chai": "^4.3.1",
|
||||||
@ -87,9 +87,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@google-github-actions/actions-utils": {
|
"node_modules/@google-github-actions/actions-utils": {
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@google-github-actions/actions-utils/-/actions-utils-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@google-github-actions/actions-utils/-/actions-utils-0.4.0.tgz",
|
||||||
"integrity": "sha512-zl6/NDnxhB+22E5wZghMnzR0onUNqJFagGtA13wlaADzO1Cb3K1MgTk/U2mPiNlBtyaMlF5XkBGLLhwX+wS2qA==",
|
"integrity": "sha512-s2ev2a3WwLg0LWPIi5b9zcaf+jTUkVrQi/iGbQAwX+l0veYsPT1wu9mWV2pZxHfGfxenCjsTw4wSnl9RTJ7ytA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yaml": "^2.0.1"
|
"yaml": "^2.0.1"
|
||||||
}
|
}
|
||||||
@ -2462,9 +2462,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@google-github-actions/actions-utils": {
|
"@google-github-actions/actions-utils": {
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@google-github-actions/actions-utils/-/actions-utils-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@google-github-actions/actions-utils/-/actions-utils-0.4.0.tgz",
|
||||||
"integrity": "sha512-zl6/NDnxhB+22E5wZghMnzR0onUNqJFagGtA13wlaADzO1Cb3K1MgTk/U2mPiNlBtyaMlF5XkBGLLhwX+wS2qA==",
|
"integrity": "sha512-s2ev2a3WwLg0LWPIi5b9zcaf+jTUkVrQi/iGbQAwX+l0veYsPT1wu9mWV2pZxHfGfxenCjsTw4wSnl9RTJ7ytA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"yaml": "^2.0.1"
|
"yaml": "^2.0.1"
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.7.0",
|
"@actions/core": "^1.7.0",
|
||||||
"@google-github-actions/actions-utils": "^0.3.0"
|
"@google-github-actions/actions-utils": "^0.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.3.1",
|
"@types/chai": "^4.3.1",
|
||||||
|
445
src/main.ts
445
src/main.ts
@ -22,6 +22,7 @@ import {
|
|||||||
parseCSV,
|
parseCSV,
|
||||||
parseDuration,
|
parseDuration,
|
||||||
pinnedToHeadWarning,
|
pinnedToHeadWarning,
|
||||||
|
withRetries,
|
||||||
} from '@google-github-actions/actions-utils';
|
} from '@google-github-actions/actions-utils';
|
||||||
|
|
||||||
import { WorkloadIdentityClient } from './client/workload_identity_client';
|
import { WorkloadIdentityClient } from './client/workload_identity_client';
|
||||||
@ -42,7 +43,7 @@ const oidcWarning =
|
|||||||
`run from a fork. For more information, please see https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token`;
|
`run from a fork. For more information, please see https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the main action, documented inline.
|
* Executes the main action.
|
||||||
*/
|
*/
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
// Warn if pinned to HEAD
|
// Warn if pinned to HEAD
|
||||||
@ -50,226 +51,244 @@ async function run(): Promise<void> {
|
|||||||
logWarning(pinnedToHeadWarning('v0'));
|
logWarning(pinnedToHeadWarning('v0'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const retries = Number(getInput('retries'));
|
||||||
|
|
||||||
|
// set to undefined when not provided [avoids Number('') -> 0]
|
||||||
|
const backoff = Number(getInput('backoff')) || undefined;
|
||||||
|
const backoffLimit = Number(getInput('backoff_limit')) || undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load configuration.
|
const mainWithRetries = withRetries(main, {
|
||||||
const projectID = getInput('project_id');
|
retries: retries,
|
||||||
const workloadIdentityProvider = getInput('workload_identity_provider');
|
backoff: backoff,
|
||||||
const serviceAccount = getInput('service_account');
|
backoffLimit: backoffLimit,
|
||||||
const audience =
|
});
|
||||||
getInput('audience') || `https://iam.googleapis.com/${workloadIdentityProvider}`;
|
|
||||||
const credentialsJSON = getInput('credentials_json');
|
|
||||||
const createCredentialsFile = getBooleanInput('create_credentials_file');
|
|
||||||
const exportEnvironmentVariables = getBooleanInput('export_environment_variables');
|
|
||||||
const tokenFormat = getInput('token_format');
|
|
||||||
const delegates = parseCSV(getInput('delegates'));
|
|
||||||
|
|
||||||
// Ensure exactly one of workload_identity_provider and credentials_json was
|
await mainWithRetries();
|
||||||
// provided.
|
|
||||||
if (!exactlyOneOf(workloadIdentityProvider, credentialsJSON)) {
|
|
||||||
throw new Error(
|
|
||||||
'The GitHub Action workflow must specify exactly one of ' +
|
|
||||||
'"workload_identity_provider" or "credentials_json"! ' +
|
|
||||||
secretsWarning,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure a service_account was provided if using WIF.
|
|
||||||
if (workloadIdentityProvider && !serviceAccount) {
|
|
||||||
throw new Error(
|
|
||||||
'The GitHub Action workflow must specify a "service_account" to ' +
|
|
||||||
'impersonate when using "workload_identity_provider"! ' +
|
|
||||||
secretsWarning,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instantiate the correct client based on the provided input parameters.
|
|
||||||
let client: AuthClient;
|
|
||||||
if (workloadIdentityProvider) {
|
|
||||||
logDebug(`Using workload identity provider "${workloadIdentityProvider}"`);
|
|
||||||
|
|
||||||
// If we're going to do the OIDC dance, we need to make sure these values
|
|
||||||
// are set. If they aren't, core.getIDToken() will fail and so will
|
|
||||||
// generating the credentials file.
|
|
||||||
const oidcTokenRequestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
||||||
const oidcTokenRequestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
||||||
if (!oidcTokenRequestToken || !oidcTokenRequestURL) {
|
|
||||||
throw new Error(oidcWarning);
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await getIDToken(audience);
|
|
||||||
client = new WorkloadIdentityClient({
|
|
||||||
projectID: projectID,
|
|
||||||
providerID: workloadIdentityProvider,
|
|
||||||
serviceAccount: serviceAccount,
|
|
||||||
token: token,
|
|
||||||
audience: audience,
|
|
||||||
oidcTokenRequestToken: oidcTokenRequestToken,
|
|
||||||
oidcTokenRequestURL: oidcTokenRequestURL,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logDebug(`Using credentials JSON`);
|
|
||||||
client = new CredentialsJSONClient({
|
|
||||||
projectID: projectID,
|
|
||||||
credentialsJSON: credentialsJSON,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always write the credentials file first, before trying to generate
|
|
||||||
// tokens. This will ensure the file is written even if token generation
|
|
||||||
// fails, which means continue-on-error actions will still have the file
|
|
||||||
// available.
|
|
||||||
if (createCredentialsFile) {
|
|
||||||
logDebug(`Creating credentials file`);
|
|
||||||
|
|
||||||
// Note: We explicitly and intentionally export to GITHUB_WORKSPACE
|
|
||||||
// instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with
|
|
||||||
// Docker-based actions on the filesystem. Exporting to GITHUB_WORKSPACE
|
|
||||||
// ensures that the exported credentials are automatically available to
|
|
||||||
// Docker-based actions without user modification.
|
|
||||||
//
|
|
||||||
// This has the unintended side-effect of leaking credentials over time,
|
|
||||||
// because GITHUB_WORKSPACE is not automatically cleaned up on self-hosted
|
|
||||||
// runners. To mitigate this issue, this action defines a post step to
|
|
||||||
// remove any created credentials.
|
|
||||||
const githubWorkspace = process.env.GITHUB_WORKSPACE;
|
|
||||||
if (!githubWorkspace) {
|
|
||||||
throw new Error('$GITHUB_WORKSPACE is not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
// There have been a number of issues where users have not used the
|
|
||||||
// "actions/checkout" step before our action. Our action relies on the
|
|
||||||
// creation of that directory; worse, if a user puts "actions/checkout"
|
|
||||||
// after our action, it will delete the exported credential. This
|
|
||||||
// following code does a small check to see if there are any files in the
|
|
||||||
// directory. It emits a warning if there are no files, since there may be
|
|
||||||
// legitimate use cases for authenticating without checking out the
|
|
||||||
// repository.
|
|
||||||
const githubWorkspaceIsEmpty = await isEmptyDir(githubWorkspace);
|
|
||||||
if (githubWorkspaceIsEmpty) {
|
|
||||||
logWarning(
|
|
||||||
`The "create_credentials_file" option is true, but the current ` +
|
|
||||||
`GitHub workspace is empty. Did you forget to use ` +
|
|
||||||
`"actions/checkout" before this step? If you do not intend to ` +
|
|
||||||
`share authentication with future steps in this job, set ` +
|
|
||||||
`"create_credentials_file" to false.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create credentials file.
|
|
||||||
const outputFile = generateCredentialsFilename();
|
|
||||||
const outputPath = pathjoin(githubWorkspace, outputFile);
|
|
||||||
const credentialsPath = await client.createCredentialsFile(outputPath);
|
|
||||||
logInfo(`Created credentials file at "${credentialsPath}"`);
|
|
||||||
|
|
||||||
// Output to be available to future steps.
|
|
||||||
setOutput('credentials_file_path', credentialsPath);
|
|
||||||
|
|
||||||
if (exportEnvironmentVariables) {
|
|
||||||
// CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE is picked up by gcloud to
|
|
||||||
// use a specific credential file (subject to change and equivalent to
|
|
||||||
// auth/credential_file_override).
|
|
||||||
exportVariableAndWarn('CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE', credentialsPath);
|
|
||||||
|
|
||||||
// GOOGLE_APPLICATION_CREDENTIALS is used by Application Default
|
|
||||||
// Credentials in all GCP client libraries.
|
|
||||||
exportVariableAndWarn('GOOGLE_APPLICATION_CREDENTIALS', credentialsPath);
|
|
||||||
|
|
||||||
// GOOGLE_GHA_CREDS_PATH is used by other Google GitHub Actions.
|
|
||||||
exportVariableAndWarn('GOOGLE_GHA_CREDS_PATH', credentialsPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the project ID environment variables to the computed values.
|
|
||||||
const computedProjectID = await client.getProjectID();
|
|
||||||
setOutput('project_id', computedProjectID);
|
|
||||||
|
|
||||||
if (exportEnvironmentVariables) {
|
|
||||||
exportVariableAndWarn('CLOUDSDK_CORE_PROJECT', computedProjectID);
|
|
||||||
exportVariableAndWarn('CLOUDSDK_PROJECT', computedProjectID);
|
|
||||||
exportVariableAndWarn('GCLOUD_PROJECT', computedProjectID);
|
|
||||||
exportVariableAndWarn('GCP_PROJECT', computedProjectID);
|
|
||||||
exportVariableAndWarn('GOOGLE_CLOUD_PROJECT', computedProjectID);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (tokenFormat) {
|
|
||||||
case '': {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case null: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'access_token': {
|
|
||||||
logDebug(`Creating access token`);
|
|
||||||
|
|
||||||
const accessTokenLifetime = parseDuration(getInput('access_token_lifetime'));
|
|
||||||
const accessTokenScopes = parseCSV(getInput('access_token_scopes'));
|
|
||||||
const accessTokenSubject = getInput('access_token_subject');
|
|
||||||
const serviceAccount = await client.getServiceAccount();
|
|
||||||
|
|
||||||
// If a subject was provided, use the traditional OAuth 2.0 flow to
|
|
||||||
// perform Domain-Wide Delegation. Otherwise, use the modern IAM
|
|
||||||
// Credentials endpoints.
|
|
||||||
let accessToken, expiration;
|
|
||||||
if (accessTokenSubject) {
|
|
||||||
if (accessTokenLifetime > 3600) {
|
|
||||||
logInfo(
|
|
||||||
`An access token subject was specified, triggering Domain-Wide ` +
|
|
||||||
`Delegation flow. This flow does not support specifying an ` +
|
|
||||||
`access token lifetime of greater than 1 hour.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsignedJWT = buildDomainWideDelegationJWT(
|
|
||||||
serviceAccount,
|
|
||||||
accessTokenSubject,
|
|
||||||
accessTokenScopes,
|
|
||||||
accessTokenLifetime,
|
|
||||||
);
|
|
||||||
const signedJWT = await client.signJWT(unsignedJWT, delegates);
|
|
||||||
({ accessToken, expiration } = await BaseClient.googleOAuthToken(signedJWT));
|
|
||||||
} else {
|
|
||||||
const authToken = await client.getAuthToken();
|
|
||||||
({ accessToken, expiration } = await BaseClient.googleAccessToken(authToken, {
|
|
||||||
serviceAccount,
|
|
||||||
delegates,
|
|
||||||
scopes: accessTokenScopes,
|
|
||||||
lifetime: accessTokenLifetime,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
setSecret(accessToken);
|
|
||||||
setOutput('access_token', accessToken);
|
|
||||||
setOutput('access_token_expiration', expiration);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'id_token': {
|
|
||||||
logDebug(`Creating id token`);
|
|
||||||
|
|
||||||
const idTokenAudience = getInput('id_token_audience', { required: true });
|
|
||||||
const idTokenIncludeEmail = getBooleanInput('id_token_include_email');
|
|
||||||
const serviceAccount = await client.getServiceAccount();
|
|
||||||
|
|
||||||
const authToken = await client.getAuthToken();
|
|
||||||
const { token } = await BaseClient.googleIDToken(authToken, {
|
|
||||||
serviceAccount,
|
|
||||||
audience: idTokenAudience,
|
|
||||||
delegates,
|
|
||||||
includeEmail: idTokenIncludeEmail,
|
|
||||||
});
|
|
||||||
setSecret(token);
|
|
||||||
setOutput('id_token', token);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error(`Unknown token format "${tokenFormat}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = errorMessage(err);
|
const msg = errorMessage(err);
|
||||||
setFailed(`google-github-actions/auth failed with: ${msg}`);
|
setFailed(`google-github-actions/auth failed with: ${msg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main wraps the main action logic into a function to be used as a parameter to the withRetries function.
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
// Load configuration.
|
||||||
|
const projectID = getInput('project_id');
|
||||||
|
const workloadIdentityProvider = getInput('workload_identity_provider');
|
||||||
|
const serviceAccount = getInput('service_account');
|
||||||
|
const audience = getInput('audience') || `https://iam.googleapis.com/${workloadIdentityProvider}`;
|
||||||
|
const credentialsJSON = getInput('credentials_json');
|
||||||
|
const createCredentialsFile = getBooleanInput('create_credentials_file');
|
||||||
|
const exportEnvironmentVariables = getBooleanInput('export_environment_variables');
|
||||||
|
const tokenFormat = getInput('token_format');
|
||||||
|
const delegates = parseCSV(getInput('delegates'));
|
||||||
|
|
||||||
|
// Ensure exactly one of workload_identity_provider and credentials_json was
|
||||||
|
// provided.
|
||||||
|
if (!exactlyOneOf(workloadIdentityProvider, credentialsJSON)) {
|
||||||
|
throw new Error(
|
||||||
|
'The GitHub Action workflow must specify exactly one of ' +
|
||||||
|
'"workload_identity_provider" or "credentials_json"! ' +
|
||||||
|
secretsWarning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure a service_account was provided if using WIF.
|
||||||
|
if (workloadIdentityProvider && !serviceAccount) {
|
||||||
|
throw new Error(
|
||||||
|
'The GitHub Action workflow must specify a "service_account" to ' +
|
||||||
|
'impersonate when using "workload_identity_provider"! ' +
|
||||||
|
secretsWarning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instantiate the correct client based on the provided input parameters.
|
||||||
|
let client: AuthClient;
|
||||||
|
if (workloadIdentityProvider) {
|
||||||
|
logDebug(`Using workload identity provider "${workloadIdentityProvider}"`);
|
||||||
|
|
||||||
|
// If we're going to do the OIDC dance, we need to make sure these values
|
||||||
|
// are set. If they aren't, core.getIDToken() will fail and so will
|
||||||
|
// generating the credentials file.
|
||||||
|
const oidcTokenRequestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||||
|
const oidcTokenRequestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||||
|
if (!oidcTokenRequestToken || !oidcTokenRequestURL) {
|
||||||
|
throw new Error(oidcWarning);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await getIDToken(audience);
|
||||||
|
client = new WorkloadIdentityClient({
|
||||||
|
projectID: projectID,
|
||||||
|
providerID: workloadIdentityProvider,
|
||||||
|
serviceAccount: serviceAccount,
|
||||||
|
token: token,
|
||||||
|
audience: audience,
|
||||||
|
oidcTokenRequestToken: oidcTokenRequestToken,
|
||||||
|
oidcTokenRequestURL: oidcTokenRequestURL,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logDebug(`Using credentials JSON`);
|
||||||
|
client = new CredentialsJSONClient({
|
||||||
|
projectID: projectID,
|
||||||
|
credentialsJSON: credentialsJSON,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always write the credentials file first, before trying to generate
|
||||||
|
// tokens. This will ensure the file is written even if token generation
|
||||||
|
// fails, which means continue-on-error actions will still have the file
|
||||||
|
// available.
|
||||||
|
if (createCredentialsFile) {
|
||||||
|
logDebug(`Creating credentials file`);
|
||||||
|
|
||||||
|
// Note: We explicitly and intentionally export to GITHUB_WORKSPACE
|
||||||
|
// instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with
|
||||||
|
// Docker-based actions on the filesystem. Exporting to GITHUB_WORKSPACE
|
||||||
|
// ensures that the exported credentials are automatically available to
|
||||||
|
// Docker-based actions without user modification.
|
||||||
|
//
|
||||||
|
// This has the unintended side-effect of leaking credentials over time,
|
||||||
|
// because GITHUB_WORKSPACE is not automatically cleaned up on self-hosted
|
||||||
|
// runners. To mitigate this issue, this action defines a post step to
|
||||||
|
// remove any created credentials.
|
||||||
|
const githubWorkspace = process.env.GITHUB_WORKSPACE;
|
||||||
|
if (!githubWorkspace) {
|
||||||
|
throw new Error('$GITHUB_WORKSPACE is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
// There have been a number of issues where users have not used the
|
||||||
|
// "actions/checkout" step before our action. Our action relies on the
|
||||||
|
// creation of that directory; worse, if a user puts "actions/checkout"
|
||||||
|
// after our action, it will delete the exported credential. This
|
||||||
|
// following code does a small check to see if there are any files in the
|
||||||
|
// directory. It emits a warning if there are no files, since there may be
|
||||||
|
// legitimate use cases for authenticating without checking out the
|
||||||
|
// repository.
|
||||||
|
const githubWorkspaceIsEmpty = await isEmptyDir(githubWorkspace);
|
||||||
|
if (githubWorkspaceIsEmpty) {
|
||||||
|
logWarning(
|
||||||
|
`The "create_credentials_file" option is true, but the current ` +
|
||||||
|
`GitHub workspace is empty. Did you forget to use ` +
|
||||||
|
`"actions/checkout" before this step? If you do not intend to ` +
|
||||||
|
`share authentication with future steps in this job, set ` +
|
||||||
|
`"create_credentials_file" to false.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create credentials file.
|
||||||
|
const outputFile = generateCredentialsFilename();
|
||||||
|
const outputPath = pathjoin(githubWorkspace, outputFile);
|
||||||
|
const credentialsPath = await client.createCredentialsFile(outputPath);
|
||||||
|
logInfo(`Created credentials file at "${credentialsPath}"`);
|
||||||
|
|
||||||
|
// Output to be available to future steps.
|
||||||
|
setOutput('credentials_file_path', credentialsPath);
|
||||||
|
|
||||||
|
if (exportEnvironmentVariables) {
|
||||||
|
// CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE is picked up by gcloud to
|
||||||
|
// use a specific credential file (subject to change and equivalent to
|
||||||
|
// auth/credential_file_override).
|
||||||
|
exportVariableAndWarn('CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE', credentialsPath);
|
||||||
|
|
||||||
|
// GOOGLE_APPLICATION_CREDENTIALS is used by Application Default
|
||||||
|
// Credentials in all GCP client libraries.
|
||||||
|
exportVariableAndWarn('GOOGLE_APPLICATION_CREDENTIALS', credentialsPath);
|
||||||
|
|
||||||
|
// GOOGLE_GHA_CREDS_PATH is used by other Google GitHub Actions.
|
||||||
|
exportVariableAndWarn('GOOGLE_GHA_CREDS_PATH', credentialsPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the project ID environment variables to the computed values.
|
||||||
|
const computedProjectID = await client.getProjectID();
|
||||||
|
setOutput('project_id', computedProjectID);
|
||||||
|
|
||||||
|
if (exportEnvironmentVariables) {
|
||||||
|
exportVariableAndWarn('CLOUDSDK_CORE_PROJECT', computedProjectID);
|
||||||
|
exportVariableAndWarn('CLOUDSDK_PROJECT', computedProjectID);
|
||||||
|
exportVariableAndWarn('GCLOUD_PROJECT', computedProjectID);
|
||||||
|
exportVariableAndWarn('GCP_PROJECT', computedProjectID);
|
||||||
|
exportVariableAndWarn('GOOGLE_CLOUD_PROJECT', computedProjectID);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (tokenFormat) {
|
||||||
|
case '': {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case null: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'access_token': {
|
||||||
|
logDebug(`Creating access token`);
|
||||||
|
|
||||||
|
const accessTokenLifetime = parseDuration(getInput('access_token_lifetime'));
|
||||||
|
const accessTokenScopes = parseCSV(getInput('access_token_scopes'));
|
||||||
|
const accessTokenSubject = getInput('access_token_subject');
|
||||||
|
const serviceAccount = await client.getServiceAccount();
|
||||||
|
|
||||||
|
// If a subject was provided, use the traditional OAuth 2.0 flow to
|
||||||
|
// perform Domain-Wide Delegation. Otherwise, use the modern IAM
|
||||||
|
// Credentials endpoints.
|
||||||
|
let accessToken, expiration;
|
||||||
|
if (accessTokenSubject) {
|
||||||
|
if (accessTokenLifetime > 3600) {
|
||||||
|
logInfo(
|
||||||
|
`An access token subject was specified, triggering Domain-Wide ` +
|
||||||
|
`Delegation flow. This flow does not support specifying an ` +
|
||||||
|
`access token lifetime of greater than 1 hour.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsignedJWT = buildDomainWideDelegationJWT(
|
||||||
|
serviceAccount,
|
||||||
|
accessTokenSubject,
|
||||||
|
accessTokenScopes,
|
||||||
|
accessTokenLifetime,
|
||||||
|
);
|
||||||
|
const signedJWT = await client.signJWT(unsignedJWT, delegates);
|
||||||
|
({ accessToken, expiration } = await BaseClient.googleOAuthToken(signedJWT));
|
||||||
|
} else {
|
||||||
|
const authToken = await client.getAuthToken();
|
||||||
|
({ accessToken, expiration } = await BaseClient.googleAccessToken(authToken, {
|
||||||
|
serviceAccount,
|
||||||
|
delegates,
|
||||||
|
scopes: accessTokenScopes,
|
||||||
|
lifetime: accessTokenLifetime,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
setSecret(accessToken);
|
||||||
|
setOutput('access_token', accessToken);
|
||||||
|
setOutput('access_token_expiration', expiration);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'id_token': {
|
||||||
|
logDebug(`Creating id token`);
|
||||||
|
|
||||||
|
const idTokenAudience = getInput('id_token_audience', { required: true });
|
||||||
|
const idTokenIncludeEmail = getBooleanInput('id_token_include_email');
|
||||||
|
const serviceAccount = await client.getServiceAccount();
|
||||||
|
|
||||||
|
const authToken = await client.getAuthToken();
|
||||||
|
const { token } = await BaseClient.googleIDToken(authToken, {
|
||||||
|
serviceAccount,
|
||||||
|
audience: idTokenAudience,
|
||||||
|
delegates,
|
||||||
|
includeEmail: idTokenIncludeEmail,
|
||||||
|
});
|
||||||
|
setSecret(token);
|
||||||
|
setOutput('id_token', token);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unknown token format "${tokenFormat}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* exportVariableAndWarn exports the given key as an environment variable set to
|
* exportVariableAndWarn exports the given key as an environment variable set to
|
||||||
* the provided value. If a value already exists, it is overwritten and an
|
* the provided value. If a value already exists, it is overwritten and an
|
||||||
|
Loading…
Reference in New Issue
Block a user