Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/cli/cdk/toolkit-lib/__tests__/wrapper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { BootstrapEcrAccessDeniedError } from '../../../../lib';
import { CdkToolkitWrapper } from '../wrapper.js';
import { describe, expect, it } from 'vitest';

/**
* Builds a wrapper with an injected fake toolkit whose `bootstrap` rejects,
* exercising the real error-remap path in CdkToolkitWrapper.bootstrap().
*/
function wrapperRejectingWith(err: unknown): CdkToolkitWrapper {
const wrapper = new CdkToolkitWrapper();
const fakeToolkit = { bootstrap: () => Promise.reject(err) };

Check failure on line 11 in src/cli/cdk/toolkit-lib/__tests__/wrapper.test.ts

View workflow job for this annotation

GitHub Actions / lint

Expected the Promise rejection reason to be an Error
Object.assign(wrapper as unknown as Record<string, unknown>, {
toolkit: fakeToolkit,
cloudAssemblySource: {},
});
return wrapper;
}

describe('CdkToolkitWrapper.bootstrap error remapping', () => {
it('remaps an ecr:CreateRepository AccessDenied failure to an actionable error', async () => {
const raw = new Error(
'ECR Permission Denied - User is not authorized to perform: ecr:CreateRepository (AccessDenied)'
);
const wrapper = wrapperRejectingWith(raw);

const error = await wrapper.bootstrap(['aws://123456789012/us-east-1']).catch((e: unknown) => e);

expect(error).toBeInstanceOf(BootstrapEcrAccessDeniedError);
expect((error as Error).message).toContain('needed only by the shared CDK bootstrap stack');
expect((error as Error).message).not.toContain('CDK bootstrap failed:');
expect((error as { cause?: unknown }).cause).toBe(raw);
});

it('passes through unrelated bootstrap failures unchanged', async () => {
const raw = new Error('Some other CloudFormation failure');
const wrapper = wrapperRejectingWith(raw);

const error = await wrapper.bootstrap(['aws://123456789012/us-east-1']).catch((e: unknown) => e);

expect(error).not.toBeInstanceOf(BootstrapEcrAccessDeniedError);
expect((error as Error).message).toContain('Some other CloudFormation failure');
});
});
19 changes: 14 additions & 5 deletions src/cli/cdk/toolkit-lib/wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CONFIG_DIR } from '../../../lib';
import { BootstrapEcrAccessDeniedError, CONFIG_DIR } from '../../../lib';
import { CDK_APP_ENTRY, CDK_PROJECT_DIR } from '../../constants';
import { isChangesetInProgressError } from '../../errors';
import { isChangesetInProgressError, isEcrCreateRepositoryAccessDeniedError } from '../../errors';
import type { CdkToolkitWrapperOptions, DeployOptions, DestroyOptions, DiffOptions, ListOptions } from './types';
import {
BaseCredentials,
Expand Down Expand Up @@ -313,9 +313,18 @@ export class CdkToolkitWrapper {
const { toolkit } = this.ensureInitialized();
const params = kmsKeyId ? { kmsKeyId } : { createCustomerMasterKey: true };
const options = { parameters: BootstrapStackParameters.withExisting(params) };
return withErrorContext('bootstrap', () =>
toolkit.bootstrap(BootstrapEnvironments.fromList(environments), options)
);
try {
return await withErrorContext('bootstrap', () =>
toolkit.bootstrap(BootstrapEnvironments.fromList(environments), options)
);
} catch (err) {
// The shared bootstrap stack unconditionally creates an ECR repository. In accounts
// that deny ecr:CreateRepository, surface actionable guidance instead of the raw error.
if (isEcrCreateRepositoryAccessDeniedError(err)) {
throw new BootstrapEcrAccessDeniedError({ cause: err });
}
throw err;
}
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/cli/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,16 @@ export function isChangesetInProgressError(err: unknown): boolean {

return false;
}

/**
* Checks if an error is an access-denied failure on `ecr:CreateRepository`.
* Surfaces during CDK bootstrap when the account's SCP/IAM denies creating the
* shared bootstrap stack's ECR ContainerAssetsRepository.
*/
export function isEcrCreateRepositoryAccessDeniedError(err: unknown): boolean {
const message = getErrorMessage(err).toLowerCase();
return (
message.includes('ecr:createrepository') &&
(message.includes('accessdenied') || message.includes('access denied') || message.includes('not authorized'))
);
}
1 change: 1 addition & 0 deletions src/cli/telemetry/schemas/common-shapes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const ErrorName = z.enum([
'AgentAlreadyExistsError',
'ArtifactSizeError',
'AwsCredentialsError',
'BootstrapEcrAccessDeniedError',
'ConfigNotFoundError',
'ConfigParseError',
'ConfigReadError',
Expand Down
22 changes: 22 additions & 0 deletions src/lib/errors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ export class AccessDeniedError extends BaseError {
}
}

/**
* Error thrown when CDK bootstrap is denied `ecr:CreateRepository`.
*
* The shared CDK bootstrap (CDKToolkit) stack always provisions an ECR
* `ContainerAssetsRepository`, even for CodeZip-only projects that never push a
* container image. In accounts whose SCP/IAM denies `ecr:CreateRepository`,
* bootstrap aborts with a raw CloudFormation error that misleadingly implies the
* agent needs ECR. This error replaces it with actionable guidance.
*/
export class BootstrapEcrAccessDeniedError extends BaseError {
constructor(options?: BaseErrorOptions) {
super(
'CDK bootstrap was denied ecr:CreateRepository. The shared CDK bootstrap stack (CDKToolkit) always ' +
'creates an ECR repository for container assets — this is needed only by the shared CDK bootstrap stack, ' +
'not by CodeZip agents, which never push a container image. To proceed, either deploy in an account that ' +
'permits ecr:CreateRepository, or bootstrap the environment ahead of time with a custom (ECR-less) ' +
'bootstrap template.',
{ defaultSource: 'user', ...options }
);
}
}

/**
* Error indicating missing system dependencies required for an operation.
*/
Expand Down
Loading