Skip to content

Commit

Permalink
Merge pull request #630 from OpenAPITools/feat/docker-support
Browse files Browse the repository at this point in the history
feat: add docker support
  • Loading branch information
kay-schecker authored Apr 26, 2022
2 parents 77c2fe0 + 69016df commit 01c4945
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 76 deletions.
12 changes: 12 additions & 0 deletions apps/generator-cli/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,18 @@ If the `version` property param is set it is not necessary to configure the `que
| openapi-generator-cli generate --generator-key v3.0 v2.0 | yes | yes |
| openapi-generator-cli generate --generator-key foo | no | no |

## Use Docker instead of running java locally

```json
{
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"useDocker": true
}
}
```

## Custom Generators

Custom generators can be used by passing the `--custom-generator=/my/custom-generator.jar` argument.
Expand Down
8 changes: 8 additions & 0 deletions apps/generator-cli/src/app/services/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ export class ConfigService {
public readonly cwd = process.env.PWD || process.env.INIT_CWD || process.cwd()
public readonly configFile = path.resolve(this.cwd, 'openapitools.json')

public get useDocker() {
return this.get('generator-cli.useDocker', false);
}

public get dockerImageName() {
return this.get('generator-cli.dockerImageName', 'openapitools/openapi-generator-cli');
}

private readonly defaultConfig = {
$schema: './node_modules/@openapitools/openapi-generator-cli/config.schema.json',
spaces: 2,
Expand Down
55 changes: 43 additions & 12 deletions apps/generator-cli/src/app/services/generator.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Inject, Injectable } from '@nestjs/common';
import { flatten, isString, kebabCase, sortBy, upperFirst } from 'lodash';
import {Inject, Injectable} from '@nestjs/common';
import {flatten, isString, kebabCase, sortBy, upperFirst} from 'lodash';

import * as concurrently from 'concurrently';
import * as path from 'path';
import * as fs from 'fs-extra';
import * as glob from 'glob';
import * as chalk from 'chalk';
import { VersionManagerService } from './version-manager.service';
import { ConfigService } from './config.service';
import { LOGGER } from '../constants';
import {VersionManagerService} from './version-manager.service';
import {ConfigService} from './config.service';
import {LOGGER} from '../constants';

interface GeneratorConfig {
glob: string
Expand Down Expand Up @@ -96,6 +97,7 @@ export class GeneratorService {
}

private buildCommand(cwd: string, params: Record<string, unknown>, customGenerator?: string, specFile?: string) {
const dockerVolumes = {};
const absoluteSpecPath = specFile ? path.resolve(cwd, specFile) : String(params.inputSpec)

const command = Object.entries({
Expand All @@ -114,7 +116,19 @@ export class GeneratorService {
case 'boolean':
return undefined
default:
return `"${v}"`

if (this.configService.useDocker) {
if (key === 'output') {
fs.ensureDirSync(v);
}

if (fs.existsSync(v)) {
dockerVolumes[`/local/${key}`] = path.resolve(cwd, v);
return `"/local/${key}"`;
}
}

return `"${v}"`;
}
})()

Expand All @@ -139,14 +153,31 @@ export class GeneratorService {
ext: ext.split('.').slice(-1).pop()
}

return this.cmd(customGenerator, Object.entries(placeholders)
.filter(([, replacement]) => !!replacement)
.reduce((cmd, [search, replacement]) => {
return cmd.split(`#{${search}}`).join(replacement)
}, command))
return this.cmd(
customGenerator,
Object.entries(placeholders)
.filter(([, replacement]) => !!replacement)
.reduce((cmd, [search, replacement]) => {
return cmd.split(`#{${search}}`).join(replacement)
}, command),
dockerVolumes,
)
}

private cmd = (customGenerator: string | undefined, appendix: string) => {
private cmd = (customGenerator: string | undefined, appendix: string, dockerVolumes = {}) => {

if (this.configService.useDocker) {
const volumes = Object.entries(dockerVolumes).map(([k, v]) => `-v "${v}:${k}"`).join(' ');

return [
`docker run --rm`,
volumes,
this.versionManager.getDockerImageName(),
'generate',
appendix
].join(' ');
}

const cliPath = this.versionManager.filePath();
const subCmd = customGenerator
? `-cp "${[cliPath, customGenerator].join(this.isWin() ? ';' : ':')}" org.openapitools.codegen.OpenAPIGenerator`
Expand Down
86 changes: 44 additions & 42 deletions apps/generator-cli/src/app/services/pass-through.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Test } from '@nestjs/testing'
import {Test} from '@nestjs/testing'
import * as chalk from 'chalk'
import { Command, createCommand } from 'commander'
import { COMMANDER_PROGRAM, LOGGER } from '../constants'
import { GeneratorService } from './generator.service'
import { PassThroughService } from './pass-through.service'
import { VersionManagerService } from './version-manager.service'
import {Command, createCommand} from 'commander'
import {COMMANDER_PROGRAM, LOGGER} from '../constants'
import {GeneratorService} from './generator.service'
import {PassThroughService} from './pass-through.service'
import {VersionManagerService} from './version-manager.service'
import {ConfigService} from "./config.service";

jest.mock('child_process')
// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand All @@ -19,6 +20,7 @@ describe('PassThroughService', () => {
const generate = jest.fn().mockResolvedValue(true)
const getSelectedVersion = jest.fn().mockReturnValue('4.2.1')
const filePath = jest.fn().mockReturnValue(`/some/path/to/4.2.1.jar`)
const configServiceMock = {useDocker: false, get: jest.fn(), cwd: '/foo/bar'};

const getCommand = (name: string) => program.commands.find(c => c.name() === name);

Expand All @@ -29,17 +31,20 @@ describe('PassThroughService', () => {
const moduleRef = await Test.createTestingModule({
providers: [
PassThroughService,
{ provide: VersionManagerService, useValue: { filePath, getSelectedVersion } },
{ provide: GeneratorService, useValue: { generate, enabled: true } },
{ provide: COMMANDER_PROGRAM, useValue: program },
{ provide: LOGGER, useValue: { log } },
{provide: VersionManagerService, useValue: {filePath, getSelectedVersion, getDockerImageName: (v) => `openapitools/openapi-generator-cli:v${v || getSelectedVersion()}`}},
{provide: GeneratorService, useValue: {generate, enabled: true}},
{provide: ConfigService, useValue: configServiceMock},
{provide: COMMANDER_PROGRAM, useValue: program},
{provide: LOGGER, useValue: {log}},
],
}).compile()

fixture = moduleRef.get(PassThroughService)

childProcess.spawn.mockReset().mockReturnValue({ on: jest.fn() })

childProcess.spawn.mockReset().mockReturnValue({on: jest.fn()})
configServiceMock.get.mockClear()
configServiceMock.get.mockReset()
configServiceMock.useDocker = false;
})

describe('API', () => {
Expand Down Expand Up @@ -147,8 +152,28 @@ describe('PassThroughService', () => {
expect(cmd['_allowUnknownOption']).toBeTruthy()
})

describe('useDocker is true', () => {

beforeEach(() => {
configServiceMock.useDocker = true;
});

it('delegates to docker', async () => {
await program.parseAsync([name, ...argv], {from: 'user'})
expect(childProcess.spawn).toHaveBeenNthCalledWith(
1,
'docker run --rm -v "/foo/bar:/local" openapitools/openapi-generator-cli:v4.2.1',
[name, ...argv],
{
stdio: 'inherit',
shell: true
}
)
})
})

it('can delegate', async () => {
await program.parseAsync([name, ...argv], { from: 'user' })
await program.parseAsync([name, ...argv], {from: 'user'})
expect(childProcess.spawn).toHaveBeenNthCalledWith(
1,
'java -jar "/some/path/to/4.2.1.jar"',
Expand All @@ -162,7 +187,7 @@ describe('PassThroughService', () => {

it('can delegate with JAVA_OPTS', async () => {
process.env['JAVA_OPTS'] = 'java-opt-1=1'
await program.parseAsync([name, ...argv], { from: 'user' })
await program.parseAsync([name, ...argv], {from: 'user'})
expect(childProcess.spawn).toHaveBeenNthCalledWith(
1,
'java java-opt-1=1 -jar "/some/path/to/4.2.1.jar"',
Expand All @@ -175,7 +200,7 @@ describe('PassThroughService', () => {
})

it('can delegate with custom jar', async () => {
await program.parseAsync([name, ...argv, '--custom-generator=../some/custom.jar'], { from: 'user' })
await program.parseAsync([name, ...argv, '--custom-generator=../some/custom.jar'], {from: 'user'})
const cpDelimiter = process.platform === 'win32' ? ';' : ':'

expect(childProcess.spawn).toHaveBeenNthCalledWith(
Expand All @@ -191,8 +216,8 @@ describe('PassThroughService', () => {

if (name === 'generate') {
it('can delegate with custom jar to generate command', async () => {
await program.parseAsync([name, ...argv, '--generator-key=genKey', '--custom-generator=../some/custom.jar'], { from: 'user' })
await program.parseAsync([name, ...argv, '--generator-key=genKey', '--custom-generator=../some/custom.jar'], {from: 'user'})

expect(generate).toHaveBeenNthCalledWith(
1,
'../some/custom.jar',
Expand All @@ -201,29 +226,6 @@ describe('PassThroughService', () => {
})
}

// if (name === 'help') {
// it('prints the help info and does not delegate, if args length = 0', async () => {
// childProcess.spawn.mockReset()
// cmd.args = []
// const logSpy = jest.spyOn(console, 'log').mockImplementation(noop)
// await program.parseAsync([name], { from: 'user' })
// expect(childProcess.spawn).toBeCalledTimes(0)
// expect(program.helpInformation).toBeCalledTimes(1)
// // expect(logSpy).toHaveBeenCalledTimes(2)
// expect(logSpy).toHaveBeenNthCalledWith(1, 'some help text')
// expect(logSpy).toHaveBeenNthCalledWith(2, 'has custom generator')
// })
// }
//
// if (name === 'generate') {
// it('generates by using the generator config', async () => {
// childProcess.spawn.mockReset()
// await program.parseAsync([name], { from: 'user' })
// expect(childProcess.spawn).toBeCalledTimes(0)
// expect(generate).toHaveBeenNthCalledWith(1)
// })
// }

})

describe('command behavior', () => {
Expand All @@ -239,13 +241,13 @@ describe('PassThroughService', () => {
${'help generate'} | ${commandHelp('generate')} | ${'a'}
${'help author'} | ${commandHelp('author')} | ${'b'}
${'help hidden'} | ${undefined} | ${'c'}
`('$cmd', ({ cmd, helpText, spawn }) => {
`('$cmd', ({cmd, helpText, spawn}) => {

let spy: jest.SpyInstance;

beforeEach(async () => {
spy = jest.spyOn(console, 'log').mockClear().mockImplementation();
await program.parseAsync(cmd.split(' '), { from: 'user' })
await program.parseAsync(cmd.split(' '), {from: 'user'})
})

describe('help text', () => {
Expand Down
31 changes: 20 additions & 11 deletions apps/generator-cli/src/app/services/pass-through.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Inject, Injectable } from '@nestjs/common'
import {Inject, Injectable} from '@nestjs/common'
import * as chalk from 'chalk'
import { exec, spawn } from 'child_process'
import { Command } from 'commander'
import { isString, startsWith, trim } from 'lodash'
import { COMMANDER_PROGRAM, LOGGER } from '../constants'
import { GeneratorService } from './generator.service'
import { VersionManagerService } from './version-manager.service'
import {exec, spawn} from 'child_process'
import {Command} from 'commander'
import {isString, startsWith, trim} from 'lodash'
import {COMMANDER_PROGRAM, LOGGER} from '../constants'
import {GeneratorService} from './generator.service'
import {VersionManagerService} from './version-manager.service'
import {ConfigService} from "./config.service";

@Injectable()
export class PassThroughService {
Expand All @@ -14,19 +15,20 @@ export class PassThroughService {
@Inject(LOGGER) private readonly logger: LOGGER,
@Inject(COMMANDER_PROGRAM) private readonly program: Command,
private readonly versionManager: VersionManagerService,
private readonly configService: ConfigService,
private readonly generatorService: GeneratorService
) {
}

public async init() {

this.program
.allowUnknownOption()
.option("--custom-generator <generator>", "Custom generator jar")
.allowUnknownOption()
.option("--custom-generator <generator>", "Custom generator jar")

const commands = (await this.getCommands()).reduce((acc, [name, desc]) => {
return acc.set(name, this.program
.command(name, { hidden: !desc })
.command(name, {hidden: !desc})
.description(desc)
.allowUnknownOption()
.action((_, c) => this.passThrough(c)))
Expand Down Expand Up @@ -93,7 +95,7 @@ export class PassThroughService {
.filter(line => startsWith(line, ' '))
.map<string>(trim)
.map(line => line.match(/^([a-z-]+)\s+(.+)/i).slice(1))
.reduce((acc, [cmd, desc]) => ({ ...acc, [cmd]: desc }), {});
.reduce((acc, [cmd, desc]) => ({...acc, [cmd]: desc}), {});

const allCommands = completion.split('\n')
.map<string>(trim)
Expand All @@ -114,6 +116,13 @@ export class PassThroughService {
});

private cmd() {
if (this.configService.useDocker) {
return [
`docker run --rm -v "${this.configService.cwd}:/local"`,
this.versionManager.getDockerImageName(),
].join(' ');
}

const customGenerator = this.program.opts()?.customGenerator;
const cliPath = this.versionManager.filePath();

Expand Down
Loading

0 comments on commit 01c4945

Please sign in to comment.