Compare commits

...

10 Commits

15 changed files with 853 additions and 63 deletions

View File

@@ -1,5 +1,88 @@
# 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)
Fix getContainerById to return undefined for non-existent containers
- Prevented creation of an invalid DockerContainer from Docker API error responses when a container does not exist.
- Changed DockerContainer._fromId to use the list+find pattern and return Promise<DockerContainer | undefined>.
- Updated DockerHost.getContainerById to return Promise<DockerContainer | undefined> for type safety and consistent behavior.
- Added tests to verify undefined is returned for non-existent container IDs and that valid IDs return DockerContainer instances.
- Bumped package version to 5.0.1 and updated changelog and readme hints to document the fix.
## 2025-11-24 - 5.0.0 - BREAKING CHANGE(DockerHost)
Rename array-returning get* methods to list* on DockerHost and related resource classes; update docs, tests and changelog
- Renamed public DockerHost methods: getContainers → listContainers, getNetworks → listNetworks, getServices → listServices, getImages → listImages, getSecrets → listSecrets.
- Renamed DockerNetwork.getContainersOnNetwork → DockerNetwork.listContainersOnNetwork and updated usages (e.g. getContainersOnNetworkForService).
- Updated internal/static method docs/comments to recommend dockerHost.list*() usage and adjusted implementations accordingly.
- Updated README, readme.hints.md, tests (test.nonci.node+deno.ts) and changelog to reflect the new list* method names.
- Bumped package version to 4.0.0.
- Migration note: replace calls to get*() with list*() for methods that return multiple items (arrays). Single-item getters such as getContainerById or getNetworkByName remain unchanged.
## 2025-11-24 - 5.0.1 - fix(DockerContainer)
Fix getContainerById() to return undefined instead of invalid container object when container doesn't exist
**Bug Fixed:**
- `getContainerById()` was creating a DockerContainer object from error responses when a container didn't exist
- The error object `{ message: "No such container: ..." }` was being passed to the constructor
- Calling `.logs()` on this invalid container returned "[object Object]" instead of logs
**Solution:**
- Changed `DockerContainer._fromId()` to use the list+filter pattern (consistent with all other resource getters)
- Now returns `undefined` when container is not found (matches DockerImage, DockerNetwork, DockerService, DockerSecret behavior)
- Updated return type to `Promise<DockerContainer | undefined>` for type safety
- Added tests to verify undefined is returned for non-existent containers
**Migration:**
No breaking changes - users should already be checking for undefined/null based on TypeScript types and documentation.
## 2025-11-24 - 4.0.0 - BREAKING CHANGE: Rename list methods for consistency
**Breaking Changes:**
- Renamed all "get*" methods that return arrays to "list*" methods for better clarity:
- `getContainers()``listContainers()`
- `getNetworks()``listNetworks()`
- `getServices()``listServices()`
- `getImages()``listImages()`
- `getSecrets()``listSecrets()`
- `getContainersOnNetwork()``listContainersOnNetwork()` (on DockerNetwork class)
**Migration Guide:**
Update all method calls from `get*()` to `list*()` where the method returns an array of resources. Single-item getters like `getContainerById()`, `getNetworkByName()`, etc. remain unchanged.
**Rationale:**
The `list*` naming convention more clearly indicates that these methods return multiple items (arrays), while `get*` methods are reserved for retrieving single items by ID or name. This follows standard API design patterns and improves code readability.
## 2025-11-24 - 3.0.2 - fix(readme)
Update README to document 3.0.0+ changes: architecture refactor, streaming improvements, health check and circular dependency fixes
- Documented major refactor to a Clean OOP / Facade pattern with DockerHost as the single entry point
- Added/clarified real-time container streaming APIs: streamLogs(), attach(), exec()
- Clarified support for flexible descriptors (accept both string references and class instances)
- Documented complete container lifecycle API (start, stop, remove, logs, inspect, stats)
- Documented new ping() health check method to verify Docker daemon availability
- Noted fix for circular dependency issues in Node.js by using type-only imports
- Mentioned improved TypeScript definitions and expanded examples, migration guides, and real-world use cases
## 2025-11-24 - 3.0.1 - fix(classes.base)
Use type-only import for DockerHost in classes.base to avoid runtime side-effects
- Changed the import in ts/classes.base.ts to a type-only import: import type { DockerHost } from './classes.host.js';
- Prevents a runtime import of classes.host when only the type is needed, reducing risk of circular dependencies and unintended side-effects during module initialization.
- No behavior changes to the public API — TypeScript-only change; intended to improve bundling and runtime stability.
## 2025-11-24 - 3.0.0 - BREAKING CHANGE(DockerHost)
Refactor public API to DockerHost facade; introduce DockerResource base; make resource static methods internal; support flexible descriptors and stream compatibility
@@ -11,7 +94,7 @@ Refactor public API to DockerHost facade; introduce DockerResource base; make re
- Streaming compatibility: updated requestStreaming to convert web ReadableStreams (smartrequest v5+) to Node.js streams via smartstream.nodewebhelpers, preserving backward compatibility for existing streaming APIs (container logs, attach, exec, image import/export, events).
- Container enhancements: added full lifecycle and streaming/interactive APIs on DockerContainer: refresh(), inspect(), start(), stop(), remove(), logs(), stats(), streamLogs(), attach(), exec().
- Service creation updated: resolves image/network/secret descriptors (strings or instances); adds labels.version from image; improved resource handling and port/secret/network resolution.
- Network and Secret classes updated to extend DockerResource and to expose refresh(), remove() and lookup methods via DockerHost (createNetwork/getNetworks/getNetworkByName, createSecret/getSecrets/getSecretByName/getSecretById).
- Network and Secret classes updated to extend DockerResource and to expose refresh(), remove() and lookup methods via DockerHost (createNetwork/listNetworks/getNetworkByName, createSecret/listSecrets/getSecretByName/getSecretById).
- Tests and docs updated: migration guide and examples added (readme.hints.md, README); test timeout reduced from 600s to 300s in package.json.
- BREAKING: Public API changes require consumers to migrate away from direct resource static calls and direct imageStore access to the new DockerHost-based factory methods and storeImage/retrieveImage APIs.

View File

@@ -1,6 +1,6 @@
{
"name": "@apiclient.xyz/docker",
"version": "3.0.0",
"version": "5.1.0",
"description": "Provides easy communication with Docker remote API from Node.js, with TypeScript support.",
"private": false,
"main": "dist_ts/index.js",

View File

@@ -1,5 +1,248 @@
# 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)
### Problem
The `getContainerById()` method had a critical bug where it would create a DockerContainer object from Docker API error responses when a container didn't exist.
**Symptoms:**
- Calling `docker.getContainerById('invalid-id')` returned a DockerContainer object with `{ message: "No such container: invalid-id" }`
- Calling `.logs()` on this invalid container returned "[object Object]" instead of logs or throwing an error
- No way to detect the error state without checking for a `.message` property
**Root Cause:**
The `DockerContainer._fromId()` method made a direct API call to `/containers/{id}/json` and blindly passed `response.body` to the constructor, even when the API returned a 404 error response.
### Solution
Changed `DockerContainer._fromId()` to use the **list+filter pattern**, matching the behavior of all other resource getter methods (DockerImage, DockerNetwork, DockerService, DockerSecret):
```typescript
// Before (buggy):
public static async _fromId(dockerHostArg: DockerHost, containerId: string): Promise<DockerContainer> {
const response = await dockerHostArg.request('GET', `/containers/${containerId}/json`);
return new DockerContainer(dockerHostArg, response.body); // Creates invalid object from error!
}
// After (fixed):
public static async _fromId(dockerHostArg: DockerHost, containerId: string): Promise<DockerContainer | undefined> {
const containers = await this._list(dockerHostArg);
return containers.find((container) => container.Id === containerId); // Returns undefined if not found
}
```
**Benefits:**
- 100% consistent with all other resource classes
- Type-safe return signature: `Promise<DockerContainer | undefined>`
- Cannot create invalid objects - `.find()` naturally returns undefined
- Users can now properly check for non-existent containers
**Usage:**
```typescript
const container = await docker.getContainerById('abc123');
if (container) {
const logs = await container.logs();
console.log(logs);
} else {
console.log('Container not found');
}
```
## OOP Refactoring - Clean Architecture (2025-11-24)
### Architecture Changes
@@ -19,7 +262,7 @@ The module has been restructured to follow a clean OOP Facade pattern:
const network = await DockerNetwork.createNetwork(dockerHost, descriptor);
// New (clean API):
const containers = await dockerHost.getContainers();
const containers = await dockerHost.listContainers();
const network = await dockerHost.createNetwork(descriptor);
```
@@ -113,7 +356,7 @@ await dockerHost.createService({
### Migration Guide
Replace all static method calls with dockerHost methods:
- `DockerContainer.getContainers(host)` → `dockerHost.getContainers()`
- `DockerContainer.getContainers(host)` → `dockerHost.listContainers()`
- `DockerImage.createFromRegistry(host, opts)` → `dockerHost.createImageFromRegistry(opts)`
- `DockerService.createService(host, desc)` → `dockerHost.createService(desc)`
- `dockerHost.imageStore.storeImage(...)` → `dockerHost.storeImage(...)`

272
readme.md
View File

@@ -48,7 +48,7 @@ await docker.ping();
console.log('✅ Docker is running');
// List all containers
const containers = await docker.getContainers();
const containers = await docker.listContainers();
console.log(`Found ${containers.length} containers`);
// Get a specific container and interact with it
@@ -71,7 +71,7 @@ The module follows a **Facade pattern** with `DockerHost` as the single entry po
const docker = new DockerHost({});
// All operations go through DockerHost
const containers = await docker.getContainers(); // List containers
const containers = await docker.listContainers(); // List containers
const container = await docker.getContainerById('id'); // Get specific container
const network = await docker.createNetwork({ Name: 'my-net' }); // Create network
const service = await docker.createService(descriptor); // Deploy service
@@ -175,13 +175,44 @@ 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
#### List All Containers
```typescript
// Get all containers (running and stopped)
const containers = await docker.getContainers();
const containers = await docker.listContainers();
containers.forEach((container) => {
console.log(`Container: ${container.Names[0]}`);
@@ -305,7 +336,7 @@ Run commands inside a running container:
```typescript
// Execute a command
const { stream, close } = await container.exec('ls -la /app', {
const { stream, close, inspect } = await container.exec('ls -la /app', {
tty: true,
user: 'root',
workingDir: '/app',
@@ -323,12 +354,84 @@ stream.on('end', async () => {
});
// 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"'],
{ 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
```typescript
@@ -406,7 +509,7 @@ writeStream.on('finish', () => {
#### List All Images
```typescript
const images = await docker.getImages();
const images = await docker.listImages();
images.forEach((img) => {
console.log(`Image: ${img.RepoTags ? img.RepoTags.join(', ') : '<none>'}`);
@@ -424,6 +527,58 @@ await image.remove({ force: true });
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
#### Create Custom Networks
@@ -432,7 +587,7 @@ console.log('Image removed');
// Create an overlay network (for swarm)
const network = await docker.createNetwork({
Name: 'my-app-network',
Driver: 'overlay',
Driver: 'overlay', // 'bridge', 'overlay', 'host', 'none', 'macvlan'
EnableIPv6: false,
Attachable: true,
});
@@ -440,11 +595,88 @@ const network = await docker.createNetwork({
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
```typescript
// Get all networks
const networks = await docker.getNetworks();
const networks = await docker.listNetworks();
networks.forEach((net) => {
console.log(`Network: ${net.Name} (${net.Driver})`);
@@ -456,7 +688,7 @@ networks.forEach((net) => {
const appNetwork = await docker.getNetworkByName('my-app-network');
// Get containers connected to this network
const containers = await appNetwork.getContainersOnNetwork();
const containers = await appNetwork.listContainersOnNetwork();
console.log(`Containers on network: ${containers.length}`);
```
@@ -523,7 +755,7 @@ console.log(`Service deployed: ${service.ID}`);
```typescript
// List all services
const services = await docker.getServices();
const services = await docker.listServices();
services.forEach((service) => {
console.log(`Service: ${service.Spec.Name}`);
@@ -566,7 +798,7 @@ const secret = await docker.createSecret({
console.log(`Secret created: ${secret.ID}`);
// List all secrets
const secrets = await docker.getSecrets();
const secrets = await docker.listSecrets();
secrets.forEach((s) => {
console.log(`Secret: ${s.Spec.Name}`);
console.log(` Labels:`, s.Spec.Labels);
@@ -868,7 +1100,7 @@ async function healthCheckService() {
try {
await docker.ping();
const containers = await docker.getContainers();
const containers = await docker.listContainers();
const unhealthy = containers.filter(c => c.State !== 'running');
if (unhealthy.length > 0) {
@@ -904,15 +1136,17 @@ async function healthCheckService() {
## 🆕 Recent Updates
### Version 2.1.0 - Architecture & Features
### Version 3.0.0+ - Architecture & Stability
-**Clean OOP Architecture**: Refactored to Facade pattern with DockerHost as single entry point
-**Container Streaming**: Added `streamLogs()`, `attach()`, and `exec()` methods
-**Flexible Descriptors**: Support both string references and class instances
-**Complete Container API**: All lifecycle methods (start, stop, remove, logs, inspect, stats)
-**DockerResource Base Class**: Consistent patterns across all resources
- 🔧 **Improved Type Safety**: Better TypeScript definitions throughout
- 📚 **Enhanced Documentation**: Comprehensive examples and migration guides
-**Container Streaming**: Real-time `streamLogs()`, `attach()`, and `exec()` methods for interactive containers
-**Flexible Descriptors**: Support both string references and class instances in all creation methods
-**Complete Container API**: Full lifecycle methods (start, stop, remove, logs, inspect, stats)
-**DockerResource Base Class**: Consistent patterns and type safety across all resources
- **Health Check Support**: New `ping()` method to verify Docker daemon availability
- 🐛 **Fixed Circular Dependencies**: Resolved Node.js module loading issues with type-only imports
- 🔧 **Improved Type Safety**: Better TypeScript definitions and interfaces throughout
- 📚 **Enhanced Documentation**: Comprehensive examples, migration guides, and real-world use cases
## License and Legal Information

View File

@@ -22,13 +22,13 @@ tap.test('should create a docker swarm', async () => {
// Containers
tap.test('should list containers', async () => {
const containers = await testDockerHost.getContainers();
const containers = await testDockerHost.listContainers();
console.log(containers);
});
// Networks
tap.test('should list networks', async () => {
const networks = await testDockerHost.getNetworks();
const networks = await testDockerHost.listNetworks();
console.log(networks);
});
@@ -86,7 +86,7 @@ tap.test('should activate swarm mode', async () => {
});
tap.test('should list all services', async (tools) => {
const services = await testDockerHost.getServices();
const services = await testDockerHost.listServices();
console.log(services);
});
@@ -165,11 +165,27 @@ tap.test('should expose a working DockerImageStore', async () => {
);
});
// CONTAINER GETTERS
tap.test('should return undefined for non-existent container', async () => {
const container = await testDockerHost.getContainerById('invalid-container-id-12345');
expect(container).toEqual(undefined);
});
tap.test('should return container for valid container ID', async () => {
const containers = await testDockerHost.listContainers();
if (containers.length > 0) {
const validId = containers[0].Id;
const container = await testDockerHost.getContainerById(validId);
expect(container).toBeInstanceOf(docker.DockerContainer);
expect(container?.Id).toEqual(validId);
}
});
// CONTAINER STREAMING FEATURES
let testContainer: docker.DockerContainer;
tap.test('should get an existing container for streaming tests', async () => {
const containers = await testDockerHost.getContainers();
const containers = await testDockerHost.listContainers();
// Use the first running container we find
testContainer = containers.find((c) => c.State === 'running');
@@ -302,6 +318,119 @@ tap.test('should complete container tests', async () => {
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 () => {
await testDockerHost.stop();
});

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@apiclient.xyz/docker',
version: '3.0.0',
version: '5.1.0',
description: 'Provides easy communication with Docker remote API from Node.js, with TypeScript support.'
}

View File

@@ -1,4 +1,4 @@
import { DockerHost } from './classes.host.js';
import type { DockerHost } from './classes.host.js';
/**
* Abstract base class for all Docker resources.

View File

@@ -10,7 +10,7 @@ export class DockerContainer extends DockerResource {
/**
* Internal: Get all containers
* Public API: Use dockerHost.getContainers() instead
* Public API: Use dockerHost.listContainers() instead
*/
public static async _list(
dockerHostArg: DockerHost,
@@ -28,13 +28,14 @@ export class DockerContainer extends DockerResource {
/**
* Internal: Get a container by ID
* Public API: Use dockerHost.getContainerById(id) instead
* Returns undefined if container does not exist
*/
public static async _fromId(
dockerHostArg: DockerHost,
containerId: string,
): Promise<DockerContainer> {
const response = await dockerHostArg.request('GET', `/containers/${containerId}/json`);
return new DockerContainer(dockerHostArg, response.body);
): Promise<DockerContainer | undefined> {
const containers = await this._list(dockerHostArg);
return containers.find((container) => container.Id === containerId);
}
/**
@@ -328,6 +329,7 @@ export class DockerContainer extends DockerResource {
): Promise<{
stream: plugins.smartstream.stream.Duplex;
close: () => Promise<void>;
inspect: () => Promise<interfaces.IExecInspectInfo>;
}> {
// Step 1: Create exec instance
const createResponse = await this.dockerHost.request('POST', `/containers/${this.Id}/exec`, {
@@ -385,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 {
stream: duplexStream,
close,
inspect,
};
}
}

View File

@@ -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
* @param userArg
@@ -129,9 +148,9 @@ export class DockerHost {
// ==============
/**
* Gets all networks
* Lists all networks
*/
public async getNetworks() {
public async listNetworks() {
return await DockerNetwork._list(this);
}
@@ -156,16 +175,17 @@ export class DockerHost {
// ==============
/**
* Gets all containers
* Lists all containers
*/
public async getContainers() {
public async listContainers() {
return await DockerContainer._list(this);
}
/**
* Gets a container by ID
* Returns undefined if container does not exist
*/
public async getContainerById(containerId: string) {
public async getContainerById(containerId: string): Promise<DockerContainer | undefined> {
return await DockerContainer._fromId(this, containerId);
}
@@ -183,9 +203,9 @@ export class DockerHost {
// ==============
/**
* Gets all services
* Lists all services
*/
public async getServices() {
public async listServices() {
return await DockerService._list(this);
}
@@ -210,9 +230,9 @@ export class DockerHost {
// ==============
/**
* Gets all images
* Lists all images
*/
public async getImages() {
public async listImages() {
return await DockerImage._list(this);
}
@@ -247,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
*/
@@ -259,9 +308,9 @@ export class DockerHost {
// ==============
/**
* Gets all secrets
* Lists all secrets
*/
public async getSecrets() {
public async listSecrets() {
return await DockerSecret._list(this);
}

View File

@@ -12,7 +12,7 @@ export class DockerImage extends DockerResource {
/**
* Internal: Get all images
* Public API: Use dockerHost.getImages() instead
* Public API: Use dockerHost.listImages() instead
*/
public static async _list(dockerHost: DockerHost) {
const images: DockerImage[] = [];

View File

@@ -51,20 +51,12 @@ export class DockerNetwork extends DockerResource {
const response = await dockerHost.request('POST', '/networks/create', {
Name: networkCreationDescriptor.Name,
CheckDuplicate: true,
Driver: 'overlay',
EnableIPv6: false,
/* IPAM: {
Driver: 'default',
Config: [
{
Subnet: `172.20.${networkCreationDescriptor.NetworkNumber}.0/16`,
IPRange: `172.20.${networkCreationDescriptor.NetworkNumber}.0/24`,
Gateway: `172.20.${networkCreationDescriptor.NetworkNumber}.11`
}
]
}, */
Internal: false,
Attachable: true,
Driver: networkCreationDescriptor.Driver || 'overlay',
EnableIPv6: networkCreationDescriptor.EnableIPv6 || false,
IPAM: networkCreationDescriptor.IPAM,
Internal: networkCreationDescriptor.Internal || false,
Attachable: networkCreationDescriptor.Attachable !== undefined ? networkCreationDescriptor.Attachable : true,
Labels: networkCreationDescriptor.Labels,
Ingress: false,
});
if (response.statusCode < 300) {
@@ -129,7 +121,7 @@ export class DockerNetwork extends DockerResource {
);
}
public async getContainersOnNetwork(): Promise<
public async listContainersOnNetwork(): Promise<
Array<{
Name: string;
EndpointID: string;
@@ -151,7 +143,7 @@ export class DockerNetwork extends DockerResource {
}
public async getContainersOnNetworkForService(serviceArg: DockerService) {
const containersOnNetwork = await this.getContainersOnNetwork();
const containersOnNetwork = await this.listContainersOnNetwork();
const containersOfService = containersOnNetwork.filter((container) => {
return container.Name.startsWith(serviceArg.Spec.Name);
});

View File

@@ -10,7 +10,7 @@ export class DockerSecret extends DockerResource {
/**
* Internal: Get all secrets
* Public API: Use dockerHost.getSecrets() instead
* Public API: Use dockerHost.listSecrets() instead
*/
public static async _list(dockerHostArg: DockerHost) {
const response = await dockerHostArg.request('GET', '/secrets');

View File

@@ -12,7 +12,7 @@ export class DockerService extends DockerResource {
/**
* Internal: Get all services
* Public API: Use dockerHost.getServices() instead
* Public API: Use dockerHost.listServices() instead
*/
public static async _list(dockerHost: DockerHost) {
const services: DockerService[] = [];

View File

@@ -10,3 +10,41 @@ export interface IContainerCreationDescriptor {
/** Network names (strings) or DockerNetwork instances */
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;
};
}

View File

@@ -3,4 +3,18 @@
*/
export interface INetworkCreationDescriptor {
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;
}