feat(host): Add DockerHost version & image-prune APIs, extend network creation options, return exec inspect info, and improve image import/store and streaming
This commit is contained in:
12
changelog.md
12
changelog.md
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-11-25 - 5.1.0 - feat(host)
|
||||||
|
Add DockerHost version & image-prune APIs, extend network creation options, return exec inspect info, and improve image import/store and streaming
|
||||||
|
|
||||||
|
- Add DockerHost.getVersion() to retrieve Docker daemon version and build info
|
||||||
|
- Add DockerHost.pruneImages() with dangling and filters support (calls /images/prune)
|
||||||
|
- Extend INetworkCreationDescriptor and DockerNetwork._create() to accept Driver, IPAM, EnableIPv6, Attachable and Labels
|
||||||
|
- Enhance DockerContainer.exec() to return an inspect() helper and introduce IExecInspectInfo to expose exec state and exit code
|
||||||
|
- Improve DockerImage._createFromTarStream() parsing of docker-load output and error messages when loaded image cannot be determined
|
||||||
|
- Implement DockerImageStore.storeImage() to persist, repackage and upload images (local processing and s3 support)
|
||||||
|
- Various streaming/request improvements for compatibility with Node streams and better handling of streaming endpoints
|
||||||
|
- Update tests to cover new features (network creation, exec inspect, etc.)
|
||||||
|
|
||||||
## 2025-11-24 - 5.0.2 - fix(DockerContainer)
|
## 2025-11-24 - 5.0.2 - fix(DockerContainer)
|
||||||
Fix getContainerById to return undefined for non-existent containers
|
Fix getContainerById to return undefined for non-existent containers
|
||||||
|
|
||||||
|
|||||||
196
readme.hints.md
196
readme.hints.md
@@ -1,5 +1,201 @@
|
|||||||
# Docker Module - Development Hints
|
# Docker Module - Development Hints
|
||||||
|
|
||||||
|
## New Features (2025-11-25 - v5.1.0)
|
||||||
|
|
||||||
|
### 1. Enhanced Network Creation with Full Configuration Support
|
||||||
|
|
||||||
|
**Problem:** Users were unable to create non-overlay networks or customize network configuration. The `INetworkCreationDescriptor` interface only had a `Name` property, and `DockerNetwork._create()` hardcoded `Driver: 'overlay'`.
|
||||||
|
|
||||||
|
**Solution:** Expanded the interface and implementation to support all Docker network configuration options:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// New interface properties:
|
||||||
|
export interface INetworkCreationDescriptor {
|
||||||
|
Name: string;
|
||||||
|
Driver?: 'bridge' | 'overlay' | 'host' | 'none' | 'macvlan'; // NEW
|
||||||
|
Attachable?: boolean; // NEW
|
||||||
|
Labels?: Record<string, string>; // NEW
|
||||||
|
IPAM?: { // NEW - IP Address Management
|
||||||
|
Driver?: string;
|
||||||
|
Config?: Array<{
|
||||||
|
Subnet?: string;
|
||||||
|
Gateway?: string;
|
||||||
|
IPRange?: string;
|
||||||
|
AuxiliaryAddresses?: Record<string, string>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
Internal?: boolean; // NEW
|
||||||
|
EnableIPv6?: boolean; // NEW
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage Example:**
|
||||||
|
```typescript
|
||||||
|
// Create bridge network with custom IPAM
|
||||||
|
const network = await docker.createNetwork({
|
||||||
|
Name: 'custom-bridge',
|
||||||
|
Driver: 'bridge',
|
||||||
|
IPAM: {
|
||||||
|
Config: [{
|
||||||
|
Subnet: '172.20.0.0/16',
|
||||||
|
Gateway: '172.20.0.1',
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
Labels: { environment: 'production' },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files Modified:**
|
||||||
|
- `ts/interfaces/network.ts` - Added all missing properties to interface
|
||||||
|
- `ts/classes.network.ts` - Updated `_create()` to pass through descriptor properties instead of hardcoding
|
||||||
|
|
||||||
|
### 2. Docker Daemon Version Information
|
||||||
|
|
||||||
|
**Added:** `dockerHost.getVersion()` method to retrieve Docker daemon version information.
|
||||||
|
|
||||||
|
**Purpose:** Essential for API compatibility checking, debugging, and ensuring minimum Docker version requirements.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
Version: string; // e.g., "20.10.21"
|
||||||
|
ApiVersion: string; // e.g., "1.41"
|
||||||
|
MinAPIVersion?: string; // Minimum supported API version
|
||||||
|
GitCommit: string;
|
||||||
|
GoVersion: string;
|
||||||
|
Os: string; // e.g., "linux"
|
||||||
|
Arch: string; // e.g., "amd64"
|
||||||
|
KernelVersion: string;
|
||||||
|
BuildTime?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage Example:**
|
||||||
|
```typescript
|
||||||
|
const version = await docker.getVersion();
|
||||||
|
console.log(`Docker ${version.Version} (API ${version.ApiVersion})`);
|
||||||
|
console.log(`Platform: ${version.Os}/${version.Arch}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files Modified:**
|
||||||
|
- `ts/classes.host.ts` - Added `getVersion()` method after `ping()`
|
||||||
|
|
||||||
|
### 3. Image Pruning for Disk Space Management
|
||||||
|
|
||||||
|
**Added:** `dockerHost.pruneImages(options?)` method to clean up unused images.
|
||||||
|
|
||||||
|
**Purpose:** Automated disk space management, CI/CD cleanup, scheduled maintenance tasks.
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
dangling?: boolean; // Remove untagged images
|
||||||
|
filters?: Record<string, string[]>; // Custom filters (until, label, etc.)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
ImagesDeleted: Array<{ Untagged?: string; Deleted?: string }>;
|
||||||
|
SpaceReclaimed: number; // Bytes freed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage Example:**
|
||||||
|
```typescript
|
||||||
|
// Remove dangling images
|
||||||
|
const result = await docker.pruneImages({ dangling: true });
|
||||||
|
console.log(`Reclaimed: ${(result.SpaceReclaimed / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
|
// Remove old images (older than 7 days)
|
||||||
|
await docker.pruneImages({
|
||||||
|
filters: {
|
||||||
|
until: ['168h']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files Modified:**
|
||||||
|
- `ts/classes.host.ts` - Added `pruneImages()` method with filter support
|
||||||
|
|
||||||
|
### 4. Exec Command Exit Codes and Inspection
|
||||||
|
|
||||||
|
**Problem:** Users could not determine if exec commands succeeded or failed. The `container.exec()` method returned a stream but provided no way to access exit codes, which are essential for:
|
||||||
|
- Health checks (e.g., `pg_isready` exit code)
|
||||||
|
- Test automation (npm test success/failure)
|
||||||
|
- Deployment validation (migration checks)
|
||||||
|
- Container readiness probes
|
||||||
|
|
||||||
|
**Solution:** Added `inspect()` method to `exec()` return value that provides comprehensive execution information.
|
||||||
|
|
||||||
|
**New Return Type:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
stream: Duplex;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
inspect: () => Promise<IExecInspectInfo>; // NEW
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**IExecInspectInfo Interface:**
|
||||||
|
```typescript
|
||||||
|
export interface IExecInspectInfo {
|
||||||
|
ExitCode: number; // 0 = success, non-zero = failure
|
||||||
|
Running: boolean; // Whether exec is still running
|
||||||
|
Pid: number; // Process ID
|
||||||
|
ContainerID: string; // Container where exec ran
|
||||||
|
ID: string; // Exec instance ID
|
||||||
|
OpenStderr: boolean;
|
||||||
|
OpenStdin: boolean;
|
||||||
|
OpenStdout: boolean;
|
||||||
|
CanRemove: boolean;
|
||||||
|
DetachKeys: string;
|
||||||
|
ProcessConfig: {
|
||||||
|
tty: boolean;
|
||||||
|
entrypoint: string;
|
||||||
|
arguments: string[];
|
||||||
|
privileged: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage Example:**
|
||||||
|
```typescript
|
||||||
|
// Health check with exit code
|
||||||
|
const { stream, close, inspect } = await container.exec('pg_isready -U postgres');
|
||||||
|
|
||||||
|
stream.on('end', async () => {
|
||||||
|
const info = await inspect();
|
||||||
|
|
||||||
|
if (info.ExitCode === 0) {
|
||||||
|
console.log('✅ Database is ready');
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Database check failed (exit code ${info.ExitCode})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await close();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Real-World Use Cases Enabled:**
|
||||||
|
- Health checks: Verify service readiness with proper exit code handling
|
||||||
|
- Test automation: Run tests in container and determine pass/fail
|
||||||
|
- Deployment validation: Execute migration checks and verify success
|
||||||
|
- CI/CD pipelines: Run build/test commands and get accurate results
|
||||||
|
|
||||||
|
**Files Modified:**
|
||||||
|
- `ts/interfaces/container.ts` - Added `IExecInspectInfo` interface
|
||||||
|
- `ts/classes.container.ts` - Updated `exec()` return type and added `inspect()` implementation
|
||||||
|
|
||||||
|
### Implementation Notes
|
||||||
|
|
||||||
|
All changes are non-breaking additions that enhance existing functionality:
|
||||||
|
- Network creation: New optional properties with sensible defaults
|
||||||
|
- getVersion(): New method, no changes to existing APIs
|
||||||
|
- pruneImages(): New method, no changes to existing APIs
|
||||||
|
- exec() inspect(): Added to return value, existing stream/close properties unchanged
|
||||||
|
|
||||||
## getContainerById() Bug Fix (2025-11-24 - v5.0.1)
|
## getContainerById() Bug Fix (2025-11-24 - v5.0.1)
|
||||||
|
|
||||||
### Problem
|
### Problem
|
||||||
|
|||||||
238
readme.md
238
readme.md
@@ -175,6 +175,37 @@ async function waitForDocker(timeoutMs = 10000): Promise<void> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Get Docker Version Information 🆕
|
||||||
|
|
||||||
|
Get detailed version information about the Docker daemon:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get Docker daemon version
|
||||||
|
const version = await docker.getVersion();
|
||||||
|
|
||||||
|
console.log(`Docker Version: ${version.Version}`);
|
||||||
|
console.log(`API Version: ${version.ApiVersion}`);
|
||||||
|
console.log(`Platform: ${version.Os}/${version.Arch}`);
|
||||||
|
console.log(`Go Version: ${version.GoVersion}`);
|
||||||
|
console.log(`Git Commit: ${version.GitCommit}`);
|
||||||
|
console.log(`Kernel Version: ${version.KernelVersion}`);
|
||||||
|
|
||||||
|
// Check API compatibility
|
||||||
|
if (version.MinAPIVersion) {
|
||||||
|
console.log(`Minimum API Version: ${version.MinAPIVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Verify API compatibility
|
||||||
|
async function checkApiCompatibility(requiredVersion: string): Promise<boolean> {
|
||||||
|
const version = await docker.getVersion();
|
||||||
|
// Compare version strings (simplified)
|
||||||
|
return version.ApiVersion >= requiredVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCompatible = await checkApiCompatibility('1.40');
|
||||||
|
console.log(`API compatible: ${isCompatible}`);
|
||||||
|
```
|
||||||
|
|
||||||
### 📦 Container Management
|
### 📦 Container Management
|
||||||
|
|
||||||
#### List All Containers
|
#### List All Containers
|
||||||
@@ -305,7 +336,7 @@ Run commands inside a running container:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Execute a command
|
// Execute a command
|
||||||
const { stream, close } = await container.exec('ls -la /app', {
|
const { stream, close, inspect } = await container.exec('ls -la /app', {
|
||||||
tty: true,
|
tty: true,
|
||||||
user: 'root',
|
user: 'root',
|
||||||
workingDir: '/app',
|
workingDir: '/app',
|
||||||
@@ -323,12 +354,84 @@ stream.on('end', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Execute with array of arguments
|
// Execute with array of arguments
|
||||||
const { stream: stream2, close: close2 } = await container.exec(
|
const { stream: stream2, close: close2, inspect: inspect2 } = await container.exec(
|
||||||
['bash', '-c', 'echo "Hello from container"'],
|
['bash', '-c', 'echo "Hello from container"'],
|
||||||
{ tty: true }
|
{ tty: true }
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Check Exec Command Exit Codes 🆕
|
||||||
|
|
||||||
|
Get the exit code and execution state of commands - essential for health checks and automation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Execute a command and check its exit code
|
||||||
|
const { stream, close, inspect } = await container.exec('pg_isready -U postgres', {
|
||||||
|
tty: false,
|
||||||
|
attachStdout: true,
|
||||||
|
attachStderr: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
output += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', async () => {
|
||||||
|
// Get execution information
|
||||||
|
const info = await inspect();
|
||||||
|
|
||||||
|
console.log(`Exit Code: ${info.ExitCode}`); // 0 = success, non-zero = failure
|
||||||
|
console.log(`Still Running: ${info.Running}`);
|
||||||
|
console.log(`Process ID: ${info.Pid}`);
|
||||||
|
console.log(`Container ID: ${info.ContainerID}`);
|
||||||
|
|
||||||
|
if (info.ExitCode === 0) {
|
||||||
|
console.log('✅ Command succeeded');
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Command failed with exit code ${info.ExitCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Example: Health check function
|
||||||
|
async function healthCheck(container: DockerContainer): Promise<boolean> {
|
||||||
|
const { stream, close, inspect } = await container.exec('curl -f http://localhost:3000/health');
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
stream.on('end', async () => {
|
||||||
|
const info = await inspect();
|
||||||
|
await close();
|
||||||
|
resolve(info.ExitCode === 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Run tests in container and get result
|
||||||
|
async function runTests(container: DockerContainer): Promise<{ passed: boolean; output: string }> {
|
||||||
|
const { stream, close, inspect } = await container.exec('npm test', {
|
||||||
|
workingDir: '/app',
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
output += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
stream.on('end', async () => {
|
||||||
|
const info = await inspect();
|
||||||
|
await close();
|
||||||
|
resolve({
|
||||||
|
passed: info.ExitCode === 0,
|
||||||
|
output: output,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### Get Container Stats
|
#### Get Container Stats
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -424,6 +527,58 @@ await image.remove({ force: true });
|
|||||||
console.log('Image removed');
|
console.log('Image removed');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Prune Unused Images 🆕
|
||||||
|
|
||||||
|
Clean up unused images to free disk space:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Prune all dangling images (untagged images)
|
||||||
|
const result = await docker.pruneImages({ dangling: true });
|
||||||
|
|
||||||
|
console.log(`Images deleted: ${result.ImagesDeleted.length}`);
|
||||||
|
console.log(`Space reclaimed: ${(result.SpaceReclaimed / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
|
// Display what was deleted
|
||||||
|
result.ImagesDeleted.forEach((deleted) => {
|
||||||
|
if (deleted.Untagged) {
|
||||||
|
console.log(` Untagged: ${deleted.Untagged}`);
|
||||||
|
}
|
||||||
|
if (deleted.Deleted) {
|
||||||
|
console.log(` Deleted: ${deleted.Deleted}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prune with custom filters
|
||||||
|
const resultWithFilters = await docker.pruneImages({
|
||||||
|
filters: {
|
||||||
|
// Remove images older than 24 hours
|
||||||
|
until: ['24h'],
|
||||||
|
// Only remove images with specific label
|
||||||
|
label: ['temporary=true'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Example: Scheduled cleanup function
|
||||||
|
async function cleanupOldImages() {
|
||||||
|
console.log('🧹 Starting image cleanup...');
|
||||||
|
|
||||||
|
// Remove dangling images
|
||||||
|
const danglingResult = await docker.pruneImages({ dangling: true });
|
||||||
|
console.log(`Removed ${danglingResult.ImagesDeleted.length} dangling images`);
|
||||||
|
|
||||||
|
// Remove old images (older than 7 days)
|
||||||
|
const oldResult = await docker.pruneImages({
|
||||||
|
filters: {
|
||||||
|
until: ['168h'], // 7 days
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`Removed ${oldResult.ImagesDeleted.length} old images`);
|
||||||
|
|
||||||
|
const totalSpace = danglingResult.SpaceReclaimed + oldResult.SpaceReclaimed;
|
||||||
|
console.log(`✅ Total space reclaimed: ${(totalSpace / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 🌐 Network Management
|
### 🌐 Network Management
|
||||||
|
|
||||||
#### Create Custom Networks
|
#### Create Custom Networks
|
||||||
@@ -432,7 +587,7 @@ console.log('Image removed');
|
|||||||
// Create an overlay network (for swarm)
|
// Create an overlay network (for swarm)
|
||||||
const network = await docker.createNetwork({
|
const network = await docker.createNetwork({
|
||||||
Name: 'my-app-network',
|
Name: 'my-app-network',
|
||||||
Driver: 'overlay',
|
Driver: 'overlay', // 'bridge', 'overlay', 'host', 'none', 'macvlan'
|
||||||
EnableIPv6: false,
|
EnableIPv6: false,
|
||||||
Attachable: true,
|
Attachable: true,
|
||||||
});
|
});
|
||||||
@@ -440,6 +595,83 @@ const network = await docker.createNetwork({
|
|||||||
console.log(`Network created: ${network.Name} (${network.Id})`);
|
console.log(`Network created: ${network.Name} (${network.Id})`);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Advanced Network Configuration 🆕
|
||||||
|
|
||||||
|
Create networks with custom drivers, IPAM configuration, and labels:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create a bridge network with custom IPAM (IP Address Management)
|
||||||
|
const bridgeNetwork = await docker.createNetwork({
|
||||||
|
Name: 'custom-bridge',
|
||||||
|
Driver: 'bridge', // Use bridge driver for single-host networking
|
||||||
|
IPAM: {
|
||||||
|
Driver: 'default',
|
||||||
|
Config: [{
|
||||||
|
Subnet: '172.20.0.0/16',
|
||||||
|
Gateway: '172.20.0.1',
|
||||||
|
IPRange: '172.20.10.0/24', // Allocate IPs from this range
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
Labels: {
|
||||||
|
environment: 'production',
|
||||||
|
team: 'backend',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Bridge network created: ${bridgeNetwork.Name}`);
|
||||||
|
console.log(` Driver: ${bridgeNetwork.Driver}`);
|
||||||
|
console.log(` Subnet: ${bridgeNetwork.IPAM.Config[0].Subnet}`);
|
||||||
|
|
||||||
|
// Create an internal network (isolated from external networks)
|
||||||
|
const internalNetwork = await docker.createNetwork({
|
||||||
|
Name: 'internal-db',
|
||||||
|
Driver: 'bridge',
|
||||||
|
Internal: true, // No external access
|
||||||
|
Attachable: true,
|
||||||
|
Labels: {
|
||||||
|
purpose: 'database',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a network with IPv6 support
|
||||||
|
const ipv6Network = await docker.createNetwork({
|
||||||
|
Name: 'ipv6-network',
|
||||||
|
Driver: 'bridge',
|
||||||
|
EnableIPv6: true,
|
||||||
|
IPAM: {
|
||||||
|
Config: [
|
||||||
|
{
|
||||||
|
Subnet: '172.28.0.0/16',
|
||||||
|
Gateway: '172.28.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Subnet: 'fd00:dead:beef::/48',
|
||||||
|
Gateway: 'fd00:dead:beef::1',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Example: Create network for microservices
|
||||||
|
async function createMicroserviceNetwork() {
|
||||||
|
return await docker.createNetwork({
|
||||||
|
Name: 'microservices',
|
||||||
|
Driver: 'overlay', // For swarm mode
|
||||||
|
Attachable: true, // Allow standalone containers to attach
|
||||||
|
IPAM: {
|
||||||
|
Config: [{
|
||||||
|
Subnet: '10.0.0.0/24',
|
||||||
|
Gateway: '10.0.0.1',
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
Labels: {
|
||||||
|
'com.docker.stack.namespace': 'production',
|
||||||
|
'version': '2.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### List and Inspect Networks
|
#### List and Inspect Networks
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
@@ -318,6 +318,119 @@ tap.test('should complete container tests', async () => {
|
|||||||
console.log('Container streaming tests completed');
|
console.log('Container streaming tests completed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NEW FEATURES TESTS (v5.1.0)
|
||||||
|
|
||||||
|
// Test 1: Network creation with custom driver and IPAM
|
||||||
|
tap.test('should create bridge network with custom IPAM config', async () => {
|
||||||
|
const network = await testDockerHost.createNetwork({
|
||||||
|
Name: 'test-bridge-network',
|
||||||
|
Driver: 'bridge',
|
||||||
|
IPAM: {
|
||||||
|
Config: [{
|
||||||
|
Subnet: '172.20.0.0/16',
|
||||||
|
Gateway: '172.20.0.1',
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
Labels: { testLabel: 'v5.1.0' },
|
||||||
|
});
|
||||||
|
expect(network).toBeInstanceOf(docker.DockerNetwork);
|
||||||
|
expect(network.Name).toEqual('test-bridge-network');
|
||||||
|
expect(network.Driver).toEqual('bridge');
|
||||||
|
console.log('Created bridge network:', network.Name, 'with driver:', network.Driver);
|
||||||
|
await network.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: getVersion() returns proper Docker daemon info
|
||||||
|
tap.test('should get Docker daemon version information', async () => {
|
||||||
|
const version = await testDockerHost.getVersion();
|
||||||
|
expect(version).toBeInstanceOf(Object);
|
||||||
|
expect(typeof version.Version).toEqual('string');
|
||||||
|
expect(typeof version.ApiVersion).toEqual('string');
|
||||||
|
expect(typeof version.Os).toEqual('string');
|
||||||
|
expect(typeof version.Arch).toEqual('string');
|
||||||
|
console.log('Docker version:', version.Version, 'API version:', version.ApiVersion);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: pruneImages() functionality
|
||||||
|
tap.test('should prune dangling images', async () => {
|
||||||
|
const result = await testDockerHost.pruneImages({ dangling: true });
|
||||||
|
expect(result).toBeInstanceOf(Object);
|
||||||
|
expect(result).toHaveProperty('ImagesDeleted');
|
||||||
|
expect(result).toHaveProperty('SpaceReclaimed');
|
||||||
|
expect(Array.isArray(result.ImagesDeleted)).toEqual(true);
|
||||||
|
expect(typeof result.SpaceReclaimed).toEqual('number');
|
||||||
|
console.log('Pruned images. Space reclaimed:', result.SpaceReclaimed, 'bytes');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: exec() inspect() returns exit codes
|
||||||
|
tap.test('should get exit code from exec command', async (tools) => {
|
||||||
|
const done = tools.defer();
|
||||||
|
|
||||||
|
// Execute a successful command (exit code 0)
|
||||||
|
const { stream, close, inspect } = await testContainer.exec('echo "test successful"', {
|
||||||
|
tty: false,
|
||||||
|
attachStdout: true,
|
||||||
|
attachStderr: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', async () => {
|
||||||
|
// Give Docker a moment to finalize the exec state
|
||||||
|
await tools.delayFor(500);
|
||||||
|
|
||||||
|
const info = await inspect();
|
||||||
|
expect(info).toBeInstanceOf(Object);
|
||||||
|
expect(typeof info.ExitCode).toEqual('number');
|
||||||
|
expect(info.ExitCode).toEqual(0); // Success
|
||||||
|
expect(typeof info.Running).toEqual('boolean');
|
||||||
|
expect(info.Running).toEqual(false); // Should be done
|
||||||
|
expect(typeof info.ContainerID).toEqual('string');
|
||||||
|
console.log('Exec inspect - ExitCode:', info.ExitCode, 'Running:', info.Running);
|
||||||
|
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('error', async (error) => {
|
||||||
|
console.error('Exec error:', error);
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await done.promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should get non-zero exit code from failed exec command', async (tools) => {
|
||||||
|
const done = tools.defer();
|
||||||
|
|
||||||
|
// Execute a command that fails (exit code 1)
|
||||||
|
const { stream, close, inspect } = await testContainer.exec('exit 1', {
|
||||||
|
tty: false,
|
||||||
|
attachStdout: true,
|
||||||
|
attachStderr: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', async () => {
|
||||||
|
// Give Docker a moment to finalize the exec state
|
||||||
|
await tools.delayFor(500);
|
||||||
|
|
||||||
|
const info = await inspect();
|
||||||
|
expect(info.ExitCode).toEqual(1); // Failure
|
||||||
|
expect(info.Running).toEqual(false);
|
||||||
|
console.log('Exec inspect (failed command) - ExitCode:', info.ExitCode);
|
||||||
|
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('error', async (error) => {
|
||||||
|
console.error('Exec error:', error);
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await done.promise;
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('cleanup', async () => {
|
tap.test('cleanup', async () => {
|
||||||
await testDockerHost.stop();
|
await testDockerHost.stop();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@apiclient.xyz/docker',
|
name: '@apiclient.xyz/docker',
|
||||||
version: '5.0.2',
|
version: '5.1.0',
|
||||||
description: 'Provides easy communication with Docker remote API from Node.js, with TypeScript support.'
|
description: 'Provides easy communication with Docker remote API from Node.js, with TypeScript support.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ export class DockerContainer extends DockerResource {
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
stream: plugins.smartstream.stream.Duplex;
|
stream: plugins.smartstream.stream.Duplex;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
|
inspect: () => Promise<interfaces.IExecInspectInfo>;
|
||||||
}> {
|
}> {
|
||||||
// Step 1: Create exec instance
|
// Step 1: Create exec instance
|
||||||
const createResponse = await this.dockerHost.request('POST', `/containers/${this.Id}/exec`, {
|
const createResponse = await this.dockerHost.request('POST', `/containers/${this.Id}/exec`, {
|
||||||
@@ -386,9 +387,15 @@ export class DockerContainer extends DockerResource {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inspect = async (): Promise<interfaces.IExecInspectInfo> => {
|
||||||
|
const inspectResponse = await this.dockerHost.request('GET', `/exec/${execId}/json`);
|
||||||
|
return inspectResponse.body;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stream: duplexStream,
|
stream: duplexStream,
|
||||||
close,
|
close,
|
||||||
|
inspect,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,25 @@ export class DockerHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Docker daemon version information
|
||||||
|
* @returns Version info including Docker version, API version, OS, architecture, etc.
|
||||||
|
*/
|
||||||
|
public async getVersion(): Promise<{
|
||||||
|
Version: string;
|
||||||
|
ApiVersion: string;
|
||||||
|
MinAPIVersion?: string;
|
||||||
|
GitCommit: string;
|
||||||
|
GoVersion: string;
|
||||||
|
Os: string;
|
||||||
|
Arch: string;
|
||||||
|
KernelVersion: string;
|
||||||
|
BuildTime?: string;
|
||||||
|
}> {
|
||||||
|
const response = await this.request('GET', '/version');
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* authenticate against a registry
|
* authenticate against a registry
|
||||||
* @param userArg
|
* @param userArg
|
||||||
@@ -248,6 +267,35 @@ export class DockerHost {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prune unused images
|
||||||
|
* @param options Optional filters (dangling, until, label)
|
||||||
|
* @returns Object with deleted images and space reclaimed
|
||||||
|
*/
|
||||||
|
public async pruneImages(options?: {
|
||||||
|
dangling?: boolean;
|
||||||
|
filters?: Record<string, string[]>;
|
||||||
|
}): Promise<{
|
||||||
|
ImagesDeleted: Array<{ Untagged?: string; Deleted?: string }>;
|
||||||
|
SpaceReclaimed: number;
|
||||||
|
}> {
|
||||||
|
const filters: Record<string, string[]> = options?.filters || {};
|
||||||
|
|
||||||
|
// Add dangling filter if specified
|
||||||
|
if (options?.dangling !== undefined) {
|
||||||
|
filters.dangling = [options.dangling.toString()];
|
||||||
|
}
|
||||||
|
|
||||||
|
let route = '/images/prune';
|
||||||
|
if (filters && Object.keys(filters).length > 0) {
|
||||||
|
route += `?filters=${encodeURIComponent(JSON.stringify(filters))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.request('POST', route);
|
||||||
|
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds an image from a Dockerfile
|
* Builds an image from a Dockerfile
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -51,20 +51,12 @@ export class DockerNetwork extends DockerResource {
|
|||||||
const response = await dockerHost.request('POST', '/networks/create', {
|
const response = await dockerHost.request('POST', '/networks/create', {
|
||||||
Name: networkCreationDescriptor.Name,
|
Name: networkCreationDescriptor.Name,
|
||||||
CheckDuplicate: true,
|
CheckDuplicate: true,
|
||||||
Driver: 'overlay',
|
Driver: networkCreationDescriptor.Driver || 'overlay',
|
||||||
EnableIPv6: false,
|
EnableIPv6: networkCreationDescriptor.EnableIPv6 || false,
|
||||||
/* IPAM: {
|
IPAM: networkCreationDescriptor.IPAM,
|
||||||
Driver: 'default',
|
Internal: networkCreationDescriptor.Internal || false,
|
||||||
Config: [
|
Attachable: networkCreationDescriptor.Attachable !== undefined ? networkCreationDescriptor.Attachable : true,
|
||||||
{
|
Labels: networkCreationDescriptor.Labels,
|
||||||
Subnet: `172.20.${networkCreationDescriptor.NetworkNumber}.0/16`,
|
|
||||||
IPRange: `172.20.${networkCreationDescriptor.NetworkNumber}.0/24`,
|
|
||||||
Gateway: `172.20.${networkCreationDescriptor.NetworkNumber}.11`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, */
|
|
||||||
Internal: false,
|
|
||||||
Attachable: true,
|
|
||||||
Ingress: false,
|
Ingress: false,
|
||||||
});
|
});
|
||||||
if (response.statusCode < 300) {
|
if (response.statusCode < 300) {
|
||||||
|
|||||||
@@ -10,3 +10,41 @@ export interface IContainerCreationDescriptor {
|
|||||||
/** Network names (strings) or DockerNetwork instances */
|
/** Network names (strings) or DockerNetwork instances */
|
||||||
networks?: (string | DockerNetwork)[];
|
networks?: (string | DockerNetwork)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about an exec instance, including exit code and running state.
|
||||||
|
* Retrieved via container.exec().inspect()
|
||||||
|
*/
|
||||||
|
export interface IExecInspectInfo {
|
||||||
|
/** Exit code of the exec command (0 = success) */
|
||||||
|
ExitCode: number;
|
||||||
|
/** Whether the exec is currently running */
|
||||||
|
Running: boolean;
|
||||||
|
/** Process ID */
|
||||||
|
Pid: number;
|
||||||
|
/** Container ID where exec runs */
|
||||||
|
ContainerID: string;
|
||||||
|
/** Exec instance ID */
|
||||||
|
ID: string;
|
||||||
|
/** Whether stderr is open */
|
||||||
|
OpenStderr: boolean;
|
||||||
|
/** Whether stdin is open */
|
||||||
|
OpenStdin: boolean;
|
||||||
|
/** Whether stdout is open */
|
||||||
|
OpenStdout: boolean;
|
||||||
|
/** Whether exec can be removed */
|
||||||
|
CanRemove: boolean;
|
||||||
|
/** Detach keys */
|
||||||
|
DetachKeys: string;
|
||||||
|
/** Process configuration */
|
||||||
|
ProcessConfig: {
|
||||||
|
/** Whether TTY is allocated */
|
||||||
|
tty: boolean;
|
||||||
|
/** Entrypoint */
|
||||||
|
entrypoint: string;
|
||||||
|
/** Command arguments */
|
||||||
|
arguments: string[];
|
||||||
|
/** Whether running in privileged mode */
|
||||||
|
privileged: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,4 +3,18 @@
|
|||||||
*/
|
*/
|
||||||
export interface INetworkCreationDescriptor {
|
export interface INetworkCreationDescriptor {
|
||||||
Name: string;
|
Name: string;
|
||||||
|
Driver?: 'bridge' | 'overlay' | 'host' | 'none' | 'macvlan';
|
||||||
|
Attachable?: boolean;
|
||||||
|
Labels?: Record<string, string>;
|
||||||
|
IPAM?: {
|
||||||
|
Driver?: string;
|
||||||
|
Config?: Array<{
|
||||||
|
Subnet?: string;
|
||||||
|
Gateway?: string;
|
||||||
|
IPRange?: string;
|
||||||
|
AuxiliaryAddresses?: Record<string, string>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
Internal?: boolean;
|
||||||
|
EnableIPv6?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user