diff --git a/packages/aws-cdk/lib/api/deployments.ts b/packages/aws-cdk/lib/api/deployments.ts index db9cdf4cd870c..d85890383e793 100644 --- a/packages/aws-cdk/lib/api/deployments.ts +++ b/packages/aws-cdk/lib/api/deployments.ts @@ -385,7 +385,7 @@ export class Deployments { const env = await this.envs.accessStackForReadOnlyStackOperations(stackArtifact); const cfn = env.sdk.cloudFormation(); - const proof = await uploadStackTemplateAssets(stackArtifact, this.assetSdkProvider, env.resolvedEnvironment); + const proof = await uploadStackTemplateAssets(stackArtifact, this.assetSdkProvider, env); // Upload the template, if necessary, before passing it to CFN let cfnParam; diff --git a/packages/aws-cdk/lib/api/environment-resources.ts b/packages/aws-cdk/lib/api/environment-resources.ts index da0a770f48bf7..0bfe0d5593ca6 100644 --- a/packages/aws-cdk/lib/api/environment-resources.ts +++ b/packages/aws-cdk/lib/api/environment-resources.ts @@ -206,6 +206,10 @@ export class NoBootstrapStackEnvironmentResources extends EnvironmentResources { public async lookupToolkit(): Promise { throw new Error('Trying to perform an operation that requires a bootstrap stack; you should not see this error, this is a bug in the CDK CLI.'); } + + public async allowCrossAccountAssetPublishing(): Promise { + return true; + } } /** diff --git a/packages/aws-cdk/lib/api/util/cloudformation.ts b/packages/aws-cdk/lib/api/util/cloudformation.ts index 7691e547d588d..34d5ed756e918 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation.ts @@ -12,6 +12,7 @@ import { AssetManifestBuilder } from '../../util/asset-manifest-builder'; import { AssetsPublishedProof, multipleAssetPublishedProof, publishAssets } from '../../util/asset-publishing'; import { SdkProvider } from '../aws-auth'; import { Deployments } from '../deployments'; +import { TargetEnvironment } from '../environment-access'; export type Template = { Parameters?: Record; @@ -365,7 +366,7 @@ function templatesFromAssetManifestArtifact(artifact: cxapi.AssetManifestArtifac async function uploadBodyParameterAndCreateChangeSet(options: PrepareChangeSetOptions): Promise { try { const env = (await options.deployments.envs.accessStackForMutableStackOperations(options.stack)); - const proof = await uploadStackTemplateAssets(options.stack, options.sdkProvider, env.resolvedEnvironment); + const proof = await uploadStackTemplateAssets(options.stack, options.sdkProvider, env); let bodyParameter; const bodyAction = await makeBodyParameter(options.stack, env, proof); @@ -377,7 +378,9 @@ async function uploadBodyParameterAndCreateChangeSet(options: PrepareChangeSetOp case 'upload': const builder = new AssetManifestBuilder(); bodyParameter = bodyAction.addToManifest(builder); - await publishAssets(builder.toManifest(options.stack.assembly.directory), options.sdkProvider, env.resolvedEnvironment); + await publishAssets(builder.toManifest(options.stack.assembly.directory), options.sdkProvider, env.resolvedEnvironment, { + allowCrossAccount: await env.resources.allowCrossAccountAssetPublishing(), + }); break; } @@ -418,13 +421,14 @@ async function uploadBodyParameterAndCreateChangeSet(options: PrepareChangeSetOp export async function uploadStackTemplateAssets( stack: cxapi.CloudFormationStackArtifact, sdkProvider: SdkProvider, - environment: cxapi.Environment, + env: TargetEnvironment, ): Promise { const assetDependencies = stack.dependencies.filter(cxapi.AssetManifestArtifact.isAssetManifestArtifact); + const allowCrossAccount = await env.resources.allowCrossAccountAssetPublishing(); return multipleAssetPublishedProof(assetDependencies, (artifact) => { const templates = templatesFromAssetManifestArtifact(artifact); - return publishAssets(templates, sdkProvider, environment); + return publishAssets(templates, sdkProvider, env.resolvedEnvironment, { allowCrossAccount }); }); } diff --git a/packages/aws-cdk/lib/util/type-brands.ts b/packages/aws-cdk/lib/util/type-brands.ts index 51127ddd14bf1..3920ddcd1cc6e 100644 --- a/packages/aws-cdk/lib/util/type-brands.ts +++ b/packages/aws-cdk/lib/util/type-brands.ts @@ -37,8 +37,6 @@ export type Branded = T & Brand; * values which are branded by construction (really just an elaborate * way to write 'as'). */ -export function createBranded>(value: TypeUnderlyingBrand): A { - return value as A; +export function createBranded(value: A): Branded { + return value as Branded; } - -type TypeUnderlyingBrand = A extends Branded ? T : never; diff --git a/packages/aws-cdk/test/api/bootstrap.test.ts b/packages/aws-cdk/test/api/bootstrap.test.ts index 6f1d89338a7b0..11fdac8cce89e 100644 --- a/packages/aws-cdk/test/api/bootstrap.test.ts +++ b/packages/aws-cdk/test/api/bootstrap.test.ts @@ -11,9 +11,6 @@ const env = { name: 'mock', }; -jest.mock('../../lib/api/util/checks', () => ({ - determineAllowCrossAccountAssetPublishing: jest.fn().mockResolvedValue(true), -})); let sdk: MockSdkProvider; let executed: boolean; let protectedTermination: boolean; diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 7afcea68f4c60..6fbbb8501d7c5 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -6,11 +6,10 @@ import { setCI } from '../../lib/logging'; import { DEFAULT_FAKE_TEMPLATE, testStack } from '../util'; import { MockedObject, mockResolvedEnvironment, MockSdk, MockSdkProvider, SyncHandlerSubsetOf } from '../util/mock-sdk'; import { NoBootstrapStackEnvironmentResources } from '../../lib/api/environment-resources'; +import { createBranded } from '../../lib/util/type-brands'; +import { StringWithoutPlaceholders } from '../../lib/api/util/placeholders'; jest.mock('../../lib/api/hotswap-deployments'); -jest.mock('../../lib/api/util/checks', () => ({ - determineAllowCrossAccountAssetPublishing: jest.fn().mockResolvedValue(true), -})); const FAKE_STACK = testStack({ stackName: 'withouterrors', @@ -83,10 +82,17 @@ function standardDeployStackArguments(): DeployStackOptions { const resolvedEnvironment = mockResolvedEnvironment(); return { stack: FAKE_STACK, - sdk, + env: { + didAssumeRole: true, + isFallbackCredentials: false, + resolvedEnvironment, + sdk, + resources: new NoBootstrapStackEnvironmentResources(resolvedEnvironment, sdk), + replacePlaceholders(x: string | undefined): Promise { + return Promise.resolve(x ? createBranded(x) : undefined); + }, + }, sdkProvider, - resolvedEnvironment, - envResources: new NoBootstrapStackEnvironmentResources(resolvedEnvironment, sdk), }; } @@ -590,19 +596,16 @@ test('deploy not skipped if template did not change but tags changed', async () }); // WHEN - const resolvedEnvironment = mockResolvedEnvironment(); await deployStack({ + ...standardDeployStackArguments(), stack: FAKE_STACK, - sdk, sdkProvider, - resolvedEnvironment, tags: [ { Key: 'Key', Value: 'NewValue', }, ], - envResources: new NoBootstrapStackEnvironmentResources(resolvedEnvironment, sdk), }); // THEN diff --git a/packages/aws-cdk/test/api/environment-resources.test.ts b/packages/aws-cdk/test/api/environment-resources.test.ts index 2e48b41cebe50..36dc6f7ae6f3d 100644 --- a/packages/aws-cdk/test/api/environment-resources.test.ts +++ b/packages/aws-cdk/test/api/environment-resources.test.ts @@ -18,6 +18,7 @@ beforeEach(() => { afterEach(() => { toolkitMock.dispose(); + jest.clearAllMocks(); }); function mockToolkitInfo(ti: ToolkitInfo) { @@ -69,10 +70,6 @@ describe('validateversion without bootstrap stack', () => { mockToolkitInfo(ToolkitInfo.bootstrapStackNotFoundInfo('TestBootstrapStack')); }); - afterEach(() => { - jest.clearAllMocks(); - }); - test('validating version with explicit SSM parameter succeeds', async () => { // GIVEN mockSdk.stubSsm({ @@ -140,4 +137,58 @@ describe('validateversion without bootstrap stack', () => { // WHEN await expect(envResources().validateVersion(8, '/abc')).rejects.toThrow(/Has the environment been bootstrapped?/); }); -}); \ No newline at end of file +}); + +describe('determineAllowCrossAccountAssetPublishing', () => { + it('should return true when hasStagingBucket is false', async () => { + mockToolkitInfo(ToolkitInfo.fromStack(mockBootstrapStack(mockSdk, { + Outputs: [{ OutputKey: 'BootstrapVersion', OutputValue: '1' }], + }))); + + const result = await envResources().allowCrossAccountAssetPublishing(); + expect(result).toBe(true); + }); + + it.each(['', '-', '*', '---'])('should return true when the bucket output does not look like a real bucket', async (notABucketName) => { + mockToolkitInfo(ToolkitInfo.fromStack(mockBootstrapStack(mockSdk, { + Outputs: [ + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + { OutputKey: 'BucketName', OutputValue: notABucketName }, + ], + }))); + + const result = await envResources().allowCrossAccountAssetPublishing(); + expect(result).toBe(true); + }); + + it('should return true when bootstrap version is >= 21', async () => { + mockToolkitInfo(ToolkitInfo.fromStack(mockBootstrapStack(mockSdk, { + Outputs: [ + { OutputKey: 'BootstrapVersion', OutputValue: '21' }, + { OutputKey: 'BucketName', OutputValue: 'some-bucket' }, + ], + }))); + + const result = await envResources().allowCrossAccountAssetPublishing(); + expect(result).toBe(true); + }); + + it('should return true if looking up the bootstrap stack fails', async () => { + mockToolkitInfo(ToolkitInfo.bootstrapStackNotFoundInfo('TestBootstrapStack')); + + const result = await envResources().allowCrossAccountAssetPublishing(); + expect(result).toBe(true); + }); + + it('should return false for other scenarios', async () => { + mockToolkitInfo(ToolkitInfo.fromStack(mockBootstrapStack(mockSdk, { + Outputs: [ + { OutputKey: 'BootstrapVersion', OutputValue: '20' }, + { OutputKey: 'BucketName', OutputValue: 'some-bucket' }, + ], + }))); + + const result = await envResources().allowCrossAccountAssetPublishing(); + expect(result).toBe(false); + }); +}); diff --git a/packages/aws-cdk/test/api/util/checks.test.ts b/packages/aws-cdk/test/api/util/checks.test.ts deleted file mode 100644 index 697cbced9254b..0000000000000 --- a/packages/aws-cdk/test/api/util/checks.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import * as AWS from 'aws-sdk'; -import * as AWSMock from 'aws-sdk-mock'; -import { ISDK } from '../../../lib/api/aws-auth'; -import { determineAllowCrossAccountAssetPublishing, getBootstrapStackInfo } from '../../../lib/api/util/checks'; - -describe('determineAllowCrossAccountAssetPublishing', () => { - let mockSDK: ISDK; - - beforeEach(() => { - mockSDK = { - cloudFormation: () => new AWS.CloudFormation(), - } as ISDK; - }); - - afterEach(() => { - AWSMock.restore(); - }); - - it('should return true when hasStagingBucket is false', async () => { - AWSMock.mock('CloudFormation', 'describeStacks', (_params: any, callback: Function) => { - callback(null, { - Stacks: [{ - Outputs: [{ OutputKey: 'BootstrapVersion', OutputValue: '1' }], - }], - }); - }); - - const result = await determineAllowCrossAccountAssetPublishing(mockSDK); - expect(result).toBe(true); - }); - - it.each(['', '-', '*', '---'])('should return true when the bucket output does not look like a real bucket', async (notABucketName) => { - AWSMock.mock('CloudFormation', 'describeStacks', (_params: any, callback: Function) => { - callback(null, { - Stacks: [{ - Outputs: [ - { OutputKey: 'BootstrapVersion', OutputValue: '1' }, - { OutputKey: 'BucketName', OutputValue: notABucketName }, - ], - }], - }); - }); - - const result = await determineAllowCrossAccountAssetPublishing(mockSDK); - expect(result).toBe(true); - }); - - it('should return true when bootstrap version is >= 21', async () => { - AWSMock.mock('CloudFormation', 'describeStacks', (_params: any, callback: Function) => { - callback(null, { - Stacks: [{ - Outputs: [ - { OutputKey: 'BootstrapVersion', OutputValue: '21' }, - { OutputKey: 'BucketName', OutputValue: 'some-bucket' }, - ], - }], - }); - }); - - const result = await determineAllowCrossAccountAssetPublishing(mockSDK); - expect(result).toBe(true); - }); - - it('should return true if looking up the bootstrap stack fails', async () => { - AWSMock.mock('CloudFormation', 'describeStacks', (_params: any, callback: Function) => { - callback(new Error('Could not read bootstrap stack')); - }); - - const result = await determineAllowCrossAccountAssetPublishing(mockSDK); - expect(result).toBe(true); - }); - - it('should return true if looking up the bootstrap stack fails', async () => { - AWSMock.mock('CloudFormation', 'describeStacks', (_params: any, callback: Function) => { - callback(new Error('Could not read bootstrap stack')); - }); - - const result = await determineAllowCrossAccountAssetPublishing(mockSDK); - expect(result).toBe(true); - }); - - it('should return false for other scenarios', async () => { - AWSMock.mock('CloudFormation', 'describeStacks', (_params: any, callback: Function) => { - callback(null, { - Stacks: [{ - Outputs: [ - { OutputKey: 'BootstrapVersion', OutputValue: '20' }, - { OutputKey: 'BucketName', OutputValue: 'some-bucket' }, - ], - }], - }); - }); - - const result = await determineAllowCrossAccountAssetPublishing(mockSDK); - expect(result).toBe(false); - }); -}); - -describe('getBootstrapStackInfo', () => { - let mockSDK: ISDK; - - beforeEach(() => { - mockSDK = { - cloudFormation: () => new AWS.CloudFormation(), - } as ISDK; - }); - - afterEach(() => { - AWSMock.restore(); - }); - - it('should return correct BootstrapStackInfo', async () => { - AWSMock.mock('CloudFormation', 'describeStacks', (_params: any, callback: Function) => { - callback(null, { - Stacks: [{ - Outputs: [ - { OutputKey: 'BootstrapVersion', OutputValue: '21' }, - { OutputKey: 'BucketName', OutputValue: 'some-bucket' }, - ], - }], - }); - }); - - const result = await getBootstrapStackInfo(mockSDK, 'CDKToolkit'); - expect(result).toEqual({ - hasStagingBucket: true, - bootstrapVersion: 21, - }); - }); - - it('should throw error when stack is not found', async () => { - AWSMock.mock('CloudFormation', 'describeStacks', (_params: any, callback: Function) => { - callback(null, { Stacks: [] }); - }); - - await expect(getBootstrapStackInfo(mockSDK, 'CDKToolkit')).rejects.toThrow('Toolkit stack CDKToolkit not found'); - }); - - it('should throw error when BootstrapVersion output is missing', async () => { - AWSMock.mock('CloudFormation', 'describeStacks', (_params: any, callback: Function) => { - callback(null, { - Stacks: [{ - Outputs: [], - }], - }); - }); - - await expect(getBootstrapStackInfo(mockSDK, 'CDKToolkit')).rejects.toThrow('Unable to find BootstrapVersion output in the toolkit stack CDKToolkit'); - }); -}); \ No newline at end of file diff --git a/packages/aws-cdk/test/util/mock-toolkitinfo.ts b/packages/aws-cdk/test/util/mock-toolkitinfo.ts index f45a4aa6ab901..6548d51c70963 100644 --- a/packages/aws-cdk/test/util/mock-toolkitinfo.ts +++ b/packages/aws-cdk/test/util/mock-toolkitinfo.ts @@ -31,6 +31,7 @@ export class MockToolkitInfo extends ToolkitInfo { public readonly version: number; public readonly variant: string; public readonly stackName = 'MockBootstrapStack'; + public readonly allowCrossAccountAssetPublishing = false; private readonly _bootstrapStack?: CloudFormationStack;