Skip to content

Commit c92231f

Browse files
authored
feat: add ability to inject external logger and pass AWS credentials through input props (#20)
* feat: allow external loggers to be plugged in. * feat: allow externally passed credentials to AWS * feat: fix log levels
1 parent 081ac1e commit c92231f

File tree

6 files changed

+74
-41
lines changed

6 files changed

+74
-41
lines changed

src/deploy.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
/* eslint-disable no-console */
21
import { ElasticBeanstalkClient } from '@aws-sdk/client-elastic-beanstalk';
32
import chalk from 'chalk';
4-
import log from 'loglevel';
3+
import loglevel from 'loglevel';
54
import { create } from './helpers/create-app-version';
65
import { deploy } from './helpers/deploy-app-version-to-env';
76
import { DBError, DBGroupDeployTriggerError, DBHealthinessCheckError } from './helpers/Errors';
87
import { waitForGroupHealthiness } from './helpers/healthiness';
9-
import { IDeployToGroupProps, IHealthCheckProps } from './helpers/Interfaces';
8+
import { IDeployToGroupProps, IHealthCheckProps, Logger } from './helpers/Interfaces';
109

1110
const AWS_CLIENT_REQUEST_MAX_ATTEMPTS_DEFAULT = 10;
1211
const DEFAULT_FORCE = false;
@@ -34,7 +33,7 @@ function verifyPromisesSettled(results: PromiseSettledResult<void>[]) {
3433
* Application. For each of those unique Applications, we must create an App
3534
* Version to use for deployments.
3635
*/
37-
async function createAppVersionsForGroup(client: ElasticBeanstalkClient, props: IDeployToGroupProps) {
36+
async function createAppVersionsForGroup(client: ElasticBeanstalkClient, props: IDeployToGroupProps, log: Logger) {
3837
const appsWithCreatedVersions: string[] = [];
3938
const appVersionPromises: Promise<void>[] = [];
4039
log.info(`Creating application versions for beanstalk group ${props.group.name}`);
@@ -46,6 +45,7 @@ async function createAppVersionsForGroup(client: ElasticBeanstalkClient, props:
4645
version: props.group.versionProps,
4746
appName: env.app,
4847
dryRun: !props.force,
48+
log,
4949
}),
5050
);
5151
appsWithCreatedVersions.push(env.app);
@@ -56,14 +56,15 @@ async function createAppVersionsForGroup(client: ElasticBeanstalkClient, props:
5656
log.info(chalk.green('All needed application versions exist.'));
5757
}
5858

59-
async function preDeployHealthcheck(client: ElasticBeanstalkClient, props: IDeployToGroupProps) {
59+
async function preDeployHealthcheck(client: ElasticBeanstalkClient, props: IDeployToGroupProps, log: Logger) {
6060
try {
6161
log.info(chalk.blue('Verifying environments are ready to receive deployment before initiating...'));
6262
await waitForGroupHealthiness({
6363
client,
6464
group: props.group,
6565
force: props.force ?? DEFAULT_FORCE,
6666
checkVersion: false,
67+
log,
6768
...(props.preDeployHealthCheckProps ?? DEFAULT_HEALTH_CHECK_PROPS),
6869
});
6970
} catch (e) {
@@ -74,14 +75,15 @@ async function preDeployHealthcheck(client: ElasticBeanstalkClient, props: IDepl
7475
}
7576
}
7677

77-
async function postDeployHealthcheck(client: ElasticBeanstalkClient, props: IDeployToGroupProps) {
78+
async function postDeployHealthcheck(client: ElasticBeanstalkClient, props: IDeployToGroupProps, log: Logger) {
7879
log.info(chalk.blue('Verifying environments successfully receive the deployment...this could take a while.'));
7980
try {
8081
await waitForGroupHealthiness({
8182
client,
8283
group: props.group,
8384
force: props.force ?? DEFAULT_FORCE,
8485
checkVersion: true,
86+
log,
8587
...(props.postDeployHealthCheckProps ?? DEFAULT_HEALTH_CHECK_PROPS),
8688
});
8789
} catch (e) {
@@ -103,7 +105,7 @@ async function postDeployHealthcheck(client: ElasticBeanstalkClient, props: IDep
103105
* For each Beanstalk Environment in the group, deploys the respective
104106
* Application Version.
105107
*/
106-
async function deployAppVersionsToGroup(client: ElasticBeanstalkClient, props: IDeployToGroupProps) {
108+
async function deployAppVersionsToGroup(client: ElasticBeanstalkClient, props: IDeployToGroupProps, log: Logger) {
107109
log.info(`Asynchronously kicking off deployment to the ${props.group.name} group of beanstalks.`);
108110
const triggerErr = new DBGroupDeployTriggerError('deployAppVersionsToGroup: ', []);
109111
const force = props.force ?? DEFAULT_FORCE;
@@ -114,6 +116,7 @@ async function deployAppVersionsToGroup(client: ElasticBeanstalkClient, props: I
114116
client,
115117
force,
116118
env,
119+
log,
117120
version: props.group.versionProps,
118121
});
119122
} catch (e) {
@@ -126,19 +129,33 @@ async function deployAppVersionsToGroup(client: ElasticBeanstalkClient, props: I
126129
if (triggerErr.errors.length !== 0) throw triggerErr;
127130
}
128131

132+
/**
133+
* Get AWS Credentials if provided
134+
*/
135+
function getCredentials(props: IDeployToGroupProps) {
136+
if (props.accessKeyId && props.secretAccessKey) {
137+
return {
138+
accessKeyId: props.accessKeyId,
139+
secretAccessKey: props.secretAccessKey,
140+
};
141+
}
142+
return;
143+
}
129144
/**
130145
* Initializes the Beanstalk Client, creates the needed Application Versions,
131146
* and verifies the Beanstalk Environments in the group are ready to receive
132147
* the deployment.
133148
*/
134-
async function preDeploySteps(props: IDeployToGroupProps): Promise<ElasticBeanstalkClient> {
149+
async function preDeploySteps(props: IDeployToGroupProps, log: Logger): Promise<ElasticBeanstalkClient> {
135150
try {
151+
const credentials = getCredentials(props);
136152
const client = new ElasticBeanstalkClient({
137153
maxAttempts: AWS_CLIENT_REQUEST_MAX_ATTEMPTS_DEFAULT,
138154
region: props.group.region,
155+
credentials,
139156
});
140-
await createAppVersionsForGroup(client, props);
141-
await preDeployHealthcheck(client, props);
157+
await createAppVersionsForGroup(client, props, log);
158+
await preDeployHealthcheck(client, props, log);
142159
return client;
143160
} catch (e) {
144161
log.error(chalk.red(e));
@@ -155,17 +172,17 @@ async function preDeploySteps(props: IDeployToGroupProps): Promise<ElasticBeanst
155172
* Beanstalk Environment in the group, then verifies they reach a healthy state
156173
* and successfully land the expected version.
157174
*/
158-
async function deploySteps(client: ElasticBeanstalkClient, props: IDeployToGroupProps) {
175+
async function deploySteps(client: ElasticBeanstalkClient, props: IDeployToGroupProps, log: Logger) {
159176
let deployErrs: DBError = new DBError('deploySteps(): ', []);
160177
try {
161-
await deployAppVersionsToGroup(client, props);
178+
await deployAppVersionsToGroup(client, props, log);
162179
} catch (e) {
163180
// We still want to see status of Beanstalks who did have deploy triggered
164181
deployErrs.errors.push(e as Error);
165182
}
166183

167184
try {
168-
await postDeployHealthcheck(client, props);
185+
await postDeployHealthcheck(client, props, log);
169186
} catch (e) {
170187
log.error(chalk.red(e));
171188
if (e instanceof DBError) {
@@ -183,10 +200,10 @@ async function deploySteps(client: ElasticBeanstalkClient, props: IDeployToGroup
183200
* Versions for their respective Beanstalk Applications, and then deploys
184201
* those versions to the Beanstalk Environments and verifies their health.
185202
*/
186-
export async function deployToGroup(props: IDeployToGroupProps) {
203+
export async function deployToGroup(props: IDeployToGroupProps, log: Logger = loglevel) {
187204
const group = props.group;
188-
log.setLevel(props.logLevel ?? log.levels.INFO);
205+
log.setLevel(props.logLevel ?? 'info');
189206
log.info(chalk.green('Beginning deploy process for beanstalk group ') + chalk.blue(group.name));
190207

191-
await deploySteps(await preDeploySteps(props), props);
208+
await deploySteps(await preDeploySteps(props, log), props, log);
192209
}

src/helpers/Interfaces.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { S3Location, EnvironmentHealthStatus } from '@aws-sdk/client-elastic-beanstalk';
2-
import log from 'loglevel';
2+
3+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
34

45
/**
56
* Properties consumed to deploy an Application Version to an existing
@@ -87,7 +88,7 @@ export interface IDeployToGroupProps {
8788
/**
8889
* Every level below the specified log level is silenced. Defaults to INFO.
8990
*/
90-
readonly logLevel?: log.LogLevelDesc;
91+
readonly logLevel?: LogLevel;
9192
/**
9293
* Configuration for health checks prior to the deployment.
9394
*/
@@ -96,4 +97,20 @@ export interface IDeployToGroupProps {
9697
* Configuration for health checks after the deployment.
9798
*/
9899
readonly postDeployHealthCheckProps?: IHealthCheckProps;
100+
/**
101+
* AWS Access Key Id, if not present taken from i.e. ENV variable
102+
*/
103+
readonly accessKeyId?: string;
104+
/**
105+
* AWS Secret Access Key, if not present taken from i.e. ENV variable
106+
*/
107+
readonly secretAccessKey?: string;
108+
}
109+
110+
export interface Logger {
111+
debug: (msg: string) => void;
112+
info: (msg: string) => void;
113+
warn: (msg: string) => void;
114+
error: (msg: string) => void;
115+
setLevel: (level: LogLevel) => void;
99116
}

src/helpers/create-app-version.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
/* eslint-disable no-console */
21
import {
32
CreateApplicationVersionCommand,
43
DescribeApplicationVersionsCommand,
54
ElasticBeanstalkClient,
65
} from '@aws-sdk/client-elastic-beanstalk';
7-
import log from 'loglevel';
86
import { DBCreateApplicationVersionError } from './Errors';
9-
import { IAppVersionProps } from './Interfaces';
7+
import { IAppVersionProps, Logger } from './Interfaces';
108

119
interface ICreateProps {
1210
appName: string;
1311
client: ElasticBeanstalkClient;
1412
dryRun?: boolean;
1513
version: IAppVersionProps;
14+
log: Logger;
1615
}
1716

1817
async function checkApplicationVersionExists(props: ICreateProps): Promise<boolean> {
@@ -24,7 +23,7 @@ async function checkApplicationVersionExists(props: ICreateProps): Promise<boole
2423
return getExistingVersionsResp.ApplicationVersions ? getExistingVersionsResp.ApplicationVersions.length > 0 : false;
2524
}
2625

27-
async function createApplicationVersion(props: ICreateProps): Promise<void> {
26+
async function createApplicationVersion(props: ICreateProps, log: Logger): Promise<void> {
2827
if (props.dryRun) {
2928
log.info(`DRY RUN: Would have created application version ${props.version.label} for app ${props.appName}`);
3029
return;
@@ -61,11 +60,11 @@ export async function create(props: ICreateProps): Promise<void> {
6160
if (props.version.errorIfExists ?? false) {
6261
throw new Error(`Failed to create new application version ${props.version.label}, it already exists.`);
6362
}
64-
log.info(
63+
props.log.info(
6564
`Not creating new application version ${props.version.label} for app ${props.appName} since it already exists.`,
6665
);
6766
} else {
68-
await createApplicationVersion(props);
67+
await createApplicationVersion(props, props.log);
6968
}
7069
} catch (e) {
7170
throw new DBCreateApplicationVersionError(props.appName, props.version.label, e as Error);

src/helpers/deploy-app-version-to-env.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
1-
/* eslint-disable no-console */
21
import { ElasticBeanstalkClient, UpdateEnvironmentCommand } from '@aws-sdk/client-elastic-beanstalk';
3-
import log from 'loglevel';
42
import { DBTriggerDeployError } from './Errors';
5-
import { IAppVersionProps, IBeanstalkEnvironment } from './Interfaces';
3+
import { IAppVersionProps, IBeanstalkEnvironment, Logger } from './Interfaces';
64

75
interface IDeployProps {
86
client: ElasticBeanstalkClient;
97
force: boolean;
108
env: IBeanstalkEnvironment;
119
version: IAppVersionProps;
10+
log: Logger;
1211
}
1312

1413
async function deployApplicationVersion(props: IDeployProps): Promise<void> {
1514
if (!props.force) {
16-
log.info(
15+
props.log.info(
1716
`DRY RUN: Would have deployed app version ${props.version.label} to beanstalk environment ${props.env.name}`,
1817
);
1918
return;
2019
}
2120

22-
log.info(`Initiating deployment of version ${props.version.label} to environment ${props.env.name}...`);
21+
props.log.info(`Initiating deployment of version ${props.version.label} to environment ${props.env.name}...`);
2322
const resp = await props.client.send(
2423
new UpdateEnvironmentCommand({
2524
ApplicationName: props.env.app,
@@ -31,7 +30,7 @@ async function deployApplicationVersion(props: IDeployProps): Promise<void> {
3130
// Verify deployment initiated successfully
3231
const statusCode = resp.$metadata.httpStatusCode;
3332
if (statusCode && statusCode >= 200 && statusCode < 300) {
34-
log.info(`Deployment of app version '${props.version.label}' triggered for '${props.env.name}'.`);
33+
props.log.info(`Deployment of app version '${props.version.label}' triggered for '${props.env.name}'.`);
3534
} else {
3635
throw new Error(`Response metadata: ${JSON.stringify(resp.$metadata, undefined, 2)}`);
3736
}

src/helpers/healthiness.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
/* eslint-disable no-console */
21
import {
32
DescribeEnvironmentsCommand,
43
DescribeEnvironmentsCommandOutput,
54
ElasticBeanstalkClient,
65
EnvironmentDescription,
76
EnvironmentHealthStatus,
87
} from '@aws-sdk/client-elastic-beanstalk';
9-
import log from 'loglevel';
108
import { DBHealthinessCheckError } from './Errors';
11-
import { IBeanstalkEnvironment, IBeanstalkGroup, IHealthCheckProps } from './Interfaces';
9+
import { IBeanstalkEnvironment, IBeanstalkGroup, IHealthCheckProps, Logger } from './Interfaces';
1210

1311
const AWS_EB_HEALTH_CHECK_UNHEALTHY_STATES: EnvironmentHealthStatus[] = ['Severe', 'Degraded', 'Warning'];
1412

@@ -24,6 +22,7 @@ interface IHealthCheckPropsPrivate extends IHealthCheckProps {
2422
client: ElasticBeanstalkClient;
2523
force: boolean;
2624
group: IBeanstalkGroup;
25+
log: Logger;
2726
}
2827

2928
interface IBeanstalkHealthStatuses {
@@ -67,11 +66,12 @@ function groupEnvsByApp(envs: IBeanstalkEnvironment[]): IEnvironmentsByApp {
6766
function getEnvironmentsHealth(
6867
envs: EnvironmentDescription[],
6968
unhealthyStatuses: EnvironmentHealthStatus[],
69+
log: Logger,
7070
expectedVersionLabel?: string,
7171
): IBeanstalkHealthStatuses {
7272
return envs.reduce(
7373
(previousValue: IBeanstalkHealthStatuses, envDesc) => {
74-
log.debug(envDesc);
74+
log.debug(JSON.stringify(envDesc, null, 4));
7575
if (!(envDesc.Status && envDesc.HealthStatus)) {
7676
throw new Error(
7777
`Beanstalk status for '${envDesc.EnvironmentName}' could not be retrieved. Cannot proceed safely.`,
@@ -148,13 +148,14 @@ async function getGroupHealth(props: IHealthCheckPropsPrivate): Promise<IBeansta
148148
}
149149

150150
if (!props.force) {
151-
log.info(`DRY RUN: Would have waited for beanstalks in app '${key}' to become healthy.`);
151+
props.log.info(`DRY RUN: Would have waited for beanstalks in app '${key}' to become healthy.`);
152152
continue;
153153
}
154154

155155
const partitioned = getEnvironmentsHealth(
156156
resp.Environments,
157157
props.unhealthyStatuses ?? AWS_EB_HEALTH_CHECK_UNHEALTHY_STATES,
158+
props.log,
158159
props.checkVersion ? props.group.versionProps.label : undefined,
159160
);
160161
statuses.healthy = [...statuses.healthy, ...partitioned.healthy];
@@ -173,7 +174,7 @@ async function getGroupHealth(props: IHealthCheckPropsPrivate): Promise<IBeansta
173174
*/
174175
export async function waitForGroupHealthiness(props: IHealthCheckPropsPrivate): Promise<void> {
175176
for (let attempt = 1; attempt <= props.attempts; attempt++) {
176-
log.info(`Checking beanstalks health... Attempt ${attempt} of ${props.attempts}`);
177+
props.log.info(`Checking beanstalks health... Attempt ${attempt} of ${props.attempts}`);
177178
let statuses: IBeanstalkHealthStatuses;
178179
try {
179180
statuses = await getGroupHealth(props);
@@ -184,17 +185,17 @@ export async function waitForGroupHealthiness(props: IHealthCheckPropsPrivate):
184185
const allAreHealthy = !props.force || statuses.healthy.length === props.group.environments.length;
185186
if (isLastAttempt && !allAreHealthy) {
186187
// Log healthy beanstalk statuses
187-
statuses.healthy.forEach((envStatus) => log.info(envStatus.msg));
188+
statuses.healthy.forEach((envStatus) => props.log.info(envStatus.msg));
188189
const errs = statuses.unhealthy.map((envStatus) => new Error(envStatus.msg));
189190
throw new DBHealthinessCheckError(`Beanstalks are not healthy after ${props.attempts} attempt(s).`, errs);
190191
}
191192

192193
// If we're not on the last attempt, log all statuses
193194
[statuses.healthy, statuses.unhealthy].forEach((statusSet) => {
194-
statusSet.forEach((envStatus) => log.info(envStatus.msg));
195+
statusSet.forEach((envStatus) => props.log.info(envStatus.msg));
195196
});
196197
if (allAreHealthy) {
197-
log.info(`All beanstalks in group '${props.group.name}' are healthy!`);
198+
props.log.info(`All beanstalks in group '${props.group.name}' are healthy!`);
198199
return;
199200
}
200201

test/deploy.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
UpdateEnvironmentCommand,
77
} from '@aws-sdk/client-elastic-beanstalk';
88
import { mockClient } from 'aws-sdk-client-mock';
9-
import { LogLevelDesc } from 'loglevel';
109
import {
1110
DBError,
1211
DBCreateApplicationVersionError,
@@ -16,13 +15,14 @@ import {
1615
IDeployToGroupProps,
1716
DBTriggerDeployError,
1817
DBGroupDeployTriggerError,
18+
LogLevel,
1919
} from '../src/index';
2020

2121
const ebMock = mockClient(ElasticBeanstalkClient);
2222

2323
const COMMON_DEPLOY_PROPS = {
2424
force: true,
25-
logLevel: 'SILENT' as LogLevelDesc,
25+
logLevel: 'silent' as LogLevel,
2626
preDeployHealthCheckProps: {
2727
attempts: 1,
2828
timeBetweenAttemptsMs: 0,

0 commit comments

Comments
 (0)