cdd-cli 3.2.2 → 4.1.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/README.es.md +222 -0
- package/README.md +108 -26
- package/dist/App.js +7 -1
- package/dist/components/ContainerCreationPrompt.js +26 -10
- package/dist/components/ControlsHUD.js +106 -0
- package/dist/components/SuggestionPanel.js +16 -7
- package/dist/components/UsageMenu.js +1 -1
- package/dist/helpers/appState.js +26 -0
- package/dist/helpers/constants.js +60 -21
- package/dist/helpers/dockerHubService.js +111 -0
- package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
- package/dist/helpers/logger.js +1 -1
- package/dist/hooks/creation/useContainerActions.js +22 -5
- package/dist/hooks/creation/useContainerCreation.js +414 -107
- package/dist/hooks/useContainerCommandRouter.js +13 -3
- package/dist/hooks/useControls.js +28 -7
- package/dist/hooks/useShellMode.js +101 -0
- package/dist/index.js +10 -1
- package/package.json +4 -1
- package/.atl/skill-registry.md +0 -30
- package/.editorconfig +0 -12
- package/.eslintrc.json +0 -30
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +0 -14
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/docs.yml +0 -42
- package/.prettierignore +0 -4
- package/.prettierrc +0 -8
- package/CHANGELOG.md +0 -165
- package/CODE_OF_CONDUCT.md +0 -12
- package/CONTRIBUTING.md +0 -35
- package/SECURITY.md +0 -5
- package/__mocks__/ink.cjs +0 -17
- package/_config.yml +0 -13
- package/babel.config.cjs +0 -23
- package/coverage/clover.xml +0 -694
- package/coverage/coverage-final.json +0 -21
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +0 -376
- package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
- package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -244
- package/coverage/lcov-report/components/index.html +0 -146
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/helpers/constants.js.html +0 -298
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +0 -931
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +0 -307
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +0 -131
- package/coverage/lcov-report/helpers/imageNameUtils.js.html +0 -181
- package/coverage/lcov-report/helpers/index.html +0 -191
- package/coverage/lcov-report/helpers/logger.js.html +0 -424
- package/coverage/lcov-report/helpers/safeCall.js.html +0 -139
- package/coverage/lcov-report/helpers/validationHelpers.js.html +0 -346
- package/coverage/lcov-report/hooks/creation/index.html +0 -146
- package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +0 -313
- package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +0 -805
- package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +0 -238
- package/coverage/lcov-report/hooks/debug/index.html +0 -116
- package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +0 -187
- package/coverage/lcov-report/hooks/index.html +0 -161
- package/coverage/lcov-report/hooks/navigation/index.html +0 -116
- package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +0 -169
- package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +0 -454
- package/coverage/lcov-report/hooks/useControls.js.html +0 -826
- package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +0 -202
- package/coverage/lcov-report/hooks/useExitHandler.js.html +0 -292
- package/coverage/lcov-report/index.html +0 -206
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov.info +0 -1402
- package/fix-imports.cjs +0 -54
- package/jest.config.cjs +0 -14
- package/src/App.jsx +0 -96
- package/src/components/ContainerCreationPrompt.jsx +0 -97
- package/src/components/ContainerList.jsx +0 -32
- package/src/components/ContainerRow.jsx +0 -84
- package/src/components/ContainerSection.jsx +0 -24
- package/src/components/Footer.jsx +0 -15
- package/src/components/Header.jsx +0 -41
- package/src/components/LogViewer.jsx +0 -36
- package/src/components/MessageFeedback.jsx +0 -25
- package/src/components/PromptField.jsx +0 -48
- package/src/components/StatsBar.jsx +0 -70
- package/src/components/SuggestionPanel.jsx +0 -53
- package/src/components/UsageMenu.jsx +0 -52
- package/src/helpers/appInfo.js +0 -23
- package/src/helpers/constants.js +0 -71
- package/src/helpers/containerOptionsBuilder.js +0 -53
- package/src/helpers/dockerService/dockerService.js +0 -28
- package/src/helpers/dockerService/serviceComponents/containerActions.js +0 -282
- package/src/helpers/dockerService/serviceComponents/containerList.js +0 -74
- package/src/helpers/dockerService/serviceComponents/containerLogs.js +0 -60
- package/src/helpers/dockerService/serviceComponents/containerStats.js +0 -69
- package/src/helpers/dockerService/serviceComponents/imageUtils.js +0 -32
- package/src/helpers/exitWithMessage.js +0 -32
- package/src/helpers/imageNameUtils.js +0 -32
- package/src/helpers/logger.js +0 -113
- package/src/helpers/safeCall.js +0 -18
- package/src/helpers/validationHelpers.js +0 -87
- package/src/hooks/creation/useContainerActions.js +0 -76
- package/src/hooks/creation/useContainerCreation.js +0 -240
- package/src/hooks/creation/useLogsViewer.js +0 -51
- package/src/hooks/debug/useDebugLogs.js +0 -34
- package/src/hooks/navigation/useContainerSelection.js +0 -28
- package/src/hooks/useContainerCommandRouter.js +0 -123
- package/src/hooks/useContainerStats.js +0 -44
- package/src/hooks/useContainers.js +0 -26
- package/src/hooks/useControls.js +0 -247
- package/src/hooks/useEraseConfirmation.js +0 -39
- package/src/hooks/useExitHandler.js +0 -69
- package/src/index.js +0 -16
- package/test/ContainerCreationPrompt.dom.test.js +0 -68
- package/test/SuggestionPanel.dom.test.js +0 -53
- package/test/constants.test.js +0 -34
- package/test/containerActions.test.js +0 -229
- package/test/containerList.test.js +0 -79
- package/test/containerOptionsBuilder.test.js +0 -36
- package/test/jest.setup.js +0 -7
- package/test/safeCall.test.js +0 -25
- package/test/useContainerCreation.dom.test.js +0 -264
- package/test/useControls.dom.test.js +0 -313
- package/test/validationHelpers.test.js +0 -111
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import PropTypes from 'prop-types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Renders an autocomplete suggestion panel as a boxed list below the image input.
|
|
7
|
-
* Displays a window of `maxVisible` items starting at `visibleOffset`.
|
|
8
|
-
* The focused row (selectedIndex) is highlighted cyan with a › prefix.
|
|
9
|
-
*
|
|
10
|
-
* @param {Object} props
|
|
11
|
-
* @param {string[]} props.items - Full list of suggestion strings
|
|
12
|
-
* @param {number} props.selectedIndex - Currently focused index (-1 = none)
|
|
13
|
-
* @param {number} props.visibleOffset - First item index of the visible window
|
|
14
|
-
* @param {number} [props.maxVisible=6] - Maximum number of items visible at once
|
|
15
|
-
* @returns {JSX.Element|null}
|
|
16
|
-
*/
|
|
17
|
-
export function SuggestionPanel({
|
|
18
|
-
items,
|
|
19
|
-
selectedIndex,
|
|
20
|
-
visibleOffset,
|
|
21
|
-
maxVisible = 6,
|
|
22
|
-
}) {
|
|
23
|
-
if (!items || items.length === 0) return null;
|
|
24
|
-
|
|
25
|
-
const visible = items.slice(visibleOffset, visibleOffset + maxVisible);
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<Box
|
|
29
|
-
flexDirection="column"
|
|
30
|
-
borderStyle="single"
|
|
31
|
-
borderColor="cyan"
|
|
32
|
-
paddingX={1}
|
|
33
|
-
data-testid="suggestion-panel"
|
|
34
|
-
>
|
|
35
|
-
{visible.map((item, localIdx) => {
|
|
36
|
-
const globalIdx = visibleOffset + localIdx;
|
|
37
|
-
const isFocused = globalIdx === selectedIndex;
|
|
38
|
-
return (
|
|
39
|
-
<Text key={item} color={isFocused ? 'cyan' : undefined} dimColor={!isFocused}>
|
|
40
|
-
{isFocused ? `› ${item}` : ` ${item}`}
|
|
41
|
-
</Text>
|
|
42
|
-
);
|
|
43
|
-
})}
|
|
44
|
-
</Box>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
SuggestionPanel.propTypes = {
|
|
49
|
-
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
50
|
-
selectedIndex: PropTypes.number.isRequired,
|
|
51
|
-
visibleOffset: PropTypes.number.isRequired,
|
|
52
|
-
maxVisible: PropTypes.number,
|
|
53
|
-
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
4
|
-
const SHORTCUTS = [
|
|
5
|
-
'Use ↑/↓ for navigation',
|
|
6
|
-
'I to initiate selected container',
|
|
7
|
-
'P to stop selected container',
|
|
8
|
-
'R to restart selected container',
|
|
9
|
-
'C to create a container',
|
|
10
|
-
'L to view logs of selected container',
|
|
11
|
-
'D to toggle debug log panel',
|
|
12
|
-
'E to remove selected container',
|
|
13
|
-
'Q to quit',
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
const GRID_COLUMNS = 3;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Small usage menu that explains available keybindings to the user.
|
|
20
|
-
* Renders entries in a responsive grid to avoid an excessively long column.
|
|
21
|
-
*
|
|
22
|
-
* @returns {JSX.Element}
|
|
23
|
-
*/
|
|
24
|
-
export default function UsageMenu() {
|
|
25
|
-
const rowsPerColumn = Math.ceil(SHORTCUTS.length / GRID_COLUMNS);
|
|
26
|
-
const columns = Array.from({ length: GRID_COLUMNS }, (_, columnIndex) =>
|
|
27
|
-
SHORTCUTS.slice(
|
|
28
|
-
columnIndex * rowsPerColumn,
|
|
29
|
-
(columnIndex + 1) * rowsPerColumn
|
|
30
|
-
)
|
|
31
|
-
).filter((column) => column.length > 0);
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<Box flexDirection="column" marginTop={1}>
|
|
35
|
-
<Box flexDirection="row">
|
|
36
|
-
{columns.map((column, columnIndex) => (
|
|
37
|
-
<Box
|
|
38
|
-
key={`shortcut-column-${columnIndex}`}
|
|
39
|
-
flexDirection="column"
|
|
40
|
-
marginRight={columnIndex === columns.length - 1 ? 0 : 3}
|
|
41
|
-
>
|
|
42
|
-
{column.map((shortcut, rowIndex) => (
|
|
43
|
-
<Text key={`shortcut-${columnIndex}-${rowIndex}`}>
|
|
44
|
-
{shortcut}
|
|
45
|
-
</Text>
|
|
46
|
-
))}
|
|
47
|
-
</Box>
|
|
48
|
-
))}
|
|
49
|
-
</Box>
|
|
50
|
-
</Box>
|
|
51
|
-
);
|
|
52
|
-
}
|
package/src/helpers/appInfo.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
let cachedVersion;
|
|
6
|
-
|
|
7
|
-
export function getAppVersion() {
|
|
8
|
-
if (cachedVersion) {
|
|
9
|
-
return cachedVersion;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const moduleDir = path.dirname(__filename);
|
|
15
|
-
const packageJsonPath = path.resolve(moduleDir, '../../package.json');
|
|
16
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
17
|
-
cachedVersion = packageJson.version ?? 'unknown';
|
|
18
|
-
return cachedVersion;
|
|
19
|
-
} catch (error) {
|
|
20
|
-
cachedVersion = 'unknown';
|
|
21
|
-
return cachedVersion;
|
|
22
|
-
}
|
|
23
|
-
}
|
package/src/helpers/constants.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Time intervals used across the app (milliseconds).
|
|
3
|
-
* - CONTAINER_LIST: how often to refresh the list of containers
|
|
4
|
-
* - CONTAINER_STATS: how often to fetch stats for running containers
|
|
5
|
-
*/
|
|
6
|
-
export const REFRESH_INTERVALS = {
|
|
7
|
-
CONTAINER_LIST: 3000,
|
|
8
|
-
CONTAINER_STATS: 1500,
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Timeout values for UI messages before they disappear.
|
|
13
|
-
*/
|
|
14
|
-
export const MESSAGE_TIMEOUTS = {
|
|
15
|
-
SHORT: 2000,
|
|
16
|
-
DEFAULT: 3000,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Delay used before exiting the process when showing an exit message.
|
|
21
|
-
*/
|
|
22
|
-
export const EXIT_DELAY = 500;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Operational timeouts for Docker operations and image pulls.
|
|
26
|
-
*/
|
|
27
|
-
export const TIMEOUTS = {
|
|
28
|
-
CONTAINER_OP: 30000,
|
|
29
|
-
PULL_IMAGE: 300000,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* List of well-known database image names (without tags or registry prefixes).
|
|
34
|
-
*/
|
|
35
|
-
export const DB_IMAGES = [
|
|
36
|
-
'mysql',
|
|
37
|
-
'mariadb',
|
|
38
|
-
'postgres',
|
|
39
|
-
'mongo',
|
|
40
|
-
'mssql',
|
|
41
|
-
'redis',
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Per-image profiles with required env vars, default exposed port, and suggested env vars.
|
|
46
|
-
* Keys are normalized image names (no tag, no registry prefix).
|
|
47
|
-
*
|
|
48
|
-
* @type {Record<string, { requiredEnv: string[], defaultPort: string, suggestedEnv: string[] }>}
|
|
49
|
-
*/
|
|
50
|
-
export const IMAGE_PROFILES = {
|
|
51
|
-
nginx: { requiredEnv: [], defaultPort: '80', suggestedEnv: [] },
|
|
52
|
-
httpd: { requiredEnv: [], defaultPort: '80', suggestedEnv: [] },
|
|
53
|
-
caddy: { requiredEnv: [], defaultPort: '80', suggestedEnv: [] },
|
|
54
|
-
node: { requiredEnv: [], defaultPort: '3000', suggestedEnv: ['NODE_ENV=production'] },
|
|
55
|
-
python: { requiredEnv: [], defaultPort: '8000', suggestedEnv: ['PYTHONUNBUFFERED=1'] },
|
|
56
|
-
openjdk: { requiredEnv: [], defaultPort: '8080', suggestedEnv: ['JAVA_OPTS=-Xms256m -Xmx512m'] },
|
|
57
|
-
golang: { requiredEnv: [], defaultPort: '8080', suggestedEnv: ['GOMAXPROCS=2'] },
|
|
58
|
-
redis: { requiredEnv: [], defaultPort: '6379', suggestedEnv: ['REDIS_PASSWORD=change-me'] },
|
|
59
|
-
memcached: { requiredEnv: [], defaultPort: '11211', suggestedEnv: [] },
|
|
60
|
-
postgres: { requiredEnv: ['POSTGRES_PASSWORD'], defaultPort: '5432', suggestedEnv: ['POSTGRES_USER=postgres', 'POSTGRES_DB=app'] },
|
|
61
|
-
mysql: { requiredEnv: ['MYSQL_ROOT_PASSWORD'], defaultPort: '3306', suggestedEnv: ['MYSQL_DATABASE=app', 'MYSQL_USER=app', 'MYSQL_PASSWORD=app123'] },
|
|
62
|
-
mariadb: { requiredEnv: ['MARIADB_ROOT_PASSWORD'], defaultPort: '3306', suggestedEnv: ['MARIADB_DATABASE=app', 'MARIADB_USER=app', 'MARIADB_PASSWORD=app123'] },
|
|
63
|
-
mongo: { requiredEnv: [], defaultPort: '27017', suggestedEnv: ['MONGO_INITDB_ROOT_USERNAME=admin', 'MONGO_INITDB_ROOT_PASSWORD=secret'] },
|
|
64
|
-
mssql: { requiredEnv: ['ACCEPT_EULA', 'SA_PASSWORD'], defaultPort: '1433', suggestedEnv: ['ACCEPT_EULA=Y', 'MSSQL_PID=Developer'] },
|
|
65
|
-
rabbitmq: { requiredEnv: [], defaultPort: '5672', suggestedEnv: ['RABBITMQ_DEFAULT_USER=guest', 'RABBITMQ_DEFAULT_PASS=guest'] },
|
|
66
|
-
kafka: { requiredEnv: [], defaultPort: '9092', suggestedEnv: ['KAFKA_CFG_LISTENERS=PLAINTEXT://:9092', 'KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092'] },
|
|
67
|
-
zookeeper: { requiredEnv: [], defaultPort: '2181', suggestedEnv: ['ALLOW_ANONYMOUS_LOGIN=yes'] },
|
|
68
|
-
elasticsearch: { requiredEnv: [], defaultPort: '9200', suggestedEnv: ['discovery.type=single-node', 'xpack.security.enabled=false'] },
|
|
69
|
-
minio: { requiredEnv: ['MINIO_ROOT_USER', 'MINIO_ROOT_PASSWORD'], defaultPort: '9000', suggestedEnv: ['MINIO_CONSOLE_ADDRESS=:9001'] },
|
|
70
|
-
wordpress: { requiredEnv: ['WORDPRESS_DB_HOST', 'WORDPRESS_DB_USER', 'WORDPRESS_DB_PASSWORD', 'WORDPRESS_DB_NAME'], defaultPort: '8080', suggestedEnv: ['WORDPRESS_TABLE_PREFIX=wp_'] },
|
|
71
|
-
};
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds Docker container creation options from raw user inputs.
|
|
3
|
-
*
|
|
4
|
-
* This is a pure function — it has no side effects and calls no services.
|
|
5
|
-
* It mirrors exactly the option-building logic in useControls.js onCreate callback.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} params
|
|
8
|
-
* @param {string} params.imageName - Docker image name (e.g. "nginx:alpine")
|
|
9
|
-
* @param {string} [params.containerName] - Optional container name
|
|
10
|
-
* @param {string} [params.portInput] - Comma-separated host:container port pairs
|
|
11
|
-
* (e.g. "8080:80,3000:3000")
|
|
12
|
-
* @param {string} [params.envInput] - Comma-separated KEY=VALUE pairs
|
|
13
|
-
* (e.g. "NODE_ENV=production,PORT=3000")
|
|
14
|
-
* @returns {object} Docker container options ready to pass to createContainer
|
|
15
|
-
*/
|
|
16
|
-
export function buildContainerOptions({
|
|
17
|
-
imageName: _imageName,
|
|
18
|
-
containerName,
|
|
19
|
-
portInput,
|
|
20
|
-
envInput,
|
|
21
|
-
}) {
|
|
22
|
-
const env = (envInput || '')
|
|
23
|
-
.split(',')
|
|
24
|
-
.map((s) => s.trim())
|
|
25
|
-
.filter(Boolean);
|
|
26
|
-
const ports = (portInput || '')
|
|
27
|
-
.split(',')
|
|
28
|
-
.map((s) => s.trim())
|
|
29
|
-
.filter(Boolean);
|
|
30
|
-
|
|
31
|
-
const ExposedPorts = {};
|
|
32
|
-
const PortBindings = {};
|
|
33
|
-
|
|
34
|
-
ports.forEach((pair) => {
|
|
35
|
-
const [host, cont] = pair.split(':');
|
|
36
|
-
if (!host || !cont) return;
|
|
37
|
-
const key = `${cont}/tcp`;
|
|
38
|
-
ExposedPorts[key] = {};
|
|
39
|
-
PortBindings[key] = PortBindings[key] || [];
|
|
40
|
-
PortBindings[key].push({ HostPort: `${host}` });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const options = {
|
|
44
|
-
Tty: true,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
if (Object.keys(ExposedPorts).length) options.ExposedPorts = ExposedPorts;
|
|
48
|
-
if (Object.keys(PortBindings).length) options.HostConfig = { PortBindings };
|
|
49
|
-
if (env.length) options.Env = env;
|
|
50
|
-
if (containerName) options.name = containerName;
|
|
51
|
-
|
|
52
|
-
return options;
|
|
53
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import Docker from 'dockerode';
|
|
2
|
-
|
|
3
|
-
// Use default dockerode configuration which automatically handles:
|
|
4
|
-
// - /var/run/docker.sock on Linux/Mac
|
|
5
|
-
// - //./pipe/docker_engine on Windows
|
|
6
|
-
// - Environment variables DOCKER_HOST, DOCKER_CERT_PATH, etc.
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Typedef for the docker client used in this project.
|
|
10
|
-
* Use a generic object shape so JSDoc can render a meaningful type.
|
|
11
|
-
*
|
|
12
|
-
* @typedef {Object} DockerClient
|
|
13
|
-
* @property {Function} listContainers
|
|
14
|
-
* @property {Function} getContainer
|
|
15
|
-
* @property {Function} listImages
|
|
16
|
-
* @property {Function} createContainer
|
|
17
|
-
* @property {Function} pull
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Shared Docker client instance (dockerode). Consumers import { docker }
|
|
22
|
-
* and use it to list/create/start/stop containers and images.
|
|
23
|
-
*
|
|
24
|
-
* @type {DockerClient}
|
|
25
|
-
*/
|
|
26
|
-
const docker = new Docker();
|
|
27
|
-
|
|
28
|
-
export { docker };
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import { docker } from '../dockerService.js';
|
|
2
|
-
import { imageExists, pullImage } from './imageUtils.js';
|
|
3
|
-
import { TIMEOUTS, IMAGE_PROFILES } from '../../constants.js';
|
|
4
|
-
import { logger } from '../../logger.js';
|
|
5
|
-
import { normalizeImageName } from '../../imageNameUtils.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Helper to add timeout to promises. If the provided promise does not settle
|
|
9
|
-
* within `ms` milliseconds it will reject with an Error.
|
|
10
|
-
*
|
|
11
|
-
* @template T
|
|
12
|
-
* @param {Promise<T>} promise - Promise to wrap
|
|
13
|
-
* @param {number} [ms=TIMEOUTS.CONTAINER_OP] - Timeout in milliseconds
|
|
14
|
-
* @returns {Promise<T>} The original promise result or a rejection on timeout
|
|
15
|
-
*/
|
|
16
|
-
function withTimeout(promise, ms = TIMEOUTS.CONTAINER_OP) {
|
|
17
|
-
return Promise.race([
|
|
18
|
-
promise,
|
|
19
|
-
new Promise((_, reject) =>
|
|
20
|
-
setTimeout(() => reject(new Error('Operation timed out')), ms)
|
|
21
|
-
),
|
|
22
|
-
]);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Remove (delete) a container by id. Force removal so running containers are stopped first.
|
|
27
|
-
*
|
|
28
|
-
* @param {string} containerId - Docker container id
|
|
29
|
-
* @returns {Promise<void>} Resolves when removal completes
|
|
30
|
-
* @throws {Error} If Docker reports an error
|
|
31
|
-
*/
|
|
32
|
-
export async function removeContainer(containerId) {
|
|
33
|
-
logger.debug('Removing container %s', containerId);
|
|
34
|
-
const container = docker.getContainer(containerId);
|
|
35
|
-
try {
|
|
36
|
-
await withTimeout(container.remove({ force: true }), TIMEOUTS.CONTAINER_OP);
|
|
37
|
-
logger.info('Removed container %s', containerId);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
logger.error('Failed to remove container %s', containerId, err);
|
|
40
|
-
throw new Error('Error removing container: ' + err.message);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Create a new container from an image. If the image is missing locally, it will be pulled.
|
|
46
|
-
*
|
|
47
|
-
* @param {string} imageName - Image name (e.g. 'nginx:alpine')
|
|
48
|
-
* @param {Object} [options] - Docker create options (Env, ExposedPorts, HostConfig, name, etc.)
|
|
49
|
-
* @param {Object} [imageProfiles] - Image profiles map for defaultPort fallback
|
|
50
|
-
* @returns {Promise<{id: string, ports: Array<{containerPort: string, hostPort: string, protocol: string, source: string}>}>}
|
|
51
|
-
* @throws {Error} If image listing/pull or creation fails
|
|
52
|
-
*/
|
|
53
|
-
export async function createContainer(
|
|
54
|
-
imageName,
|
|
55
|
-
options = {},
|
|
56
|
-
imageProfiles = IMAGE_PROFILES
|
|
57
|
-
) {
|
|
58
|
-
logger.info('Creating container from image %s', imageName);
|
|
59
|
-
let exists;
|
|
60
|
-
try {
|
|
61
|
-
exists = await withTimeout(imageExists(imageName), 10000);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
logger.error('Could not check local images for %s', imageName, err);
|
|
64
|
-
throw new Error('Error listing local images: ' + err.message);
|
|
65
|
-
}
|
|
66
|
-
if (!exists) {
|
|
67
|
-
logger.info('Image %s not found locally, pulling', imageName);
|
|
68
|
-
try {
|
|
69
|
-
await withTimeout(pullImage(imageName), TIMEOUTS.PULL_IMAGE);
|
|
70
|
-
logger.info('Pulled image %s', imageName);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
logger.error('Failed to pull image %s', imageName, err);
|
|
73
|
-
throw new Error('Could not pull image: ' + err.message);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const createOpts = {
|
|
77
|
-
Image: imageName,
|
|
78
|
-
Tty: true,
|
|
79
|
-
...options,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const hasUserDefinedPorts = Boolean(
|
|
83
|
-
createOpts.ExposedPorts && Object.keys(createOpts.ExposedPorts).length
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
/** @type {Array<{containerPort: string, hostPort: string, protocol: string, source: string}>} */
|
|
87
|
-
const assignedPorts = [];
|
|
88
|
-
|
|
89
|
-
if (!hasUserDefinedPorts) {
|
|
90
|
-
try {
|
|
91
|
-
const image = docker.getImage(imageName);
|
|
92
|
-
if (image && typeof image.inspect === 'function') {
|
|
93
|
-
let usedHostPorts = new Set();
|
|
94
|
-
if (typeof docker.listContainers === 'function') {
|
|
95
|
-
try {
|
|
96
|
-
const containers = await withTimeout(
|
|
97
|
-
docker.listContainers({ all: true }),
|
|
98
|
-
TIMEOUTS.CONTAINER_OP
|
|
99
|
-
);
|
|
100
|
-
containers.forEach((container) => {
|
|
101
|
-
(container.Ports || []).forEach((portInfo) => {
|
|
102
|
-
if (portInfo && portInfo.PublicPort) {
|
|
103
|
-
usedHostPorts.add(String(portInfo.PublicPort));
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
} catch (err) {
|
|
108
|
-
logger.warn(
|
|
109
|
-
'Could not inspect existing containers to auto-map ports',
|
|
110
|
-
err
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const reservePort = (port) => {
|
|
115
|
-
const portStr = String(port);
|
|
116
|
-
usedHostPorts.add(portStr);
|
|
117
|
-
};
|
|
118
|
-
const pickNextAvailablePort = (base) => {
|
|
119
|
-
const numericBase = Number.parseInt(base, 10);
|
|
120
|
-
if (Number.isNaN(numericBase)) {
|
|
121
|
-
if (!usedHostPorts.has(base)) {
|
|
122
|
-
usedHostPorts.add(base);
|
|
123
|
-
return base;
|
|
124
|
-
}
|
|
125
|
-
let counter = 1;
|
|
126
|
-
let candidate = `${base}-${counter}`;
|
|
127
|
-
while (usedHostPorts.has(candidate)) {
|
|
128
|
-
counter += 1;
|
|
129
|
-
candidate = `${base}-${counter}`;
|
|
130
|
-
}
|
|
131
|
-
usedHostPorts.add(candidate);
|
|
132
|
-
return candidate;
|
|
133
|
-
}
|
|
134
|
-
let candidate = numericBase;
|
|
135
|
-
while (usedHostPorts.has(String(candidate))) {
|
|
136
|
-
candidate += 1;
|
|
137
|
-
}
|
|
138
|
-
reservePort(candidate);
|
|
139
|
-
return String(candidate);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const inspectData = await withTimeout(
|
|
143
|
-
image.inspect(),
|
|
144
|
-
TIMEOUTS.CONTAINER_OP
|
|
145
|
-
);
|
|
146
|
-
let exposed =
|
|
147
|
-
inspectData?.Config?.ExposedPorts ||
|
|
148
|
-
inspectData?.ContainerConfig?.ExposedPorts ||
|
|
149
|
-
{};
|
|
150
|
-
let exposedKeys = Object.keys(exposed || {});
|
|
151
|
-
|
|
152
|
-
// If no ExposedPorts in image, fall back to IMAGE_PROFILES defaultPort
|
|
153
|
-
if (!exposedKeys.length) {
|
|
154
|
-
const baseName = normalizeImageName(imageName);
|
|
155
|
-
const profile = imageProfiles[baseName];
|
|
156
|
-
if (profile && profile.defaultPort) {
|
|
157
|
-
const fallbackKey = `${profile.defaultPort}/tcp`;
|
|
158
|
-
exposed = { [fallbackKey]: {} };
|
|
159
|
-
exposedKeys = [fallbackKey];
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (exposedKeys.length) {
|
|
164
|
-
createOpts.ExposedPorts = createOpts.ExposedPorts || {};
|
|
165
|
-
createOpts.HostConfig = { ...(createOpts.HostConfig || {}) };
|
|
166
|
-
createOpts.HostConfig.PortBindings = {
|
|
167
|
-
...(createOpts.HostConfig.PortBindings || {}),
|
|
168
|
-
};
|
|
169
|
-
exposedKeys.forEach((portKey) => {
|
|
170
|
-
if (!createOpts.ExposedPorts[portKey]) {
|
|
171
|
-
createOpts.ExposedPorts[portKey] = exposed[portKey] || {};
|
|
172
|
-
}
|
|
173
|
-
if (
|
|
174
|
-
!createOpts.HostConfig.PortBindings[portKey] ||
|
|
175
|
-
!createOpts.HostConfig.PortBindings[portKey].length
|
|
176
|
-
) {
|
|
177
|
-
const parts = portKey.split('/');
|
|
178
|
-
const containerPort = parts[0];
|
|
179
|
-
const protocol = parts[1] || 'tcp';
|
|
180
|
-
const hostPort = pickNextAvailablePort(containerPort);
|
|
181
|
-
createOpts.HostConfig.PortBindings[portKey] = [
|
|
182
|
-
{ HostPort: hostPort },
|
|
183
|
-
];
|
|
184
|
-
assignedPorts.push({
|
|
185
|
-
containerPort,
|
|
186
|
-
hostPort,
|
|
187
|
-
protocol,
|
|
188
|
-
source: 'auto',
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
} catch (err) {
|
|
195
|
-
logger.warn(
|
|
196
|
-
'Could not inspect image %s for default ports',
|
|
197
|
-
imageName,
|
|
198
|
-
err
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
} else {
|
|
202
|
-
// User-defined ports: collect them with source: "user"
|
|
203
|
-
const portBindings = createOpts.HostConfig?.PortBindings || {};
|
|
204
|
-
Object.entries(portBindings).forEach(([portKey, bindings]) => {
|
|
205
|
-
const parts = portKey.split('/');
|
|
206
|
-
const containerPort = parts[0];
|
|
207
|
-
const protocol = parts[1] || 'tcp';
|
|
208
|
-
(bindings || []).forEach((b) => {
|
|
209
|
-
assignedPorts.push({
|
|
210
|
-
containerPort,
|
|
211
|
-
hostPort: b.HostPort,
|
|
212
|
-
protocol,
|
|
213
|
-
source: 'user',
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
const container = await withTimeout(
|
|
221
|
-
docker.createContainer(createOpts),
|
|
222
|
-
TIMEOUTS.CONTAINER_OP
|
|
223
|
-
);
|
|
224
|
-
const containerId = container.id || container.Id;
|
|
225
|
-
logger.info('Created container %s from image %s', containerId, imageName);
|
|
226
|
-
return { id: containerId, ports: assignedPorts };
|
|
227
|
-
} catch (err) {
|
|
228
|
-
logger.error('Failed to create container from image %s', imageName, err);
|
|
229
|
-
throw new Error('Error creating container: ' + err.message);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Start a container by id.
|
|
235
|
-
* @param {string} containerId - Docker container id
|
|
236
|
-
* @returns {Promise<void>}
|
|
237
|
-
*/
|
|
238
|
-
export async function startContainer(containerId) {
|
|
239
|
-
const container = docker.getContainer(containerId);
|
|
240
|
-
logger.debug('Starting container %s', containerId);
|
|
241
|
-
try {
|
|
242
|
-
await withTimeout(container.start(), TIMEOUTS.CONTAINER_OP);
|
|
243
|
-
logger.info('Started container %s', containerId);
|
|
244
|
-
} catch (err) {
|
|
245
|
-
logger.error('Failed to start container %s', containerId, err);
|
|
246
|
-
throw err;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Stop a container by id.
|
|
252
|
-
* @param {string} containerId - Docker container id
|
|
253
|
-
* @returns {Promise<void>}
|
|
254
|
-
*/
|
|
255
|
-
export async function stopContainer(containerId) {
|
|
256
|
-
const container = docker.getContainer(containerId);
|
|
257
|
-
logger.debug('Stopping container %s', containerId);
|
|
258
|
-
try {
|
|
259
|
-
await withTimeout(container.stop(), TIMEOUTS.CONTAINER_OP);
|
|
260
|
-
logger.info('Stopped container %s', containerId);
|
|
261
|
-
} catch (err) {
|
|
262
|
-
logger.error('Failed to stop container %s', containerId, err);
|
|
263
|
-
throw err;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Restart a container by id.
|
|
269
|
-
* @param {string} containerId - Docker container id
|
|
270
|
-
* @returns {Promise<void>}
|
|
271
|
-
*/
|
|
272
|
-
export async function restartContainer(containerId) {
|
|
273
|
-
const container = docker.getContainer(containerId);
|
|
274
|
-
logger.debug('Restarting container %s', containerId);
|
|
275
|
-
try {
|
|
276
|
-
await withTimeout(container.restart(), TIMEOUTS.CONTAINER_OP);
|
|
277
|
-
logger.info('Restarted container %s', containerId);
|
|
278
|
-
} catch (err) {
|
|
279
|
-
logger.error('Failed to restart container %s', containerId, err);
|
|
280
|
-
throw err;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { docker } from '../dockerService.js';
|
|
2
|
-
import { logger } from '../../logger.js';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_CONTAINER_NAME = 'Unknown';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Produce a safe, display-friendly container name from the raw Docker payload.
|
|
8
|
-
*
|
|
9
|
-
* @param {Object} container Docker container summary as returned by dockerode.
|
|
10
|
-
* @returns {string} Normalized name without leading slashes and with a fallback applied.
|
|
11
|
-
*/
|
|
12
|
-
function normalizeContainerName(container) {
|
|
13
|
-
const names = container?.Names;
|
|
14
|
-
|
|
15
|
-
let rawName;
|
|
16
|
-
if (
|
|
17
|
-
Array.isArray(names) &&
|
|
18
|
-
names.length > 0 &&
|
|
19
|
-
typeof names[0] === 'string'
|
|
20
|
-
) {
|
|
21
|
-
rawName = names[0];
|
|
22
|
-
} else if (typeof names === 'string' && names.trim().length > 0) {
|
|
23
|
-
rawName = names;
|
|
24
|
-
} else if (
|
|
25
|
-
typeof container?.Name === 'string' &&
|
|
26
|
-
container.Name.trim().length > 0
|
|
27
|
-
) {
|
|
28
|
-
rawName = container.Name;
|
|
29
|
-
} else {
|
|
30
|
-
rawName = DEFAULT_CONTAINER_NAME;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (typeof rawName !== 'string') {
|
|
34
|
-
return DEFAULT_CONTAINER_NAME;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const cleanedName = rawName.replace(/^\/+/u, '').trim();
|
|
38
|
-
return cleanedName.length > 0 ? cleanedName : DEFAULT_CONTAINER_NAME;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Return a list of containers with normalized fields for the UI.
|
|
43
|
-
* @returns {Promise<Array<Object>>}
|
|
44
|
-
*/
|
|
45
|
-
export async function getContainers() {
|
|
46
|
-
logger.debug('Listing containers');
|
|
47
|
-
try {
|
|
48
|
-
const containers = await docker.listContainers({ all: true });
|
|
49
|
-
logger.debug('Retrieved %d containers', containers.length);
|
|
50
|
-
return containers.map((container) => ({
|
|
51
|
-
id: container.Id,
|
|
52
|
-
name: normalizeContainerName(container),
|
|
53
|
-
image: container.Image,
|
|
54
|
-
state: container.State,
|
|
55
|
-
status: container.Status,
|
|
56
|
-
ports: (() => {
|
|
57
|
-
const publicPorts = container.Ports.filter(
|
|
58
|
-
(port) => port.PublicPort
|
|
59
|
-
).map((port) => `${port.PublicPort}:${port.PrivatePort}`);
|
|
60
|
-
if (publicPorts.length > 0) {
|
|
61
|
-
return [...new Set(publicPorts)];
|
|
62
|
-
}
|
|
63
|
-
// Fall back to exposed private ports when there are no bindings.
|
|
64
|
-
const privatePorts = container.Ports.filter(
|
|
65
|
-
(port) => port.PrivatePort
|
|
66
|
-
).map((port) => `${port.PrivatePort}`);
|
|
67
|
-
return [...new Set(privatePorts)];
|
|
68
|
-
})(),
|
|
69
|
-
}));
|
|
70
|
-
} catch (err) {
|
|
71
|
-
logger.error('Failed to list containers', err);
|
|
72
|
-
throw err;
|
|
73
|
-
}
|
|
74
|
-
}
|