Skip to content

Commit 4973a8c

Browse files
elhedranmergify[bot]
authored andcommitted
fix(certificatemanager): increase minimum validation total timeout (#3914)
* fix(certificatemanager): add minimum backoff Add a minimum component to the backoff and retry timer * fix(certificatemanager): stub out sleep Provide stub function for sleep to allow long running sleep to cause test to fail * fix(certificatemanager): spy on sleep spy on sleep function to determine total time attempted to sleep * fix(certificatemanager): Failing test Failing test demonstrating min sleep is as low as 4.6 seconds. * fix(certificatemanager): increase attempts Increase max attempts in validation, still under 10, but such that minimum sleep duration of one minute is reached (but not more than 3 minutes). Can adjust tests if window is not suitable, but I find that at least 30 seconds is needed in my region
1 parent efd45ef commit 4973a8c

File tree

2 files changed

+127
-3
lines changed
  • packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler

2 files changed

+127
-3
lines changed

packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
const aws = require('aws-sdk');
44

5-
const sleep = function (ms) {
5+
const defaultSleep = function (ms) {
66
return new Promise(resolve => setTimeout(resolve, ms));
77
};
88

99
// These are used for test purposes only
1010
let defaultResponseURL;
1111
let waiter;
12+
let sleep = defaultSleep;
13+
let random = Math.random;
1214

1315
/**
1416
* Upload a CloudFormation response object to S3.
@@ -97,7 +99,7 @@ const requestCertificate = async function (requestId, domainName, subjectAlterna
9799
console.log('Waiting for ACM to provide DNS records for validation...');
98100

99101
let record;
100-
const maxAttempts = 6;
102+
const maxAttempts = 10;
101103
for (let attempt = 0; attempt < maxAttempts - 1 && !record; attempt++) {
102104
const { Certificate } = await acm.describeCertificate({
103105
CertificateArn: reqCertResponse.CertificateArn
@@ -108,7 +110,10 @@ const requestCertificate = async function (requestId, domainName, subjectAlterna
108110
record = options[0].ResourceRecord;
109111
} else {
110112
// Exponential backoff with jitter based on 200ms base
111-
await sleep(Math.random() * (Math.pow(2, attempt) * 200));
113+
// component of backoff fixed to ensure minimum total wait time on
114+
// slow targets.
115+
const base = Math.pow(2, attempt);
116+
await sleep(random() * base * 50 + base * 150);
112117
}
113118
}
114119
if (!record) {
@@ -248,3 +253,31 @@ exports.withWaiter = function (w) {
248253
exports.resetWaiter = function () {
249254
waiter = undefined;
250255
};
256+
257+
/**
258+
* @private
259+
*/
260+
exports.withSleep = function(s) {
261+
sleep = s;
262+
}
263+
264+
/**
265+
* @private
266+
*/
267+
exports.resetSleep = function() {
268+
sleep = defaultSleep;
269+
}
270+
271+
/**
272+
* @private
273+
*/
274+
exports.withRandom = function(r) {
275+
random = r;
276+
}
277+
278+
/**
279+
* @private
280+
*/
281+
exports.resetRandom = function() {
282+
random = Math.random;
283+
}

packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/test/handler.test.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ describe('DNS Validated Certificate Handler', () => {
1515
const testCertificateArn = 'arn:aws:acm:region:123456789012:certificate/12345678-1234-1234-1234-123456789012';
1616
const testRRName = '_3639ac514e785e898d2646601fa951d5.example.com';
1717
const testRRValue = '_x2.acm-validations.aws';
18+
const spySleep = sinon.spy(function(ms) {
19+
return Promise.resolve();
20+
});
1821

1922
beforeEach(() => {
2023
handler.withDefaultResponseURL(ResponseURL);
@@ -28,13 +31,16 @@ describe('DNS Validated Certificate Handler', () => {
2831
}
2932
};
3033
});
34+
handler.withSleep(spySleep);
3135
console.log = function () { };
3236
});
3337
afterEach(() => {
3438
// Restore waiters and logger
3539
handler.resetWaiter();
40+
handler.resetSleep();
3641
AWS.restore();
3742
console.log = origLog;
43+
spySleep.resetHistory();
3844
});
3945

4046
test('Fails if the event payload is empty', () => {
@@ -171,9 +177,94 @@ describe('DNS Validated Certificate Handler', () => {
171177
ValidationMethod: 'DNS'
172178
}));
173179
expect(request.isDone()).toBe(true);
180+
expect(spySleep.callCount).toBeLessThan(10);
181+
});
182+
});
183+
184+
test('Fails after at more than 60 seconds', () => {
185+
handler.withRandom(() => 0);
186+
const requestCertificateFake = sinon.fake.resolves({
187+
CertificateArn: testCertificateArn,
188+
});
189+
190+
const describeCertificateFake = sinon.fake.resolves({
191+
CertificateArn: testCertificateArn,
192+
Certificate: {
193+
}
194+
});
195+
196+
AWS.mock('ACM', 'requestCertificate', requestCertificateFake);
197+
AWS.mock('ACM', 'describeCertificate', describeCertificateFake);
198+
199+
const request = nock(ResponseURL).put('/', body => {
200+
return body.Status === 'FAILED' &&
201+
body.Reason.startsWith('Response from describeCertificate did not contain DomainValidationOptions');
202+
}).reply(200);
203+
204+
return LambdaTester(handler.certificateRequestHandler)
205+
.event({
206+
RequestType: 'Create',
207+
RequestId: testRequestId,
208+
ResourceProperties: {
209+
DomainName: testDomainName,
210+
HostedZoneId: testHostedZoneId,
211+
Region: 'us-east-1',
212+
}
213+
})
214+
.expectResolve(() => {
215+
sinon.assert.calledWith(requestCertificateFake, sinon.match({
216+
DomainName: testDomainName,
217+
ValidationMethod: 'DNS'
218+
}));
219+
expect(request.isDone()).toBe(true);
220+
const totalSleep = spySleep.getCalls().map(call => call.args[0]).reduce((p, n) => p + n, 0);
221+
expect(totalSleep).toBeGreaterThan(60 * 1000);
174222
});
175223
});
176224

225+
test('Fails after at less than 360 seconds', () => {
226+
handler.withRandom(() => 1);
227+
const requestCertificateFake = sinon.fake.resolves({
228+
CertificateArn: testCertificateArn,
229+
});
230+
231+
const describeCertificateFake = sinon.fake.resolves({
232+
CertificateArn: testCertificateArn,
233+
Certificate: {
234+
}
235+
});
236+
237+
AWS.mock('ACM', 'requestCertificate', requestCertificateFake);
238+
AWS.mock('ACM', 'describeCertificate', describeCertificateFake);
239+
240+
const request = nock(ResponseURL).put('/', body => {
241+
return body.Status === 'FAILED' &&
242+
body.Reason.startsWith('Response from describeCertificate did not contain DomainValidationOptions');
243+
}).reply(200);
244+
245+
return LambdaTester(handler.certificateRequestHandler)
246+
.event({
247+
RequestType: 'Create',
248+
RequestId: testRequestId,
249+
ResourceProperties: {
250+
DomainName: testDomainName,
251+
HostedZoneId: testHostedZoneId,
252+
Region: 'us-east-1',
253+
}
254+
})
255+
.expectResolve(() => {
256+
sinon.assert.calledWith(requestCertificateFake, sinon.match({
257+
DomainName: testDomainName,
258+
ValidationMethod: 'DNS'
259+
}));
260+
expect(request.isDone()).toBe(true);
261+
expect(spySleep.callCount).toBeLessThan(10);
262+
const totalSleep = spySleep.getCalls().map(call => call.args[0]).reduce((p, n) => p + n, 0);
263+
expect(totalSleep).toBeLessThan(360 *1000);
264+
});
265+
});
266+
267+
177268
test('Deletes a certificate if RequestType is Delete', () => {
178269
const deleteCertificateFake = sinon.fake.resolves({});
179270
AWS.mock('ACM', 'deleteCertificate', deleteCertificateFake);

0 commit comments

Comments
 (0)