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 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
22 changes: 13 additions & 9 deletions bin/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,34 @@ import {
EventType,
IPublishProgress,
IPublishProgressListener,
globalOutputHandler,
} from '../lib';

export async function publish(args: { path: string; assets?: string[]; profile?: string }) {
let manifest = AssetManifest.fromPath(args.path);
log('verbose', `Loaded manifest from ${args.path}: ${manifest.entries.length} assets found`);

if (args.assets && args.assets.length > 0) {
const selection = args.assets.map((a) => DestinationPattern.parse(a));
manifest = manifest.select(selection);
log('verbose', `Applied selection: ${manifest.entries.length} assets selected.`);
}

// AssetPublishing will assign the global output handler
const pub = new AssetPublishing(manifest, {
aws: new DefaultAwsClient(args.profile),
progressListener: new ConsoleProgress(),
throwOnError: false,
});

globalOutputHandler.verbose(
`Loaded manifest from ${args.path}: ${manifest.entries.length} assets found`
);

if (args.assets && args.assets.length > 0) {
const selection = args.assets.map((a) => DestinationPattern.parse(a));
manifest = manifest.select(selection);
globalOutputHandler.verbose(`Applied selection: ${manifest.entries.length} assets selected.`);
}

await pub.publish();

if (pub.hasFailures) {
for (const failure of pub.failures) {
// eslint-disable-next-line no-console
console.error('Failure:', failure.error.stack);
globalOutputHandler.error(`Failure: ${failure.error.stack}`);
}

process.exitCode = 1;
Expand Down
11 changes: 3 additions & 8 deletions lib/private/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { cdkCredentialsConfig, obtainEcrCredentials } from './docker-credentials';
import { Logger, shell, ShellOptions, ProcessFailedError } from './shell';
import { shell, ShellOptions, ProcessFailedError } from './shell';
import { createCriticalSection } from './util';
import { IECRClient } from '../aws';

Expand Down Expand Up @@ -55,8 +55,6 @@ export interface DockerCacheOption {
export class Docker {
private configDir: string | undefined = undefined;

constructor(private readonly logger?: Logger) {}

/**
* Whether an image with the given tag exists
*/
Expand Down Expand Up @@ -200,10 +198,8 @@ export class Docker {
const pathToCdkAssets = path.resolve(__dirname, '..', '..', 'bin');
try {
await shell([getDockerCmd(), ...configArgs, ...args], {
logger: this.logger,
...options,
env: {
...process.env,
...options.env,
PATH: `${pathToCdkAssets}${path.delimiter}${options.env?.PATH ?? process.env.PATH}`,
},
Expand Down Expand Up @@ -234,7 +230,6 @@ export class Docker {
export interface DockerFactoryOptions {
readonly repoUri: string;
readonly ecr: IECRClient;
readonly logger: (m: string) => void;
}

/**
Expand All @@ -249,7 +244,7 @@ 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();

// 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 +263,7 @@ 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();
await this.loginOncePerDestination(docker, options);
return docker;
}
Expand Down
2 changes: 0 additions & 2 deletions lib/private/handlers/container-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export class ContainerImageAssetHandler implements IAssetHandler {

const dockerForBuilding = await this.host.dockerFactory.forBuild({
repoUri: initOnce.repoUri,
logger: (m: string) => this.host.emitMessage(EventType.DEBUG, m),
ecr: initOnce.ecr,
});

Expand Down Expand Up @@ -85,7 +84,6 @@ export class ContainerImageAssetHandler implements IAssetHandler {

const dockerForPushing = await this.host.dockerFactory.forEcrPush({
repoUri: initOnce.repoUri,
logger: (m: string) => this.host.emitMessage(EventType.DEBUG, m),
ecr: initOnce.ecr,
});

Expand Down
37 changes: 20 additions & 17 deletions lib/private/shell.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as child_process from 'child_process';
import { EventType, globalOutputHandler } from '../progress';

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

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

Expand All @@ -15,9 +15,9 @@ export interface ShellOptions extends child_process.SpawnOptions {
* string.
*/
export async function shell(command: string[], options: ShellOptions = {}): Promise<string> {
if (options.logger) {
options.logger(renderCommandLine(command));
}
globalOutputHandler.publishEvent(EventType.START, command.join(' '));
globalOutputHandler.info(renderCommandLine(command));

const child = child_process.spawn(command[0], command.slice(1), {
...options,
stdio: [options.input ? 'pipe' : 'ignore', 'pipe', 'pipe'],
Expand All @@ -32,36 +32,39 @@ 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);
globalOutputHandler.publishEvent(chunk, EventType.DEBUG);
}
stdout.push(chunk);
});

child.stderr!.on('data', (chunk) => {
if (!options.quiet) {
process.stderr.write(chunk);
globalOutputHandler.publishEvent(chunk, EventType.DEBUG);
}

stderr.push(chunk);
});

child.once('error', reject);
child.once('error', (error) => {
globalOutputHandler.publishEvent(EventType.FAIL, error.message);
reject(error);
});

child.once('close', (code, signal) => {
if (code === 0) {
resolve(Buffer.concat(stdout).toString('utf-8'));
const output = Buffer.concat(stdout).toString('utf-8');
globalOutputHandler.publishEvent(EventType.SUCCESS, output);
resolve(output);
} else {
const out = Buffer.concat(stderr).toString('utf-8').trim();
reject(
new ProcessFailed(
code,
signal,
`${renderCommandLine(command)} exited with ${code != null ? 'error code' : 'signal'} ${code ?? signal}: ${out}`
)
const errorOutput = Buffer.concat(stderr).toString('utf-8').trim();
const error = new ProcessFailed(
code,
signal,
`${renderCommandLine(command)} exited with ${code != null ? 'error code' : 'signal'} ${code ?? signal}: ${errorOutput}`
);
globalOutputHandler.publishEvent(EventType.FAIL, error.message);
reject(error);
}
});
});
Expand Down
47 changes: 47 additions & 0 deletions lib/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,50 @@ export interface IPublishProgress {
*/
abort(): void;
}

class GlobalOutputHandler {
private progressListener: IPublishProgressListener | undefined;
private completionProgress: number;

constructor(completionProgress: number = 0, progressListener?: IPublishProgressListener) {
this.progressListener = progressListener;
this.completionProgress = completionProgress;
}

public setListener(listener: IPublishProgressListener) {
this.progressListener = listener;
}

public setCompletionProgress(progress: number) {
this.completionProgress = progress;
}

public publishEvent(eventType: EventType = EventType.DEBUG, text: string) {
const progressEvent: IPublishProgress = {
message: text,
abort: () => {},
percentComplete: this.completionProgress,
};
if (this.progressListener) {
this.progressListener.onPublishEvent(eventType, progressEvent);
}
}

public verbose(text: string) {
this.publishEvent(EventType.DEBUG, text);
}

public error(text: string) {
this.publishEvent(EventType.FAIL, text);
}

public info(text: string) {
this.publishEvent(EventType.SUCCESS, text);
}

public hasListener() {
return this.progressListener !== undefined;
}
}

export let globalOutputHandler = new GlobalOutputHandler();
21 changes: 16 additions & 5 deletions lib/publishing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { IAssetHandler, IHandlerHost, type PublishOptions } from './private/asse
import { DockerFactory } from './private/docker';
import { makeAssetHandler } from './private/handlers';
import { pLimit } from './private/p-limit';
import { EventType, IPublishProgress, IPublishProgressListener } from './progress';
import {
EventType,
IPublishProgress,
IPublishProgressListener,
globalOutputHandler,
} from './progress';

export interface AssetPublishingOptions {
/**
Expand Down Expand Up @@ -113,6 +118,10 @@ export class AssetPublishing implements IPublishProgress {
},
dockerFactory: new DockerFactory(),
};
if (options.progressListener) {
globalOutputHandler.setListener(options.progressListener);
}
globalOutputHandler.setCompletionProgress(this.percentComplete);
}

/**
Expand Down Expand Up @@ -249,10 +258,12 @@ export class AssetPublishing implements IPublishProgress {
}

public get percentComplete() {
if (this.totalOperations === 0) {
return 100;
}
return Math.floor((this.completedOperations / this.totalOperations) * 100);
const completionProgress =
this.totalOperations === 0
? 100
: Math.floor((this.completedOperations / this.totalOperations) * 100);
globalOutputHandler.setCompletionProgress(completionProgress);
return completionProgress;
}

public abort(): void {
Expand Down
Loading
Loading