391 lines
12 KiB
Markdown
391 lines
12 KiB
Markdown
# 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
|
||
The module has been restructured to follow a clean OOP Facade pattern:
|
||
- **DockerHost** is now the single entry point for all Docker operations
|
||
- All resource classes extend abstract `DockerResource` base class
|
||
- Static methods are prefixed with `_` to indicate internal use
|
||
- Public API is exclusively through DockerHost methods
|
||
|
||
### Key Changes
|
||
|
||
**1. Factory Pattern**
|
||
- All resource creation/retrieval goes through DockerHost:
|
||
```typescript
|
||
// Old (deprecated):
|
||
const container = await DockerContainer.getContainers(dockerHost);
|
||
const network = await DockerNetwork.createNetwork(dockerHost, descriptor);
|
||
|
||
// New (clean API):
|
||
const containers = await dockerHost.listContainers();
|
||
const network = await dockerHost.createNetwork(descriptor);
|
||
```
|
||
|
||
**2. Container Management Methods Added**
|
||
The DockerContainer class now has full CRUD and streaming operations:
|
||
|
||
**Lifecycle:**
|
||
- `container.start()` - Start container
|
||
- `container.stop(options?)` - Stop container
|
||
- `container.remove(options?)` - Remove container
|
||
- `container.refresh()` - Reload state
|
||
|
||
**Information:**
|
||
- `container.inspect()` - Get detailed info
|
||
- `container.logs(options)` - Get logs as string (one-shot)
|
||
- `container.stats(options)` - Get stats
|
||
|
||
**Streaming & Interactive:**
|
||
- `container.streamLogs(options)` - Stream logs continuously (follow mode)
|
||
- `container.attach(options)` - Attach to main process (PID 1) with bidirectional stream
|
||
- `container.exec(command, options)` - Execute commands in container interactively
|
||
|
||
**Example - Stream Logs:**
|
||
```typescript
|
||
const container = await dockerHost.getContainerById('abc123');
|
||
const logStream = await container.streamLogs({ timestamps: true });
|
||
|
||
logStream.on('data', (chunk) => {
|
||
console.log(chunk.toString());
|
||
});
|
||
```
|
||
|
||
**Example - Attach to Container:**
|
||
```typescript
|
||
const { stream, close } = await container.attach({
|
||
stdin: true,
|
||
stdout: true,
|
||
stderr: true
|
||
});
|
||
|
||
// Pipe to/from process
|
||
process.stdin.pipe(stream);
|
||
stream.pipe(process.stdout);
|
||
|
||
// Later: detach
|
||
await close();
|
||
```
|
||
|
||
**Example - Execute Command:**
|
||
```typescript
|
||
const { stream, close } = await container.exec('ls -la /app', {
|
||
tty: true
|
||
});
|
||
|
||
stream.on('data', (chunk) => {
|
||
console.log(chunk.toString());
|
||
});
|
||
|
||
stream.on('end', async () => {
|
||
await close();
|
||
});
|
||
```
|
||
|
||
**3. DockerResource Base Class**
|
||
All resource classes now extend `DockerResource`:
|
||
- Consistent `dockerHost` property (not `dockerHostRef`)
|
||
- Required `refresh()` method
|
||
- Standardized constructor pattern
|
||
|
||
**4. ImageStore Encapsulation**
|
||
- `dockerHost.imageStore` is now private
|
||
- Use `dockerHost.storeImage(name, stream)` instead
|
||
- Use `dockerHost.retrieveImage(name)` instead
|
||
|
||
**5. Creation Descriptors Support Both Primitives and Instances**
|
||
Interfaces now accept both strings and class instances:
|
||
```typescript
|
||
// Both work:
|
||
await dockerHost.createService({
|
||
image: 'nginx:latest', // String
|
||
networks: ['my-network'], // String array
|
||
secrets: ['my-secret'] // String array
|
||
});
|
||
|
||
await dockerHost.createService({
|
||
image: imageInstance, // DockerImage instance
|
||
networks: [networkInstance], // DockerNetwork array
|
||
secrets: [secretInstance] // DockerSecret array
|
||
});
|
||
```
|
||
|
||
### Migration Guide
|
||
Replace all static method calls with dockerHost methods:
|
||
- `DockerContainer.getContainers(host)` → `dockerHost.listContainers()`
|
||
- `DockerImage.createFromRegistry(host, opts)` → `dockerHost.createImageFromRegistry(opts)`
|
||
- `DockerService.createService(host, desc)` → `dockerHost.createService(desc)`
|
||
- `dockerHost.imageStore.storeImage(...)` → `dockerHost.storeImage(...)`
|
||
|
||
## smartrequest v5+ Migration (2025-11-17)
|
||
|
||
### Breaking Change
|
||
smartrequest v5.0.0+ returns web `ReadableStream` objects (Web Streams API) instead of Node.js streams.
|
||
|
||
### Solution Implemented
|
||
All streaming methods now convert web ReadableStreams to Node.js streams using:
|
||
```typescript
|
||
plugins.smartstream.nodewebhelpers.convertWebReadableToNodeReadable(webStream)
|
||
```
|
||
|
||
### Files Modified
|
||
- `ts/classes.host.ts`:
|
||
- `requestStreaming()` - Converts web stream to Node.js stream before returning
|
||
- `getEventObservable()` - Works with converted Node.js stream
|
||
|
||
- `ts/classes.image.ts`:
|
||
- `createFromTarStream()` - Uses converted Node.js stream for event handling
|
||
- `exportToTarStream()` - Uses converted Node.js stream for backpressure management
|
||
|
||
### Testing
|
||
- Build: All 11 type errors resolved
|
||
- Tests: Node.js tests pass (DockerHost, DockerContainer, DockerImage, DockerImageStore)
|
||
|
||
### Notes
|
||
- The conversion maintains backward compatibility with existing code expecting Node.js stream methods (`.on()`, `.emit()`, `.pause()`, `.resume()`)
|
||
- smartstream's `nodewebhelpers` module provides bidirectional conversion utilities between web and Node.js streams
|