From 9eb21bd79ca69a1f3f0cabf7d31810941f17da8b Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:53:54 -0700 Subject: [PATCH] chore(cli-testing): add a retry for test (#29908) One of our tests can remove customPermissionsBoundary creates a policy using createPolicy. Change to IAM policies/roles use eventual consistency. So, while the changes will show up right away if we were to call an API to describe that policy/role, the updates may not have actually propagated to all regions yet. This is likely the cause of the intermittent test failures for this test. This change adds the eventually block and uses it to retry initial creation of this stack in the case that the policy changes have not made it to the relevant region just yet. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../cli-integ/lib/eventually.ts | 42 +++++++++++++++++++ .../bootstrapping.integtest.ts | 29 ++++++++----- 2 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 packages/@aws-cdk-testing/cli-integ/lib/eventually.ts diff --git a/packages/@aws-cdk-testing/cli-integ/lib/eventually.ts b/packages/@aws-cdk-testing/cli-integ/lib/eventually.ts new file mode 100644 index 0000000000000..6936ff32f76fd --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/eventually.ts @@ -0,0 +1,42 @@ +/** + * @param maxAttempts the maximum number of attempts + * @param interval interval in milliseconds to observe between attempts + */ +export type EventuallyOptions = { + maxAttempts?: number; + interval?: number; +}; + +const wait = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); +const DEFAULT_INTERVAL = 1000; +const DEFAULT_MAX_ATTEMPTS = 10; + +/** + * Runs a function on an interval until the maximum number of attempts has + * been reached. + * + * Default interval = 1000 milliseconds + * Default maxAttempts = 10 + * + * @param fn function to run + * @param options EventuallyOptions + */ +const eventually = async (call: () => Promise, options?: EventuallyOptions): Promise => { + const opts = { + interval: options?.interval ? options.interval : DEFAULT_INTERVAL, + maxAttempts: options?.maxAttempts ? options.maxAttempts : DEFAULT_MAX_ATTEMPTS, + }; + + while (opts.maxAttempts-- >= 0) { + try { + return await call(); + } catch (err) { + if (opts.maxAttempts <= 0) throw err; + } + await wait(opts.interval); + } + + throw new Error('An unexpected error has occurred.'); +}; + +export default eventually; \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts index 18889bde7dd7e..ab56611bf39e0 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as yaml from 'yaml'; import { integTest, randomString, withoutBootstrap } from '../../lib'; +import eventually from '../../lib/eventually'; jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime @@ -283,17 +284,25 @@ integTest('can remove customPermissionsBoundary', withoutBootstrap(async (fixtur }), }); policyArn = policy.Policy?.Arn; - await fixture.cdkBootstrapModern({ - // toolkitStackName doesn't matter for this particular invocation - toolkitStackName: bootstrapStackName, - customPermissionsBoundary: policyName, - }); - const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName }); - expect( - response.Stacks?.[0].Parameters?.some( - param => (param.ParameterKey === 'InputPermissionsBoundary' && param.ParameterValue === policyName), - )).toEqual(true); + // Policy creation and consistency across regions is "almost immediate" + // See: https://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_general.html#troubleshoot_general_eventual-consistency + // We will put this in an `eventually` block to retry stack creation with a reasonable timeout + const createStackWithPermissionBoundary = async (): Promise => { + await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: bootstrapStackName, + customPermissionsBoundary: policyName, + }); + + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName }); + expect( + response.Stacks?.[0].Parameters?.some( + param => (param.ParameterKey === 'InputPermissionsBoundary' && param.ParameterValue === policyName), + )).toEqual(true); + }; + + await eventually(createStackWithPermissionBoundary, { maxAttempts: 3 }); await fixture.cdkBootstrapModern({ // toolkitStackName doesn't matter for this particular invocation