BREAKING CHANGE(DockerHost): Refactor public API to DockerHost facade; introduce DockerResource base; make resource static methods internal; support flexible descriptors and stream compatibility

This commit is contained in:
2025-11-24 12:20:30 +00:00
parent cc9c20882e
commit 6fe70e0a1d
16 changed files with 1388 additions and 335 deletions

512
readme.md
View File

@@ -2,11 +2,16 @@
> **Powerful TypeScript client for Docker Remote API** - Build, manage, and orchestrate Docker containers, images, networks, and swarm services with type-safe elegance.
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## 🚀 Features
- 🎯 **Full TypeScript Support** - Complete type definitions for all Docker API entities
- 🔄 **Async/Await Ready** - Modern promise-based architecture for seamless async operations
- 📦 **Container Management** - Create, list, inspect, and manage containers effortlessly
- 📦 **Container Management** - Full lifecycle control: create, start, stop, remove, inspect containers
- 🔌 **Interactive Containers** - Stream logs, attach to processes, execute commands in real-time
- 🖼️ **Image Handling** - Pull from registries, build from tarballs, export, and manage tags
- 🌐 **Network Operations** - Create and manage Docker networks with full IPAM support
- 🔐 **Secrets Management** - Handle Docker secrets securely in swarm mode
@@ -23,7 +28,7 @@
pnpm add @apiclient.xyz/docker
# Using npm
npm install @apiclient.xyz/docker --save
npm install @apiclient.xyz/docker
# Using yarn
yarn add @apiclient.xyz/docker
@@ -46,10 +51,40 @@ console.log('✅ Docker is running');
const containers = await docker.getContainers();
console.log(`Found ${containers.length} containers`);
// Get a specific container and interact with it
const container = await docker.getContainerById('abc123');
await container.start();
// Stream logs in real-time
const logStream = await container.streamLogs({ follow: true });
logStream.on('data', (chunk) => console.log(chunk.toString()));
// Don't forget to clean up
await docker.stop();
```
## 🏗️ Clean Architecture
The module follows a **Facade pattern** with `DockerHost` as the single entry point:
```typescript
const docker = new DockerHost({});
// All operations go through DockerHost
const containers = await docker.getContainers(); // 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
const image = await docker.createImageFromRegistry({ imageUrl: 'nginx' });
// Resources support both strings and instances
await docker.createService({
image: 'nginx:latest', // String works!
networks: ['my-network'], // String array works!
secrets: [secretInstance] // Or use actual instances
});
```
## 🔌 Socket Path Configuration
The library determines which Docker socket to use in the following priority order:
@@ -160,39 +195,178 @@ containers.forEach((container) => {
#### Get Container by ID
```typescript
import { DockerContainer } from '@apiclient.xyz/docker';
const container = await DockerContainer.getContainerById(docker, 'abc123');
const container = await docker.getContainerById('abc123');
if (container) {
console.log(`Found: ${container.Names[0]}`);
console.log(`Running: ${container.State === 'running'}`);
}
```
#### Container Lifecycle Operations
```typescript
// Get a container
const container = await docker.getContainerById('abc123');
// Start the container
await container.start();
console.log('Container started');
// Stop the container (with optional timeout)
await container.stop({ t: 10 }); // 10 seconds graceful stop
console.log('Container stopped');
// Restart by starting again
await container.start();
// Remove the container
await container.remove({ force: true, v: true }); // force + remove volumes
console.log('Container removed');
```
#### Inspect Container Details
```typescript
const container = await docker.getContainerById('abc123');
// Get detailed information
const details = await container.inspect();
console.log('Container details:', details);
// Or just refresh the container state
await container.refresh();
console.log('Updated state:', container.State);
```
#### Get Container Logs
```typescript
// Get logs as a string (one-shot)
const logs = await container.logs({
stdout: true,
stderr: true,
timestamps: true,
tail: 100, // Last 100 lines
});
console.log(logs);
```
#### Stream Logs in Real-Time 🔥
```typescript
// Stream logs continuously (follow mode)
const logStream = await container.streamLogs({
stdout: true,
stderr: true,
timestamps: true,
tail: 50, // Start with last 50 lines, then follow
});
logStream.on('data', (chunk) => {
console.log(chunk.toString());
});
logStream.on('error', (err) => {
console.error('Stream error:', err);
});
// Stop streaming when done
// logStream.destroy();
```
#### Attach to Container Process 🔥
Attach to the container's main process (PID 1) for interactive session:
```typescript
const { stream, close } = await container.attach({
stdin: true,
stdout: true,
stderr: true,
logs: true, // Include previous logs
});
// Pipe to/from process streams
process.stdin.pipe(stream);
stream.pipe(process.stdout);
// Handle stream events
stream.on('end', () => {
console.log('Attachment ended');
});
// Later: detach cleanly
await close();
```
#### Execute Commands in Container 🔥
Run commands inside a running container:
```typescript
// Execute a command
const { stream, close } = await container.exec('ls -la /app', {
tty: true,
user: 'root',
workingDir: '/app',
env: ['DEBUG=true'],
});
// Handle output
stream.on('data', (chunk) => {
console.log(chunk.toString());
});
stream.on('end', async () => {
console.log('Command finished');
await close();
});
// Execute with array of arguments
const { stream: stream2, close: close2 } = await container.exec(
['bash', '-c', 'echo "Hello from container"'],
{ tty: true }
);
```
#### Get Container Stats
```typescript
// Get stats (one-shot)
const stats = await container.stats({ stream: false });
console.log('CPU Usage:', stats.cpu_stats);
console.log('Memory Usage:', stats.memory_stats);
```
#### Create Containers
```typescript
const newContainer = await docker.createContainer({
Hostname: 'my-app',
Domainname: 'local',
networks: ['my-network'], // Can use string or DockerNetwork instance
});
console.log(`Container created: ${newContainer.Id}`);
```
### 🖼️ Image Management
#### Pull Images from Registry
```typescript
import { DockerImage } from '@apiclient.xyz/docker';
// Pull from Docker Hub
const image = await DockerImage.createFromRegistry(docker, {
creationObject: {
imageUrl: 'nginx',
imageTag: 'alpine', // Optional, defaults to 'latest'
},
const image = await docker.createImageFromRegistry({
imageUrl: 'nginx',
imageTag: 'alpine', // Optional, defaults to 'latest'
});
console.log(`Image pulled: ${image.RepoTags[0]}`);
console.log(`Size: ${(image.Size / 1024 / 1024).toFixed(2)} MB`);
// Pull from private registry
const privateImage = await DockerImage.createFromRegistry(docker, {
creationObject: {
imageUrl: 'registry.example.com/my-app',
imageTag: 'v2.0.0',
},
const privateImage = await docker.createImageFromRegistry({
imageUrl: 'registry.example.com/my-app',
imageTag: 'v2.0.0',
});
```
@@ -200,16 +374,12 @@ const privateImage = await DockerImage.createFromRegistry(docker, {
```typescript
import * as fs from 'fs';
import { DockerImage } from '@apiclient.xyz/docker';
// Import from a tar file
const tarStream = fs.createReadStream('./my-image.tar');
const importedImage = await DockerImage.createFromTarStream(docker, {
tarStream,
creationObject: {
imageUrl: 'my-app',
imageTag: 'v1.0.0',
},
const importedImage = await docker.createImageFromTarStream(tarStream, {
imageUrl: 'my-app',
imageTag: 'v1.0.0',
});
console.log(`Imported: ${importedImage.RepoTags[0]}`);
@@ -219,7 +389,7 @@ console.log(`Imported: ${importedImage.RepoTags[0]}`);
```typescript
// Get image by name
const image = await DockerImage.getImageByName(docker, 'nginx:alpine');
const image = await docker.getImageByName('nginx:alpine');
// Export to tar stream
const exportStream = await image.exportToTarStream();
@@ -233,18 +403,6 @@ writeStream.on('finish', () => {
});
```
#### Tag Images
```typescript
// Tag an existing image
await DockerImage.tagImageByIdOrName(docker, 'nginx:alpine', {
registry: 'myregistry.com',
imageName: 'web-server',
imageTag: 'v1.0.0',
});
// Result: myregistry.com/web-server:v1.0.0
```
#### List All Images
```typescript
@@ -258,31 +416,25 @@ images.forEach((img) => {
});
```
#### Remove Images
```typescript
const image = await docker.getImageByName('nginx:alpine');
await image.remove({ force: true });
console.log('Image removed');
```
### 🌐 Network Management
#### Create Custom Networks
```typescript
import { DockerNetwork } from '@apiclient.xyz/docker';
// Create a bridge network
const network = await DockerNetwork.createNetwork(docker, {
// Create an overlay network (for swarm)
const network = await docker.createNetwork({
Name: 'my-app-network',
Driver: 'bridge',
Driver: 'overlay',
EnableIPv6: false,
IPAM: {
Driver: 'default',
Config: [
{
Subnet: '172.28.0.0/16',
Gateway: '172.28.0.1',
},
],
},
Labels: {
project: 'my-app',
environment: 'production',
},
Attachable: true,
});
console.log(`Network created: ${network.Name} (${network.Id})`);
@@ -301,7 +453,7 @@ networks.forEach((net) => {
});
// Get specific network by name
const appNetwork = await DockerNetwork.getNetworkByName(docker, 'my-app-network');
const appNetwork = await docker.getNetworkByName('my-app-network');
// Get containers connected to this network
const containers = await appNetwork.getContainersOnNetwork();
@@ -311,7 +463,7 @@ console.log(`Containers on network: ${containers.length}`);
#### Remove a Network
```typescript
const network = await DockerNetwork.getNetworkByName(docker, 'my-app-network');
const network = await docker.getNetworkByName('my-app-network');
await network.remove();
console.log('Network removed');
```
@@ -329,39 +481,35 @@ console.log('Swarm mode activated');
#### Deploy Services
```typescript
import { DockerService, DockerImage, DockerNetwork, DockerSecret } from '@apiclient.xyz/docker';
// Create prerequisites
const network = await DockerNetwork.createNetwork(docker, {
const network = await docker.createNetwork({
Name: 'app-network',
Driver: 'overlay', // Use overlay for swarm
});
const image = await DockerImage.createFromRegistry(docker, {
creationObject: {
imageUrl: 'nginx',
imageTag: 'latest',
},
const image = await docker.createImageFromRegistry({
imageUrl: 'nginx',
imageTag: 'latest',
});
const secret = await DockerSecret.createSecret(docker, {
const secret = await docker.createSecret({
name: 'api-key',
version: '1.0.0',
contentArg: 'super-secret-key',
labels: { app: 'my-app' },
});
// Create a service
const service = await DockerService.createService(docker, {
// Create a service (supports both strings and instances!)
const service = await docker.createService({
name: 'web-api',
image: image,
image: image, // Or use string: 'nginx:latest'
labels: {
app: 'api',
version: '1.0.0',
},
networks: [network],
networks: [network], // Or use strings: ['app-network']
networkAlias: 'api',
secrets: [secret],
secrets: [secret], // Or use strings: ['api-key']
ports: ['80:3000'], // host:container
resources: {
memorySizeMB: 512,
@@ -380,13 +528,13 @@ const services = await docker.getServices();
services.forEach((service) => {
console.log(`Service: ${service.Spec.Name}`);
console.log(` Image: ${service.Spec.TaskTemplate.ContainerSpec.Image}`);
if (service.Spec.Mode.Replicated) {
console.log(` Replicas: ${service.Spec.Mode.Replicated.Replicas}`);
}
});
// Get service by name
const myService = await DockerService.getServiceByName(docker, 'web-api');
const myService = await docker.getServiceByName('web-api');
// Refresh service state
await myService.refresh();
// Check if service needs update
const needsUpdate = await myService.needsUpdate();
@@ -404,10 +552,8 @@ console.log('Service removed');
Secrets are only available in Docker Swarm mode.
```typescript
import { DockerSecret } from '@apiclient.xyz/docker';
// Create a secret
const secret = await DockerSecret.createSecret(docker, {
const secret = await docker.createSecret({
name: 'database-password',
version: '1.0.0',
contentArg: 'my-super-secret-password',
@@ -420,14 +566,14 @@ const secret = await DockerSecret.createSecret(docker, {
console.log(`Secret created: ${secret.ID}`);
// List all secrets
const secrets = await DockerSecret.getSecrets(docker);
const secrets = await docker.getSecrets();
secrets.forEach((s) => {
console.log(`Secret: ${s.Spec.Name}`);
console.log(` Labels:`, s.Spec.Labels);
});
// Get secret by name
const dbSecret = await DockerSecret.getSecretByName(docker, 'database-password');
const dbSecret = await docker.getSecretByName('database-password');
// Update secret content
await dbSecret.update('new-password-value');
@@ -437,24 +583,27 @@ await dbSecret.remove();
console.log('Secret removed');
```
### 💾 S3 Image Storage
### 💾 Image Storage
Store and retrieve Docker images from S3-compatible storage:
Store and retrieve Docker images from local storage or S3:
```typescript
// Configure S3 storage for the image store
// Store image to local storage
const imageStream = fs.createReadStream('./my-app.tar');
await docker.storeImage('my-app-v1', imageStream);
console.log('Image stored locally');
// Retrieve image from storage
const storedImageStream = await docker.retrieveImage('my-app-v1');
storedImageStream.pipe(fs.createWriteStream('./restored-image.tar'));
// Configure S3 storage (optional)
await docker.addS3Storage({
endpoint: 's3.amazonaws.com',
accessKey: 'AKIAIOSFODNN7EXAMPLE',
accessSecret: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
bucketName: 'my-docker-images',
});
// Store an image to S3
const imageStream = fs.createReadStream('./my-app.tar');
await docker.imageStore.storeImage('my-app-v1', imageStream);
console.log('Image stored to S3');
```
### 📊 Event Monitoring
@@ -498,14 +647,12 @@ await docker.auth({
console.log('✅ Authenticated with registry');
// Or read credentials from Docker config file
const authToken = await docker.getAuthTokenFromDockerConfig('registry.example.com');
await docker.getAuthTokenFromDockerConfig('registry.example.com');
// Now you can pull private images
const privateImage = await DockerImage.createFromRegistry(docker, {
creationObject: {
imageUrl: 'registry.example.com/private/app',
imageTag: 'latest',
},
const privateImage = await docker.createImageFromRegistry({
imageUrl: 'registry.example.com/private/app',
imageTag: 'latest',
});
```
@@ -516,7 +663,7 @@ const privateImage = await DockerImage.createFromRegistry(docker, {
Deploy a complete multi-service application stack:
```typescript
import { DockerHost, DockerNetwork, DockerSecret, DockerService, DockerImage } from '@apiclient.xyz/docker';
import { DockerHost } from '@apiclient.xyz/docker';
async function deployStack() {
const docker = new DockerHost({});
@@ -527,7 +674,7 @@ async function deployStack() {
console.log('✅ Swarm initialized');
// Create overlay network for service communication
const network = await DockerNetwork.createNetwork(docker, {
const network = await docker.createNetwork({
Name: 'app-network',
Driver: 'overlay',
Attachable: true,
@@ -535,7 +682,7 @@ async function deployStack() {
console.log('✅ Network created');
// Create secrets
const dbPassword = await DockerSecret.createSecret(docker, {
const dbPassword = await docker.createSecret({
name: 'db-password',
version: '1.0.0',
contentArg: 'strong-database-password',
@@ -543,30 +690,14 @@ async function deployStack() {
});
console.log('✅ Secrets created');
// Pull images
const postgresImage = await DockerImage.createFromRegistry(docker, {
creationObject: {
imageUrl: 'postgres',
imageTag: '14-alpine',
},
});
const appImage = await DockerImage.createFromRegistry(docker, {
creationObject: {
imageUrl: 'my-app',
imageTag: 'latest',
},
});
console.log('✅ Images pulled');
// Deploy database service
const dbService = await DockerService.createService(docker, {
const dbService = await docker.createService({
name: 'postgres-db',
image: postgresImage,
image: 'postgres:14-alpine', // Using string for convenience
labels: { tier: 'database' },
networks: [network],
networks: ['app-network'], // Using string array
networkAlias: 'postgres',
secrets: [dbPassword],
secrets: ['db-password'], // Using string array
ports: [],
resources: {
memorySizeMB: 1024,
@@ -575,13 +706,13 @@ async function deployStack() {
console.log('✅ Database service deployed');
// Deploy application service
const appService = await DockerService.createService(docker, {
const appService = await docker.createService({
name: 'web-app',
image: appImage,
image: 'my-app:latest',
labels: { tier: 'application' },
networks: [network],
networks: ['app-network'],
networkAlias: 'app',
secrets: [dbPassword],
secrets: ['db-password'],
ports: ['80:3000'],
resources: {
memorySizeMB: 512,
@@ -595,6 +726,49 @@ async function deployStack() {
deployStack().catch(console.error);
```
### Container Debugging Session
Interactive debugging session with a running container:
```typescript
async function debugContainer(containerId: string) {
const docker = new DockerHost({});
await docker.start();
const container = await docker.getContainerById(containerId);
// First, check container state
await container.inspect();
console.log(`Container: ${container.Names[0]}`);
console.log(`State: ${container.State}`);
// Get recent logs
const logs = await container.logs({ tail: 50 });
console.log('Recent logs:', logs);
// Stream live logs in one terminal
console.log('\n--- Live Logs ---');
const logStream = await container.streamLogs({ timestamps: true });
logStream.on('data', (chunk) => {
process.stdout.write(chunk);
});
// Execute diagnostic commands
console.log('\n--- Running Diagnostics ---');
const { stream, close } = await container.exec('ps aux', { tty: true });
stream.on('data', (chunk) => {
console.log(chunk.toString());
});
stream.on('end', async () => {
console.log('\nDiagnostics complete');
await close();
await docker.stop();
});
}
```
### Image Pipeline: Pull, Tag, Export
```typescript
@@ -603,23 +777,13 @@ async function imagePipeline() {
await docker.start();
// Pull latest image
const image = await DockerImage.createFromRegistry(docker, {
creationObject: {
imageUrl: 'node',
imageTag: '18-alpine',
},
const image = await docker.createImageFromRegistry({
imageUrl: 'node',
imageTag: '18-alpine',
});
console.log('✅ Image pulled');
// Tag for private registry
await DockerImage.tagImageByIdOrName(docker, 'node:18-alpine', {
registry: 'registry.company.com',
imageName: 'base/node',
imageTag: 'v18-alpine',
});
console.log('✅ Image tagged');
// Export to tar
// Export to tar for backup/transfer
const exportStream = await image.exportToTarStream();
const writeStream = fs.createWriteStream('./node-18-alpine.tar');
@@ -631,6 +795,11 @@ async function imagePipeline() {
});
console.log('✅ Image exported to tar');
// Store in image store (with S3 backup if configured)
const tarStream = fs.createReadStream('./node-18-alpine.tar');
await docker.storeImage('node-18-alpine-backup', tarStream);
console.log('✅ Image stored in image store');
await docker.stop();
}
```
@@ -645,7 +814,11 @@ import type {
IImageCreationDescriptor,
IServiceCreationDescriptor,
ISecretCreationDescriptor,
IContainerCreationDescriptor,
INetworkCreationDescriptor,
TLabels,
TPorts,
DockerResource,
} from '@apiclient.xyz/docker';
// Full IntelliSense support
@@ -678,55 +851,68 @@ const docker = new DockerHost({
await docker.start();
// Build and push process
const image = await DockerImage.createFromTarStream(docker, {
tarStream: buildArtifactStream,
creationObject: {
imageUrl: 'my-app',
imageTag: process.env.CI_COMMIT_SHA,
},
const buildStream = fs.createReadStream('./build-artifact.tar');
const image = await docker.createImageFromTarStream(buildStream, {
imageUrl: 'my-app',
imageTag: process.env.CI_COMMIT_SHA,
});
await DockerImage.tagImageByIdOrName(docker, `my-app:${process.env.CI_COMMIT_SHA}`, {
registry: 'registry.company.com',
imageName: 'production/my-app',
imageTag: 'latest',
});
// Push to registry (authentication required)
// Note: Pushing requires proper registry authentication
console.log(`✅ Image built: my-app:${process.env.CI_COMMIT_SHA}`);
```
### Dynamic Service Scaling
### Health Check Service
```typescript
// Monitor and scale services based on load
const services = await docker.getServices();
const webService = services.find(s => s.Spec.Name === 'web-app');
async function healthCheckService() {
const docker = new DockerHost({});
if (webService && webService.Spec.Mode.Replicated) {
const currentReplicas = webService.Spec.Mode.Replicated.Replicas;
console.log(`Current replicas: ${currentReplicas}`);
try {
await docker.ping();
const containers = await docker.getContainers();
// Scale based on your metrics
// (Scaling API would need to be implemented)
const unhealthy = containers.filter(c => c.State !== 'running');
if (unhealthy.length > 0) {
console.warn(`⚠️ ${unhealthy.length} containers not running`);
// Send alerts, restart services, etc.
}
return { healthy: true, containers: containers.length };
} catch (error) {
console.error('❌ Docker health check failed:', error);
return { healthy: false, error: error.message };
}
}
```
## 📖 API Documentation
- **Package Repository**: [https://code.foss.global/apiclient.xyz/docker](https://code.foss.global/apiclient.xyz/docker)
- **npm Package**: [https://www.npmjs.com/package/@apiclient.xyz/docker](https://www.npmjs.com/package/@apiclient.xyz/docker)
- **Docker Engine API Reference**: [https://docs.docker.com/engine/api/latest/](https://docs.docker.com/engine/api/latest/)
- **Issues & Bug Reports**: [https://code.foss.global/apiclient.xyz/docker/issues](https://code.foss.global/apiclient.xyz/docker/issues)
## 🔑 Key Concepts
- **DockerHost**: Main entry point for Docker API communication
- **DockerHost**: Main entry point - all operations flow through this facade
- **Flexible Descriptors**: Accept both string references and class instances
- **Health Checks**: Use `ping()` method to verify Docker daemon accessibility
- **Socket Path Priority**: Constructor option → `DOCKER_HOST` env → CI mode → default socket
- **Swarm Mode Required**: Services and secrets require Docker Swarm to be activated
- **Type Safety**: Full TypeScript support with comprehensive interfaces
- **Streaming Support**: Real-time event monitoring and tar stream operations
- **S3 Integration**: Built-in image storage/retrieval from S3-compatible storage
- **Streaming Support**: Real-time log streaming, event monitoring, and container attachment
- **Interactive Containers**: Attach to processes, execute commands, stream logs
- **Clean Architecture**: Facade pattern with internal delegation for maintainability
## 🆕 Recent Updates
### Version 2.1.0 - Architecture & Features
-**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
## License and Legal Information