Skip to content

Commit

Permalink
fix(cli): disallow import of internal cli libraries (#33021)
Browse files Browse the repository at this point in the history
### Reason for this change

Previously it was possible to import any subpaths from the `aws-cdk`
package ("the CLI"). This was never intended to be allowed, or
supported. In practice, these subpaths imports allowed users to depend
on internal CLI APIs that are not intended for public usage and do not
receive the same backwards compatibility guarantees as other parts of
the AWS CDK.
    
**With this change we are explicitly disallowing unsanctioned subpath
imports.**
    
We are currently in the process of making most CLI features available
through a new Programmatic Toolkit library. Please see the [respective
RFC](aws/aws-cdk-rfcs#654) and let us know if
you have a use case that is not currently covered by the proposed
feature set.
    
In order to not immediately break all customers using unsanctioned
subpath imports, we have identified a subset of symbols that we will
keep exporting in the short-time future. **You are still very strongly
encouraged to move off any of these features asap.** We are actively
considering to emit warnings and enact brown-outs to inform users of
this removal.

### Description of changes

Added the new legacy exports to `aws-cdk`.
Also change some imports in `aws-cdk` to use the lower-level path
instead of `lib/index`.

In `cli-lib-alpha` we now import from the CLI package via file paths,
instead of the package. This is intentional because we don't actually
need or want to depended on the `aws-cdk` the package as `cli-lib-alpha`
is bundling everything itself. Although we still have a dependency on
`aws-cdk` at the moment because we need its build to run to produce some
other artifacts. Soon these imports will change to
`../../../tmp-aws-cdk/lib` and import from the temporary package that
holds all library code. We already do the same in the toolkit package.

### Describe any new or updated permissions being added

none

### Description of how you validated changes

Existing tests

### Checklist
- [x] My code adheres to the [CONTRIBUTING
GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and
[DESIGN
GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache-2.0 license*
  • Loading branch information
mrgrain authored Jan 20, 2025
1 parent fa2327d commit e5ac918
Show file tree
Hide file tree
Showing 21 changed files with 306 additions and 21 deletions.
4 changes: 2 additions & 2 deletions packages/@aws-cdk/cli-lib-alpha/lib/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { exec as runCli } from 'aws-cdk/lib';
import { exec as runCli } from '../../../aws-cdk/lib';
// eslint-disable-next-line import/no-extraneous-dependencies
import { createAssembly, prepareContext, prepareDefaultEnvironment } from 'aws-cdk/lib/api/cxapp/exec';
import { createAssembly, prepareContext, prepareDefaultEnvironment } from '../../../aws-cdk/lib/api/cxapp/exec';
import { SharedOptions, DeployOptions, DestroyOptions, BootstrapOptions, SynthOptions, ListOptions, StackActivityProgress, HotswapMode } from './commands';

/**
Expand Down
6 changes: 3 additions & 3 deletions packages/@aws-cdk/cli-lib-alpha/test/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { join } from 'path';
import * as core from 'aws-cdk-lib/core';
import * as cli from 'aws-cdk/lib';
import * as cli from '../../../aws-cdk/lib';
import { AwsCdkCli } from '../lib';

// These tests synthesize an actual CDK app and take a bit longer
jest.setTimeout(60_000);

jest.mock('aws-cdk/lib', () => {
const original = jest.requireActual('aws-cdk/lib');
jest.mock('../../../aws-cdk/lib', () => {
const original = jest.requireActual('../../../aws-cdk/lib');
return {
...original,
exec: jest.fn(original.exec),
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/cli-lib-alpha/test/commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as core from 'aws-cdk-lib/core';
import * as cli from 'aws-cdk/lib';
import * as cli from '../../../aws-cdk/lib';
import { AwsCdkCli } from '../lib';
import { HotswapMode, RequireApproval, StackActivityProgress } from '../lib/commands';

jest.mock('aws-cdk/lib');
jest.mock('../../../aws-cdk/lib');
jest.mocked(cli.exec).mockResolvedValue(0);

afterEach(() => {
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/toolkit/lib/api/aws-cdk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { DEFAULT_TOOLKIT_STACK_NAME, SdkProvider } from '../../../../aws-cdk/lib';
export type { SuccessfulDeployStackResult } from '../../../../aws-cdk/lib';
export { DEFAULT_TOOLKIT_STACK_NAME, SdkProvider } from '../../../../aws-cdk/lib/api';
export type { SuccessfulDeployStackResult } from '../../../../aws-cdk/lib/api';
export { formatSdkLoggerContent } from '../../../../aws-cdk/lib/api/aws-auth/sdk-logger';
export { CloudAssembly, sanitizePatterns, StackCollection, ExtendedStackSelection } from '../../../../aws-cdk/lib/api/cxapp/cloud-assembly';
export { prepareDefaultEnvironment, prepareContext, spaceAvailableForContext } from '../../../../aws-cdk/lib/api/cxapp/exec';
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export * from './api';
export { cli, exec } from './cli';

// Re-export the legacy exports under a name
// We import and re-export them from the index.ts file to generate a single bundle of all code and dependencies
export * as legacy from './legacy-exports-source';
32 changes: 32 additions & 0 deletions packages/aws-cdk/lib/legacy-exports-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// This is a barrel export file, of all known symbols that are imported by users from the `aws-cdk` package.
// Importing these symbols was never officially supported, but here we are.
// In order to preserver backwards-compatibly for these users, we re-export and preserve them as explicit subpath exports.
// See https://github.com/aws/aws-cdk/pull/33021 for more information.

// Note: All type exports are in `legacy-exports.ts`
export * from './legacy-logging-source';
export { deepClone, flatten, ifDefined, isArray, isEmpty, numberFromBool, partition } from './util';
export { deployStack } from './api/deploy-stack';
export { cli, exec } from './cli';
export { SdkProvider } from './api/aws-auth';
export { PluginHost } from './api/plugin';
export { contentHash } from './util/content-hash';
export { Command, Configuration, PROJECT_CONTEXT, Settings } from './settings';
export { Bootstrapper } from './api/bootstrap';
export { CloudExecutable } from './api/cxapp/cloud-executable';
export { execProgram } from './api/cxapp/exec';
export { RequireApproval } from './diff';
export { leftPad } from './api/util/string-manipulation';
export { formatAsBanner } from './util/console-formatters';
export { enableTracing } from './util/tracing';
export { aliases, command, describe } from './commands/docs';
export { lowerCaseFirstCharacter } from './api/hotswap/common';
export { deepMerge } from './util/objects';
export { Deployments } from './api/deployments';
export { rootDir } from './util/directories';
export { latestVersionIfHigher, versionNumber } from './version';
export { availableInitTemplates } from './init';
export { cached } from './api/aws-auth/cached';
export { CfnEvaluationException } from './api/evaluate-cloudformation-template';
export { CredentialPlugins } from './api/aws-auth/credential-plugins';
export { AwsCliCompatible } from './api/aws-auth/awscli-compatible';
91 changes: 91 additions & 0 deletions packages/aws-cdk/lib/legacy-exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// This is the legacy symbols export file.
// We export a number of known symbols that are imported by users from the `aws-cdk` package.
// Importing these symbols was never officially supported, but here we are.
// See https://github.com/aws/aws-cdk/pull/33021 for more information.
//
// In package.json, section `exports`, we declare all known subpaths as an explicit subpath export resolving to this file.
// This way existing unsanctioned imports don't break immediately.
//
// When attempting to import a subpath other than the explicitly exported ones, the following runtime error will be thrown:
// Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/private/subpath' is not defined by "exports" in aws-cdk/package.json
//
// TypeScript can warn users about the not-exported subpath at compile time. However it requires a reasonably modern tsconfig.json.
// Specifically `moduleResolution` must be set to either "node16" or "nodenext".

// We need to import the legacy exports via index.ts
// This is because we will bundle all code and dependencies into index.js at build time.
// It's the only place where the code exists as a working, self-contained copy.
// While we could have bundled `legacy-exports.ts` separately, it would create an other copy of the pretty much identical bundle
// and add an additional 16mb+ to the published package.
// To avoid this, we deduplicated the bundled code and run everything through index.ts.
import { legacy } from './index';

// We also need to re-export some types
// These don't need to participate in the bundling, so we can just put them here
export type { Obj } from './util';
export type { Account } from './api/aws-auth';
export type { ContextProviderPlugin } from './api/plugin';
export type { BootstrapEnvironmentOptions, BootstrapSource } from './api/bootstrap';
export type { StackSelector } from './api/cxapp/cloud-assembly';
export type { DeployStackResult } from './api/deploy-stack';
export type { Component } from './notices';
export type { LoggerFunction } from './legacy-logging-source';

// Re-export all symbols via index.js
// We do this, because index.js is the the fail that will end up with all dependencies bundled
export const {
deepClone,
flatten,
ifDefined,
isArray,
isEmpty,
numberFromBool,
partition,
deployStack,
cli,
exec,
SdkProvider,
PluginHost,
contentHash,
Command,
Configuration,
PROJECT_CONTEXT,
Settings,
Bootstrapper,
CloudExecutable,
execProgram,
RequireApproval,
leftPad,
formatAsBanner,
enableTracing,
aliases,
command,
describe,
lowerCaseFirstCharacter,
deepMerge,
Deployments,
rootDir,
latestVersionIfHigher,
versionNumber,
availableInitTemplates,
cached,
CfnEvaluationException,
CredentialPlugins,
AwsCliCompatible,
withCorkedLogging,
LogLevel,
logLevel,
CI,
setLogLevel,
setCI,
increaseVerbosity,
trace,
debug,
error,
warning,
success,
highlight,
print,
data,
prefix,
} = legacy;
122 changes: 122 additions & 0 deletions packages/aws-cdk/lib/legacy-logging-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// This is an exact copy of the file `packages/aws-cdk/lib/logging.ts` from 2024-11-29
// https://github.com/aws/aws-cdk/blob/81cde0e2e1f83f80273d14724d5518cc20dc5a80/packages/aws-cdk/lib/logging.ts
// After this we started refactoring the file and functionality changed significantly.
// In order to preserver backwards-compatibly for users with unsanctioned usage of this file,
// we keep a copy of the original version around.
// See https://github.com/aws/aws-cdk/pull/33021 for more information.

import { Writable } from 'stream';
import * as util from 'util';
import * as chalk from 'chalk';

type StyleFn = (str: string) => string;
const { stdout, stderr } = process;

type WritableFactory = () => Writable;

export async function withCorkedLogging<A>(block: () => Promise<A>): Promise<A> {
corkLogging();
try {
return await block();
} finally {
uncorkLogging();
}
}

let CORK_COUNTER = 0;
const logBuffer: [Writable, string][] = [];

function corked() {
return CORK_COUNTER !== 0;
}

function corkLogging() {
CORK_COUNTER += 1;
}

function uncorkLogging() {
CORK_COUNTER -= 1;
if (!corked()) {
logBuffer.forEach(([stream, str]) => stream.write(str + '\n'));
logBuffer.splice(0);
}
}

const logger = (stream: Writable | WritableFactory, styles?: StyleFn[], timestamp?: boolean) => (fmt: string, ...args: unknown[]) => {
const ts = timestamp ? `[${formatTime(new Date())}] ` : '';

let str = ts + util.format(fmt, ...args);
if (styles && styles.length) {
str = styles.reduce((a, style) => style(a), str);
}

const realStream = typeof stream === 'function' ? stream() : stream;

// Logger is currently corked, so we store the message to be printed
// later when we are uncorked.
if (corked()) {
logBuffer.push([realStream, str]);
return;
}

realStream.write(str + '\n');
};

function formatTime(d: Date) {
return `${lpad(d.getHours(), 2)}:${lpad(d.getMinutes(), 2)}:${lpad(d.getSeconds(), 2)}`;

function lpad(x: any, w: number) {
const s = `${x}`;
return '0'.repeat(Math.max(w - s.length, 0)) + s;
}
}

export enum LogLevel {
/** Not verbose at all */
DEFAULT = 0,
/** Pretty verbose */
DEBUG = 1,
/** Extremely verbose */
TRACE = 2,
}

export let logLevel = LogLevel.DEFAULT;
export let CI = false;

export function setLogLevel(newLogLevel: LogLevel) {
logLevel = newLogLevel;
}

export function setCI(newCI: boolean) {
CI = newCI;
}

export function increaseVerbosity() {
logLevel += 1;
}

const stream = () => CI ? stdout : stderr;
const _debug = logger(stream, [chalk.gray], true);

export const trace = (fmt: string, ...args: unknown[]) => logLevel >= LogLevel.TRACE && _debug(fmt, ...args);
export const debug = (fmt: string, ...args: unknown[]) => logLevel >= LogLevel.DEBUG && _debug(fmt, ...args);
export const error = logger(stderr, [chalk.red]);
export const warning = logger(stream, [chalk.yellow]);
export const success = logger(stream, [chalk.green]);
export const highlight = logger(stream, [chalk.bold]);
export const print = logger(stream);
export const data = logger(stdout);

export type LoggerFunction = (fmt: string, ...args: unknown[]) => void;

/**
* Create a logger output that features a constant prefix string.
*
* @param prefixString the prefix string to be appended before any log entry.
* @param fn the logger function to be used (typically one of the other functions in this module)
*
* @returns a new LoggerFunction.
*/
export function prefix(prefixString: string, fn: LoggerFunction): LoggerFunction {
return (fmt: string, ...args: any[]) => fn(`%s ${fmt}`, prefixString, ...args);
}
36 changes: 36 additions & 0 deletions packages/aws-cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,42 @@
"bin": {
"cdk": "bin/cdk"
},
"exports": {
".": "./lib/legacy-exports.js",
"./bin/cdk": "./bin/cdk",
"./package.json": "./package.json",
"./lib/api/bootstrap/bootstrap-template.yaml": "./lib/api/bootstrap/bootstrap-template.yaml",
"./lib/util": "./lib/legacy-exports.js",
"./lib": "./lib/legacy-exports.js",
"./lib/api/plugin": "./lib/legacy-exports.js",
"./lib/util/content-hash": "./lib/legacy-exports.js",
"./lib/settings": "./lib/legacy-exports.js",
"./lib/api/bootstrap": "./lib/legacy-exports.js",
"./lib/api/cxapp/cloud-assembly": "./lib/legacy-exports.js",
"./lib/api/cxapp/cloud-executable": "./lib/legacy-exports.js",
"./lib/api/cxapp/exec": "./lib/legacy-exports.js",
"./lib/diff": "./lib/legacy-exports.js",
"./lib/api/util/string-manipulation": "./lib/legacy-exports.js",
"./lib/util/console-formatters": "./lib/legacy-exports.js",
"./lib/util/tracing": "./lib/legacy-exports.js",
"./lib/commands/docs": "./lib/legacy-exports.js",
"./lib/api/hotswap/common": "./lib/legacy-exports.js",
"./lib/util/objects": "./lib/legacy-exports.js",
"./lib/api/deployments": "./lib/legacy-exports.js",
"./lib/util/directories": "./lib/legacy-exports.js",
"./lib/version": "./lib/legacy-exports.js",
"./lib/init": "./lib/legacy-exports.js",
"./lib/api/aws-auth/cached": "./lib/legacy-exports.js",
"./lib/api/deploy-stack": "./lib/legacy-exports.js",
"./lib/api/evaluate-cloudformation-template": "./lib/legacy-exports.js",
"./lib/api/aws-auth/credential-plugins": "./lib/legacy-exports.js",
"./lib/api/aws-auth/awscli-compatible": "./lib/legacy-exports.js",
"./lib/notices": "./lib/legacy-exports.js",
"./lib/index": "./lib/legacy-exports.js",
"./lib/api/aws-auth/index.js": "./lib/legacy-exports.js",
"./lib/api/aws-auth": "./lib/legacy-exports.js",
"./lib/logging": "./lib/legacy-exports.js"
},
"scripts": {
"build": "cdk-build",
"user-input-gen": "ts-node --preferTsExts scripts/user-input-gen.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/test/api/fake-cloudformation-stack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ICloudFormationClient } from '../../lib';
import { ICloudFormationClient } from '../../lib/api';
import { CloudFormationStack, Template } from '../../lib/api/util/cloudformation';
import { StackStatus } from '../../lib/api/util/cloudformation/stack-status';
import { MockSdk } from '../util/mock-sdk';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
StackResourceSummary,
StackStatus,
} from '@aws-sdk/client-cloudformation';
import { SdkProvider } from '../../../lib';
import { SdkProvider } from '../../../lib/api';
import { findCloudWatchLogGroups } from '../../../lib/api/logs/find-cloudwatch-logs';
import { testStack, TestStackArtifact } from '../../util';
import {
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/test/cdk-toolkit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import { GetParameterCommand } from '@aws-sdk/client-ssm';
import * as fs from 'fs-extra';
import * as promptly from 'promptly';
import { instanceMockFrom, MockCloudExecutable, TestStackArtifact } from './util';
import { SdkProvider } from '../lib';
import { SdkProvider } from '../lib/api';
import {
mockCloudFormationClient,
MockSdk,
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/test/context-providers/amis.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'aws-sdk-client-mock';
import { DescribeImagesCommand } from '@aws-sdk/client-ec2';
import { SDK, SdkForEnvironment } from '../../lib';
import { SDK, SdkForEnvironment } from '../../lib/api';
import { AmiContextProviderPlugin } from '../../lib/context-providers/ami';
import { FAKE_CREDENTIAL_CHAIN, MockSdkProvider, mockEC2Client } from '../util/mock-sdk';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DescribeAvailabilityZonesCommand } from '@aws-sdk/client-ec2';
import { SDK, SdkForEnvironment } from '../../lib';
import { SDK, SdkForEnvironment } from '../../lib/api';
import { AZContextProviderPlugin } from '../../lib/context-providers/availability-zones';
import { FAKE_CREDENTIAL_CHAIN, mockEC2Client, MockSdkProvider } from '../util/mock-sdk';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DescribeVpcEndpointServicesCommand } from '@aws-sdk/client-ec2';
import { SDK, SdkForEnvironment } from '../../lib';
import { SDK, SdkForEnvironment } from '../../lib/api';
import {
EndpointServiceAZContextProviderPlugin,
} from '../../lib/context-providers/endpoint-service-availability-zones';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GetHostedZoneCommand, ListHostedZonesByNameCommand } from '@aws-sdk/client-route-53';
import { SDK, SdkForEnvironment } from '../../lib';
import { SDK, SdkForEnvironment } from '../../lib/api';
import { HostedZoneContextProviderPlugin } from '../../lib/context-providers/hosted-zones';
import { FAKE_CREDENTIAL_CHAIN, mockRoute53Client, MockSdkProvider } from '../util/mock-sdk';

Expand Down
Loading

0 comments on commit e5ac918

Please sign in to comment.