cdd-cli 3.1.7 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +1 -1
- package/.github/workflows/ci.yml +3 -0
- package/.github/workflows/docs.yml +8 -1
- package/CHANGELOG.md +40 -0
- package/CONTRIBUTING.md +28 -15
- package/README.md +56 -88
- package/babel.config.cjs +18 -1
- package/coverage/clover.xml +604 -0
- package/coverage/coverage-final.json +17 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/helpers/constants.js.html +235 -0
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +211 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +853 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +289 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +131 -0
- package/coverage/lcov-report/helpers/index.html +176 -0
- package/coverage/lcov-report/helpers/logger.js.html +424 -0
- package/coverage/lcov-report/helpers/safeCall.js.html +139 -0
- package/coverage/lcov-report/helpers/validationHelpers.js.html +394 -0
- package/coverage/lcov-report/hooks/creation/index.html +146 -0
- package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +292 -0
- package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +478 -0
- package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +238 -0
- package/coverage/lcov-report/hooks/debug/index.html +116 -0
- package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +172 -0
- package/coverage/lcov-report/hooks/index.html +161 -0
- package/coverage/lcov-report/hooks/navigation/index.html +116 -0
- package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +160 -0
- package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +415 -0
- package/coverage/lcov-report/hooks/useControls.js.html +685 -0
- package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +202 -0
- package/coverage/lcov-report/hooks/useExitHandler.js.html +283 -0
- package/coverage/lcov-report/index.html +191 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +1216 -0
- package/dist/components/ContainerRow.js +5 -78
- package/dist/components/LogViewer.js +4 -9
- package/dist/helpers/containerOptionsBuilder.js +58 -0
- package/dist/hooks/creation/useContainerCreation.js +16 -1
- package/dist/hooks/debug/useDebugLogs.js +46 -0
- package/dist/hooks/navigation/useContainerSelection.js +36 -0
- package/dist/hooks/useContainerCommandRouter.js +151 -0
- package/dist/{helpers/actionHelpers.js → hooks/useContainerStats.js} +76 -78
- package/dist/hooks/useControls.js +83 -342
- package/dist/hooks/useEraseConfirmation.js +47 -0
- package/dist/hooks/useExitHandler.js +68 -0
- package/jest.config.cjs +1 -1
- package/package.json +2 -3
- package/src/components/ContainerRow.jsx +3 -38
- package/src/components/LogViewer.jsx +4 -10
- package/src/helpers/constants.js +20 -0
- package/src/helpers/containerOptionsBuilder.js +42 -0
- package/src/helpers/dockerService/serviceComponents/containerActions.js +55 -8
- package/src/helpers/validationHelpers.js +75 -18
- package/src/hooks/creation/useContainerCreation.js +34 -4
- package/src/hooks/debug/useDebugLogs.js +29 -0
- package/src/hooks/navigation/useContainerSelection.js +25 -0
- package/src/hooks/useContainerCommandRouter.js +110 -0
- package/src/hooks/useContainerStats.js +44 -0
- package/src/hooks/useControls.js +75 -274
- package/src/hooks/useEraseConfirmation.js +39 -0
- package/src/hooks/useExitHandler.js +66 -0
- package/test/containerActions.test.js +104 -13
- package/test/containerOptionsBuilder.test.js +36 -0
- package/test/useContainerCreation.dom.test.js +46 -2
- package/test/useControls.dom.test.js +117 -0
- package/test/validationHelpers.test.js +51 -0
- package/.babelrc +0 -6
- package/FIXES_APPLIED.md +0 -419
- package/SECURITY.md +0 -5
- package/dist/hooks/useLogsStream.js +0 -41
- package/docs/_config.yml +0 -13
- package/docs/components_ContainerCreationPrompt.jsx.html +0 -114
- package/docs/components_ContainerRow.jsx.html +0 -151
- package/docs/components_ContainerSection.jsx.html +0 -75
- package/docs/components_Footer.jsx.html +0 -66
- package/docs/components_Header.jsx.html +0 -84
- package/docs/components_LogViewer.jsx.html +0 -91
- package/docs/components_MessageFeedback.jsx.html +0 -76
- package/docs/components_PromptField.jsx.html +0 -99
- package/docs/components_StatsBar.jsx.html +0 -105
- package/docs/components_UsageMenu.jsx.html +0 -73
- package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Bold-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Light-webfont.svg +0 -1831
- package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
- package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.svg +0 -1831
- package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/docs/global.html +0 -6145
- package/docs/helpers_actionHelpers.js.html +0 -94
- package/docs/helpers_constants.js.html +0 -81
- package/docs/helpers_dockerService_dockerService.js.html +0 -79
- package/docs/helpers_dockerService_serviceComponents_containerActions.js.html +0 -153
- package/docs/helpers_dockerService_serviceComponents_containerList.js.html +0 -81
- package/docs/helpers_dockerService_serviceComponents_containerLogs.js.html +0 -85
- package/docs/helpers_dockerService_serviceComponents_containerStats.js.html +0 -107
- package/docs/helpers_dockerService_serviceComponents_imageUtils.js.html +0 -82
- package/docs/helpers_exitWithMessage.js.html +0 -77
- package/docs/helpers_safeCall.js.html +0 -68
- package/docs/helpers_validationHelpers.js.html +0 -96
- package/docs/hooks_creation_useContainerActions.js.html +0 -120
- package/docs/hooks_creation_useContainerCreation.js.html +0 -152
- package/docs/hooks_creation_useLogsViewer.js.html +0 -102
- package/docs/hooks_useControls.js.html +0 -298
- package/docs/hooks_useLogsStream.js.html +0 -82
- package/docs/index.html +0 -65
- package/docs/index.js.html +0 -65
- package/docs/module-index.html +0 -171
- package/docs/scripts/linenumber.js +0 -25
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
- package/docs/scripts/prettify/lang-css.js +0 -2
- package/docs/scripts/prettify/prettify.js +0 -28
- package/docs/styles/jsdoc-default.css +0 -358
- package/docs/styles/prettify-jsdoc.css +0 -111
- package/docs/styles/prettify-tomorrow.css +0 -132
- package/esbuild.config.cjs +0 -11
- package/src/helpers/actionHelpers.js +0 -52
- package/src/hooks/useLogsStream.js +0 -31
- package/test/actionHelpers.test.js +0 -106
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
let buildContainerOptions;
|
|
2
|
+
|
|
3
|
+
beforeAll(async () => {
|
|
4
|
+
const mod = await import('../src/helpers/containerOptionsBuilder.js');
|
|
5
|
+
buildContainerOptions = mod.buildContainerOptions;
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
describe('buildContainerOptions', () => {
|
|
9
|
+
test('basic image only sets Tty', () => {
|
|
10
|
+
const opts = buildContainerOptions({ imageName: 'nginx' });
|
|
11
|
+
expect(opts.Tty).toBe(true);
|
|
12
|
+
expect(opts.Env).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('env var with value containing = is preserved intact', () => {
|
|
16
|
+
const opts = buildContainerOptions({ imageName: 'nginx', envInput: 'TOKEN=a=b' });
|
|
17
|
+
expect(opts.Env).toEqual(['TOKEN=a=b']);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('multiple env vars with = in values are all preserved', () => {
|
|
21
|
+
const opts = buildContainerOptions({ imageName: 'nginx', envInput: 'TOKEN=a=b,DSN=user:pass@host/db?opt=1' });
|
|
22
|
+
expect(opts.Env).toContain('TOKEN=a=b');
|
|
23
|
+
expect(opts.Env).toContain('DSN=user:pass@host/db?opt=1');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('port input builds ExposedPorts and PortBindings', () => {
|
|
27
|
+
const opts = buildContainerOptions({ imageName: 'nginx', portInput: '8080:80' });
|
|
28
|
+
expect(opts.ExposedPorts).toEqual({ '80/tcp': {} });
|
|
29
|
+
expect(opts.HostConfig.PortBindings['80/tcp']).toEqual([{ HostPort: '8080' }]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('container name is included when provided', () => {
|
|
33
|
+
const opts = buildContainerOptions({ imageName: 'nginx', containerName: 'my-nginx' });
|
|
34
|
+
expect(opts.name).toBe('my-nginx');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
1
4
|
import React, { useEffect } from 'react';
|
|
2
5
|
import { render, act } from '@testing-library/react';
|
|
3
6
|
import { useContainerCreation } from '../src/hooks/creation/useContainerCreation.js';
|
|
7
|
+
import { IMAGE_PROFILES } from '../src/helpers/constants.js';
|
|
4
8
|
|
|
5
|
-
function HookTester({ onCreate, onCancel, dbImages, expose }) {
|
|
6
|
-
const hook = useContainerCreation({ onCreate, onCancel, dbImages });
|
|
9
|
+
function HookTester({ onCreate, onCancel, dbImages, imageProfiles, expose }) {
|
|
10
|
+
const hook = useContainerCreation({ onCreate, onCancel, dbImages, imageProfiles });
|
|
7
11
|
// keep exposing the latest hook values on every render
|
|
8
12
|
useEffect(() => {
|
|
9
13
|
if (expose) expose.current = hook;
|
|
@@ -94,4 +98,44 @@ describe('useContainerCreation (DOM render)', () => {
|
|
|
94
98
|
expect(created.length).toBe(1);
|
|
95
99
|
expect(created[0]).toEqual({ imageName: 'redis', containerName: '', portInput: '', envInput: 'FOO=bar' });
|
|
96
100
|
});
|
|
101
|
+
|
|
102
|
+
test('step 3 with mysql image: missing MYSQL_ROOT_PASSWORD blocks creation', () => {
|
|
103
|
+
const created = [];
|
|
104
|
+
const expose = { current: null };
|
|
105
|
+
|
|
106
|
+
render(<HookTester onCreate={(d) => created.push(d)} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
|
|
107
|
+
|
|
108
|
+
// advance to step 3 (env) with mysql image
|
|
109
|
+
act(() => { expose.current.setImageName('mysql:8'); });
|
|
110
|
+
act(() => { expose.current.nextStep(); }); // step 0 → 1
|
|
111
|
+
act(() => { expose.current.nextStep(); }); // step 1 → 2
|
|
112
|
+
act(() => { expose.current.nextStep(); }); // step 2 → 3
|
|
113
|
+
|
|
114
|
+
expect(expose.current.step).toBe(3);
|
|
115
|
+
|
|
116
|
+
// try to advance without required env var
|
|
117
|
+
act(() => { expose.current.nextStep(); });
|
|
118
|
+
|
|
119
|
+
expect(expose.current.step).toBe(3); // still blocked
|
|
120
|
+
expect(expose.current.message).toMatch(/MYSQL_ROOT_PASSWORD/i);
|
|
121
|
+
expect(created.length).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('step 3 with mysql image: valid env advances and calls onCreate', () => {
|
|
125
|
+
const created = [];
|
|
126
|
+
const expose = { current: null };
|
|
127
|
+
|
|
128
|
+
render(<HookTester onCreate={(d) => created.push(d)} onCancel={() => {}} dbImages={[]} imageProfiles={IMAGE_PROFILES} expose={expose} />);
|
|
129
|
+
|
|
130
|
+
act(() => { expose.current.setImageName('mysql:8'); });
|
|
131
|
+
act(() => { expose.current.nextStep(); });
|
|
132
|
+
act(() => { expose.current.nextStep(); });
|
|
133
|
+
act(() => { expose.current.nextStep(); }); // at step 3
|
|
134
|
+
|
|
135
|
+
act(() => { expose.current.setEnvInput('MYSQL_ROOT_PASSWORD=secret'); });
|
|
136
|
+
act(() => { expose.current.nextStep(); }); // should call onCreate
|
|
137
|
+
|
|
138
|
+
expect(created.length).toBe(1);
|
|
139
|
+
expect(created[0].imageName).toBe('mysql:8');
|
|
140
|
+
});
|
|
97
141
|
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import React, { useEffect } from 'react';
|
|
5
|
+
import { render, act } from '@testing-library/react';
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
7
|
+
|
|
8
|
+
// Mock ink so that useInput and useApp are no-ops (require real TTY in production)
|
|
9
|
+
await jest.unstable_mockModule('ink', () => ({
|
|
10
|
+
useInput: () => {},
|
|
11
|
+
useApp: () => ({ exit: () => {} }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Mock containerActions — expose all named exports so dependents don't break
|
|
15
|
+
const mockSvcCreateContainer = jest.fn();
|
|
16
|
+
await jest.unstable_mockModule(
|
|
17
|
+
'../src/helpers/dockerService/serviceComponents/containerActions.js',
|
|
18
|
+
() => ({
|
|
19
|
+
createContainer: mockSvcCreateContainer,
|
|
20
|
+
removeContainer: jest.fn().mockResolvedValue(undefined),
|
|
21
|
+
startContainer: jest.fn().mockResolvedValue(undefined),
|
|
22
|
+
stopContainer: jest.fn().mockResolvedValue(undefined),
|
|
23
|
+
restartContainer: jest.fn().mockResolvedValue(undefined),
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Mock getLogsStream to avoid side effects
|
|
28
|
+
await jest.unstable_mockModule(
|
|
29
|
+
'../src/helpers/dockerService/serviceComponents/containerLogs.js',
|
|
30
|
+
() => ({ getLogsStream: () => {} })
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const { useControls } = await import('../src/hooks/useControls.js');
|
|
34
|
+
|
|
35
|
+
function HookTester({ containers, expose }) {
|
|
36
|
+
const hook = useControls(containers);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (expose) expose.current = hook;
|
|
39
|
+
});
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Helper: advance creation wizard to the final step and trigger onCreate
|
|
44
|
+
async function completeCreationWizard(expose, imageName = 'nginx') {
|
|
45
|
+
act(() => { expose.current.creation.setImageName(imageName); });
|
|
46
|
+
act(() => { expose.current.creation.nextStep(); }); // step 0 → 1
|
|
47
|
+
act(() => { expose.current.creation.nextStep(); }); // step 1 → 2
|
|
48
|
+
act(() => { expose.current.creation.nextStep(); }); // step 2 → 3
|
|
49
|
+
// step 3: call nextStep to trigger onCreate (async)
|
|
50
|
+
await act(async () => { expose.current.creation.nextStep(); });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('useControls (FR6 — port mapping in success message)', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
mockSvcCreateContainer.mockReset();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('success message includes port mapping after container creation', async () => {
|
|
59
|
+
const ports = [
|
|
60
|
+
{ containerPort: '3306', hostPort: '3306', protocol: 'tcp', source: 'auto' },
|
|
61
|
+
];
|
|
62
|
+
mockSvcCreateContainer.mockResolvedValue({ id: 'cid-abc', ports });
|
|
63
|
+
|
|
64
|
+
const expose = { current: null };
|
|
65
|
+
render(<HookTester containers={[]} expose={expose} />);
|
|
66
|
+
|
|
67
|
+
await completeCreationWizard(expose, 'nginx');
|
|
68
|
+
|
|
69
|
+
expect(expose.current.actions.message).toBe(
|
|
70
|
+
'Created container cid-abc | Ports: 3306→3306/tcp'
|
|
71
|
+
);
|
|
72
|
+
expect(expose.current.actions.messageColor).toBe('green');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('success message has no port section when ports array is empty', async () => {
|
|
76
|
+
mockSvcCreateContainer.mockResolvedValue({ id: 'cid-xyz', ports: [] });
|
|
77
|
+
|
|
78
|
+
const expose = { current: null };
|
|
79
|
+
render(<HookTester containers={[]} expose={expose} />);
|
|
80
|
+
|
|
81
|
+
await completeCreationWizard(expose, 'alpine');
|
|
82
|
+
|
|
83
|
+
expect(expose.current.actions.message).toBe('Created container cid-xyz');
|
|
84
|
+
expect(expose.current.actions.messageColor).toBe('green');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('success message includes multiple port mappings', async () => {
|
|
88
|
+
const ports = [
|
|
89
|
+
{ containerPort: '80', hostPort: '8080', protocol: 'tcp', source: 'user' },
|
|
90
|
+
{ containerPort: '443', hostPort: '8443', protocol: 'tcp', source: 'user' },
|
|
91
|
+
];
|
|
92
|
+
mockSvcCreateContainer.mockResolvedValue({ id: 'cid-multi', ports });
|
|
93
|
+
|
|
94
|
+
const expose = { current: null };
|
|
95
|
+
render(<HookTester containers={[]} expose={expose} />);
|
|
96
|
+
|
|
97
|
+
await completeCreationWizard(expose, 'nginx');
|
|
98
|
+
|
|
99
|
+
expect(expose.current.actions.message).toBe(
|
|
100
|
+
'Created container cid-multi | Ports: 8080→80/tcp, 8443→443/tcp'
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('error message is shown when svcCreateContainer rejects', async () => {
|
|
105
|
+
mockSvcCreateContainer.mockRejectedValue(new Error('pull access denied'));
|
|
106
|
+
|
|
107
|
+
const expose = { current: null };
|
|
108
|
+
render(<HookTester containers={[]} expose={expose} />);
|
|
109
|
+
|
|
110
|
+
await completeCreationWizard(expose, 'badimage');
|
|
111
|
+
|
|
112
|
+
expect(expose.current.actions.message).toBe(
|
|
113
|
+
'Error creating container: pull access denied'
|
|
114
|
+
);
|
|
115
|
+
expect(expose.current.actions.messageColor).toBe('red');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
let validatePorts;
|
|
2
2
|
let validateEnvVars;
|
|
3
|
+
let IMAGE_PROFILES;
|
|
3
4
|
|
|
4
5
|
beforeAll(async () => {
|
|
5
6
|
const mod = await import('../src/helpers/validationHelpers.js');
|
|
6
7
|
validatePorts = mod.validatePorts;
|
|
7
8
|
validateEnvVars = mod.validateEnvVars;
|
|
9
|
+
const constants = await import('../src/helpers/constants.js');
|
|
10
|
+
IMAGE_PROFILES = constants.IMAGE_PROFILES;
|
|
8
11
|
});
|
|
9
12
|
|
|
10
13
|
describe('validatePorts', () => {
|
|
@@ -58,3 +61,51 @@ describe('validateEnvVars', () => {
|
|
|
58
61
|
expect(validateEnvVars('MY-VAR=value')).toBe(false);
|
|
59
62
|
});
|
|
60
63
|
});
|
|
64
|
+
|
|
65
|
+
describe('validateEnvVars — contextual validation with IMAGE_PROFILES', () => {
|
|
66
|
+
test('mysql:8 — missing MYSQL_ROOT_PASSWORD is invalid', () => {
|
|
67
|
+
const result = validateEnvVars('FOO=bar', 'mysql:8', IMAGE_PROFILES);
|
|
68
|
+
expect(result.valid).toBe(false);
|
|
69
|
+
expect(result.errors.some(e => e.includes('MYSQL_ROOT_PASSWORD'))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('mysql:8 — with MYSQL_ROOT_PASSWORD is valid', () => {
|
|
73
|
+
const result = validateEnvVars('MYSQL_ROOT_PASSWORD=secret', 'mysql:8', IMAGE_PROFILES);
|
|
74
|
+
expect(result.valid).toBe(true);
|
|
75
|
+
expect(result.errors).toHaveLength(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('docker.io/library/postgres:16 — missing POSTGRES_PASSWORD is invalid', () => {
|
|
79
|
+
const result = validateEnvVars('', 'docker.io/library/postgres:16', IMAGE_PROFILES);
|
|
80
|
+
expect(result.valid).toBe(false);
|
|
81
|
+
expect(result.errors.some(e => e.includes('POSTGRES_PASSWORD'))).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('docker.io/library/postgres:16 — with POSTGRES_PASSWORD is valid', () => {
|
|
85
|
+
const result = validateEnvVars('POSTGRES_PASSWORD=pass', 'docker.io/library/postgres:16', IMAGE_PROFILES);
|
|
86
|
+
expect(result.valid).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('TOKEN=a=b is preserved (split on first = only)', () => {
|
|
90
|
+
const result = validateEnvVars('TOKEN=a=b', 'nginx', IMAGE_PROFILES);
|
|
91
|
+
expect(result.valid).toBe(true);
|
|
92
|
+
expect(result.parsedEnv['TOKEN']).toBe('a=b');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('image not in profiles — only syntactic validation applies', () => {
|
|
96
|
+
const result = validateEnvVars('FOO=bar', 'nginx', IMAGE_PROFILES);
|
|
97
|
+
expect(result.valid).toBe(true);
|
|
98
|
+
expect(result.parsedEnv['FOO']).toBe('bar');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('empty input for image not in profiles — valid', () => {
|
|
102
|
+
const result = validateEnvVars('', 'nginx', IMAGE_PROFILES);
|
|
103
|
+
expect(result.valid).toBe(true);
|
|
104
|
+
expect(result.parsedEnv).toEqual({});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('syntactically invalid var is still rejected', () => {
|
|
108
|
+
const result = validateEnvVars('INVALID-VAR=x', 'nginx', IMAGE_PROFILES);
|
|
109
|
+
expect(result.valid).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
package/.babelrc
DELETED
package/FIXES_APPLIED.md
DELETED
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
# Applied Fixes - CDD CLI
|
|
2
|
-
|
|
3
|
-
**Date:** 2025-10-17
|
|
4
|
-
**Project:** CDD-CLI (CLI Docker Dashboard)
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Fixes Summary
|
|
9
|
-
|
|
10
|
-
A total of 11 critical fixes were applied to address security, stability, and portability issues identified during the code audit.
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## 1. APPLIED CRITICAL FIXES
|
|
15
|
-
|
|
16
|
-
### ✅ 1.1. Import Order Fix
|
|
17
|
-
|
|
18
|
-
**File:** `src/helpers/dockerService/serviceComponents/containerActions.js`
|
|
19
|
-
|
|
20
|
-
**Issue:** Imports were declared after exports.
|
|
21
|
-
|
|
22
|
-
**Applied fix:**
|
|
23
|
-
```javascript
|
|
24
|
-
// BEFORE (incorrect):
|
|
25
|
-
export async function removeContainer(containerId) { ... }
|
|
26
|
-
import { docker } from "../dockerService";
|
|
27
|
-
|
|
28
|
-
// AFTER (correct):
|
|
29
|
-
import { docker } from "../dockerService";
|
|
30
|
-
import { imageExists, pullImage } from "./imageUtils.js";
|
|
31
|
-
export async function removeContainer(containerId) { ... }
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**Impact:** Removes potential reference errors and follows ES6 standards.
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
### ✅ 1.2. Optional Ports in Container Creation
|
|
39
|
-
|
|
40
|
-
**File:** `src/hooks/creation/useContainerCreation.js`
|
|
41
|
-
|
|
42
|
-
**Issue:** Users were forced to specify ports, but many containers don’t need them.
|
|
43
|
-
|
|
44
|
-
**Applied fix:**
|
|
45
|
-
```javascript
|
|
46
|
-
// BEFORE:
|
|
47
|
-
if (!portInput.trim()) {
|
|
48
|
-
setMessage("You must specify at least one port to expose");
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// AFTER:
|
|
53
|
-
// Ports are optional — only validate if provided
|
|
54
|
-
if (portInput.trim() && !validatePorts(portInput)) {
|
|
55
|
-
setMessage("Port format must be host:container...");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
**Impact:** Allows creating containers without exposed ports (workers, internal services, etc.).
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
### ✅ 1.3. Error Handling in getLogsStream
|
|
65
|
-
|
|
66
|
-
**File:** `src/helpers/dockerService/serviceComponents/containerLogs.js`
|
|
67
|
-
|
|
68
|
-
**Issue:** No try-catch to handle synchronous exceptions.
|
|
69
|
-
|
|
70
|
-
**Applied fix:**
|
|
71
|
-
```javascript
|
|
72
|
-
export function getLogsStream(containerId, onData, onEnd, onError) {
|
|
73
|
-
try {
|
|
74
|
-
const container = docker.getContainer(containerId);
|
|
75
|
-
// ... rest of the code
|
|
76
|
-
} catch (err) {
|
|
77
|
-
onError?.(err);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
**Impact:** Prevents crashes when the container doesn’t exist or connection errors occur.
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
### ✅ 1.4. Cross-Platform Docker Socket Configuration
|
|
87
|
-
|
|
88
|
-
**File:** `src/helpers/dockerService/dockerService.js`
|
|
89
|
-
|
|
90
|
-
**Issue:** Hardcoded path `/var/run/docker.sock` only works on Linux/macOS.
|
|
91
|
-
|
|
92
|
-
**Applied fix:**
|
|
93
|
-
```javascript
|
|
94
|
-
// BEFORE:
|
|
95
|
-
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
|
96
|
-
|
|
97
|
-
// AFTER:
|
|
98
|
-
// Use default configuration that automatically handles:
|
|
99
|
-
// - /var/run/docker.sock on Linux/macOS
|
|
100
|
-
// - //./pipe/docker_engine on Windows
|
|
101
|
-
const docker = new Docker();
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
**Impact:** Works on Windows, Linux, and macOS without manual changes.
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
### ✅ 1.5. Race Condition Prevention in Stats
|
|
109
|
-
|
|
110
|
-
**File:** `src/components/ContainerRow.jsx`
|
|
111
|
-
|
|
112
|
-
**Issue:** State updates on unmounted components caused memory leaks.
|
|
113
|
-
|
|
114
|
-
**Applied fix:**
|
|
115
|
-
```javascript
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
if (state !== "running") return;
|
|
118
|
-
|
|
119
|
-
let isMounted = true; // ← Mount flag
|
|
120
|
-
|
|
121
|
-
const fetchStats = async () => {
|
|
122
|
-
try {
|
|
123
|
-
const s = await getStats(id);
|
|
124
|
-
if (isMounted) { // ← Only update if mounted
|
|
125
|
-
setStats(s);
|
|
126
|
-
}
|
|
127
|
-
} catch (err) {
|
|
128
|
-
if (isMounted) {
|
|
129
|
-
setStatsError("Error fetching stats");
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
fetchStats();
|
|
135
|
-
const timer = setInterval(fetchStats, 1500);
|
|
136
|
-
|
|
137
|
-
return () => {
|
|
138
|
-
isMounted = false; // ← Cleanup
|
|
139
|
-
clearInterval(timer);
|
|
140
|
-
};
|
|
141
|
-
}, [id, state]);
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
**Impact:** Eliminates React warnings and prevents memory leaks.
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
### ✅ 1.6. Environment Variables Validation
|
|
149
|
-
|
|
150
|
-
**File:** `src/helpers/validationHelpers.js`
|
|
151
|
-
|
|
152
|
-
**Issue:** `validateEnvVars` always returned `true`.
|
|
153
|
-
|
|
154
|
-
**Applied fix:**
|
|
155
|
-
```javascript
|
|
156
|
-
export function validateEnvVars(envInput) {
|
|
157
|
-
if (!envInput || !envInput.trim()) return true; // Empty is valid
|
|
158
|
-
|
|
159
|
-
const vars = envInput.split(",").map(v => v.trim()).filter(Boolean);
|
|
160
|
-
const invalid = vars.find(v => {
|
|
161
|
-
const parts = v.split("=");
|
|
162
|
-
if (parts.length < 2) return true; // Must be VAR=value
|
|
163
|
-
const varName = parts[0].trim();
|
|
164
|
-
// Names must be alphanumeric with underscores
|
|
165
|
-
if (!/^[A-Z_][A-Z0-9_]*$/i.test(varName)) return true;
|
|
166
|
-
return false;
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
return !invalid;
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
**Impact:** Detects malformed variables before sending them to Docker.
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
### ✅ 1.7. CPU Stats Calculation Fix
|
|
178
|
-
|
|
179
|
-
**File:** `src/helpers/dockerService/serviceComponents/containerStats.js`
|
|
180
|
-
|
|
181
|
-
**Issue:** Not normalized by number of CPUs, producing wrong values on multi-core hosts.
|
|
182
|
-
|
|
183
|
-
**Applied fix:**
|
|
184
|
-
```javascript
|
|
185
|
-
// Determine number of CPUs
|
|
186
|
-
const numCpus = stream.cpu_stats.online_cpus ||
|
|
187
|
-
stream.cpu_stats.cpu_usage.percpu_usage?.length || 1;
|
|
188
|
-
|
|
189
|
-
// Compute normalized percentage
|
|
190
|
-
const cpuPercent = systemDelta > 0
|
|
191
|
-
? ((cpuDelta / systemDelta) * numCpus * 100)
|
|
192
|
-
: 0;
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
**Impact:** Correct CPU stats on multi-core systems.
|
|
196
|
-
|
|
197
|
-
---
|
|
198
|
-
|
|
199
|
-
### ✅ 1.8. Timeouts in Docker Operations
|
|
200
|
-
|
|
201
|
-
**File:** `src/helpers/dockerService/serviceComponents/containerActions.js`
|
|
202
|
-
|
|
203
|
-
**Issue:** Operations without timeouts could freeze the UI indefinitely.
|
|
204
|
-
|
|
205
|
-
**Applied fix:**
|
|
206
|
-
```javascript
|
|
207
|
-
function withTimeout(promise, ms = 30000) {
|
|
208
|
-
return Promise.race([
|
|
209
|
-
promise,
|
|
210
|
-
new Promise((_, reject) =>
|
|
211
|
-
setTimeout(() => reject(new Error('Operation timed out')), ms)
|
|
212
|
-
)
|
|
213
|
-
]);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export async function startContainer(containerId) {
|
|
217
|
-
const container = docker.getContainer(containerId);
|
|
218
|
-
await withTimeout(container.start(), 30000);
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
**Impact:** Prevents frozen UI during long or failing operations.
|
|
223
|
-
|
|
224
|
-
---
|
|
225
|
-
|
|
226
|
-
### ✅ 1.9. In-Memory Log Limit
|
|
227
|
-
|
|
228
|
-
**File:** `src/hooks/useControls.js`
|
|
229
|
-
|
|
230
|
-
**Issue:** Logs accumulated indefinitely causing a memory leak.
|
|
231
|
-
|
|
232
|
-
**Applied fix:**
|
|
233
|
-
```javascript
|
|
234
|
-
getLogsStream(
|
|
235
|
-
containers[selected].id,
|
|
236
|
-
(data) => logsViewer.setLogs((prev) => {
|
|
237
|
-
const newLogs = [...prev, ...data.split("\n").filter(Boolean)];
|
|
238
|
-
// Limit to last 1000 lines
|
|
239
|
-
return newLogs.slice(-1000);
|
|
240
|
-
}),
|
|
241
|
-
// ...
|
|
242
|
-
);
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
**Impact:** Prevents memory leaks on long-running log streams.
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
|
-
### ✅ 1.10. Duplicate Message Fix
|
|
250
|
-
|
|
251
|
-
**File:** `src/helpers/actionHelpers.js`
|
|
252
|
-
|
|
253
|
-
**Issue:** Success message was identical to the start message.
|
|
254
|
-
|
|
255
|
-
**Applied fix:**
|
|
256
|
-
```javascript
|
|
257
|
-
// BEFORE:
|
|
258
|
-
setMessage(`${actionLabel} container...`); // start
|
|
259
|
-
await actionFn(c.id);
|
|
260
|
-
setMessage(`${actionLabel} container...`); // success (duplicate)
|
|
261
|
-
|
|
262
|
-
// AFTER:
|
|
263
|
-
setMessage(`${actionLabel} container...`); // start
|
|
264
|
-
await actionFn(c.id);
|
|
265
|
-
setMessage(`${actionLabel} container completed successfully`); // success
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
**Impact:** Clear feedback that the operation completed.
|
|
269
|
-
|
|
270
|
-
---
|
|
271
|
-
|
|
272
|
-
### ✅ 1.11. Container Names Validation
|
|
273
|
-
|
|
274
|
-
**File:** `src/helpers/dockerService/serviceComponents/containerList.js`
|
|
275
|
-
|
|
276
|
-
**Issue:** Didn’t validate when `Names` was empty.
|
|
277
|
-
|
|
278
|
-
**Applied fix:**
|
|
279
|
-
```javascript
|
|
280
|
-
// BEFORE:
|
|
281
|
-
name: container.Names[0].replace("/", ""),
|
|
282
|
-
|
|
283
|
-
// AFTER:
|
|
284
|
-
name: (container.Names && container.Names[0] || 'Unknown').replace("/", ""),
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
**Impact:** Prevents crashes when Docker returns unexpected data.
|
|
288
|
-
|
|
289
|
-
---
|
|
290
|
-
|
|
291
|
-
## 2. TEST IMPROVEMENTS
|
|
292
|
-
|
|
293
|
-
### ✅ Tests for validateEnvVars
|
|
294
|
-
|
|
295
|
-
**File:** `test/validationHelpers.test.js`
|
|
296
|
-
|
|
297
|
-
**New tests added:**
|
|
298
|
-
- Empty input is valid
|
|
299
|
-
- Valid single env var
|
|
300
|
-
- Valid multiple env vars
|
|
301
|
-
- Valid env var with underscores
|
|
302
|
-
- Invalid env var without equals sign
|
|
303
|
-
- Invalid env var with invalid name
|
|
304
|
-
- Invalid env var with special characters in name
|
|
305
|
-
|
|
306
|
-
**Result:**
|
|
307
|
-
```
|
|
308
|
-
Test Suites: 1 passed, 1 total
|
|
309
|
-
Tests: 12 passed, 12 total (previously: 5)
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
---
|
|
313
|
-
|
|
314
|
-
## 3. SECURITY ANALYSIS
|
|
315
|
-
|
|
316
|
-
### ✅ CodeQL Security Scan
|
|
317
|
-
|
|
318
|
-
**Result:** ✅ 0 vulnerabilities found
|
|
319
|
-
|
|
320
|
-
```
|
|
321
|
-
Analysis Result for 'javascript'. Found 0 alert(s):
|
|
322
|
-
- javascript: No alerts found.
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
---
|
|
326
|
-
|
|
327
|
-
## 4. BUILD VERIFICATION
|
|
328
|
-
|
|
329
|
-
### ✅ Successful Build
|
|
330
|
-
|
|
331
|
-
```bash
|
|
332
|
-
$ npm run build
|
|
333
|
-
Successfully compiled 28 files with Babel (815ms).
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
---
|
|
337
|
-
|
|
338
|
-
## 5. OVERALL IMPACT OF FIXES
|
|
339
|
-
|
|
340
|
-
### Security
|
|
341
|
-
- ✅ No security vulnerabilities detected
|
|
342
|
-
- ✅ Improved input validation
|
|
343
|
-
- ✅ Robust error handling
|
|
344
|
-
|
|
345
|
-
### Stability
|
|
346
|
-
- ✅ Memory leak prevention
|
|
347
|
-
- ✅ Race condition prevention
|
|
348
|
-
- ✅ Crash prevention due to unhandled errors
|
|
349
|
-
- ✅ Timeouts for potentially long operations
|
|
350
|
-
|
|
351
|
-
### Portability
|
|
352
|
-
- ✅ Windows compatibility
|
|
353
|
-
- ✅ Linux compatibility
|
|
354
|
-
- ✅ macOS compatibility
|
|
355
|
-
|
|
356
|
-
### User Experience
|
|
357
|
-
- ✅ Optional ports in container creation
|
|
358
|
-
- ✅ Clearer feedback messages
|
|
359
|
-
- ✅ Correct CPU statistics
|
|
360
|
-
- ✅ Better error handling with informative messages
|
|
361
|
-
|
|
362
|
-
### Code Quality
|
|
363
|
-
- ✅ ES6 compliant
|
|
364
|
-
- ✅ Better test coverage (5 → 12 tests)
|
|
365
|
-
- ✅ More maintainable code
|
|
366
|
-
|
|
367
|
-
---
|
|
368
|
-
|
|
369
|
-
## 6. MODIFIED FILES
|
|
370
|
-
|
|
371
|
-
1. `src/helpers/dockerService/serviceComponents/containerActions.js`
|
|
372
|
-
2. `src/hooks/creation/useContainerCreation.js`
|
|
373
|
-
3. `src/helpers/dockerService/serviceComponents/containerLogs.js`
|
|
374
|
-
4. `src/helpers/dockerService/dockerService.js`
|
|
375
|
-
5. `src/components/ContainerRow.jsx`
|
|
376
|
-
6. `src/helpers/validationHelpers.js`
|
|
377
|
-
7. `src/helpers/dockerService/serviceComponents/containerStats.js`
|
|
378
|
-
8. `src/hooks/useControls.js`
|
|
379
|
-
9. `src/helpers/actionHelpers.js`
|
|
380
|
-
10. `src/helpers/dockerService/serviceComponents/containerList.js`
|
|
381
|
-
11. `test/validationHelpers.test.js`
|
|
382
|
-
|
|
383
|
-
---
|
|
384
|
-
|
|
385
|
-
## 7. FUTURE RECOMMENDATIONS
|
|
386
|
-
|
|
387
|
-
While the critical issues have been addressed, the internal audit yielded additional recommendations for future improvements:
|
|
388
|
-
|
|
389
|
-
### Medium Priority
|
|
390
|
-
- Add more unit tests
|
|
391
|
-
- Implement PropTypes or migrate to TypeScript
|
|
392
|
-
- Extract magic numbers into constants
|
|
393
|
-
- Improve the logging system
|
|
394
|
-
|
|
395
|
-
### Low Priority
|
|
396
|
-
- Implement i18n (internationalization)
|
|
397
|
-
- Improve JSDoc documentation
|
|
398
|
-
- Consider websockets for real-time updates
|
|
399
|
-
- Implement retry logic for Docker reconnection
|
|
400
|
-
|
|
401
|
-
---
|
|
402
|
-
|
|
403
|
-
## 8. CONCLUSION
|
|
404
|
-
|
|
405
|
-
We applied 11 critical fixes that significantly improve:
|
|
406
|
-
|
|
407
|
-
- Security: 0 vulnerabilities
|
|
408
|
-
- Stability: Prevention of memory leaks and race conditions
|
|
409
|
-
- Portability: Works on Windows, Linux, and macOS
|
|
410
|
-
- Quality: +140% more tests (5 → 12)
|
|
411
|
-
|
|
412
|
-
All fixes have been tested and verified via:
|
|
413
|
-
- ✅ Unit tests (12/12 passing)
|
|
414
|
-
- ✅ Successful build
|
|
415
|
-
- ✅ CodeQL security analysis (0 alerts)
|
|
416
|
-
|
|
417
|
-
---
|
|
418
|
-
|
|
419
|
-
**End of fixes document**
|