Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: emit Docker and Shell logging to ProgressListener #221

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
22 changes: 20 additions & 2 deletions bin/cdk-assets.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import * as yargs from 'yargs';
import { list } from './list';
import { setLogThreshold, VERSION } from './logging';
import { log, setGlobalProgressListener, setLogThreshold, VERSION } from './logging';
import { publish } from './publish';
import { AssetManifest } from '../lib';
import { AssetManifest, EventType, IPublishProgress, IPublishProgressListener } from '../lib';

class DefaultProgressListener implements IPublishProgressListener {
public onPublishEvent(type: EventType, event: IPublishProgress): void {
switch (type) {
case EventType.FAIL:
log('error', event.message);
break;
case EventType.DEBUG:
log('verbose', event.message);
break;
default:
log('info', event.message);
}
}
}

async function main() {
const defaultListener = new DefaultProgressListener();
setGlobalProgressListener(defaultListener);

const argv = yargs
.usage('$0 <cmd> [args]')
.option('verbose', {
Expand Down
68 changes: 62 additions & 6 deletions bin/logging.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import * as fs from 'fs';
import * as path from 'path';

export type LogLevel = 'verbose' | 'info' | 'error';
let logThreshold: LogLevel = 'info';
import { EventType, IPublishProgress, IPublishProgressListener } from '../lib/progress';

export const VERSION = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'package.json'), { encoding: 'utf-8' })
).version;

const LOG_LEVELS: Record<LogLevel, number> = {
export type LogLevel = 'verbose' | 'info' | 'error';
let logThreshold: LogLevel = 'info';

// Global default progress listener that will be set if using the cli
// If using the library, you should set your own listener
let globalProgressListener: IPublishProgressListener | undefined;

export const LOG_LEVELS: Record<LogLevel, number> = {
verbose: 1,
info: 2,
error: 3,
Expand All @@ -18,9 +23,60 @@ export function setLogThreshold(threshold: LogLevel) {
logThreshold = threshold;
}

export function log(level: LogLevel, message: string) {
export function setGlobalProgressListener(listener: IPublishProgressListener) {
globalProgressListener = listener;
}

// Convert log level to event type
function logLevelToEventType(level: LogLevel): EventType {
switch (level) {
case 'error':
return EventType.FAIL;
case 'verbose':
return EventType.DEBUG;
default:
return EventType.DEBUG;
}
}

export function log(level: LogLevel, message: string, percentComplete?: number) {
if (LOG_LEVELS[level] >= LOG_LEVELS[logThreshold]) {
// eslint-disable-next-line no-console
console.error(`${level.padEnd(7, ' ')}: ${message}`);

// Write to progress listener if configured
if (globalProgressListener) {
const progressEvent: IPublishProgress = {
message: `${message}`,
percentComplete: percentComplete,
abort: () => {},
};
globalProgressListener.onPublishEvent(logLevelToEventType(level), progressEvent);
HBobertz marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

export class ShellOutputHandler {
constructor(private readonly progressListener?: IPublishProgressListener) {}

public handleOutput(chunk: any, isError: boolean = false) {
const text = chunk.toString();

if (isError) {
process.stderr.write(text);
} else {
process.stdout.write(text);
}
HBobertz marked this conversation as resolved.
Show resolved Hide resolved

// Send to progress listener if configured
if (this.progressListener) {
const progressEvent: IPublishProgress = {
message: text,
abort: () => {},
};
this.progressListener.onPublishEvent(
isError ? EventType.FAIL : EventType.DEBUG,
progressEvent
);
}
}
}
6 changes: 5 additions & 1 deletion bin/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ const EVENT_TO_LEVEL: Record<EventType, LogLevel> = {

class ConsoleProgress implements IPublishProgressListener {
public onPublishEvent(type: EventType, event: IPublishProgress): void {
log(EVENT_TO_LEVEL[type], `[${event.percentComplete}%] ${type}: ${event.message}`);
log(
EVENT_TO_LEVEL[type],
`[${event.percentComplete}%] ${type}: ${event.message}`,
event.percentComplete
);
}
}
30 changes: 22 additions & 8 deletions lib/private/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cdkCredentialsConfig, obtainEcrCredentials } from './docker-credentials
import { Logger, shell, ShellOptions, ProcessFailedError } from './shell';
import { createCriticalSection } from './util';
import { IECRClient } from '../aws';
import { IPublishProgressListener } from '../progress';

interface BuildOptions {
readonly directory: string;
Expand Down Expand Up @@ -52,10 +53,15 @@ export interface DockerCacheOption {
readonly params?: { [key: string]: string };
}

export interface DockerOptions {
readonly logger?: Logger;
readonly progressListener?: IPublishProgressListener;
}

export class Docker {
private configDir: string | undefined = undefined;

constructor(private readonly logger?: Logger) {}
constructor(private readonly options?: DockerOptions) {}

/**
* Whether an image with the given tag exists
Expand Down Expand Up @@ -194,19 +200,20 @@ export class Docker {
this.configDir = undefined;
}

private async execute(args: string[], options: ShellOptions = {}) {
private async execute(args: string[], shellOptions: ShellOptions = {}) {
const configArgs = this.configDir ? ['--config', this.configDir] : [];

const pathToCdkAssets = path.resolve(__dirname, '..', '..', 'bin');
try {
await shell([getDockerCmd(), ...configArgs, ...args], {
logger: this.logger,
...options,
logger: this.options?.logger,
...shellOptions,
env: {
...process.env,
...options.env,
PATH: `${pathToCdkAssets}${path.delimiter}${options.env?.PATH ?? process.env.PATH}`,
...shellOptions.env,
PATH: `${pathToCdkAssets}${path.delimiter}${shellOptions.env?.PATH ?? process.env.PATH}`,
},
progressListener: this.options?.progressListener,
});
} catch (e: any) {
if (e.code === 'ENOENT') {
Expand Down Expand Up @@ -235,6 +242,7 @@ export interface DockerFactoryOptions {
readonly repoUri: string;
readonly ecr: IECRClient;
readonly logger: (m: string) => void;
readonly progressListener?: IPublishProgressListener;
}

/**
Expand All @@ -249,7 +257,10 @@ export class DockerFactory {
* Gets a Docker instance for building images.
*/
public async forBuild(options: DockerFactoryOptions): Promise<Docker> {
const docker = new Docker(options.logger);
const docker = new Docker({
logger: options.logger,
progressListener: options.progressListener,
});

// Default behavior is to login before build so that the Dockerfile can reference images in the ECR repo
// However, if we're in a pipelines environment (for example),
Expand All @@ -268,7 +279,10 @@ export class DockerFactory {
* Gets a Docker instance for pushing images to ECR.
*/
public async forEcrPush(options: DockerFactoryOptions) {
const docker = new Docker(options.logger);
const docker = new Docker({
logger: options.logger,
progressListener: options.progressListener,
});
await this.loginOncePerDestination(docker, options);
return docker;
}
Expand Down
12 changes: 8 additions & 4 deletions lib/private/shell.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as child_process from 'child_process';
import { ShellOutputHandler } from '../../bin/logging';
import { IPublishProgressListener } from '../progress';

export type Logger = (x: string) => void;

export interface ShellOptions extends child_process.SpawnOptions {
readonly quiet?: boolean;
readonly logger?: Logger;
readonly input?: string;
readonly progressListener?: IPublishProgressListener;
}

/**
Expand All @@ -18,6 +21,9 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom
if (options.logger) {
options.logger(renderCommandLine(command));
}

const outputHandler = new ShellOutputHandler(options.progressListener);

const child = child_process.spawn(command[0], command.slice(1), {
...options,
stdio: [options.input ? 'pipe' : 'ignore', 'pipe', 'pipe'],
Expand All @@ -32,19 +38,17 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom
const stdout = new Array<any>();
const stderr = new Array<any>();

// Both write to stdout and collect
child.stdout!.on('data', (chunk) => {
if (!options.quiet) {
process.stdout.write(chunk);
outputHandler.handleOutput(chunk, false);
}
stdout.push(chunk);
});

child.stderr!.on('data', (chunk) => {
if (!options.quiet) {
process.stderr.write(chunk);
outputHandler.handleOutput(chunk, true);
}

stderr.push(chunk);
});

Expand Down
2 changes: 1 addition & 1 deletion lib/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface IPublishProgress {
/**
* How far along are we?
*/
readonly percentComplete: number;
readonly percentComplete?: number;
HBobertz marked this conversation as resolved.
Show resolved Hide resolved

/**
* Abort the current publishing operation
Expand Down
3 changes: 2 additions & 1 deletion test/fake-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export class FakeListener implements IPublishProgressListener {

constructor(private readonly doAbort = false) {}

public onPublishEvent(_type: EventType, event: IPublishProgress): void {
public onPublishEvent(type: EventType, event: IPublishProgress): void {
this.types.push(type);
this.messages.push(event.message);

if (this.doAbort) {
Expand Down
Loading
Loading