packages/@aws-cdk/toolkit-lib/lib/api/notices/notices.ts (113 lines of code) (raw):
import * as path from 'path';
import { cdkCacheDir } from '../../util';
import type { SdkHttpOptions } from '../aws-auth';
import type { Context } from '../context';
import type { IIoHost } from '../io';
import { CachedDataSource } from './cached-data-source';
import { NoticesFilter } from './filter';
import type { BootstrappedEnvironment, Notice, NoticeDataSource } from './types';
import { WebsiteNoticeDataSource } from './web-data-source';
import type { IoHelper } from '../io/private';
import { IO, asIoHelper, IoDefaultMessages } from '../io/private';
const CACHE_FILE_PATH = path.join(cdkCacheDir(), 'notices.json');
export interface NoticesProps {
/**
* CDK context
*/
readonly context: Context;
/**
* Include notices that have already been acknowledged.
*
* @default false
*/
readonly includeAcknowledged?: boolean;
/**
* Global CLI option for output directory for synthesized cloud assembly
*
* @default 'cdk.out'
*/
readonly output?: string;
/**
* Options for the HTTP request
*/
readonly httpOptions?: SdkHttpOptions;
/**
* Where messages are going to be sent
*/
readonly ioHost: IIoHost;
/**
* The version of the CLI
*/
readonly cliVersion: string;
}
export interface NoticesPrintOptions {
/**
* Whether to append the total number of unacknowledged notices to the display.
*
* @default false
*/
readonly showTotal?: boolean;
}
export interface NoticesRefreshOptions {
/**
* Whether to force a cache refresh regardless of expiration time.
*
* @default false
*/
readonly force?: boolean;
/**
* Data source for fetch notices from.
*
* @default - WebsiteNoticeDataSource
*/
readonly dataSource?: NoticeDataSource;
}
/**
* Provides access to notices the CLI can display.
*/
export class Notices {
/**
* Create an instance. Note that this replaces the singleton.
*/
public static create(props: NoticesProps): Notices {
this._instance = new Notices(props);
return this._instance;
}
/**
* Get the singleton instance. May return `undefined` if `create` has not been called.
*/
public static get(): Notices | undefined {
return this._instance;
}
private static _instance: Notices | undefined;
private readonly context: Context;
private readonly output: string;
private readonly acknowledgedIssueNumbers: Set<Number>;
private readonly includeAcknowlegded: boolean;
private readonly httpOptions: SdkHttpOptions;
private readonly ioHelper: IoHelper;
private readonly ioMessages: IoDefaultMessages;
private readonly cliVersion: string;
private data: Set<Notice> = new Set();
// sets don't deduplicate interfaces, so we use a map.
private readonly bootstrappedEnvironments: Map<string, BootstrappedEnvironment> = new Map();
private constructor(props: NoticesProps) {
this.context = props.context;
this.acknowledgedIssueNumbers = new Set(this.context.get('acknowledged-issue-numbers') ?? []);
this.includeAcknowlegded = props.includeAcknowledged ?? false;
this.output = props.output ?? 'cdk.out';
this.httpOptions = props.httpOptions ?? {};
this.ioHelper = asIoHelper(props.ioHost, 'notices' as any /* forcing a CliAction to a ToolkitAction */);
this.ioMessages = new IoDefaultMessages(this.ioHelper);
this.cliVersion = props.cliVersion;
}
/**
* Add a bootstrap information to filter on. Can have multiple values
* in case of multi-environment deployments.
*/
public addBootstrappedEnvironment(bootstrapped: BootstrappedEnvironment) {
const key = [
bootstrapped.bootstrapStackVersion,
bootstrapped.environment.account,
bootstrapped.environment.region,
bootstrapped.environment.name,
].join(':');
this.bootstrappedEnvironments.set(key, bootstrapped);
}
/**
* Refresh the list of notices this instance is aware of.
* To make sure this never crashes the CLI process, all failures are caught and
* silently logged.
*
* If context is configured to not display notices, this will no-op.
*/
public async refresh(options: NoticesRefreshOptions = {}) {
try {
const underlyingDataSource = options.dataSource ?? new WebsiteNoticeDataSource(this.ioHelper, this.httpOptions);
const dataSource = new CachedDataSource(this.ioMessages, CACHE_FILE_PATH, underlyingDataSource, options.force ?? false);
const notices = await dataSource.fetch();
this.data = new Set(this.includeAcknowlegded ? notices : notices.filter(n => !this.acknowledgedIssueNumbers.has(n.issueNumber)));
} catch (e: any) {
this.ioMessages.debug(`Could not refresh notices: ${e}`);
}
}
/**
* Display the relevant notices (unless context dictates we shouldn't).
*/
public display(options: NoticesPrintOptions = {}) {
const filteredNotices = new NoticesFilter(this.ioMessages).filter({
data: Array.from(this.data),
cliVersion: this.cliVersion,
outDir: this.output,
bootstrappedEnvironments: Array.from(this.bootstrappedEnvironments.values()),
});
if (filteredNotices.length > 0) {
void this.ioMessages.notify(IO.CDK_TOOLKIT_I0100.msg([
'',
'NOTICES (What\'s this? https://github.com/aws/aws-cdk/wiki/CLI-Notices)',
'',
].join('\n')));
for (const filtered of filteredNotices) {
const formatted = filtered.format() + '\n';
switch (filtered.notice.severity) {
case 'warning':
void this.ioMessages.notify(IO.CDK_TOOLKIT_W0101.msg(formatted));
break;
case 'error':
void this.ioMessages.notify(IO.CDK_TOOLKIT_E0101.msg(formatted));
break;
default:
void this.ioMessages.notify(IO.CDK_TOOLKIT_I0101.msg(formatted));
break;
}
}
void this.ioMessages.notify(IO.CDK_TOOLKIT_I0100.msg(
`If you don’t want to see a notice anymore, use "cdk acknowledge <id>". For example, "cdk acknowledge ${filteredNotices[0].notice.issueNumber}".`,
));
}
if (options.showTotal ?? false) {
void this.ioMessages.notify(IO.CDK_TOOLKIT_I0100.msg(
`\nThere are ${filteredNotices.length} unacknowledged notice(s).`,
));
}
}
}