Skip to content

Commit

Permalink
Merge ceed553 into 9ec559a
Browse files Browse the repository at this point in the history
  • Loading branch information
fboucquez committed Jan 12, 2022
2 parents 9ec559a + ceed553 commit 2ae409f
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 101 deletions.
13 changes: 4 additions & 9 deletions src/commands/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,9 @@ export default class Verify extends Command {
CommandUtils.showBanner();
const { flags } = this.parse(Verify);
const logger = LoggerFactory.getLogger(flags.logger);
const report = await new VerifyService(logger).createReport();
logger.info(`OS: ${report.platform}`);
report.lines.forEach((line) => {
if (line.recommendation) {
logger.warn(`${line.header} - Warning! - ${line.message} - ${line.recommendation}`);
} else {
logger.info(`${line.header} - OK! - ${line.message}`);
}
});
const service = new VerifyService(logger);
const report = await service.createReport();
service.logReport(report);
service.validateReport(report);
}
}
230 changes: 152 additions & 78 deletions src/service/VerifyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as os from 'os';
import * as semver from 'semver';
import { Logger } from '../logger';
import { BootstrapUtils } from './BootstrapUtils';

export interface VerifyReport {
platform: string;
lines: ReportLine[];
Expand All @@ -40,71 +41,102 @@ const defaultExpectedVersions: ExpectedVersions = {
dockerCompose: '1.25.0',
};

export class VerifyService {
private readonly expectedVersions: ExpectedVersions;
public readonly semverOptions = { loose: true };

constructor(private readonly logger: Logger, expectedVersions: Partial<ExpectedVersions> = {}) {
this.expectedVersions = { ...defaultExpectedVersions, ...expectedVersions };
}

public async createReport(): Promise<VerifyReport> {
const lines: ReportLine[] = [];
const platform = `${os.type()} - ${os.release()} - ${os.platform()}`;
lines.push(await this.testNodeJs());
const docker = await this.testDocker();
lines.push(docker);
lines.push(await this.testDockerCompose());
if (!docker.recommendation) lines.push(await this.testDockerRun());
if (!BootstrapUtils.isWindows()) {
lines.push(await this.testSudo());
}
return { lines, platform };
}
export interface VerifyAction {
shouldRun(lines: ReportLine[]): boolean;
verify(): Promise<ReportLine>;
}

export class AppVersionService {
public static readonly semverOptions = { loose: true };
constructor(private readonly logger: Logger) {}
public loadVersion(text: string): string | undefined {
return text
.replace(',', '')
.split(' ')
.map((word) => {
const coerce = semver.coerce(word.trim(), this.semverOptions);
const coerce = semver.coerce(word.trim(), AppVersionService.semverOptions);
return coerce?.raw;
})
.find((a) => a)
?.trim();
}

public async testNodeJs(): Promise<ReportLine> {
const header = 'NodeVersion';
const recommendationUrl = `https://nodejs.org/en/download/package-manager/`;
const output = process.versions.node;
return this.verifyInstalledApp(async () => output, header, this.expectedVersions.node, recommendationUrl);
public async loadVersionFromCommand(command: string): Promise<string | undefined> {
return this.loadVersion((await BootstrapUtils.exec(this.logger, command)).stdout.trim());
}

public async testDocker(): Promise<ReportLine> {
const header = 'Docker Version';
const command = 'docker --version';
const recommendationUrl = `https://docs.docker.com/get-docker/`;
return this.verifyInstalledApp(
async () => await this.loadVersionFromCommand(command),
header,
this.expectedVersions.docker,
recommendationUrl,
);
public async verifyInstalledApp(
versionLoader: () => Promise<string | undefined>,
header: string,
minVersion: string,
recommendationUrl: string,
): Promise<ReportLine> {
const recommendationPrefix = `At least version ${minVersion} is required.`;
const recommendationSuffix = `Check ${recommendationUrl}`;
try {
const version = await versionLoader();
if (!version) {
return {
header,
message: `Version could not be found! Output: ${versionLoader}`,
recommendation: `${recommendationPrefix} ${recommendationSuffix}`,
};
}
if (semver.lt(version, minVersion, AppVersionService.semverOptions)) {
return {
header,
message: version,
recommendation: `${recommendationPrefix} Currently installed version is ${version}. ${recommendationSuffix}`,
};
}
return { header, message: version };
} catch (e) {
return {
header,
message: `Error: ${e.message}`,
recommendation: `${recommendationPrefix} ${recommendationSuffix}`,
};
}
}
public async testDockerCompose(): Promise<ReportLine> {
const header = 'Docker Compose Version';
const command = 'docker-compose --version';
const recommendationUrl = `https://docs.docker.com/compose/install/`;
return this.verifyInstalledApp(
async () => await this.loadVersionFromCommand(command),
header,
this.expectedVersions.dockerCompose,
recommendationUrl,
}

export class AppVersionVerifyAction implements VerifyAction {
constructor(
readonly service: AppVersionService,
readonly params: {
header: string;
version?: string;
command?: string;
recommendationUrl: string;
expectedVersion: string;
},
) {}

verify(): Promise<ReportLine> {
return this.service.verifyInstalledApp(
async () => {
if (this.params.version) {
return this.params.version;
}
if (this.params.command) {
return this.service.loadVersionFromCommand(this.params.command);
}
throw new Error('Either version or command must be provided!');
},
this.params.header,
this.params.expectedVersion,
this.params.recommendationUrl,
);
}

public async testDockerRun(): Promise<ReportLine> {
shouldRun(): boolean {
return true;
}
}

export class DockerRunVerifyAction implements VerifyAction {
constructor(private readonly logger: Logger) {}
async verify(): Promise<ReportLine> {
const header = 'Docker Run Test';
const command = 'docker run hello-world';
const recommendationUrl = `https://www.digitalocean.com/community/questions/how-to-fix-docker-got-permission-denied-while-trying-to-connect-to-the-docker-daemon-socket`;
Expand All @@ -128,8 +160,13 @@ export class VerifyService {
};
}
}
shouldRun(lines: ReportLine[]): boolean {
return !!lines.find((l) => l.header === 'Docker Version' && !l.recommendation);
}
}

public async testSudo(): Promise<ReportLine> {
export class SudoRunVerifyAction implements VerifyAction {
async verify(): Promise<ReportLine> {
const header = 'Sudo User Test';
if (BootstrapUtils.isRoot()) {
return {
Expand All @@ -140,40 +177,77 @@ export class VerifyService {
}
return { header, message: `Your are not the sudo user!` };
}
shouldRun(): boolean {
return !BootstrapUtils.isWindows();
}
}

public async loadVersionFromCommand(command: string): Promise<string | undefined> {
return this.loadVersion((await BootstrapUtils.exec(this.logger, command)).stdout.trim());
export class VerifyService {
private readonly expectedVersions: ExpectedVersions;
public static readonly currentNodeJsVersion = process.versions.node;

public actions: VerifyAction[] = [];

constructor(private readonly logger: Logger, expectedVersions: Partial<ExpectedVersions> = {}) {
this.expectedVersions = { ...defaultExpectedVersions, ...expectedVersions };

const appVersionService = new AppVersionService(this.logger);
this.actions.push(
new AppVersionVerifyAction(appVersionService, {
header: 'NodeVersion',
version: VerifyService.currentNodeJsVersion,
recommendationUrl: `https://nodejs.org/en/download/package-manager/`,
expectedVersion: this.expectedVersions.node,
}),
);
this.actions.push(
new AppVersionVerifyAction(appVersionService, {
header: 'Docker Version',
command: 'docker --version',
recommendationUrl: `https://docs.docker.com/get-docker/`,
expectedVersion: this.expectedVersions.docker,
}),
);

this.actions.push(
new AppVersionVerifyAction(appVersionService, {
header: 'Docker Compose Version',
command: 'docker-compose --version',
recommendationUrl: `https://docs.docker.com/compose/install/`,
expectedVersion: this.expectedVersions.dockerCompose,
}),
);
this.actions.push(new DockerRunVerifyAction(this.logger));
this.actions.push(new SudoRunVerifyAction());
}

private async verifyInstalledApp(
versionLoader: () => Promise<string | undefined>,
header: string,
minVersion: string,
recommendationUrl: string,
): Promise<ReportLine> {
try {
const version = await versionLoader();
if (!version) {
return {
header,
message: `Version could not be found! Output: ${versionLoader}`,
recommendation: `At least version ${minVersion} is required. Check ${recommendationUrl}`,
};
}
if (semver.lt(version, minVersion, this.semverOptions)) {
return {
header,
message: version,
recommendation: `At least version ${minVersion} is required. Currently installed version is ${version}. Check ${recommendationUrl}`,
};
public async createReport(): Promise<VerifyReport> {
const lines: ReportLine[] = [];
const platform = `${os.type()} - ${os.release()} - ${os.platform()}`;
for (const action of this.actions) {
if (action.shouldRun(lines)) lines.push(await action.verify());
}
return { lines, platform };
}

public logReport(report: VerifyReport): void {
this.logger.info(`OS: ${report.platform}`);
report.lines.forEach((line) => {
if (line.recommendation) {
this.logger.error(`${line.header} - Error! - ${line.message} - ${line.recommendation}`);
} else {
this.logger.info(`${line.header} - OK! - ${line.message}`);
}
return { header, message: version };
} catch (e) {
return {
header,
message: `Error: ${e.message}`,
recommendation: `At least version ${minVersion} is required. Check ${recommendationUrl}`,
};
});
}

public validateReport(report: VerifyReport): void {
const errors = report.lines.filter((r) => r.recommendation);
if (errors.length) {
throw new Error(
'There has been an error. Check the report:\n' +
errors.map((line) => ` - ${line.header} - Error! - ${line.message} - ${line.recommendation}`).join('\n'),
);
}
}
}
41 changes: 27 additions & 14 deletions test/service/VerifyService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,34 @@
import { expect } from 'chai';
import * as os from 'os';
import * as semver from 'semver';
import { LoggerFactory, LogType } from '../../src';
import { AppVersionService, LoggerFactory, LogType } from '../../src';
import { VerifyService } from '../../src/service';
const logger = LoggerFactory.getLogger(LogType.Silent);
describe('VerifyService', () => {
const currentNodeJsVersion = process.versions.node;

describe('AppVersionService', () => {
it('loadVersion', async () => {
const service = new VerifyService(logger);
const service = new AppVersionService(logger);
expect(service.loadVersion('Docker version 19.03.8, build afacb8b7f0')).eq('19.03.8');
expect(service.loadVersion('Docker version 19.0.8, build afacb8b7f0')).eq('19.0.8');
expect(service.loadVersion('Docker version 19 build a')).eq('19.0.0');
});
});

describe('VerifyService', () => {
const currentNodeJsVersion = VerifyService.currentNodeJsVersion;
async function getCurrentVersions() {
const appVersionService = new AppVersionService(logger);
const currentDockerVersion = await appVersionService.loadVersionFromCommand('docker --version');
const currentDockerComposeVersion = await appVersionService.loadVersionFromCommand('docker-compose --version');
expect(semver.valid(VerifyService.currentNodeJsVersion, AppVersionService.semverOptions));
expect(semver.valid(currentDockerVersion, AppVersionService.semverOptions));
expect(semver.valid(currentDockerComposeVersion, AppVersionService.semverOptions));
return { currentDockerVersion, currentDockerComposeVersion };
}

it('VerifyService verify current installation', async () => {
const service = new VerifyService(logger);
const currentDockerVersion = await service.loadVersionFromCommand('docker --version');
const currentDockerComposeVersion = await service.loadVersionFromCommand('docker-compose --version');
expect(semver.valid(currentNodeJsVersion, service.semverOptions));
expect(semver.valid(currentDockerVersion, service.semverOptions));
expect(semver.valid(currentDockerComposeVersion, service.semverOptions));
const { currentDockerVersion, currentDockerComposeVersion } = await getCurrentVersions();
const report = await service.createReport();
const expected = {
lines: [
Expand Down Expand Up @@ -63,6 +71,8 @@ describe('VerifyService', () => {
platform: `${os.type()} - ${os.release()} - ${os.platform()}`,
};
expect(report).to.be.deep.eq(expected);
service.logReport(report);
expect(() => service.validateReport(report)).not.to.throw();
});

it('VerifyService verify current installation when too old', async () => {
Expand All @@ -72,11 +82,7 @@ describe('VerifyService', () => {
dockerCompose: '1.29.5',
};
const service = new VerifyService(logger, expectedVersions);
const currentDockerVersion = await service.loadVersionFromCommand('docker --version');
const currentDockerComposeVersion = await service.loadVersionFromCommand('docker-compose --version');
expect(semver.valid(currentNodeJsVersion, service.semverOptions));
expect(semver.valid(currentDockerVersion, service.semverOptions));
expect(semver.valid(currentDockerComposeVersion, service.semverOptions));
const { currentDockerVersion, currentDockerComposeVersion } = await getCurrentVersions();

const report = await service.createReport();
const expected = {
Expand Down Expand Up @@ -104,5 +110,12 @@ describe('VerifyService', () => {
platform: `${os.type()} - ${os.release()} - ${os.platform()}`,
};
expect(report).to.be.deep.eq(expected);
service.logReport(report);
expect(() => service.validateReport(report)).to.throw(
`There has been an error. Check the report:
- NodeVersion - Error! - ${currentNodeJsVersion} - At least version ${expectedVersions.node} is required. Currently installed version is ${currentNodeJsVersion}. Check https://nodejs.org/en/download/package-manager/
- Docker Version - Error! - ${currentDockerVersion} - At least version ${expectedVersions.docker} is required. Currently installed version is ${currentDockerVersion}. Check https://docs.docker.com/get-docker/
- Docker Compose Version - Error! - ${currentDockerComposeVersion} - At least version ${expectedVersions.dockerCompose} is required. Currently installed version is ${currentDockerComposeVersion}. Check https://docs.docker.com/compose/install/`,
);
});
});

0 comments on commit 2ae409f

Please sign in to comment.