diff --git a/.gitignore b/.gitignore index 32870916d6561..3fb446b50982f 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ read*lock # Nx cache .nx/ +.traces +*.tmp diff --git a/build.sh b/build.sh index 2816034dba8f8..1a3d4c5039e5b 100755 --- a/build.sh +++ b/build.sh @@ -47,6 +47,10 @@ done export NODE_OPTIONS="--max-old-space-size=8196 --experimental-worker ${NODE_OPTIONS:-}" +if $ci; then + export CI=true +fi + # Temporary log memory for long builds (this may mess with tests that check stderr) # export NODE_OPTIONS="-r $PWD/scripts/log-memory.js ${NODE_OPTIONS:-}" @@ -116,6 +120,10 @@ echo "========================================================================== echo "building..." time npx lerna run $bail --concurrency=$concurrency $runtarget $flags || fail +echo "=============[ MEMORY AND TIMING TRACES ]====================================================" +cat .traces/* +echo "=============================================================================================" + if [ "$check_compat" == "true" ]; then /bin/bash scripts/check-api-compatibility.sh fi diff --git a/packages/@aws-cdk/custom-resource-handlers/.gitignore b/packages/@aws-cdk/custom-resource-handlers/.gitignore index cbbe46ddaf561..567f7a21e9ad5 100644 --- a/packages/@aws-cdk/custom-resource-handlers/.gitignore +++ b/packages/@aws-cdk/custom-resource-handlers/.gitignore @@ -32,4 +32,3 @@ scripts/*.d.ts !lib/aws-certificatemanager/dns-validated-certificate-handler/index.js !test/aws-certificatemanager/dns-validated-certificate-handler.test.js -!lib/aws-cloudfront/edge-function/index.js diff --git a/packages/@aws-cdk/custom-resource-handlers/test/aws-certificatemanager/dns-validated-certificate-handler.test.js b/packages/@aws-cdk/custom-resource-handlers/test/aws-certificatemanager/dns-validated-certificate-handler.test.js deleted file mode 100644 index a42372f2b387e..0000000000000 --- a/packages/@aws-cdk/custom-resource-handlers/test/aws-certificatemanager/dns-validated-certificate-handler.test.js +++ /dev/null @@ -1,1490 +0,0 @@ -'use strict'; - -const LambdaTester = require('lambda-tester').noVersionCheck(); -const sinon = require('sinon'); -const handler = require('..'); -const nock = require('nock'); -const { mockClient } = require('aws-sdk-client-mock'); -const acm = require('@aws-sdk/client-acm'); -const r53 = require('@aws-sdk/client-route-53'); -const ResponseURL = 'https://cloudwatch-response-mock.example.com/'; - -jest.mock('@aws-sdk/client-acm', () => { - const actual = jest.requireActual('@aws-sdk/client-acm'); - return { - ...actual, - waitUntilCertificateValidated: async () => {}, - }; -}); - -jest.mock('@aws-sdk/client-route-53', () => { - const actual = jest.requireActual('@aws-sdk/client-route-53'); - return { - ...actual, - waitUntilResourceRecordSetsChanged: async () => {}, - }; -}); - -const acmMock = mockClient(acm.ACMClient); -const r53Mock = mockClient(r53.Route53Client); - -describe('DNS Validated Certificate Handler', () => { - let origLog = console.log; - const testRequestId = 'f4ef1b10-c39a-44e3-99c0-fbf7e53c3943'; - const testDomainName = 'test.example.com'; - const testSubjectAlternativeName = 'foo.example.com'; - const testHostedZoneId = '/hostedzone/Z3P5QSUBK4POTI'; - const testCertificateArn = 'arn:aws:acm:region:123456789012:certificate/12345678-1234-1234-1234-123456789012'; - const testRRName = '_3639ac514e785e898d2646601fa951d5.example.com'; - const testRRValue = '_x2.acm-validations.aws'; - const testAltRRName = '_3639ac514e785e898d2646601fa951d5.foo.example.com'; - const testAltRRValue = '_x3.acm-validations.aws'; - const testTags = { Tag1: 'Test1', Tag2: 'Test2' }; - const testTagsValue = [{ Key: 'Tag1', Value: 'Test1' }, { Key: 'Tag2', Value: 'Test2' }]; - const spySleep = sinon.spy(function (ms) { - return Promise.resolve(); - }); - - beforeEach(() => { - handler.withDefaultResponseURL(ResponseURL); - handler.withWaiter(function () { - // Mock waiter is merely a self-fulfilling promise - return new Promise((resolve) => { - resolve(); - }); - }); - handler.withSleep(spySleep); - console.log = function () { }; - }); - afterEach(() => { - // Restore waiters and logger - handler.resetWaiter(); - handler.resetSleep(); - handler.resetMaxAttempts(); - r53Mock.reset(); - acmMock.reset(); - nock.cleanAll(); - console.log = origLog; - spySleep.resetHistory(); - }); - - test('Empty event payload fails', () => { - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'FAILED' && body.Reason === 'Unsupported request type undefined'; - }).reply(200); - return LambdaTester(handler.certificateRequestHandler) - .event({}) - .expectResolve(() => { - expect(request.isDone()).toBe(true); - }); - }); - - test('Bogus operation fails', () => { - const bogusType = 'bogus'; - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'FAILED' && body.Reason === 'Unsupported request type ' + bogusType; - }).reply(200); - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: bogusType - }) - .expectResolve(() => { - expect(request.isDone()).toBe(true); - }); - }); - - test('Create operation requests a certificate', () => { - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.stub(); - describeCertificateFake.onFirstCall().resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - describeCertificateFake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [{ - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }] - } - }); - - const addTagsToCertificateFake = sinon.fake.resolves({}); - - const changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - acmMock.on(acm.AddTagsToCertificateCommand).callsFake(addTagsToCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Create', - RequestId: testRequestId, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: testTags - } - }) - .expectResolve(() => { - sinon.assert.calledWith(requestCertificateFake, sinon.match({ - DomainName: testDomainName, - ValidationMethod: 'DNS', - Options: { - CertificateTransparencyLoggingPreference: undefined - } - })); - sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({ - ChangeBatch: { - Changes: [{ - Action: 'UPSERT', - ResourceRecordSet: { - Name: testRRName, - Type: 'CNAME', - TTL: 60, - ResourceRecords: [{ - Value: testRRValue - }] - } - }] - }, - HostedZoneId: testHostedZoneId - })); - sinon.assert.calledWith(addTagsToCertificateFake, sinon.match({ - "CertificateArn": testCertificateArn, - "Tags": testTagsValue, - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Create operation with `SubjectAlternativeNames` requests a certificate with validation records for all options', () => { - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.stub(); - describeCertificateFake.onFirstCall().resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - describeCertificateFake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [ - { - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }, { - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testAltRRName, - Type: 'CNAME', - Value: testAltRRValue - } - } - ] - } - }); - - const addTagsToCertificateFake = sinon.fake.resolves({ - Certificate: testCertificateArn, - Tags: testTags, - }); - - const changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - acmMock.on(acm.AddTagsToCertificateCommand).callsFake(addTagsToCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Create', - RequestId: testRequestId, - ResourceProperties: { - DomainName: testDomainName, - SubjectAlternativeNames: [testSubjectAlternativeName], - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: testTags, - } - }) - .expectResolve(() => { - sinon.assert.calledWith(requestCertificateFake, sinon.match({ - DomainName: testDomainName, - ValidationMethod: 'DNS', - SubjectAlternativeNames: [testSubjectAlternativeName] - })); - sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({ - ChangeBatch: { - Changes: [ - { - Action: 'UPSERT', - ResourceRecordSet: { - Name: testRRName, - Type: 'CNAME', - TTL: 60, - ResourceRecords: [{ - Value: testRRValue - }] - } - }, { - Action: 'UPSERT', - ResourceRecordSet: { - Name: testAltRRName, - Type: 'CNAME', - TTL: 60, - ResourceRecords: [{ - Value: testAltRRValue - }] - } - } - ] - }, - HostedZoneId: testHostedZoneId - })); - sinon.assert.calledWith(addTagsToCertificateFake, sinon.match({ - "CertificateArn": testCertificateArn, - "Tags": testTagsValue, - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Create operation with `SubjectAlternativeNames` requests a certificate for all options without duplicates', () => { - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.stub(); - describeCertificateFake.onFirstCall().resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - describeCertificateFake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [ - { - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }, { - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testAltRRName, - Type: 'CNAME', - Value: testAltRRValue - } - }, { - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - } - ] - } - }); - - const addTagsToCertificateFake = sinon.fake.resolves({ - Certificate: testCertificateArn, - Tags: testTags, - }); - - const changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - acmMock.on(acm.AddTagsToCertificateCommand).callsFake(addTagsToCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Create', - RequestId: testRequestId, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: testTags, - } - }) - .expectResolve(() => { - sinon.assert.calledWith(requestCertificateFake, sinon.match({ - DomainName: testDomainName, - ValidationMethod: 'DNS' - })); - sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({ - ChangeBatch: { - Changes: [ - { - Action: 'UPSERT', - ResourceRecordSet: { - Name: testRRName, - Type: 'CNAME', - TTL: 60, - ResourceRecords: [{ - Value: testRRValue - }] - } - }, { - Action: 'UPSERT', - ResourceRecordSet: { - Name: testAltRRName, - Type: 'CNAME', - TTL: 60, - ResourceRecords: [{ - Value: testAltRRValue - }] - } - } - ] - }, - HostedZoneId: testHostedZoneId - })); - sinon.assert.calledWith(addTagsToCertificateFake, sinon.match({ - "CertificateArn": testCertificateArn, - "Tags": testTagsValue, - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Create operation with `SubjectAlternativeNames` gracefully handles partial results from DescribeCertificate', () => { - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.stub(); - describeCertificateFake.onFirstCall().resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [ - { - ValidationStatus: 'PENDING_VALIDATION', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }, - { - ValidationStatus: 'PENDING_VALIDATION', - }, - ] - } - }); - describeCertificateFake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [ - { - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }, - { - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testAltRRName, - Type: 'CNAME', - Value: testAltRRValue - } - }, - ] - } - }); - - const addTagsToCertificateFake = sinon.fake.resolves({ - Certificate: testCertificateArn, - Tags: testTags, - }); - - const changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - acmMock.on(acm.AddTagsToCertificateCommand).callsFake(addTagsToCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Create', - RequestId: testRequestId, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: testTags, - } - }) - .expectResolve(() => { - sinon.assert.calledWith(requestCertificateFake, sinon.match({ - DomainName: testDomainName, - ValidationMethod: 'DNS' - })); - sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({ - ChangeBatch: { - Changes: [ - { - Action: 'UPSERT', - ResourceRecordSet: { - Name: testRRName, - Type: 'CNAME', - TTL: 60, - ResourceRecords: [{ - Value: testRRValue - }] - } - }, { - Action: 'UPSERT', - ResourceRecordSet: { - Name: testAltRRName, - Type: 'CNAME', - TTL: 60, - ResourceRecords: [{ - Value: testAltRRValue - }] - } - } - ] - }, - HostedZoneId: testHostedZoneId - })); - sinon.assert.calledWith(addTagsToCertificateFake, sinon.match({ - "CertificateArn": testCertificateArn, - "Tags": testTagsValue, - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Create operation fails after more than 60s if certificate has no DomainValidationOptions', () => { - handler.withRandom(() => 0); - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.fake.resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'FAILED' && - body.Reason.startsWith('Response from describeCertificate did not contain DomainValidationOptions'); - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Create', - RequestId: testRequestId, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - } - }) - .expectResolve(() => { - sinon.assert.calledWith(requestCertificateFake, sinon.match({ - DomainName: testDomainName, - ValidationMethod: 'DNS' - })); - expect(request.isDone()).toBe(true); - const totalSleep = spySleep.getCalls().map(call => call.args[0]).reduce((p, n) => p + n, 0); - expect(totalSleep).toBeGreaterThan(60 * 1000); - }); - }); - - test('Create operation fails within 360s and 10 attempts if certificate has no DomainValidationOptions', () => { - handler.withRandom(() => 1); - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.fake.resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'FAILED' && - body.Reason.startsWith('Response from describeCertificate did not contain DomainValidationOptions'); - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Create', - RequestId: testRequestId, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - } - }) - .expectResolve(() => { - sinon.assert.calledWith(requestCertificateFake, sinon.match({ - DomainName: testDomainName, - ValidationMethod: 'DNS' - })); - expect(request.isDone()).toBe(true); - expect(spySleep.callCount).toBe(10); - const totalSleep = spySleep.getCalls().map(call => call.args[0]).reduce((p, n) => p + n, 0); - expect(totalSleep).toBeLessThan(360 * 1000); - }); - }); - - test('Create operation with a maximum of 1 attempts describes the certificate once', () => { - handler.withMaxAttempts(1); - - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - - const describeCertificateFake = sinon.fake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [{ - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }] - } - }); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const addTagsToCertificateFake = sinon.fake.resolves({ - Certificate: testCertificateArn, - Tags: testTags, - }); - acmMock.on(acm.AddTagsToCertificateCommand).callsFake(addTagsToCertificateFake); - - const changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Create', - RequestId: testRequestId, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: testTags, - } - }) - .expectResolve(() => { - sinon.assert.calledOnce(describeCertificateFake); - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn, - })); - sinon.assert.calledWith(addTagsToCertificateFake, sinon.match({ - "CertificateArn": testCertificateArn, - "Tags": testTagsValue, - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Create operation succeeds with no tags passed', () => { - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.stub(); - describeCertificateFake.onFirstCall().resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - describeCertificateFake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [{ - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }] - } - }); - - const changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - - const addTagsToCertificateFake = sinon.fake.resolves({ - Certificate: testCertificateArn, - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - acmMock.on(acm.AddTagsToCertificateCommand).callsFake(addTagsToCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Create', - RequestId: testRequestId, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1' - } - }) - .expectResolve(() => { - sinon.assert.calledWith(requestCertificateFake, sinon.match({ - DomainName: testDomainName, - ValidationMethod: 'DNS' - })); - sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({ - ChangeBatch: { - Changes: [{ - Action: 'UPSERT', - ResourceRecordSet: { - Name: testRRName, - Type: 'CNAME', - TTL: 60, - ResourceRecords: [{ - Value: testRRValue - }] - } - }] - }, - HostedZoneId: testHostedZoneId - })); - sinon.assert.neverCalledWith(addTagsToCertificateFake, sinon.match({ - "CertificateArn": testCertificateArn, - "Tags": testTagsValue, - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Create operation with `CertificateTransparencyLoggingPreference` requests a certificate with that preference set', () => { - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.stub(); - describeCertificateFake.onFirstCall().resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - describeCertificateFake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [{ - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }] - } - }); - - const addTagsToCertificateFake = sinon.fake.resolves({}); - - const changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - acmMock.on(acm.AddTagsToCertificateCommand).callsFake(addTagsToCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Create', - RequestId: testRequestId, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - CertificateTransparencyLoggingPreference: 'DISABLED', - Tags: testTags - } - }) - .expectResolve(() => { - sinon.assert.calledWith(requestCertificateFake, sinon.match({ - DomainName: testDomainName, - ValidationMethod: 'DNS', - Options: { - CertificateTransparencyLoggingPreference: 'DISABLED' - } - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Delete operation deletes the certificate', () => { - const describeCertificateFake = sinon.fake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - } - }); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const deleteCertificateFake = sinon.fake.resolves({}); - acmMock.on(acm.DeleteCertificateCommand).callsFake(deleteCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - } - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.calledWith(deleteCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Delete operation is idempotent', () => { - const error = new Error(); - error.name = 'ResourceNotFoundException'; - - const describeCertificateFake = sinon.fake.rejects(error); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const deleteCertificateFake = sinon.fake.rejects(error); - acmMock.on(acm.DeleteCertificateCommand).callsFake(deleteCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - } - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.neverCalledWith(deleteCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Update operation requests a certificate', () => { - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.stub(); - describeCertificateFake.onFirstCall().resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - describeCertificateFake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [{ - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }] - } - }); - - const addTagsToCertificateFake = sinon.fake.resolves({}); - - const changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - acmMock.on(acm.AddTagsToCertificateCommand).callsFake(addTagsToCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Update', - RequestId: testRequestId, - OldResourceProperties: { - DomainName: 'example.com', - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: testTags - }, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: testTags - } - }) - .expectResolve(() => { - sinon.assert.calledWith(requestCertificateFake, sinon.match({ - DomainName: testDomainName, - ValidationMethod: 'DNS', - Options: { - CertificateTransparencyLoggingPreference: undefined - } - })); - sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({ - ChangeBatch: { - Changes: [{ - Action: 'UPSERT', - ResourceRecordSet: { - Name: testRRName, - Type: 'CNAME', - TTL: 60, - ResourceRecords: [{ - Value: testRRValue - }] - } - }] - }, - HostedZoneId: testHostedZoneId - })); - sinon.assert.calledWith(addTagsToCertificateFake, sinon.match({ - "CertificateArn": testCertificateArn, - "Tags": testTagsValue, - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Update operation updates tags only', () => { - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.stub(); - describeCertificateFake.onFirstCall().resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - describeCertificateFake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [{ - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }] - } - }); - - const addTagsToCertificateFake = sinon.fake.resolves({}); - - const changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - acmMock.on(acm.AddTagsToCertificateCommand).callsFake(addTagsToCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Update', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - OldResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: testTags, - }, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: { - ...testTags, - Tag4: 'Value4', - }, - } - }) - .expectResolve(() => { - sinon.assert.notCalled(requestCertificateFake); - sinon.assert.notCalled(changeResourceRecordSetsFake); - sinon.assert.calledWith(addTagsToCertificateFake, sinon.match({ - "CertificateArn": testCertificateArn, - "Tags": [{ Key: 'Tag1', Value: 'Test1' }, { Key: 'Tag2', Value: 'Test2' }, { Key: 'Tag4', Value: 'Value4' }], - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Update operation does not request certificate if removal policy is changed', () => { - const requestCertificateFake = sinon.fake.resolves({ - CertificateArn: testCertificateArn, - }); - - const describeCertificateFake = sinon.stub(); - describeCertificateFake.onFirstCall().resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - describeCertificateFake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [{ - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }] - } - }); - - const addTagsToCertificateFake = sinon.fake.resolves({}); - - const changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - - acmMock.on(acm.RequestCertificateCommand).callsFake(requestCertificateFake); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - acmMock.on(acm.AddTagsToCertificateCommand).callsFake(addTagsToCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Update', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - OldResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: testTags, - }, - ResourceProperties: { - DomainName: testDomainName, - HostedZoneId: testHostedZoneId, - Region: 'us-east-1', - Tags: testTags, - RemovalPolicy: 'retain', - } - }) - .expectResolve(() => { - sinon.assert.notCalled(requestCertificateFake); - sinon.assert.notCalled(changeResourceRecordSetsFake); - sinon.assert.calledWith(addTagsToCertificateFake, sinon.match({ - "CertificateArn": testCertificateArn, - "Tags": testTagsValue, - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Delete operation succeeds if certificate becomes not-in-use', () => { - const usedByArn = 'arn:aws:cloudfront::123456789012:distribution/d111111abcdef8'; - - const describeCertificateFake = sinon.stub(); - describeCertificateFake.onFirstCall().resolves({ - Certificate: { - CertificateArn: testCertificateArn, - InUseBy: [usedByArn], - } - }); - describeCertificateFake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - InUseBy: [], - } - }); - - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const deleteCertificateFake = sinon.fake.resolves({}); - acmMock.on(acm.DeleteCertificateCommand).callsFake(deleteCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - } - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.calledWith(deleteCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Delete operation fails within 360s and 10 attempts if certificate is in-use', () => { - const usedByArn = 'arn:aws:cloudfront::123456789012:distribution/d111111abcdef8'; - - const describeCertificateFake = sinon.fake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - InUseBy: [usedByArn], - } - }); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const error = new Error(); - error.name = 'ResourceInUseException'; - const deleteCertificateFake = sinon.fake.rejects(error); - acmMock.on(acm.DeleteCertificateCommand).callsFake(deleteCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'FAILED'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - } - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.neverCalledWith(deleteCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - const totalSleep = spySleep.getCalls().map(call => call.args[0]).reduce((p, n) => p + n, 0); - expect(totalSleep).toBeLessThan(360 * 1000); - expect(spySleep.callCount).toBe(10); - expect(request.isDone()).toBe(true); - }); - }); - - test('Delete operation fails if some other error is encountered during describe', () => { - const error = new Error(); - error.name = 'SomeOtherException'; - - const describeCertificateFake = sinon.fake.rejects(error); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const deleteCertificateFake = sinon.fake.resolves({}); - acmMock.on(acm.DeleteCertificateCommand).callsFake(deleteCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'FAILED'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - } - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.neverCalledWith(deleteCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Delete operation fails if some other error is encountered during delete', () => { - const describeCertificateFake = sinon.fake.resolves({ - Certificate: { - CertificateArn: testCertificateArn - } - }); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const error = new Error(); - error.name = 'SomeOtherException'; - const deleteCertificateFake = sinon.fake.rejects(error); - acmMock.on(acm.DeleteCertificateCommand).callsFake(deleteCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'FAILED'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - } - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.calledWith(deleteCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('Delete operation with a maximum of 1 attempts describes the certificate once', () => { - handler.withMaxAttempts(1); - - const describeCertificateFake = sinon.fake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - } - }); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const deleteCertificateFake = sinon.fake.resolves({}); - acmMock.on(acm.DeleteCertificateCommand).callsFake(deleteCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - } - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.calledWith(deleteCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - expect(request.isDone()).toBe(true); - }); - }); - - describe('Delete option record cleanup', () => { - let describeCertificateFake; - let deleteCertificateFake; - let changeResourceRecordSetsFake; - - beforeEach(() => { - deleteCertificateFake = sinon.fake.resolves({}); - acmMock.on(acm.DeleteCertificateCommand).callsFake(deleteCertificateFake); - changeResourceRecordSetsFake = sinon.fake.resolves({ - ChangeInfo: { - Id: 'bogus' - } - }); - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - - describeCertificateFake = sinon.fake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - DomainValidationOptions: [{ - ValidationStatus: 'SUCCESS', - ResourceRecord: { - Name: testRRName, - Type: 'CNAME', - Value: testRRValue - } - }] - } - }); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - }); - - test('ignores records if CleanupRecords is not set', () => { - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - HostedZoneId: testHostedZoneId, - } - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.calledWith(deleteCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.notCalled(changeResourceRecordSetsFake); - expect(request.isDone()).toBe(true); - }); - }); - - test('ignores records if CleanupRecords is not set to "true"', () => { - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - HostedZoneId: testHostedZoneId, - CleanupRecords: 'TRUE', // Not "true" - } - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.calledWith(deleteCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.notCalled(changeResourceRecordSetsFake); - expect(request.isDone()).toBe(true); - }); - }); - - test('deletes records if CleanupRecords is set to true and records are present', () => { - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'SUCCESS'; - }).reply(200); - - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - HostedZoneId: testHostedZoneId, - CleanupRecords: 'true', - }, - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.calledWith(deleteCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({ - ChangeBatch: { - Changes: [{ - Action: 'DELETE', - ResourceRecordSet: { - Name: testRRName, - Type: 'CNAME', - TTL: 60, - ResourceRecords: [{ - Value: testRRValue - }] - } - }] - }, - HostedZoneId: testHostedZoneId - })); - expect(request.isDone()).toBe(true); - }); - }); - - test('fails if CleanupRecords is set to true and records are not present', () => { - describeCertificateFake = sinon.fake.resolves({ - Certificate: { - CertificateArn: testCertificateArn, - } - }); - acmMock.on(acm.DescribeCertificateCommand).callsFake(describeCertificateFake); - - const request = nock(ResponseURL).put('/', body => { - return body.Status === 'FAILED' && - body.Reason.startsWith('Response from describeCertificate did not contain DomainValidationOptions'); - }).reply(200); - - r53Mock.on(r53.ChangeResourceRecordSetsCommand).callsFake(changeResourceRecordSetsFake); - - return LambdaTester(handler.certificateRequestHandler) - .event({ - RequestType: 'Delete', - RequestId: testRequestId, - PhysicalResourceId: testCertificateArn, - ResourceProperties: { - Region: 'us-east-1', - HostedZoneId: testHostedZoneId, - CleanupRecords: 'true', - }, - }) - .expectResolve(() => { - sinon.assert.calledWith(describeCertificateFake, sinon.match({ - CertificateArn: testCertificateArn - })); - sinon.assert.notCalled(deleteCertificateFake); - sinon.assert.notCalled(changeResourceRecordSetsFake); - expect(request.isDone()).toBe(true); - }); - }); - }); -}); diff --git a/scripts/jupyter-notebooks/.gitignore b/scripts/jupyter-notebooks/.gitignore new file mode 100644 index 0000000000000..85c55ebcf1e22 --- /dev/null +++ b/scripts/jupyter-notebooks/.gitignore @@ -0,0 +1,2 @@ +.env +.venv diff --git a/scripts/jupyter-notebooks/build-tracing/requirements.txt b/scripts/jupyter-notebooks/build-tracing/requirements.txt new file mode 100644 index 0000000000000..de7aa26158fca --- /dev/null +++ b/scripts/jupyter-notebooks/build-tracing/requirements.txt @@ -0,0 +1,2 @@ +jupyter==1.1.1 +plotnine==0.13.6 diff --git a/scripts/jupyter-notebooks/build-tracing/visualize-build.py b/scripts/jupyter-notebooks/build-tracing/visualize-build.py new file mode 100644 index 0000000000000..904bbbba5a33e --- /dev/null +++ b/scripts/jupyter-notebooks/build-tracing/visualize-build.py @@ -0,0 +1,78 @@ +# To use this script, install the `Jupyter` extension for VS Code, create a +# virtual environment and install `requirements.txt`, then select the virtual +# env in VS Code. + +# %% +from pathlib import Path +import csv +from plotnine import * +import pandas as pd +from itertools import groupby + +trace_dir = Path(__file__).parent / '..' / '..' / '..' / '.traces' + +rows = [] +for file in trace_dir.glob('*'): + with open(file) as f: + reader = csv.reader(f) + for row in reader: + if not row: continue + rows.append(dict( + command=row[0], + package=row[1], + start=int(row[2]), + end=int(row[3]), + mem=float(row[4]), + )) + +# Convert rows to DataFrame +df = pd.DataFrame(rows) + +# Create Gantt chart +gantt = (ggplot(df, aes(xmin='start', xmax='end', y='package', fill='command')) + + geom_segment(aes(x='start', xend='end', y='package', yend='package'), size=8) +# + geom_rect(aes(x='start', xend='end', ymin='package', ymax='package'), size=8) + + labs(title='Package Build Timeline', x='Time', y='Package') + + theme(figure_size=(12, 6)) + + scale_x_datetime(date_labels='%H:%M:%S') +) + +gantt.show() + +# Calculate memory usage +mem_mut = [] +for row in rows: + mem_mut.append((row['start'], row['mem'])) + mem_mut.append((row['end'], -row['mem'])) +mem_mut.sort(key=lambda x: x[0]) + +mem_timeline = [] +for (t, mem_delta) in mem_mut: + if mem_timeline: + mem_timeline.append(dict(t=t, mem=mem_timeline[-1]['mem'] + mem_delta)) + else: + mem_timeline.append(dict(t=t, mem=mem_delta)) + +mem_plot = (ggplot(pd.DataFrame(mem_timeline), aes(x='t', y='mem')) + + geom_line() + + labs(title='Memory Usage Timeline', x='Time', y='Memory Used (MB)') + + scale_x_datetime(date_labels='%H:%M:%S') +) + +mem_plot.show() + +# Avg Memory Usage by command +print('Avg Memory Usage by command') +print(df.groupby('command')['mem'].mean().sort_values(ascending=False)) + +# Avg Memory Usage by command that is not for aws-cdk-lib +print('Avg Memory Usage by command, except aws-cdk-lib') +non_cdk_lib = df[df['package'] != 'aws-cdk-lib'] +print(non_cdk_lib.groupby('command')['mem'].mean().sort_values(ascending=False)) + +# Avg Memory usage by package +print('Avg Memory Usage by package') +print(df.groupby('package')['mem'].mean().sort_values(ascending=False)) + + +# %% diff --git a/tools/@aws-cdk/cdk-build-tools/bin/cdk-build.ts b/tools/@aws-cdk/cdk-build-tools/bin/cdk-build.ts index 0507b33126ff2..2f24c65582f79 100644 --- a/tools/@aws-cdk/cdk-build-tools/bin/cdk-build.ts +++ b/tools/@aws-cdk/cdk-build-tools/bin/cdk-build.ts @@ -46,12 +46,12 @@ async function main() { if (options.pre) { const commands = options.pre.join(' && '); - await shell([commands], { timers, env }); + await shell([commands], { timers, env, trace: { command: 'pre', pkg: options.currentPackageName } }); } const gen = genScript(); if (args.gen && gen) { - await shell([gen], { timers, env }); + await shell([gen], { timers, env, trace: { command: 'gen', pkg: options.currentPackageName } }); } const overrides: CompilerOverrides = { eslint: args.eslint, jsii: args.jsii, tsc: args.tsc }; @@ -62,7 +62,7 @@ async function main() { if (options.post) { const commands = options.post.join(' && '); - await shell([commands], { timers, env }); + await shell([commands], { timers, env, trace: { command: 'post', pkg: options.currentPackageName } }); } } diff --git a/tools/@aws-cdk/cdk-build-tools/bin/cdk-test.ts b/tools/@aws-cdk/cdk-build-tools/bin/cdk-test.ts index 09d0cf359e309..271e67f0fa50c 100644 --- a/tools/@aws-cdk/cdk-build-tools/bin/cdk-test.ts +++ b/tools/@aws-cdk/cdk-build-tools/bin/cdk-test.ts @@ -47,12 +47,18 @@ async function main() { const testFiles = await unitTestFiles(); if (testFiles.length > 0) { - await shell([args.jest], unitTestOptions); + await shell([args.jest], { + ...unitTestOptions, + trace: { command: 'jest', pkg: options.currentPackageName }, + }); } // Run integration test if the package has integ test files if (await hasIntegTests()) { - await shell(['integ-runner'], defaultShellOptions); + await shell(['integ-runner'], { + ...defaultShellOptions, + trace: { command: 'integ-runner', pkg: options.currentPackageName }, + }); } } diff --git a/tools/@aws-cdk/cdk-build-tools/lib/compile.ts b/tools/@aws-cdk/cdk-build-tools/lib/compile.ts index 173fea93685b5..edf8e39d8f734 100644 --- a/tools/@aws-cdk/cdk-build-tools/lib/compile.ts +++ b/tools/@aws-cdk/cdk-build-tools/lib/compile.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import { makeExecutable, shell } from './os'; import { CDKBuildOptions, CompilerOverrides, currentPackageJson, packageCompiler } from './package-info'; import { Timers } from './timer'; @@ -7,7 +8,10 @@ import { Timers } from './timer'; */ export async function compileCurrentPackage(options: CDKBuildOptions, timers: Timers, compilers: CompilerOverrides = {}): Promise { const env = options.env; - await shell(packageCompiler(compilers, options), { timers, env }); + const compiler = packageCompiler(compilers, options); + + let compilerName = path.basename(compiler[0]); + await shell(compiler, { timers, env, trace: { command: compilerName, pkg: options.currentPackageName } }); // Find files in bin/ that look like they should be executable, and make them so. const scripts = currentPackageJson().bin || {}; diff --git a/tools/@aws-cdk/cdk-build-tools/lib/lint.ts b/tools/@aws-cdk/cdk-build-tools/lib/lint.ts index 972b522099de6..6de40cb3456ed 100644 --- a/tools/@aws-cdk/cdk-build-tools/lib/lint.ts +++ b/tools/@aws-cdk/cdk-build-tools/lib/lint.ts @@ -23,14 +23,14 @@ export async function lintCurrentPackage( await shell([ eslintPath, ...fixOption, - ], { timers, env }); + ], { timers, env, trace: { command: 'eslint', pkg: options.currentPackageName } }); } if (!options.pkglint?.disable) { await shell([ 'pkglint', ...fixOption, - ], { timers, env }); + ], { timers, env, trace: { command: 'pkglint', pkg: options.currentPackageName } }); } if (await fs.pathExists('README.md')) { @@ -45,5 +45,5 @@ export async function lintCurrentPackage( ], { timers }); } - await shell([path.join(__dirname, '..', 'bin', 'cdk-awslint')], { timers, env }); + await shell([path.join(__dirname, '..', 'bin', 'cdk-awslint')], { timers, env, trace: { command: 'awslint', pkg: options.currentPackageName } }); } diff --git a/tools/@aws-cdk/cdk-build-tools/lib/os.ts b/tools/@aws-cdk/cdk-build-tools/lib/os.ts index 2af5c1ac08376..61c5c95be2160 100644 --- a/tools/@aws-cdk/cdk-build-tools/lib/os.ts +++ b/tools/@aws-cdk/cdk-build-tools/lib/os.ts @@ -1,5 +1,6 @@ import * as child_process from 'child_process'; import * as fs from 'fs'; +import * as path from 'path'; import * as util from 'util'; import * as chalk from 'chalk'; import { Timers } from './timer'; @@ -7,6 +8,10 @@ import { Timers } from './timer'; interface ShellOptions { timers?: Timers; env?: child_process.SpawnOptions['env']; + trace?: { + command: string; + pkg: string; + }; } /** @@ -15,9 +20,24 @@ interface ShellOptions { * Is platform-aware, handles errors nicely. */ export async function shell(command: string[], options: ShellOptions = {}): Promise { - const [cmd, ...args] = command; + let [cmd, ...args] = command; + + const timeFile = '.time.tmp'; + + // Try to trace memory usage with /usr/bin/time if we're tracing + if (options.trace && process.platform === 'darwin') { + args.unshift('-l', '-o', timeFile, cmd); + cmd = '/usr/bin/time'; + } + if (options.trace && process.platform === 'linux') { + args.unshift('-v', '-o', timeFile, cmd); + cmd = '/usr/bin/time'; + } + const timer = (options.timers || new Timers()).start(cmd); + const startTime = new Date(); + await makeShellScriptExecutable(cmd); // yarn exec runs the provided command with the correct environment for the workspace. @@ -38,6 +58,7 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom return new Promise((resolve, reject) => { const stdout = new Array(); + const stderr = new Array(); child.stdout!.on('data', chunk => { process.stdout.write(chunk); @@ -45,6 +66,7 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom }); child.stderr!.on('data', chunk => { + stderr.push(chunk); process.stderr.write(makeRed(chunk.toString())); }); @@ -52,6 +74,31 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom child.once('exit', code => { timer.end(); + const endTime = new Date(); + + if (options.trace) { + // The end of stderr contains the timing measurements + const timing = fs.readFileSync(timeFile, 'utf-8'); + let maxMemMB = 0; + + if (process.platform === 'darwin') { + const f = timing.match(/(\d+) maximum resident set size/); // Bytes + if (f) { + maxMemMB = Number(f[1]) / (1024 * 1024); + } + } + if (process.platform === 'linux') { + const f = timing.match(/Maximum resident set size \(kbytes\): (\d+)/); // kbytes + if (f) { + maxMemMB = Number(f[1]) / 1024; + } + } + + fs.unlinkSync(timeFile); + + writeTrace(options.trace.command, options.trace.pkg, startTime, endTime, maxMemMB); + } + if (code === 0) { resolve(Buffer.concat(stdout).toString('utf-8')); } else { @@ -173,3 +220,43 @@ async function isShellScript(script: string): Promise { return buffer.equals(Buffer.from('#!')); } + +/** + * Write a trace file. + * + * We write each to a different file to avoid race conditions appending to a shared file + * in parallel processes. + */ +function writeTrace(command: string, pkg: string, start: Date, end: Date, maxMemMB: number) { + const lernaJson = findUp('lerna.json'); + if (!lernaJson) { + return; + } + const dir = path.join(path.dirname(lernaJson), '.traces'); + fs.mkdirSync(dir, { recursive: true }); + + fs.writeFileSync( + path.join(dir, `${slugify(command)}.csv`), + `"${command}","${pkg}",${Math.floor(start.getTime() / 1000)},${Math.floor(end.getTime() / 1000)},${maxMemMB}\n`, + ); +} + +export function findUp(name: string, directory: string = process.cwd()): string | undefined { + const absoluteDirectory = path.resolve(directory); + + const file = path.join(directory, name); + if (fs.existsSync(file)) { + return file; + } + + const { root } = path.parse(absoluteDirectory); + if (absoluteDirectory == root) { + return undefined; + } + + return findUp(name, path.dirname(absoluteDirectory)); +} + +function slugify(x: string): string { + return x.replace(/[^a-zA-Z0-9]/g, '-'); +} diff --git a/tools/@aws-cdk/cdk-build-tools/lib/package-info.ts b/tools/@aws-cdk/cdk-build-tools/lib/package-info.ts index afcb0781a93cd..0a458e0021c4f 100644 --- a/tools/@aws-cdk/cdk-build-tools/lib/package-info.ts +++ b/tools/@aws-cdk/cdk-build-tools/lib/package-info.ts @@ -21,14 +21,23 @@ export function cdkBuildOptions(): CDKBuildOptions { // now it's easiest to just read them from the package JSON. // Our package directories are littered with .json files enough // already. - return currentPackageJson()['cdk-build'] || {}; + const pj = currentPackageJson(); + + return { + ...pj['cdk-build'], + currentPackageName: pj.name, + }; } /** * Return the cdk-package options */ export function cdkPackageOptions(): CDKPackageOptions { - return currentPackageJson()['cdk-package'] || {}; + const pj = currentPackageJson(); + return { + ...pj['cdk-package'], + currentPackageName: pj.name, + }; } /** @@ -121,6 +130,8 @@ export function genScript(): string | undefined { } export interface CDKBuildOptions { + currentPackageName: string; + /** * What CloudFormation scope to generate resources for, if any */ @@ -185,6 +196,8 @@ export interface CDKBuildOptions { } export interface CDKPackageOptions { + currentPackageName: string; + /** * Optional commands (formatted as a list of strings, which will be joined together with the && operator) to run before packaging */