Skip to content

Commit 01c4945

Browse files
authored
Merge pull request #630 from OpenAPITools/feat/docker-support
feat: add docker support
2 parents 77c2fe0 + 69016df commit 01c4945

File tree

7 files changed

+179
-76
lines changed

7 files changed

+179
-76
lines changed

apps/generator-cli/src/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,18 @@ If the `version` property param is set it is not necessary to configure the `que
200200
| openapi-generator-cli generate --generator-key v3.0 v2.0 | yes | yes |
201201
| openapi-generator-cli generate --generator-key foo | no | no |
202202

203+
## Use Docker instead of running java locally
204+
205+
```json
206+
{
207+
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
208+
"spaces": 2,
209+
"generator-cli": {
210+
"useDocker": true
211+
}
212+
}
213+
```
214+
203215
## Custom Generators
204216

205217
Custom generators can be used by passing the `--custom-generator=/my/custom-generator.jar` argument.

apps/generator-cli/src/app/services/config.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ export class ConfigService {
1010
public readonly cwd = process.env.PWD || process.env.INIT_CWD || process.cwd()
1111
public readonly configFile = path.resolve(this.cwd, 'openapitools.json')
1212

13+
public get useDocker() {
14+
return this.get('generator-cli.useDocker', false);
15+
}
16+
17+
public get dockerImageName() {
18+
return this.get('generator-cli.dockerImageName', 'openapitools/openapi-generator-cli');
19+
}
20+
1321
private readonly defaultConfig = {
1422
$schema: './node_modules/@openapitools/openapi-generator-cli/config.schema.json',
1523
spaces: 2,

apps/generator-cli/src/app/services/generator.service.ts

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import { Inject, Injectable } from '@nestjs/common';
2-
import { flatten, isString, kebabCase, sortBy, upperFirst } from 'lodash';
1+
import {Inject, Injectable} from '@nestjs/common';
2+
import {flatten, isString, kebabCase, sortBy, upperFirst} from 'lodash';
33

44
import * as concurrently from 'concurrently';
55
import * as path from 'path';
6+
import * as fs from 'fs-extra';
67
import * as glob from 'glob';
78
import * as chalk from 'chalk';
8-
import { VersionManagerService } from './version-manager.service';
9-
import { ConfigService } from './config.service';
10-
import { LOGGER } from '../constants';
9+
import {VersionManagerService} from './version-manager.service';
10+
import {ConfigService} from './config.service';
11+
import {LOGGER} from '../constants';
1112

1213
interface GeneratorConfig {
1314
glob: string
@@ -96,6 +97,7 @@ export class GeneratorService {
9697
}
9798

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

101103
const command = Object.entries({
@@ -114,7 +116,19 @@ export class GeneratorService {
114116
case 'boolean':
115117
return undefined
116118
default:
117-
return `"${v}"`
119+
120+
if (this.configService.useDocker) {
121+
if (key === 'output') {
122+
fs.ensureDirSync(v);
123+
}
124+
125+
if (fs.existsSync(v)) {
126+
dockerVolumes[`/local/${key}`] = path.resolve(cwd, v);
127+
return `"/local/${key}"`;
128+
}
129+
}
130+
131+
return `"${v}"`;
118132
}
119133
})()
120134

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

142-
return this.cmd(customGenerator, Object.entries(placeholders)
143-
.filter(([, replacement]) => !!replacement)
144-
.reduce((cmd, [search, replacement]) => {
145-
return cmd.split(`#{${search}}`).join(replacement)
146-
}, command))
156+
return this.cmd(
157+
customGenerator,
158+
Object.entries(placeholders)
159+
.filter(([, replacement]) => !!replacement)
160+
.reduce((cmd, [search, replacement]) => {
161+
return cmd.split(`#{${search}}`).join(replacement)
162+
}, command),
163+
dockerVolumes,
164+
)
147165
}
148166

149-
private cmd = (customGenerator: string | undefined, appendix: string) => {
167+
private cmd = (customGenerator: string | undefined, appendix: string, dockerVolumes = {}) => {
168+
169+
if (this.configService.useDocker) {
170+
const volumes = Object.entries(dockerVolumes).map(([k, v]) => `-v "${v}:${k}"`).join(' ');
171+
172+
return [
173+
`docker run --rm`,
174+
volumes,
175+
this.versionManager.getDockerImageName(),
176+
'generate',
177+
appendix
178+
].join(' ');
179+
}
180+
150181
const cliPath = this.versionManager.filePath();
151182
const subCmd = customGenerator
152183
? `-cp "${[cliPath, customGenerator].join(this.isWin() ? ';' : ':')}" org.openapitools.codegen.OpenAPIGenerator`

apps/generator-cli/src/app/services/pass-through.service.spec.ts

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { Test } from '@nestjs/testing'
1+
import {Test} from '@nestjs/testing'
22
import * as chalk from 'chalk'
3-
import { Command, createCommand } from 'commander'
4-
import { COMMANDER_PROGRAM, LOGGER } from '../constants'
5-
import { GeneratorService } from './generator.service'
6-
import { PassThroughService } from './pass-through.service'
7-
import { VersionManagerService } from './version-manager.service'
3+
import {Command, createCommand} from 'commander'
4+
import {COMMANDER_PROGRAM, LOGGER} from '../constants'
5+
import {GeneratorService} from './generator.service'
6+
import {PassThroughService} from './pass-through.service'
7+
import {VersionManagerService} from './version-manager.service'
8+
import {ConfigService} from "./config.service";
89

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

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

@@ -29,17 +31,20 @@ describe('PassThroughService', () => {
2931
const moduleRef = await Test.createTestingModule({
3032
providers: [
3133
PassThroughService,
32-
{ provide: VersionManagerService, useValue: { filePath, getSelectedVersion } },
33-
{ provide: GeneratorService, useValue: { generate, enabled: true } },
34-
{ provide: COMMANDER_PROGRAM, useValue: program },
35-
{ provide: LOGGER, useValue: { log } },
34+
{provide: VersionManagerService, useValue: {filePath, getSelectedVersion, getDockerImageName: (v) => `openapitools/openapi-generator-cli:v${v || getSelectedVersion()}`}},
35+
{provide: GeneratorService, useValue: {generate, enabled: true}},
36+
{provide: ConfigService, useValue: configServiceMock},
37+
{provide: COMMANDER_PROGRAM, useValue: program},
38+
{provide: LOGGER, useValue: {log}},
3639
],
3740
}).compile()
3841

3942
fixture = moduleRef.get(PassThroughService)
4043

41-
childProcess.spawn.mockReset().mockReturnValue({ on: jest.fn() })
42-
44+
childProcess.spawn.mockReset().mockReturnValue({on: jest.fn()})
45+
configServiceMock.get.mockClear()
46+
configServiceMock.get.mockReset()
47+
configServiceMock.useDocker = false;
4348
})
4449

4550
describe('API', () => {
@@ -147,8 +152,28 @@ describe('PassThroughService', () => {
147152
expect(cmd['_allowUnknownOption']).toBeTruthy()
148153
})
149154

155+
describe('useDocker is true', () => {
156+
157+
beforeEach(() => {
158+
configServiceMock.useDocker = true;
159+
});
160+
161+
it('delegates to docker', async () => {
162+
await program.parseAsync([name, ...argv], {from: 'user'})
163+
expect(childProcess.spawn).toHaveBeenNthCalledWith(
164+
1,
165+
'docker run --rm -v "/foo/bar:/local" openapitools/openapi-generator-cli:v4.2.1',
166+
[name, ...argv],
167+
{
168+
stdio: 'inherit',
169+
shell: true
170+
}
171+
)
172+
})
173+
})
174+
150175
it('can delegate', async () => {
151-
await program.parseAsync([name, ...argv], { from: 'user' })
176+
await program.parseAsync([name, ...argv], {from: 'user'})
152177
expect(childProcess.spawn).toHaveBeenNthCalledWith(
153178
1,
154179
'java -jar "/some/path/to/4.2.1.jar"',
@@ -162,7 +187,7 @@ describe('PassThroughService', () => {
162187

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

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

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

192217
if (name === 'generate') {
193218
it('can delegate with custom jar to generate command', async () => {
194-
await program.parseAsync([name, ...argv, '--generator-key=genKey', '--custom-generator=../some/custom.jar'], { from: 'user' })
195-
219+
await program.parseAsync([name, ...argv, '--generator-key=genKey', '--custom-generator=../some/custom.jar'], {from: 'user'})
220+
196221
expect(generate).toHaveBeenNthCalledWith(
197222
1,
198223
'../some/custom.jar',
@@ -201,29 +226,6 @@ describe('PassThroughService', () => {
201226
})
202227
}
203228

204-
// if (name === 'help') {
205-
// it('prints the help info and does not delegate, if args length = 0', async () => {
206-
// childProcess.spawn.mockReset()
207-
// cmd.args = []
208-
// const logSpy = jest.spyOn(console, 'log').mockImplementation(noop)
209-
// await program.parseAsync([name], { from: 'user' })
210-
// expect(childProcess.spawn).toBeCalledTimes(0)
211-
// expect(program.helpInformation).toBeCalledTimes(1)
212-
// // expect(logSpy).toHaveBeenCalledTimes(2)
213-
// expect(logSpy).toHaveBeenNthCalledWith(1, 'some help text')
214-
// expect(logSpy).toHaveBeenNthCalledWith(2, 'has custom generator')
215-
// })
216-
// }
217-
//
218-
// if (name === 'generate') {
219-
// it('generates by using the generator config', async () => {
220-
// childProcess.spawn.mockReset()
221-
// await program.parseAsync([name], { from: 'user' })
222-
// expect(childProcess.spawn).toBeCalledTimes(0)
223-
// expect(generate).toHaveBeenNthCalledWith(1)
224-
// })
225-
// }
226-
227229
})
228230

229231
describe('command behavior', () => {
@@ -239,13 +241,13 @@ describe('PassThroughService', () => {
239241
${'help generate'} | ${commandHelp('generate')} | ${'a'}
240242
${'help author'} | ${commandHelp('author')} | ${'b'}
241243
${'help hidden'} | ${undefined} | ${'c'}
242-
`('$cmd', ({ cmd, helpText, spawn }) => {
244+
`('$cmd', ({cmd, helpText, spawn}) => {
243245

244246
let spy: jest.SpyInstance;
245247

246248
beforeEach(async () => {
247249
spy = jest.spyOn(console, 'log').mockClear().mockImplementation();
248-
await program.parseAsync(cmd.split(' '), { from: 'user' })
250+
await program.parseAsync(cmd.split(' '), {from: 'user'})
249251
})
250252

251253
describe('help text', () => {

apps/generator-cli/src/app/services/pass-through.service.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Inject, Injectable } from '@nestjs/common'
1+
import {Inject, Injectable} from '@nestjs/common'
22
import * as chalk from 'chalk'
3-
import { exec, spawn } from 'child_process'
4-
import { Command } from 'commander'
5-
import { isString, startsWith, trim } from 'lodash'
6-
import { COMMANDER_PROGRAM, LOGGER } from '../constants'
7-
import { GeneratorService } from './generator.service'
8-
import { VersionManagerService } from './version-manager.service'
3+
import {exec, spawn} from 'child_process'
4+
import {Command} from 'commander'
5+
import {isString, startsWith, trim} from 'lodash'
6+
import {COMMANDER_PROGRAM, LOGGER} from '../constants'
7+
import {GeneratorService} from './generator.service'
8+
import {VersionManagerService} from './version-manager.service'
9+
import {ConfigService} from "./config.service";
910

1011
@Injectable()
1112
export class PassThroughService {
@@ -14,19 +15,20 @@ export class PassThroughService {
1415
@Inject(LOGGER) private readonly logger: LOGGER,
1516
@Inject(COMMANDER_PROGRAM) private readonly program: Command,
1617
private readonly versionManager: VersionManagerService,
18+
private readonly configService: ConfigService,
1719
private readonly generatorService: GeneratorService
1820
) {
1921
}
2022

2123
public async init() {
2224

2325
this.program
24-
.allowUnknownOption()
25-
.option("--custom-generator <generator>", "Custom generator jar")
26+
.allowUnknownOption()
27+
.option("--custom-generator <generator>", "Custom generator jar")
2628

2729
const commands = (await this.getCommands()).reduce((acc, [name, desc]) => {
2830
return acc.set(name, this.program
29-
.command(name, { hidden: !desc })
31+
.command(name, {hidden: !desc})
3032
.description(desc)
3133
.allowUnknownOption()
3234
.action((_, c) => this.passThrough(c)))
@@ -93,7 +95,7 @@ export class PassThroughService {
9395
.filter(line => startsWith(line, ' '))
9496
.map<string>(trim)
9597
.map(line => line.match(/^([a-z-]+)\s+(.+)/i).slice(1))
96-
.reduce((acc, [cmd, desc]) => ({ ...acc, [cmd]: desc }), {});
98+
.reduce((acc, [cmd, desc]) => ({...acc, [cmd]: desc}), {});
9799

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

116118
private cmd() {
119+
if (this.configService.useDocker) {
120+
return [
121+
`docker run --rm -v "${this.configService.cwd}:/local"`,
122+
this.versionManager.getDockerImageName(),
123+
].join(' ');
124+
}
125+
117126
const customGenerator = this.program.opts()?.customGenerator;
118127
const cliPath = this.versionManager.filePath();
119128

0 commit comments

Comments
 (0)