Skip to content

Commit 348567d

Browse files
committed
feat(nx-docker): add registries, version, and namespace options to Docker executor schema
BREAKING CHANGE: Change the api of the Docker executor schema The `registries`, `version`, and `namespace` options have been added to the Docker executor schema. This change is backwards incompatible and may require updates to existing configurations.
1 parent 14fbdc0 commit 348567d

File tree

5 files changed

+232
-294
lines changed

5 files changed

+232
-294
lines changed

packages/nx-docker/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"dependencies": {
2626
"@nx/devkit": "^20.0.0",
2727
"@ebizbase/nx-devkit": "1.0.0",
28-
"tslib": "^2.3.0"
28+
"tslib": "^2.3.0",
29+
"semver": "^7.6.3"
2930
},
3031
"devDependencies": {},
3132
"publishConfig": {
Lines changed: 112 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,252 +1,158 @@
1-
// executor.spec.ts
21
import { ExecutorContext, logger } from '@nx/devkit';
3-
import { existsSync } from 'fs';
4-
import { DockerExecutorSchema } from './schema';
5-
import executor from './executor';
6-
import { DockerUtils, ProjectUtils } from '@ebizbase/nx-devkit';
72
import { execFileSync } from 'child_process';
3+
import { existsSync, mkdirSync } from 'fs';
4+
import { DockerUtils } from '@ebizbase/nx-devkit';
5+
import semverParse from 'semver/functions/parse';
6+
import executor from './executor';
7+
import { DockerExecutorSchema } from './schema';
88

9-
jest.mock('@nx/devkit', () => ({
10-
logger: {
11-
error: jest.fn(),
12-
fatal: jest.fn(),
13-
warn: jest.fn(),
14-
info: jest.fn(),
15-
},
16-
}));
17-
18-
jest.mock('fs', () => ({
19-
existsSync: jest.fn(),
20-
}));
21-
22-
jest.mock('@ebizbase/nx-devkit');
23-
jest.mock('child_process', () => ({
24-
execFileSync: jest.fn(),
25-
}));
26-
27-
describe('executor', () => {
28-
const context: ExecutorContext = {
9+
jest.mock('@nx/devkit');
10+
jest.mock('fs');
11+
jest.mock('child_process');
12+
jest.mock('semver/functions/parse');
13+
jest.mock('@ebizbase/nx-devkit', () => {
14+
return {
15+
ProjectUtils: jest.requireActual('@ebizbase/nx-devkit').ProjectUtils,
16+
DockerUtils: jest.fn().mockImplementation(() => ({
17+
checkDockerInstalled: jest.fn(),
18+
checkBuildxInstalled: jest.fn(),
19+
})),
20+
};
21+
});
22+
23+
const mockDockerUtils = DockerUtils as jest.MockedClass<typeof DockerUtils>;
24+
describe('Docker Executor', () => {
25+
const mockContext: ExecutorContext = {
2926
isVerbose: false,
27+
root: '/workspace',
3028
projectName: 'test-project',
29+
cwd: '/workspace',
3130
projectsConfigurations: {
32-
version: 1,
31+
version: 2,
3332
projects: {
34-
'test-project': { root: '/path/to/test-project' },
33+
'test-project': {
34+
root: 'apps/test-project',
35+
sourceRoot: 'apps/test-project/src',
36+
projectType: 'application',
37+
metadata: {
38+
version: '1.0.0',
39+
},
40+
targets: {},
41+
},
3542
},
3643
},
37-
root: '/path/to/root',
3844
nxJsonConfiguration: {},
39-
cwd: '/path/to/root',
4045
projectGraph: {
4146
nodes: {},
42-
dependencies: {},
47+
dependencies: {}
4348
},
4449
};
45-
const options: DockerExecutorSchema = {
46-
file: undefined,
47-
context: undefined,
48-
tags: ['latest'],
49-
args: ['ARG1=value1'],
50-
outputs: ['image'],
51-
flatforms: [],
52-
};
53-
let dockerUtils: jest.Mocked<DockerUtils>;
54-
let projectUtils: jest.Mocked<ProjectUtils>;
5550

56-
beforeEach(() => {
57-
dockerUtils = new DockerUtils() as jest.Mocked<DockerUtils>;
58-
dockerUtils.checkDockerInstalled.mockReturnValue(true);
59-
dockerUtils.checkBuildxInstalled.mockReturnValue(true);
51+
let options: DockerExecutorSchema;
6052

61-
(DockerUtils as jest.Mock).mockImplementation(() => dockerUtils);
6253

63-
projectUtils = new ProjectUtils(context) as jest.Mocked<ProjectUtils>;
64-
projectUtils.getProjectRoot.mockReturnValue('/path/to/test-project');
65-
66-
(ProjectUtils as jest.Mock).mockImplementation(() => projectUtils);
54+
beforeEach(() => {
55+
jest.clearAllMocks();
56+
options = {
57+
version: '1.0.0',
58+
namespace: 'test-namespace',
59+
outputs: ['dist'],
60+
cacheFrom: ['type=local,src=/path/to/dir'],
61+
cacheTo: ['type=local,src=/path/to/dir'],
62+
addHost: ['host:ip'],
63+
allow: ['network:network'],
64+
annotation: ['key=value'],
65+
attest: ['type=local,src=/path/to/dir'],
66+
args: ['key=value'],
67+
labels: { key: 'value' },
68+
metadataFile: 'metadata.json',
69+
shmSize: '2gb',
70+
ulimit: ['nofile=1024:1024'],
71+
target: 'target',
72+
tags: ['latest', '{major}.{minor}'],
73+
registries: ['registry.example.com'],
74+
file: './Dockerfile',
75+
context: './',
76+
flatforms: ['linux/amd64', 'linux/arm64'],
77+
};
78+
(logger.info as jest.Mock).mockImplementation(() => { });
79+
(logger.fatal as jest.Mock).mockImplementation(() => { });
6780
(existsSync as jest.Mock).mockReturnValue(true);
68-
});
69-
70-
afterEach(() => {
71-
jest.resetAllMocks();
72-
});
81+
(semverParse as jest.Mock).mockImplementation(() => ({ major: 1, minor: 0, patch: 0 }));
82+
(mkdirSync as jest.Mock).mockImplementation(() => { });
7383

74-
it('should return success when Docker build is successful', async () => {
75-
// Arrange
7684

77-
// Act
78-
const result = await executor(options, context);
7985

80-
// Assert
81-
expect(result).toEqual({ success: true });
82-
expect(execFileSync).toHaveBeenCalled();
8386
});
8487

85-
it('should return failure if Docker is not installed', async () => {
86-
// Arrange
87-
dockerUtils.checkDockerInstalled.mockReturnValue(false);
88-
89-
// Act
90-
const result = await executor(options, context);
91-
92-
// Assert
93-
expect(result).toEqual({ success: false });
94-
expect(logger.error).toHaveBeenCalledWith(
95-
'Docker is not installed or docker daemon is not running'
96-
);
97-
});
98-
99-
it('should warn if buildx is not installed and fallback to docker build', async () => {
100-
// Arrange
101-
dockerUtils.checkBuildxInstalled.mockReturnValue(false);
102-
103-
// Act
104-
await executor(options, context);
105-
106-
// Assert
107-
expect(logger.warn).toHaveBeenCalledWith(
108-
'Buildx is not installed falling back to docker build. Docker buildx is not installed so performance may be degraded'
109-
);
110-
expect(execFileSync).toHaveBeenCalledWith('docker', expect.arrayContaining(['build']), {
111-
stdio: 'inherit',
112-
cwd: context.root,
113-
});
114-
});
115-
116-
it('should return failure if project name is missing', async () => {
117-
// Arrange
118-
(ProjectUtils as jest.Mock).mockImplementation(() => {
119-
throw new Error('No project name provided');
120-
});
121-
122-
// Act
123-
const result = await executor(options, context);
124-
125-
// Assert
126-
expect(result).toEqual({ success: false });
127-
expect(logger.fatal).toHaveBeenCalledWith('No project name provided', expect.any(Error));
88+
afterAll(() => {
89+
jest.resetAllMocks();
12890
});
12991

130-
it('should return failure if Dockerfile is missing', async () => {
131-
// Arrange
132-
(existsSync as jest.Mock).mockImplementation((path) => !path.includes('Dockerfile'));
92+
it('should validate options and run docker command successfully', async () => {
93+
mockDockerUtils.mockImplementation(() => ({
94+
checkDockerInstalled: jest.fn().mockReturnValue(true),
95+
checkBuildxInstalled: jest.fn().mockReturnValue(true),
96+
}));
13397

134-
// Act
135-
const result = await executor(options, context);
98+
const result = await executor(options, mockContext);
13699

137-
// Assert
138-
expect(result).toEqual({ success: false });
139-
expect(logger.error).toHaveBeenCalledWith(
140-
'Dockerfile not found at /path/to/test-project/Dockerfile'
100+
expect(execFileSync).toHaveBeenLastCalledWith(
101+
'docker',
102+
expect.arrayContaining(['buildx', 'build']),
103+
{ stdio: 'inherit', cwd: '/workspace' }
141104
);
105+
expect(result.success).toBe(true);
142106
});
143107

144-
it('should return failure if context path is missing', async () => {
145-
// Arrange
146-
(existsSync as jest.Mock).mockImplementation((path) => !path.includes('.'));
147-
148-
// Act
149-
const result = await executor(options, context);
150108

151-
// Assert
152-
expect(result).toEqual({ success: false });
153-
expect(logger.error).toHaveBeenCalledWith('Context path not found at .');
154-
});
109+
it('should failed when docker and buildx installed', async () => {
110+
mockDockerUtils.mockImplementation(() => ({
111+
checkDockerInstalled: jest.fn().mockReturnValue(false),
112+
checkBuildxInstalled: jest.fn().mockReturnValue(false),
113+
}));
155114

156-
it('should build Docker image with provided tags and build args', async () => {
157-
// Arrange
158-
const expectedCommand = 'docker';
159-
const expectedCommandArgs = [
160-
'buildx',
161-
'build',
162-
'--output=image',
163-
'--build-arg',
164-
'ARG1=value1',
165-
'-t',
166-
'latest',
167-
'-f',
168-
'/path/to/test-project/Dockerfile',
169-
'.',
170-
];
171-
172-
// Act
173-
await executor(options, context);
174-
175-
// Assert
176-
expect(execFileSync).toHaveBeenCalledWith(expectedCommand, expectedCommandArgs, {
177-
stdio: 'inherit',
178-
cwd: context.root,
179-
});
115+
const result = await executor(options, mockContext);
116+
expect(result.success).toBe(false);
117+
expect(logger.fatal).toHaveBeenCalledWith('Docker is not installed or daemon is not running');
180118
});
181119

182-
it('should log failure if Docker build fails', async () => {
183-
// Arrange
184-
(execFileSync as jest.Mock).mockImplementation(() => {
185-
throw new Error('Docker build failed');
186-
});
187120

188-
// Act
189-
const result = await executor(options, context);
121+
it('should failed when not yet installed buildx', async () => {
122+
mockDockerUtils.mockImplementation(() => ({
123+
checkDockerInstalled: jest.fn().mockReturnValue(true),
124+
checkBuildxInstalled: jest.fn().mockReturnValue(false),
125+
}));
190126

191-
// Assert
192-
expect(result).toEqual({ success: false });
193-
expect(logger.fatal).toHaveBeenCalledWith('Failed to build Docker image', expect.any(Error));
127+
const result = await executor(options, mockContext);
128+
expect(result.success).toBe(false);
129+
expect(logger.fatal).toHaveBeenCalledWith('Buildx is not installed');
194130
});
195131

196-
it('should build Docker image with tag arguments when tags are provided', async () => {
197-
options.tags = ['latest', 'v1.0.0'];
198132

199-
await executor(options, context);
200-
201-
const expectedTagArgs = ['-t', 'latest', '-t', 'v1.0.0'];
202-
expect(execFileSync).toHaveBeenCalledWith('docker', expect.arrayContaining(expectedTagArgs), {
203-
stdio: 'inherit',
204-
cwd: context.root,
205-
});
133+
it('should failed when project metadata and executor options not containt version', async () => {
134+
mockDockerUtils.mockImplementation(() => ({
135+
checkDockerInstalled: jest.fn().mockReturnValue(true),
136+
checkBuildxInstalled: jest.fn().mockReturnValue(true),
137+
}));
138+
mockContext.projectsConfigurations.projects['test-project'].metadata = {};
139+
options.version = undefined;
140+
const result = await executor(options, mockContext);
141+
expect(result.success).toBe(false);
142+
expect(logger.fatal).toHaveBeenCalledWith('No version provided. Specify in options or metadata of project.json');
206143
});
207144

208-
it('should build Docker image with build arguments when args are provided', async () => {
209-
options.args = ['ARG1=value1', 'ARG2=value2'];
210-
211-
await executor(options, context);
212-
213-
const expectedBuildArgs = ['--build-arg', 'ARG1=value1', '--build-arg', 'ARG2=value2'];
214-
expect(execFileSync).toHaveBeenCalledWith('docker', expect.arrayContaining(expectedBuildArgs), {
215-
stdio: 'inherit',
216-
cwd: context.root,
217-
});
218-
});
219-
220-
it('should not add build arguments if args are not provided', async () => {
221-
options.args = undefined;
222-
223-
await executor(options, context);
224-
225-
expect(execFileSync).toHaveBeenCalledWith(
226-
'docker',
227-
expect.not.arrayContaining(['--build-arg']),
228-
{ stdio: 'inherit', cwd: context.root }
229-
);
145+
it('should failed when project metadata and executor options not containt namespace', async () => {
146+
mockDockerUtils.mockImplementation(() => ({
147+
checkDockerInstalled: jest.fn().mockReturnValue(true),
148+
checkBuildxInstalled: jest.fn().mockReturnValue(true),
149+
}));
150+
options.namespace = undefined;
151+
const result = await executor(options, mockContext);
152+
expect(result.success).toBe(false);
153+
expect(logger.fatal).toHaveBeenCalledWith('Namespace is required');
230154
});
231155

232-
it('should build Docker image with labels arguments when labels are provided', async () => {
233-
options.labels = { 'com.example.label': 'label-value' };
234-
await executor(options, context);
235-
const expectedTagArgs = ['--label', 'com.example.label="label-value"'];
236-
expect(execFileSync).toHaveBeenCalledWith('docker', expect.arrayContaining(expectedTagArgs), {
237-
stdio: 'inherit',
238-
cwd: context.root,
239-
});
240-
});
241-
242-
it('should build Docker image without labels arguments when labels are not provided', async () => {
243-
options.labels = undefined;
244-
await executor(options, context);
245-
expect(execFileSync).toHaveBeenCalledWith('docker', expect.not.arrayContaining(['--label']), {
246-
stdio: 'inherit',
247-
cwd: context.root,
248-
});
249-
});
250156

251157

252158
});

0 commit comments

Comments
 (0)