diff --git a/index.js b/index.js index 229993bb4..c70f46cec 100644 --- a/index.js +++ b/index.js @@ -236,6 +236,29 @@ function getStsClient(region) { }); } +let defaultSleep = function (ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; +let sleep = defaultSleep; + +// retryAndBackoff retries with exponential backoff the promise if the error isRetryable upto maxRetries time. +const retryAndBackoff = async (fn, isRetryable, retries = 0, maxRetries = 12, base = 50) => { + try { + return await fn(); + } catch (err) { + if (!isRetryable) { + throw err; + } + // It's retryable, so sleep and retry. + await sleep(Math.random() * (Math.pow(2, retries) * base) ); + retries += 1; + if (retries === maxRetries) { + throw err; + } + return await retryAndBackoff(fn, isRetryable, retries, maxRetries, base); + } +} + async function run() { try { // Get inputs @@ -303,17 +326,18 @@ async function run() { // Get role credentials if configured to do so if (roleToAssume) { - const roleCredentials = await assumeRole({ - sourceAccountId, - region, - roleToAssume, - roleExternalId, - roleDurationSeconds, - roleSessionName, - roleSkipSessionTagging, - webIdentityTokenFile, - webIdentityToken - }); + const roleCredentials = await retryAndBackoff( + async () => { return await assumeRole({ + sourceAccountId, + region, + roleToAssume, + roleExternalId, + roleDurationSeconds, + roleSessionName, + roleSkipSessionTagging, + webIdentityTokenFile, + webIdentityToken + }) }, true); exportCredentials(roleCredentials); // We need to validate the credentials in 2 of our use-cases // First: self-hosted runners. If the GITHUB_ACTIONS environment variable @@ -337,7 +361,14 @@ async function run() { } } -module.exports = run; +exports.withSleep = function (s) { + sleep = s; +}; +exports.reset = function () { + sleep = defaultSleep; +}; + +exports.run = run /* istanbul ignore next */ if (require.main === module) { diff --git a/index.test.js b/index.test.js index 860346d27..eea0e3e15 100644 --- a/index.test.js +++ b/index.test.js @@ -1,7 +1,7 @@ const core = require('@actions/core'); const assert = require('assert'); const aws = require('aws-sdk'); -const run = require('./index.js'); +const { run, withSleep, reset } = require('./index.js'); jest.mock('@actions/core'); @@ -156,10 +156,15 @@ describe('Configure AWS Credentials', () => { } } }); + + withSleep(() => { + return Promise.resolve(); + }); }); afterEach(() => { process.env = OLD_ENV; + reset(); }); test('exports env vars', async () => { @@ -612,6 +617,23 @@ describe('Configure AWS Credentials', () => { expect(core.setSecret).toHaveBeenNthCalledWith(3, FAKE_STS_SESSION_TOKEN); }); + test('role assumption fails after maximun trials using OIDC Provider', async () => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-token'; + + core.getInput = jest + .fn() + .mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION})); + + mockStsAssumeRoleWithWebIdentity.mockReset(); + mockStsAssumeRoleWithWebIdentity.mockImplementation(() => { + throw new Error(); + }); + + await assert.rejects(() => run()); + expect(mockStsAssumeRoleWithWebIdentity).toHaveBeenCalledTimes(12) + }); + test('role external ID provided', async () => { core.getInput = jest .fn()