From 4f0d20e91cecf7236aab74e5cedea103215f4042 Mon Sep 17 00:00:00 2001 From: Michael Sambol Date: Sat, 16 Dec 2023 14:41:20 -0600 Subject: [PATCH] add tests --- .../cli-integ/lib/with-cli-no-stacks.ts | 134 ++++++++++++++++++ .../cli-integ/resources/cdk-apps/app/app.js | 6 + .../resources/cdk-apps/no-stack-app/app.js | 11 ++ .../resources/cdk-apps/no-stack-app/cdk.json | 7 + .../cli-integ-tests/cli-lib.integtest.ts | 22 +++ .../tests/cli-integ-tests/cli.integtest.ts | 25 ++++ packages/aws-cdk/README.md | 14 ++ .../aws-cdk/lib/api/cxapp/cloud-assembly.ts | 8 +- .../aws-cdk/test/api/cloud-assembly.test.ts | 32 +++++ 9 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk-testing/cli-integ/lib/with-cli-no-stacks.ts create mode 100755 packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/no-stack-app/app.js create mode 100644 packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/no-stack-app/cdk.json diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cli-no-stacks.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cli-no-stacks.ts new file mode 100644 index 0000000000000..625af386db31a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cli-no-stacks.ts @@ -0,0 +1,134 @@ +import * as os from 'os'; +import * as path from 'path'; +import { TestContext } from './integ-test'; +import { RESOURCES_DIR } from './resources'; +import { AwsContext, withAws } from './with-aws'; +import { cloneDirectory, installNpmPackages, TestFixture, DEFAULT_TEST_TIMEOUT_S, CdkCliOptions } from './with-cdk-app'; +import { withTimeout } from './with-timeout'; + +/** + * Higher order function to execute a block with a CliLib Integration CDK app fixture + */ +export function withCliLibIntegrationCdkApp(block: (context: CliLibIntegrationTestFixture) => Promise) { + return async (context: A) => { + const randy = context.randomString; + const stackNamePrefix = `cdktest-${randy}`; + const integTestDir = path.join(os.tmpdir(), `cdk-integ-${randy}`); + + context.log(` Stack prefix: ${stackNamePrefix}\n`); + context.log(` Test directory: ${integTestDir}\n`); + context.log(` Region: ${context.aws.region}\n`); + + await cloneDirectory(path.join(RESOURCES_DIR, 'cdk-apps', 'no-stack-app'), integTestDir, context.output); + const fixture = new CliLibIntegrationTestFixture( + integTestDir, + stackNamePrefix, + context.output, + context.aws, + context.randomString); + + let success = true; + try { + const installationVersion = fixture.packages.requestedFrameworkVersion(); + + if (fixture.packages.majorVersion() === '1') { + throw new Error('This test suite is only compatible with AWS CDK v2'); + } + + const alphaInstallationVersion = fixture.packages.requestedAlphaVersion(); + await installNpmPackages(fixture, { + 'aws-cdk-lib': installationVersion, + '@aws-cdk/cli-lib-alpha': alphaInstallationVersion, + '@aws-cdk/aws-lambda-go-alpha': alphaInstallationVersion, + '@aws-cdk/aws-lambda-python-alpha': alphaInstallationVersion, + 'constructs': '^10', + }); + + await block(fixture); + } catch (e: any) { + // We survive certain cases involving gopkg.in + if (errorCausedByGoPkg(e.message)) { + return; + } + success = false; + throw e; + } finally { + if (process.env.INTEG_NO_CLEAN) { + context.log(`Left test directory in '${integTestDir}' ($INTEG_NO_CLEAN)\n`); + } else { + await fixture.dispose(success); + } + } + }; +} + +/** + * Return whether or not the error is being caused by gopkg.in being down + * + * Our Go build depends on https://gopkg.in/, which has errors pretty often + * (every couple of days). It is run by a single volunteer. + */ +function errorCausedByGoPkg(error: string) { + // The error is different depending on what request fails. Messages recognized: + //////////////////////////////////////////////////////////////////// + // go: github.com/aws/aws-lambda-go@v1.28.0 requires + // gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776: invalid version: git ls-remote -q origin in /go/pkg/mod/cache/vcs/0901dc1ef67fcce1c9b3ae51078740de4a0e2dc673e720584ac302973af82f36: exit status 128: + // remote: Cannot obtain refs from GitHub: cannot talk to GitHub: Get https://github.com/go-yaml/yaml.git/info/refs?service=git-upload-pack: net/http: request canceled (Client.Timeout exceeded while awaiting headers) + // fatal: unable to access 'https://gopkg.in/yaml.v3/': The requested URL returned error: 502 + //////////////////////////////////////////////////////////////////// + // go: downloading github.com/aws/aws-lambda-go v1.28.0 + // go: github.com/aws/aws-lambda-go@v1.28.0 requires + // gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776: unrecognized import path "gopkg.in/yaml.v3": reading https://gopkg.in/yaml.v3?go-get=1: 502 Bad Gateway + // server response: Cannot obtain refs from GitHub: cannot talk to GitHub: Get https://github.com/go-yaml/yaml.git/info/refs?service=git-upload-pack: net/http: request canceled (Client.Timeout exceeded while awaiting headers) + //////////////////////////////////////////////////////////////////// + // go: github.com/aws/aws-lambda-go@v1.28.0 requires + // gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776: invalid version: git fetch -f origin refs/heads/*:refs/heads/* refs/tags/*:refs/tags/* in /go/pkg/mod/cache/vcs/0901dc1ef67fcce1c9b3ae51078740de4a0e2dc673e720584ac302973af82f36: exit status 128: + // error: RPC failed; HTTP 502 curl 22 The requested URL returned error: 502 + // fatal: the remote end hung up unexpectedly + //////////////////////////////////////////////////////////////////// + + return (error.includes('gopkg\.in.*invalid version.*exit status 128') + || error.match(/unrecognized import path[^\n]gopkg\.in/)); +} + +/** + * SAM Integration test fixture for CDK - SAM integration test cases + */ +export function withCliLibFixture(block: (context: CliLibIntegrationTestFixture) => Promise) { + return withAws(withTimeout(DEFAULT_TEST_TIMEOUT_S, withCliLibIntegrationCdkApp(block))); +} + +export class CliLibIntegrationTestFixture extends TestFixture { + /** + * + */ + public async cdk(args: string[], options: CdkCliOptions = {}) { + const action = args[0]; + const stackName = args[1]; + + const cliOpts: Record = { + stacks: stackName ? [stackName] : undefined, + }; + + if (action === 'deploy') { + cliOpts.requireApproval = options.neverRequireApproval ? 'never' : 'broadening'; + } + + return this.shell(['node', '--input-type=module', `<<__EOS__ + import { AwsCdkCli } from '@aws-cdk/cli-lib-alpha'; + const cli = AwsCdkCli.fromCdkAppDirectory(); + + await cli.${action}(${JSON.stringify(cliOpts)}); +__EOS__`], { + ...options, + modEnv: { + AWS_REGION: this.aws.region, + AWS_DEFAULT_REGION: this.aws.region, + STACK_NAME_PREFIX: this.stackNamePrefix, + PACKAGE_LAYOUT_VERSION: this.packages.majorVersion(), + ...options.modEnv, + }, + }); + } + +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index 048e6fabd5165..322b828baf3f1 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -403,6 +403,8 @@ class BuiltinLambdaStack extends cdk.Stack { } } +class StackWithNoResources extends cdk.Stack {} + const app = new cdk.App({ context: { '@aws-cdk/core:assetHashSalt': process.env.CODEBUILD_BUILD_ID, // Force all assets to be unique, but consistent in one build @@ -492,6 +494,10 @@ switch (stackSet) { stage.synth({ validateOnSynthesis: true }); break; + case 'stage-with-no-resources': + new StackWithNoResources(app, `${stackPrefix}-stage-with-no-resources`); + break; + default: throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`); } diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/no-stack-app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/no-stack-app/app.js new file mode 100755 index 0000000000000..ff84c957c7397 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/no-stack-app/app.js @@ -0,0 +1,11 @@ +const cdk = require('aws-cdk-lib/core'); + +const stackPrefix = process.env.STACK_NAME_PREFIX; +if (!stackPrefix) { + throw new Error(`the STACK_NAME_PREFIX environment variable is required`); +} + +const app = new cdk.App(); +new NoStackApp(app, `${stackPrefix}-no-stack-1`); + +app.synth(); diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/no-stack-app/cdk.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/no-stack-app/cdk.json new file mode 100644 index 0000000000000..44809158dbdac --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/no-stack-app/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "node app.js", + "versionReporting": false, + "context": { + "aws-cdk:enableDiffNoFail": "true" + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-lib.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-lib.integtest.ts index 2d0da77b48f63..6a72987783691 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-lib.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-lib.integtest.ts @@ -47,6 +47,28 @@ integTest('cli-lib deploy', withCliLibFixture(async (fixture) => { } })); +integTest('cli-lib deploy no stack', withCliLibFixture(async (fixture) => { + const stackName = fixture.fullStackName('no-stack-1'); + + try { + // deploy the stack + await fixture.cdk(['deploy', stackName], { + options: ['--ignore-no-stacks'], + }); + + // verify the number of resources in the stack + const expectedStack = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackName, + }); + expect(expectedStack.StackResources?.length).toEqual(0); + } finally { + // delete the stack + await fixture.cdk(['destroy', stackName], { + captureStderr: false, + }); + } +})); + integTest('security related changes without a CLI are expected to fail when approval is required', withCliLibFixture(async (fixture) => { const stdErr = await fixture.cdk(['deploy', fixture.fullStackName('simple-1')], { onlyStderr: true, diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index ccdf07b166f33..76caa2dc70a48 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -780,6 +780,31 @@ integTest('deploy stack without resource', withDefaultFixture(async (fixture) => .rejects.toThrow('conditional-resource does not exist'); })); +integTest('deploy --ignore-no-stacks', withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('stage-with-no-resources', { + options: ['--ignore-no-stacks'], + modEnv: { + INTEG_STACK_SET: 'stage-with-no-resources', + }, + }); + + // verify that we only deployed both stacks (there are 2 ARNs in the output) + /* eslint-disable no-console */ + console.log(stackArn); +})); + +integTest('deploy stack with no resources and no --ignore-no-stacks', withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('stage-with-no-resources', { + modEnv: { + INTEG_STACK_SET: 'stage-with-no-resources', + }, + }); + + // verify that we only deployed both stacks (there are 2 ARNs in the output) + /* eslint-disable no-console */ + console.log(stackArn); +})); + integTest('IAM diff', withDefaultFixture(async (fixture) => { const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 864c7de02c968..cb19137245fe2 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -382,6 +382,20 @@ $ cdk deploy --method=prepare-change-set --change-set-name MyChangeSetName For more control over when stack changes are deployed, the CDK can generate a CloudFormation change set but not execute it. +#### Ignore No Stacks + +You may have an app with multiple environments, e.g., dev and prod. When starting +development, your prod app may not have any resources or the resources are commented +out. In this scenario, you will receive an error message stating that the app has no +stacks. + +To bypass this error messages, you can pass the `--ignore-no-stacks` flag to the +`deploy` command: + +```console +$ cdk deploy --ignore-no-stacks +``` + #### Hotswap deployments for faster development You can pass the `--hotswap` flag to the `deploy` command: diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index c2efeda10dce4..3637c61879d7e 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -106,8 +106,12 @@ export class CloudAssembly { const allTopLevel = selector.allTopLevel ?? false; const patterns = sanitizePatterns(selector.patterns); - if (stacks.length === 0 && !options.ignoreNoStacks) { - throw new Error('This app contains no stacks'); + if (stacks.length === 0) { + if (options.ignoreNoStacks) { + return new StackCollection(this, []); + } else { + throw new Error('This app contains no stacks'); + } } if (allTopLevel) { diff --git a/packages/aws-cdk/test/api/cloud-assembly.test.ts b/packages/aws-cdk/test/api/cloud-assembly.test.ts index 52c9ba4949d18..97c71c5df2b36 100644 --- a/packages/aws-cdk/test/api/cloud-assembly.test.ts +++ b/packages/aws-cdk/test/api/cloud-assembly.test.ts @@ -156,6 +156,38 @@ test('select behavior with nested assemblies: repeat', async() => { expect(x.stackCount).toBe(2); }); +test('select behavior with no stacks and ignore stacks option', async() => { + // GIVEN + const cxasm = await testCloudAssemblyNoStacks(); + + // WHEN + const x = await cxasm.selectStacks({ patterns: [] }, { + defaultBehavior: DefaultSelection.AllStacks, + ignoreNoStacks: true, + }); + + // THEN + expect(x.stackCount).toBe(0); +}); + +test('select behavior with no stacks and no ignore stacks option', async() => { + // GIVEN + const cxasm = await testCloudAssemblyNoStacks(); + + // WHEN & THEN + await expect(cxasm.selectStacks({ patterns: [] }, { defaultBehavior: DefaultSelection.AllStacks, ignoreNoStacks: false })) + .rejects.toThrow('This app contains no stacks'); +}); + +test('select behavior with no stacks and default ignore stacks options (false)', async() => { + // GIVEN + const cxasm = await testCloudAssemblyNoStacks(); + + // WHEN & THEN + await expect(cxasm.selectStacks({ patterns: [] }, { defaultBehavior: DefaultSelection.AllStacks })) + .rejects.toThrow('This app contains no stacks'); +}); + async function testCloudAssembly({ env }: { env?: string, versionReporting?: boolean } = {}) { const cloudExec = new MockCloudExecutable({ stacks: [{