Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d043d20a8 | |||
| 6fe70e0a1d |
15
changelog.md
15
changelog.md
@@ -1,5 +1,20 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Refactored architecture: DockerHost is now the single public entry point (Facade) for all operations; direct static calls like DockerImage.createFromRegistry(...) are now internal and replaced by DockerHost.createImageFromRegistry(...) and similar factory methods.
|
||||||
|
- Introduced DockerResource abstract base class used by all resource classes (DockerContainer, DockerImage, DockerNetwork, DockerSecret, DockerService) with a required refresh() method and standardized dockerHost property.
|
||||||
|
- Static methods on resource classes were renamed / scoped as internal (prefixed with _): _list, _fromName/_fromId, _create, _createFromRegistry, _createFromTarStream, _build, etc. Consumers should call DockerHost methods instead.
|
||||||
|
- Creation descriptor interfaces (container, service, etc.) now accept either string identifiers or resource instances (e.g. image: string | DockerImage, networks: (string | DockerNetwork)[], secrets: (string | DockerSecret)[]). DockerHost resolves instances internally.
|
||||||
|
- DockerImageStore imageStore has been made private on DockerHost; new public methods DockerHost.storeImage(name, stream) and DockerHost.retrieveImage(name) provide access to the image store.
|
||||||
|
- 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).
|
||||||
|
- 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.
|
||||||
|
|
||||||
## 2025-11-18 - 2.1.0 - feat(DockerHost)
|
## 2025-11-18 - 2.1.0 - feat(DockerHost)
|
||||||
Add DockerHost.ping() to check Docker daemon availability and document health-check usage
|
Add DockerHost.ping() to check Docker daemon availability and document health-check usage
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@apiclient.xyz/docker",
|
"name": "@apiclient.xyz/docker",
|
||||||
"version": "2.1.0",
|
"version": "3.0.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.",
|
||||||
"private": false,
|
"private": false,
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --verbose --logfile --timeout 600)",
|
"test": "(tstest test/ --verbose --logfile --timeout 300)",
|
||||||
"build": "(tsbuild --web --allowimplicitany)",
|
"build": "(tsbuild --web --allowimplicitany)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
|
|||||||
118
readme.hints.md
118
readme.hints.md
@@ -1,5 +1,123 @@
|
|||||||
# Docker Module - Development Hints
|
# Docker Module - Development Hints
|
||||||
|
|
||||||
|
## 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.getContainers();
|
||||||
|
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.getContainers()`
|
||||||
|
- `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)
|
## smartrequest v5+ Migration (2025-11-17)
|
||||||
|
|
||||||
### Breaking Change
|
### Breaking Change
|
||||||
|
|||||||
512
readme.md
512
readme.md
@@ -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.
|
> **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
|
## 🚀 Features
|
||||||
|
|
||||||
- 🎯 **Full TypeScript Support** - Complete type definitions for all Docker API entities
|
- 🎯 **Full TypeScript Support** - Complete type definitions for all Docker API entities
|
||||||
- 🔄 **Async/Await Ready** - Modern promise-based architecture for seamless async operations
|
- 🔄 **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
|
- 🖼️ **Image Handling** - Pull from registries, build from tarballs, export, and manage tags
|
||||||
- 🌐 **Network Operations** - Create and manage Docker networks with full IPAM support
|
- 🌐 **Network Operations** - Create and manage Docker networks with full IPAM support
|
||||||
- 🔐 **Secrets Management** - Handle Docker secrets securely in swarm mode
|
- 🔐 **Secrets Management** - Handle Docker secrets securely in swarm mode
|
||||||
@@ -23,7 +28,7 @@
|
|||||||
pnpm add @apiclient.xyz/docker
|
pnpm add @apiclient.xyz/docker
|
||||||
|
|
||||||
# Using npm
|
# Using npm
|
||||||
npm install @apiclient.xyz/docker --save
|
npm install @apiclient.xyz/docker
|
||||||
|
|
||||||
# Using yarn
|
# Using yarn
|
||||||
yarn add @apiclient.xyz/docker
|
yarn add @apiclient.xyz/docker
|
||||||
@@ -46,10 +51,40 @@ console.log('✅ Docker is running');
|
|||||||
const containers = await docker.getContainers();
|
const containers = await docker.getContainers();
|
||||||
console.log(`Found ${containers.length} containers`);
|
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
|
// Don't forget to clean up
|
||||||
await docker.stop();
|
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
|
## 🔌 Socket Path Configuration
|
||||||
|
|
||||||
The library determines which Docker socket to use in the following priority order:
|
The library determines which Docker socket to use in the following priority order:
|
||||||
@@ -160,39 +195,178 @@ containers.forEach((container) => {
|
|||||||
#### Get Container by ID
|
#### Get Container by ID
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { DockerContainer } from '@apiclient.xyz/docker';
|
const container = await docker.getContainerById('abc123');
|
||||||
|
|
||||||
const container = await DockerContainer.getContainerById(docker, 'abc123');
|
|
||||||
if (container) {
|
if (container) {
|
||||||
console.log(`Found: ${container.Names[0]}`);
|
console.log(`Found: ${container.Names[0]}`);
|
||||||
console.log(`Running: ${container.State === 'running'}`);
|
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
|
### 🖼️ Image Management
|
||||||
|
|
||||||
#### Pull Images from Registry
|
#### Pull Images from Registry
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { DockerImage } from '@apiclient.xyz/docker';
|
|
||||||
|
|
||||||
// Pull from Docker Hub
|
// Pull from Docker Hub
|
||||||
const image = await DockerImage.createFromRegistry(docker, {
|
const image = await docker.createImageFromRegistry({
|
||||||
creationObject: {
|
imageUrl: 'nginx',
|
||||||
imageUrl: 'nginx',
|
imageTag: 'alpine', // Optional, defaults to 'latest'
|
||||||
imageTag: 'alpine', // Optional, defaults to 'latest'
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Image pulled: ${image.RepoTags[0]}`);
|
console.log(`Image pulled: ${image.RepoTags[0]}`);
|
||||||
console.log(`Size: ${(image.Size / 1024 / 1024).toFixed(2)} MB`);
|
console.log(`Size: ${(image.Size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
// Pull from private registry
|
// Pull from private registry
|
||||||
const privateImage = await DockerImage.createFromRegistry(docker, {
|
const privateImage = await docker.createImageFromRegistry({
|
||||||
creationObject: {
|
imageUrl: 'registry.example.com/my-app',
|
||||||
imageUrl: 'registry.example.com/my-app',
|
imageTag: 'v2.0.0',
|
||||||
imageTag: 'v2.0.0',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -200,16 +374,12 @@ const privateImage = await DockerImage.createFromRegistry(docker, {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { DockerImage } from '@apiclient.xyz/docker';
|
|
||||||
|
|
||||||
// Import from a tar file
|
// Import from a tar file
|
||||||
const tarStream = fs.createReadStream('./my-image.tar');
|
const tarStream = fs.createReadStream('./my-image.tar');
|
||||||
const importedImage = await DockerImage.createFromTarStream(docker, {
|
const importedImage = await docker.createImageFromTarStream(tarStream, {
|
||||||
tarStream,
|
imageUrl: 'my-app',
|
||||||
creationObject: {
|
imageTag: 'v1.0.0',
|
||||||
imageUrl: 'my-app',
|
|
||||||
imageTag: 'v1.0.0',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Imported: ${importedImage.RepoTags[0]}`);
|
console.log(`Imported: ${importedImage.RepoTags[0]}`);
|
||||||
@@ -219,7 +389,7 @@ console.log(`Imported: ${importedImage.RepoTags[0]}`);
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Get image by name
|
// Get image by name
|
||||||
const image = await DockerImage.getImageByName(docker, 'nginx:alpine');
|
const image = await docker.getImageByName('nginx:alpine');
|
||||||
|
|
||||||
// Export to tar stream
|
// Export to tar stream
|
||||||
const exportStream = await image.exportToTarStream();
|
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
|
#### List All Images
|
||||||
|
|
||||||
```typescript
|
```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
|
### 🌐 Network Management
|
||||||
|
|
||||||
#### Create Custom Networks
|
#### Create Custom Networks
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { DockerNetwork } from '@apiclient.xyz/docker';
|
// Create an overlay network (for swarm)
|
||||||
|
const network = await docker.createNetwork({
|
||||||
// Create a bridge network
|
|
||||||
const network = await DockerNetwork.createNetwork(docker, {
|
|
||||||
Name: 'my-app-network',
|
Name: 'my-app-network',
|
||||||
Driver: 'bridge',
|
Driver: 'overlay',
|
||||||
EnableIPv6: false,
|
EnableIPv6: false,
|
||||||
IPAM: {
|
Attachable: true,
|
||||||
Driver: 'default',
|
|
||||||
Config: [
|
|
||||||
{
|
|
||||||
Subnet: '172.28.0.0/16',
|
|
||||||
Gateway: '172.28.0.1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
Labels: {
|
|
||||||
project: 'my-app',
|
|
||||||
environment: 'production',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Network created: ${network.Name} (${network.Id})`);
|
console.log(`Network created: ${network.Name} (${network.Id})`);
|
||||||
@@ -301,7 +453,7 @@ networks.forEach((net) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get specific network by name
|
// 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
|
// Get containers connected to this network
|
||||||
const containers = await appNetwork.getContainersOnNetwork();
|
const containers = await appNetwork.getContainersOnNetwork();
|
||||||
@@ -311,7 +463,7 @@ console.log(`Containers on network: ${containers.length}`);
|
|||||||
#### Remove a Network
|
#### Remove a Network
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const network = await DockerNetwork.getNetworkByName(docker, 'my-app-network');
|
const network = await docker.getNetworkByName('my-app-network');
|
||||||
await network.remove();
|
await network.remove();
|
||||||
console.log('Network removed');
|
console.log('Network removed');
|
||||||
```
|
```
|
||||||
@@ -329,39 +481,35 @@ console.log('Swarm mode activated');
|
|||||||
#### Deploy Services
|
#### Deploy Services
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { DockerService, DockerImage, DockerNetwork, DockerSecret } from '@apiclient.xyz/docker';
|
|
||||||
|
|
||||||
// Create prerequisites
|
// Create prerequisites
|
||||||
const network = await DockerNetwork.createNetwork(docker, {
|
const network = await docker.createNetwork({
|
||||||
Name: 'app-network',
|
Name: 'app-network',
|
||||||
Driver: 'overlay', // Use overlay for swarm
|
Driver: 'overlay', // Use overlay for swarm
|
||||||
});
|
});
|
||||||
|
|
||||||
const image = await DockerImage.createFromRegistry(docker, {
|
const image = await docker.createImageFromRegistry({
|
||||||
creationObject: {
|
imageUrl: 'nginx',
|
||||||
imageUrl: 'nginx',
|
imageTag: 'latest',
|
||||||
imageTag: 'latest',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const secret = await DockerSecret.createSecret(docker, {
|
const secret = await docker.createSecret({
|
||||||
name: 'api-key',
|
name: 'api-key',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
contentArg: 'super-secret-key',
|
contentArg: 'super-secret-key',
|
||||||
labels: { app: 'my-app' },
|
labels: { app: 'my-app' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a service
|
// Create a service (supports both strings and instances!)
|
||||||
const service = await DockerService.createService(docker, {
|
const service = await docker.createService({
|
||||||
name: 'web-api',
|
name: 'web-api',
|
||||||
image: image,
|
image: image, // Or use string: 'nginx:latest'
|
||||||
labels: {
|
labels: {
|
||||||
app: 'api',
|
app: 'api',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
},
|
},
|
||||||
networks: [network],
|
networks: [network], // Or use strings: ['app-network']
|
||||||
networkAlias: 'api',
|
networkAlias: 'api',
|
||||||
secrets: [secret],
|
secrets: [secret], // Or use strings: ['api-key']
|
||||||
ports: ['80:3000'], // host:container
|
ports: ['80:3000'], // host:container
|
||||||
resources: {
|
resources: {
|
||||||
memorySizeMB: 512,
|
memorySizeMB: 512,
|
||||||
@@ -380,13 +528,13 @@ const services = await docker.getServices();
|
|||||||
services.forEach((service) => {
|
services.forEach((service) => {
|
||||||
console.log(`Service: ${service.Spec.Name}`);
|
console.log(`Service: ${service.Spec.Name}`);
|
||||||
console.log(` Image: ${service.Spec.TaskTemplate.ContainerSpec.Image}`);
|
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
|
// 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
|
// Check if service needs update
|
||||||
const needsUpdate = await myService.needsUpdate();
|
const needsUpdate = await myService.needsUpdate();
|
||||||
@@ -404,10 +552,8 @@ console.log('Service removed');
|
|||||||
Secrets are only available in Docker Swarm mode.
|
Secrets are only available in Docker Swarm mode.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { DockerSecret } from '@apiclient.xyz/docker';
|
|
||||||
|
|
||||||
// Create a secret
|
// Create a secret
|
||||||
const secret = await DockerSecret.createSecret(docker, {
|
const secret = await docker.createSecret({
|
||||||
name: 'database-password',
|
name: 'database-password',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
contentArg: 'my-super-secret-password',
|
contentArg: 'my-super-secret-password',
|
||||||
@@ -420,14 +566,14 @@ const secret = await DockerSecret.createSecret(docker, {
|
|||||||
console.log(`Secret created: ${secret.ID}`);
|
console.log(`Secret created: ${secret.ID}`);
|
||||||
|
|
||||||
// List all secrets
|
// List all secrets
|
||||||
const secrets = await DockerSecret.getSecrets(docker);
|
const secrets = await docker.getSecrets();
|
||||||
secrets.forEach((s) => {
|
secrets.forEach((s) => {
|
||||||
console.log(`Secret: ${s.Spec.Name}`);
|
console.log(`Secret: ${s.Spec.Name}`);
|
||||||
console.log(` Labels:`, s.Spec.Labels);
|
console.log(` Labels:`, s.Spec.Labels);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get secret by name
|
// Get secret by name
|
||||||
const dbSecret = await DockerSecret.getSecretByName(docker, 'database-password');
|
const dbSecret = await docker.getSecretByName('database-password');
|
||||||
|
|
||||||
// Update secret content
|
// Update secret content
|
||||||
await dbSecret.update('new-password-value');
|
await dbSecret.update('new-password-value');
|
||||||
@@ -437,24 +583,27 @@ await dbSecret.remove();
|
|||||||
console.log('Secret removed');
|
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
|
```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({
|
await docker.addS3Storage({
|
||||||
endpoint: 's3.amazonaws.com',
|
endpoint: 's3.amazonaws.com',
|
||||||
accessKey: 'AKIAIOSFODNN7EXAMPLE',
|
accessKey: 'AKIAIOSFODNN7EXAMPLE',
|
||||||
accessSecret: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
accessSecret: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||||
bucketName: 'my-docker-images',
|
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
|
### 📊 Event Monitoring
|
||||||
@@ -498,14 +647,12 @@ await docker.auth({
|
|||||||
console.log('✅ Authenticated with registry');
|
console.log('✅ Authenticated with registry');
|
||||||
|
|
||||||
// Or read credentials from Docker config file
|
// 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
|
// Now you can pull private images
|
||||||
const privateImage = await DockerImage.createFromRegistry(docker, {
|
const privateImage = await docker.createImageFromRegistry({
|
||||||
creationObject: {
|
imageUrl: 'registry.example.com/private/app',
|
||||||
imageUrl: 'registry.example.com/private/app',
|
imageTag: 'latest',
|
||||||
imageTag: 'latest',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -516,7 +663,7 @@ const privateImage = await DockerImage.createFromRegistry(docker, {
|
|||||||
Deploy a complete multi-service application stack:
|
Deploy a complete multi-service application stack:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { DockerHost, DockerNetwork, DockerSecret, DockerService, DockerImage } from '@apiclient.xyz/docker';
|
import { DockerHost } from '@apiclient.xyz/docker';
|
||||||
|
|
||||||
async function deployStack() {
|
async function deployStack() {
|
||||||
const docker = new DockerHost({});
|
const docker = new DockerHost({});
|
||||||
@@ -527,7 +674,7 @@ async function deployStack() {
|
|||||||
console.log('✅ Swarm initialized');
|
console.log('✅ Swarm initialized');
|
||||||
|
|
||||||
// Create overlay network for service communication
|
// Create overlay network for service communication
|
||||||
const network = await DockerNetwork.createNetwork(docker, {
|
const network = await docker.createNetwork({
|
||||||
Name: 'app-network',
|
Name: 'app-network',
|
||||||
Driver: 'overlay',
|
Driver: 'overlay',
|
||||||
Attachable: true,
|
Attachable: true,
|
||||||
@@ -535,7 +682,7 @@ async function deployStack() {
|
|||||||
console.log('✅ Network created');
|
console.log('✅ Network created');
|
||||||
|
|
||||||
// Create secrets
|
// Create secrets
|
||||||
const dbPassword = await DockerSecret.createSecret(docker, {
|
const dbPassword = await docker.createSecret({
|
||||||
name: 'db-password',
|
name: 'db-password',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
contentArg: 'strong-database-password',
|
contentArg: 'strong-database-password',
|
||||||
@@ -543,30 +690,14 @@ async function deployStack() {
|
|||||||
});
|
});
|
||||||
console.log('✅ Secrets created');
|
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
|
// Deploy database service
|
||||||
const dbService = await DockerService.createService(docker, {
|
const dbService = await docker.createService({
|
||||||
name: 'postgres-db',
|
name: 'postgres-db',
|
||||||
image: postgresImage,
|
image: 'postgres:14-alpine', // Using string for convenience
|
||||||
labels: { tier: 'database' },
|
labels: { tier: 'database' },
|
||||||
networks: [network],
|
networks: ['app-network'], // Using string array
|
||||||
networkAlias: 'postgres',
|
networkAlias: 'postgres',
|
||||||
secrets: [dbPassword],
|
secrets: ['db-password'], // Using string array
|
||||||
ports: [],
|
ports: [],
|
||||||
resources: {
|
resources: {
|
||||||
memorySizeMB: 1024,
|
memorySizeMB: 1024,
|
||||||
@@ -575,13 +706,13 @@ async function deployStack() {
|
|||||||
console.log('✅ Database service deployed');
|
console.log('✅ Database service deployed');
|
||||||
|
|
||||||
// Deploy application service
|
// Deploy application service
|
||||||
const appService = await DockerService.createService(docker, {
|
const appService = await docker.createService({
|
||||||
name: 'web-app',
|
name: 'web-app',
|
||||||
image: appImage,
|
image: 'my-app:latest',
|
||||||
labels: { tier: 'application' },
|
labels: { tier: 'application' },
|
||||||
networks: [network],
|
networks: ['app-network'],
|
||||||
networkAlias: 'app',
|
networkAlias: 'app',
|
||||||
secrets: [dbPassword],
|
secrets: ['db-password'],
|
||||||
ports: ['80:3000'],
|
ports: ['80:3000'],
|
||||||
resources: {
|
resources: {
|
||||||
memorySizeMB: 512,
|
memorySizeMB: 512,
|
||||||
@@ -595,6 +726,49 @@ async function deployStack() {
|
|||||||
deployStack().catch(console.error);
|
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
|
### Image Pipeline: Pull, Tag, Export
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -603,23 +777,13 @@ async function imagePipeline() {
|
|||||||
await docker.start();
|
await docker.start();
|
||||||
|
|
||||||
// Pull latest image
|
// Pull latest image
|
||||||
const image = await DockerImage.createFromRegistry(docker, {
|
const image = await docker.createImageFromRegistry({
|
||||||
creationObject: {
|
imageUrl: 'node',
|
||||||
imageUrl: 'node',
|
imageTag: '18-alpine',
|
||||||
imageTag: '18-alpine',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
console.log('✅ Image pulled');
|
console.log('✅ Image pulled');
|
||||||
|
|
||||||
// Tag for private registry
|
// Export to tar for backup/transfer
|
||||||
await DockerImage.tagImageByIdOrName(docker, 'node:18-alpine', {
|
|
||||||
registry: 'registry.company.com',
|
|
||||||
imageName: 'base/node',
|
|
||||||
imageTag: 'v18-alpine',
|
|
||||||
});
|
|
||||||
console.log('✅ Image tagged');
|
|
||||||
|
|
||||||
// Export to tar
|
|
||||||
const exportStream = await image.exportToTarStream();
|
const exportStream = await image.exportToTarStream();
|
||||||
const writeStream = fs.createWriteStream('./node-18-alpine.tar');
|
const writeStream = fs.createWriteStream('./node-18-alpine.tar');
|
||||||
|
|
||||||
@@ -631,6 +795,11 @@ async function imagePipeline() {
|
|||||||
});
|
});
|
||||||
console.log('✅ Image exported to tar');
|
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();
|
await docker.stop();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -645,7 +814,11 @@ import type {
|
|||||||
IImageCreationDescriptor,
|
IImageCreationDescriptor,
|
||||||
IServiceCreationDescriptor,
|
IServiceCreationDescriptor,
|
||||||
ISecretCreationDescriptor,
|
ISecretCreationDescriptor,
|
||||||
|
IContainerCreationDescriptor,
|
||||||
|
INetworkCreationDescriptor,
|
||||||
TLabels,
|
TLabels,
|
||||||
|
TPorts,
|
||||||
|
DockerResource,
|
||||||
} from '@apiclient.xyz/docker';
|
} from '@apiclient.xyz/docker';
|
||||||
|
|
||||||
// Full IntelliSense support
|
// Full IntelliSense support
|
||||||
@@ -678,55 +851,68 @@ const docker = new DockerHost({
|
|||||||
await docker.start();
|
await docker.start();
|
||||||
|
|
||||||
// Build and push process
|
// Build and push process
|
||||||
const image = await DockerImage.createFromTarStream(docker, {
|
const buildStream = fs.createReadStream('./build-artifact.tar');
|
||||||
tarStream: buildArtifactStream,
|
const image = await docker.createImageFromTarStream(buildStream, {
|
||||||
creationObject: {
|
imageUrl: 'my-app',
|
||||||
imageUrl: 'my-app',
|
imageTag: process.env.CI_COMMIT_SHA,
|
||||||
imageTag: process.env.CI_COMMIT_SHA,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await DockerImage.tagImageByIdOrName(docker, `my-app:${process.env.CI_COMMIT_SHA}`, {
|
console.log(`✅ Image built: 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dynamic Service Scaling
|
### Health Check Service
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Monitor and scale services based on load
|
async function healthCheckService() {
|
||||||
const services = await docker.getServices();
|
const docker = new DockerHost({});
|
||||||
const webService = services.find(s => s.Spec.Name === 'web-app');
|
|
||||||
|
|
||||||
if (webService && webService.Spec.Mode.Replicated) {
|
try {
|
||||||
const currentReplicas = webService.Spec.Mode.Replicated.Replicas;
|
await docker.ping();
|
||||||
console.log(`Current replicas: ${currentReplicas}`);
|
const containers = await docker.getContainers();
|
||||||
|
|
||||||
// Scale based on your metrics
|
const unhealthy = containers.filter(c => c.State !== 'running');
|
||||||
// (Scaling API would need to be implemented)
|
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
|
## 📖 API Documentation
|
||||||
|
|
||||||
- **Package Repository**: [https://code.foss.global/apiclient.xyz/docker](https://code.foss.global/apiclient.xyz/docker)
|
- **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/)
|
- **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
|
## 🔑 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
|
- **Health Checks**: Use `ping()` method to verify Docker daemon accessibility
|
||||||
- **Socket Path Priority**: Constructor option → `DOCKER_HOST` env → CI mode → default socket
|
- **Socket Path Priority**: Constructor option → `DOCKER_HOST` env → CI mode → default socket
|
||||||
- **Swarm Mode Required**: Services and secrets require Docker Swarm to be activated
|
- **Swarm Mode Required**: Services and secrets require Docker Swarm to be activated
|
||||||
- **Type Safety**: Full TypeScript support with comprehensive interfaces
|
- **Type Safety**: Full TypeScript support with comprehensive interfaces
|
||||||
- **Streaming Support**: Real-time event monitoring and tar stream operations
|
- **Streaming Support**: Real-time log streaming, event monitoring, and container attachment
|
||||||
- **S3 Integration**: Built-in image storage/retrieval from S3-compatible storage
|
- **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
|
## License and Legal Information
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ tap.test('should list networks', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should create a network', async () => {
|
tap.test('should create a network', async () => {
|
||||||
const newNetwork = await docker.DockerNetwork.createNetwork(testDockerHost, {
|
const newNetwork = await testDockerHost.createNetwork({
|
||||||
Name: 'webgateway',
|
Name: 'webgateway',
|
||||||
});
|
});
|
||||||
expect(newNetwork).toBeInstanceOf(docker.DockerNetwork);
|
expect(newNetwork).toBeInstanceOf(docker.DockerNetwork);
|
||||||
@@ -41,20 +41,15 @@ tap.test('should create a network', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should remove a network', async () => {
|
tap.test('should remove a network', async () => {
|
||||||
const webgateway = await docker.DockerNetwork.getNetworkByName(
|
const webgateway = await testDockerHost.getNetworkByName('webgateway');
|
||||||
testDockerHost,
|
|
||||||
'webgateway',
|
|
||||||
);
|
|
||||||
await webgateway.remove();
|
await webgateway.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Images
|
// Images
|
||||||
tap.test('should pull an image from imagetag', async () => {
|
tap.test('should pull an image from imagetag', async () => {
|
||||||
const image = await docker.DockerImage.createFromRegistry(testDockerHost, {
|
const image = await testDockerHost.createImageFromRegistry({
|
||||||
creationObject: {
|
imageUrl: 'hosttoday/ht-docker-node',
|
||||||
imageUrl: 'hosttoday/ht-docker-node',
|
imageTag: 'alpine',
|
||||||
imageTag: 'alpine',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(image).toBeInstanceOf(docker.DockerImage);
|
expect(image).toBeInstanceOf(docker.DockerImage);
|
||||||
console.log(image);
|
console.log(image);
|
||||||
@@ -71,7 +66,7 @@ tap.test('should return a change Observable', async (tools) => {
|
|||||||
|
|
||||||
// SECRETS
|
// SECRETS
|
||||||
tap.test('should create a secret', async () => {
|
tap.test('should create a secret', async () => {
|
||||||
const mySecret = await docker.DockerSecret.createSecret(testDockerHost, {
|
const mySecret = await testDockerHost.createSecret({
|
||||||
name: 'testSecret',
|
name: 'testSecret',
|
||||||
version: '1.0.3',
|
version: '1.0.3',
|
||||||
contentArg: `{ "hi": "wow"}`,
|
contentArg: `{ "hi": "wow"}`,
|
||||||
@@ -81,10 +76,7 @@ tap.test('should create a secret', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should remove a secret by name', async () => {
|
tap.test('should remove a secret by name', async () => {
|
||||||
const mySecret = await docker.DockerSecret.getSecretByName(
|
const mySecret = await testDockerHost.getSecretByName('testSecret');
|
||||||
testDockerHost,
|
|
||||||
'testSecret',
|
|
||||||
);
|
|
||||||
await mySecret.remove();
|
await mySecret.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,24 +91,19 @@ tap.test('should list all services', async (tools) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should create a service', async () => {
|
tap.test('should create a service', async () => {
|
||||||
const testNetwork = await docker.DockerNetwork.createNetwork(testDockerHost, {
|
const testNetwork = await testDockerHost.createNetwork({
|
||||||
Name: 'testNetwork',
|
Name: 'testNetwork',
|
||||||
});
|
});
|
||||||
const testSecret = await docker.DockerSecret.createSecret(testDockerHost, {
|
const testSecret = await testDockerHost.createSecret({
|
||||||
name: 'testSecret',
|
name: 'testSecret',
|
||||||
version: '0.0.1',
|
version: '0.0.1',
|
||||||
labels: {},
|
labels: {},
|
||||||
contentArg: '{"hi": "wow"}',
|
contentArg: '{"hi": "wow"}',
|
||||||
});
|
});
|
||||||
const testImage = await docker.DockerImage.createFromRegistry(
|
const testImage = await testDockerHost.createImageFromRegistry({
|
||||||
testDockerHost,
|
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||||
{
|
});
|
||||||
creationObject: {
|
const testService = await testDockerHost.createService({
|
||||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const testService = await docker.DockerService.createService(testDockerHost, {
|
|
||||||
image: testImage,
|
image: testImage,
|
||||||
labels: {},
|
labels: {},
|
||||||
name: 'testService',
|
name: 'testService',
|
||||||
@@ -133,14 +120,9 @@ tap.test('should create a service', async () => {
|
|||||||
|
|
||||||
tap.test('should export images', async (toolsArg) => {
|
tap.test('should export images', async (toolsArg) => {
|
||||||
const done = toolsArg.defer();
|
const done = toolsArg.defer();
|
||||||
const testImage = await docker.DockerImage.createFromRegistry(
|
const testImage = await testDockerHost.createImageFromRegistry({
|
||||||
testDockerHost,
|
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||||
{
|
});
|
||||||
creationObject: {
|
|
||||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const fsWriteStream = plugins.smartfile.fsStream.createWriteStream(
|
const fsWriteStream = plugins.smartfile.fsStream.createWriteStream(
|
||||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||||
);
|
);
|
||||||
@@ -155,13 +137,10 @@ tap.test('should import images', async () => {
|
|||||||
const fsReadStream = plugins.smartfile.fsStream.createReadStream(
|
const fsReadStream = plugins.smartfile.fsStream.createReadStream(
|
||||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||||
);
|
);
|
||||||
const importedImage = await docker.DockerImage.createFromTarStream(
|
const importedImage = await testDockerHost.createImageFromTarStream(
|
||||||
testDockerHost,
|
fsReadStream,
|
||||||
{
|
{
|
||||||
tarStream: fsReadStream,
|
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||||
creationObject: {
|
|
||||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(importedImage).toBeInstanceOf(docker.DockerImage);
|
expect(importedImage).toBeInstanceOf(docker.DockerImage);
|
||||||
@@ -177,8 +156,8 @@ tap.test('should expose a working DockerImageStore', async () => {
|
|||||||
};
|
};
|
||||||
await testDockerHost.addS3Storage(s3Descriptor);
|
await testDockerHost.addS3Storage(s3Descriptor);
|
||||||
|
|
||||||
//
|
// Use the new public API instead of direct imageStore access
|
||||||
await testDockerHost.imageStore.storeImage(
|
await testDockerHost.storeImage(
|
||||||
'hello2',
|
'hello2',
|
||||||
plugins.smartfile.fsStream.createReadStream(
|
plugins.smartfile.fsStream.createReadStream(
|
||||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||||
@@ -186,6 +165,143 @@ tap.test('should expose a working DockerImageStore', async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// CONTAINER STREAMING FEATURES
|
||||||
|
let testContainer: docker.DockerContainer;
|
||||||
|
|
||||||
|
tap.test('should get an existing container for streaming tests', async () => {
|
||||||
|
const containers = await testDockerHost.getContainers();
|
||||||
|
|
||||||
|
// Use the first running container we find
|
||||||
|
testContainer = containers.find((c) => c.State === 'running');
|
||||||
|
|
||||||
|
if (!testContainer) {
|
||||||
|
throw new Error('No running containers found for streaming tests');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(testContainer).toBeInstanceOf(docker.DockerContainer);
|
||||||
|
console.log('Using existing container for tests:', testContainer.Names[0], testContainer.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should stream container logs', async (tools) => {
|
||||||
|
const done = tools.defer();
|
||||||
|
const logStream = await testContainer.streamLogs({
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
timestamps: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let receivedData = false;
|
||||||
|
|
||||||
|
logStream.on('data', (chunk) => {
|
||||||
|
console.log('Received log chunk:', chunk.toString().slice(0, 100));
|
||||||
|
receivedData = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
logStream.on('error', (error) => {
|
||||||
|
console.error('Stream error:', error);
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for 2 seconds to collect logs, then close
|
||||||
|
await tools.delayFor(2000);
|
||||||
|
logStream.destroy();
|
||||||
|
done.resolve();
|
||||||
|
|
||||||
|
await done.promise;
|
||||||
|
console.log('Log streaming test completed. Received data:', receivedData);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should get container logs (one-shot)', async () => {
|
||||||
|
const logs = await testContainer.logs({
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
tail: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(typeof logs).toEqual('string');
|
||||||
|
console.log('Container logs (last 10 lines):', logs.slice(0, 200));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should execute command in container', async (tools) => {
|
||||||
|
const done = tools.defer();
|
||||||
|
const { stream, close } = await testContainer.exec('echo "Hello from exec"', {
|
||||||
|
tty: false,
|
||||||
|
attachStdout: true,
|
||||||
|
attachStderr: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
output += chunk.toString();
|
||||||
|
console.log('Exec output:', chunk.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', async () => {
|
||||||
|
await close();
|
||||||
|
console.log('Exec completed. Full output:', output);
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('error', async (error) => {
|
||||||
|
console.error('Exec error:', error);
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await done.promise;
|
||||||
|
expect(output.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should attach to container', async (tools) => {
|
||||||
|
const done = tools.defer();
|
||||||
|
const { stream, close } = await testContainer.attach({
|
||||||
|
stream: true,
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
stdin: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let receivedData = false;
|
||||||
|
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
console.log('Attach received:', chunk.toString().slice(0, 100));
|
||||||
|
receivedData = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('error', async (error) => {
|
||||||
|
console.error('Attach error:', error);
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor for 2 seconds then detach
|
||||||
|
await tools.delayFor(2000);
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
|
||||||
|
await done.promise;
|
||||||
|
console.log('Attach test completed. Received data:', receivedData);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should get container stats', async () => {
|
||||||
|
const stats = await testContainer.stats({ stream: false });
|
||||||
|
expect(stats).toBeInstanceOf(Object);
|
||||||
|
console.log('Container stats keys:', Object.keys(stats));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should inspect container', async () => {
|
||||||
|
const inspection = await testContainer.inspect();
|
||||||
|
expect(inspection).toBeInstanceOf(Object);
|
||||||
|
expect(inspection.Id).toEqual(testContainer.Id);
|
||||||
|
console.log('Container state:', inspection.State?.Status);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should complete container tests', async () => {
|
||||||
|
// Using existing container, no cleanup needed
|
||||||
|
console.log('Container streaming tests completed');
|
||||||
|
});
|
||||||
|
|
||||||
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: '2.1.0',
|
version: '3.0.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.'
|
||||||
}
|
}
|
||||||
|
|||||||
27
ts/classes.base.ts
Normal file
27
ts/classes.base.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { DockerHost } from './classes.host.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for all Docker resources.
|
||||||
|
* Provides standardized patterns for resource management and lifecycle.
|
||||||
|
*/
|
||||||
|
export abstract class DockerResource {
|
||||||
|
/**
|
||||||
|
* Reference to the DockerHost that manages this resource.
|
||||||
|
* All API operations go through this host instance.
|
||||||
|
*/
|
||||||
|
protected readonly dockerHost: DockerHost;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Docker resource instance.
|
||||||
|
* @param dockerHost The DockerHost instance that manages this resource
|
||||||
|
*/
|
||||||
|
constructor(dockerHost: DockerHost) {
|
||||||
|
this.dockerHost = dockerHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes this resource's state from the Docker daemon.
|
||||||
|
* Implementations should fetch current data and update instance properties.
|
||||||
|
*/
|
||||||
|
abstract refresh(): Promise<void>;
|
||||||
|
}
|
||||||
@@ -2,21 +2,23 @@ import * as plugins from './plugins.js';
|
|||||||
import * as interfaces from './interfaces/index.js';
|
import * as interfaces from './interfaces/index.js';
|
||||||
|
|
||||||
import { DockerHost } from './classes.host.js';
|
import { DockerHost } from './classes.host.js';
|
||||||
|
import { DockerResource } from './classes.base.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
export class DockerContainer {
|
export class DockerContainer extends DockerResource {
|
||||||
// STATIC
|
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get all containers
|
* Internal: Get all containers
|
||||||
|
* Public API: Use dockerHost.getContainers() instead
|
||||||
*/
|
*/
|
||||||
public static async getContainers(
|
public static async _list(
|
||||||
dockerHostArg: DockerHost,
|
dockerHostArg: DockerHost,
|
||||||
): Promise<DockerContainer[]> {
|
): Promise<DockerContainer[]> {
|
||||||
const result: DockerContainer[] = [];
|
const result: DockerContainer[] = [];
|
||||||
const response = await dockerHostArg.request('GET', '/containers/json');
|
const response = await dockerHostArg.request('GET', '/containers/json');
|
||||||
|
|
||||||
// TODO: Think about getting the config by inpsecting the container
|
// TODO: Think about getting the config by inspecting the container
|
||||||
for (const containerResult of response.body) {
|
for (const containerResult of response.body) {
|
||||||
result.push(new DockerContainer(dockerHostArg, containerResult));
|
result.push(new DockerContainer(dockerHostArg, containerResult));
|
||||||
}
|
}
|
||||||
@@ -24,46 +26,49 @@ export class DockerContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gets an container by Id
|
* Internal: Get a container by ID
|
||||||
* @param containerId
|
* Public API: Use dockerHost.getContainerById(id) instead
|
||||||
*/
|
*/
|
||||||
public static async getContainerById(containerId: string) {
|
public static async _fromId(
|
||||||
// TODO: implement get container by id
|
dockerHostArg: DockerHost,
|
||||||
|
containerId: string,
|
||||||
|
): Promise<DockerContainer> {
|
||||||
|
const response = await dockerHostArg.request('GET', `/containers/${containerId}/json`);
|
||||||
|
return new DockerContainer(dockerHostArg, response.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create a container
|
* Internal: Create a container
|
||||||
|
* Public API: Use dockerHost.createContainer(descriptor) instead
|
||||||
*/
|
*/
|
||||||
public static async create(
|
public static async _create(
|
||||||
dockerHost: DockerHost,
|
dockerHost: DockerHost,
|
||||||
containerCreationDescriptor: interfaces.IContainerCreationDescriptor,
|
containerCreationDescriptor: interfaces.IContainerCreationDescriptor,
|
||||||
) {
|
): Promise<DockerContainer> {
|
||||||
// check for unique hostname
|
// Check for unique hostname
|
||||||
const existingContainers = await DockerContainer.getContainers(dockerHost);
|
const existingContainers = await DockerContainer._list(dockerHost);
|
||||||
const sameHostNameContainer = existingContainers.find((container) => {
|
const sameHostNameContainer = existingContainers.find((container) => {
|
||||||
// TODO implement HostName Detection;
|
// TODO implement HostName Detection;
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await dockerHost.request('POST', '/containers/create', {
|
const response = await dockerHost.request('POST', '/containers/create', {
|
||||||
Hostname: containerCreationDescriptor.Hostname,
|
Hostname: containerCreationDescriptor.Hostname,
|
||||||
Domainname: containerCreationDescriptor.Domainname,
|
Domainname: containerCreationDescriptor.Domainname,
|
||||||
User: 'root',
|
User: 'root',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode < 300) {
|
if (response.statusCode < 300) {
|
||||||
logger.log('info', 'Container created successfully');
|
logger.log('info', 'Container created successfully');
|
||||||
|
// Return the created container instance
|
||||||
|
return await DockerContainer._fromId(dockerHost, response.body.Id);
|
||||||
} else {
|
} else {
|
||||||
logger.log(
|
logger.log('error', 'There has been a problem when creating the container');
|
||||||
'error',
|
throw new Error(`Failed to create container: ${response.statusCode}`);
|
||||||
'There has been a problem when creating the container',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE PROPERTIES
|
||||||
// references
|
|
||||||
public dockerHost: DockerHost;
|
|
||||||
|
|
||||||
// properties
|
|
||||||
public Id: string;
|
public Id: string;
|
||||||
public Names: string[];
|
public Names: string[];
|
||||||
public Image: string;
|
public Image: string;
|
||||||
@@ -95,10 +100,294 @@ export class DockerContainer {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
public Mounts: any;
|
public Mounts: any;
|
||||||
|
|
||||||
constructor(dockerHostArg: DockerHost, dockerContainerObjectArg: any) {
|
constructor(dockerHostArg: DockerHost, dockerContainerObjectArg: any) {
|
||||||
this.dockerHost = dockerHostArg;
|
super(dockerHostArg);
|
||||||
Object.keys(dockerContainerObjectArg).forEach((keyArg) => {
|
Object.keys(dockerContainerObjectArg).forEach((keyArg) => {
|
||||||
this[keyArg] = dockerContainerObjectArg[keyArg];
|
this[keyArg] = dockerContainerObjectArg[keyArg];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// INSTANCE METHODS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes this container's state from the Docker daemon
|
||||||
|
*/
|
||||||
|
public async refresh(): Promise<void> {
|
||||||
|
const updated = await DockerContainer._fromId(this.dockerHost, this.Id);
|
||||||
|
Object.assign(this, updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inspects the container and returns detailed information
|
||||||
|
*/
|
||||||
|
public async inspect(): Promise<any> {
|
||||||
|
const response = await this.dockerHost.request('GET', `/containers/${this.Id}/json`);
|
||||||
|
// Update instance with fresh data
|
||||||
|
Object.assign(this, response.body);
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the container
|
||||||
|
*/
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
const response = await this.dockerHost.request('POST', `/containers/${this.Id}/start`);
|
||||||
|
if (response.statusCode >= 300) {
|
||||||
|
throw new Error(`Failed to start container: ${response.statusCode}`);
|
||||||
|
}
|
||||||
|
await this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the container
|
||||||
|
* @param options Options for stopping (e.g., timeout in seconds)
|
||||||
|
*/
|
||||||
|
public async stop(options?: { t?: number }): Promise<void> {
|
||||||
|
const queryParams = options?.t ? `?t=${options.t}` : '';
|
||||||
|
const response = await this.dockerHost.request('POST', `/containers/${this.Id}/stop${queryParams}`);
|
||||||
|
if (response.statusCode >= 300) {
|
||||||
|
throw new Error(`Failed to stop container: ${response.statusCode}`);
|
||||||
|
}
|
||||||
|
await this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the container
|
||||||
|
* @param options Options for removal (force, remove volumes, remove link)
|
||||||
|
*/
|
||||||
|
public async remove(options?: { force?: boolean; v?: boolean; link?: boolean }): Promise<void> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
if (options?.force) queryParams.append('force', '1');
|
||||||
|
if (options?.v) queryParams.append('v', '1');
|
||||||
|
if (options?.link) queryParams.append('link', '1');
|
||||||
|
|
||||||
|
const queryString = queryParams.toString();
|
||||||
|
const response = await this.dockerHost.request(
|
||||||
|
'DELETE',
|
||||||
|
`/containers/${this.Id}${queryString ? '?' + queryString : ''}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode >= 300) {
|
||||||
|
throw new Error(`Failed to remove container: ${response.statusCode}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets container logs
|
||||||
|
* @param options Log options (stdout, stderr, timestamps, tail, since, follow)
|
||||||
|
*/
|
||||||
|
public async logs(options?: {
|
||||||
|
stdout?: boolean;
|
||||||
|
stderr?: boolean;
|
||||||
|
timestamps?: boolean;
|
||||||
|
tail?: number | 'all';
|
||||||
|
since?: number;
|
||||||
|
follow?: boolean;
|
||||||
|
}): Promise<string> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('stdout', options?.stdout !== false ? '1' : '0');
|
||||||
|
queryParams.append('stderr', options?.stderr !== false ? '1' : '0');
|
||||||
|
if (options?.timestamps) queryParams.append('timestamps', '1');
|
||||||
|
if (options?.tail) queryParams.append('tail', options.tail.toString());
|
||||||
|
if (options?.since) queryParams.append('since', options.since.toString());
|
||||||
|
if (options?.follow) queryParams.append('follow', '1');
|
||||||
|
|
||||||
|
const response = await this.dockerHost.request('GET', `/containers/${this.Id}/logs?${queryParams.toString()}`);
|
||||||
|
|
||||||
|
// Docker returns logs with a special format (8 bytes header + payload)
|
||||||
|
// For simplicity, we'll return the raw body as string
|
||||||
|
return response.body.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets container stats
|
||||||
|
* @param options Stats options (stream for continuous stats)
|
||||||
|
*/
|
||||||
|
public async stats(options?: { stream?: boolean }): Promise<any> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('stream', options?.stream ? '1' : '0');
|
||||||
|
|
||||||
|
const response = await this.dockerHost.request('GET', `/containers/${this.Id}/stats?${queryParams.toString()}`);
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams container logs continuously (follow mode)
|
||||||
|
* Returns a readable stream that emits log data as it's produced
|
||||||
|
* @param options Log streaming options
|
||||||
|
*/
|
||||||
|
public async streamLogs(options?: {
|
||||||
|
stdout?: boolean;
|
||||||
|
stderr?: boolean;
|
||||||
|
timestamps?: boolean;
|
||||||
|
tail?: number | 'all';
|
||||||
|
since?: number;
|
||||||
|
}): Promise<plugins.smartstream.stream.Readable> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('stdout', options?.stdout !== false ? '1' : '0');
|
||||||
|
queryParams.append('stderr', options?.stderr !== false ? '1' : '0');
|
||||||
|
queryParams.append('follow', '1'); // Always follow for streaming
|
||||||
|
if (options?.timestamps) queryParams.append('timestamps', '1');
|
||||||
|
if (options?.tail) queryParams.append('tail', options.tail.toString());
|
||||||
|
if (options?.since) queryParams.append('since', options.since.toString());
|
||||||
|
|
||||||
|
const response = await this.dockerHost.requestStreaming(
|
||||||
|
'GET',
|
||||||
|
`/containers/${this.Id}/logs?${queryParams.toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// requestStreaming returns Node.js stream
|
||||||
|
return response as plugins.smartstream.stream.Readable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches to the container's main process (PID 1)
|
||||||
|
* Returns a duplex stream for bidirectional communication
|
||||||
|
* @param options Attach options
|
||||||
|
*/
|
||||||
|
public async attach(options?: {
|
||||||
|
stream?: boolean;
|
||||||
|
stdin?: boolean;
|
||||||
|
stdout?: boolean;
|
||||||
|
stderr?: boolean;
|
||||||
|
logs?: boolean;
|
||||||
|
}): Promise<{
|
||||||
|
stream: plugins.smartstream.stream.Duplex;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('stream', options?.stream !== false ? '1' : '0');
|
||||||
|
queryParams.append('stdin', options?.stdin ? '1' : '0');
|
||||||
|
queryParams.append('stdout', options?.stdout !== false ? '1' : '0');
|
||||||
|
queryParams.append('stderr', options?.stderr !== false ? '1' : '0');
|
||||||
|
if (options?.logs) queryParams.append('logs', '1');
|
||||||
|
|
||||||
|
const response = await this.dockerHost.requestStreaming(
|
||||||
|
'POST',
|
||||||
|
`/containers/${this.Id}/attach?${queryParams.toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a duplex stream for bidirectional communication
|
||||||
|
const nodeStream = response as plugins.smartstream.stream.Readable;
|
||||||
|
|
||||||
|
// Convert to duplex by wrapping in SmartDuplex
|
||||||
|
const duplexStream = new plugins.smartstream.SmartDuplex({
|
||||||
|
writeFunction: async (chunk) => {
|
||||||
|
// Write data is sent to the container's stdin
|
||||||
|
return chunk;
|
||||||
|
},
|
||||||
|
readableObjectMode: false,
|
||||||
|
writableObjectMode: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pipe container output to our duplex readable side
|
||||||
|
nodeStream.on('data', (chunk) => {
|
||||||
|
duplexStream.push(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeStream.on('end', () => {
|
||||||
|
duplexStream.push(null); // Signal end of stream
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeStream.on('error', (error) => {
|
||||||
|
duplexStream.destroy(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to close the attachment
|
||||||
|
const close = async () => {
|
||||||
|
duplexStream.end();
|
||||||
|
if (nodeStream.destroy) {
|
||||||
|
nodeStream.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
stream: duplexStream,
|
||||||
|
close,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a command in the container
|
||||||
|
* Returns a duplex stream for command interaction
|
||||||
|
* @param command Command to execute (string or array of strings)
|
||||||
|
* @param options Exec options
|
||||||
|
*/
|
||||||
|
public async exec(
|
||||||
|
command: string | string[],
|
||||||
|
options?: {
|
||||||
|
tty?: boolean;
|
||||||
|
attachStdin?: boolean;
|
||||||
|
attachStdout?: boolean;
|
||||||
|
attachStderr?: boolean;
|
||||||
|
env?: string[];
|
||||||
|
workingDir?: string;
|
||||||
|
user?: string;
|
||||||
|
}
|
||||||
|
): Promise<{
|
||||||
|
stream: plugins.smartstream.stream.Duplex;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}> {
|
||||||
|
// Step 1: Create exec instance
|
||||||
|
const createResponse = await this.dockerHost.request('POST', `/containers/${this.Id}/exec`, {
|
||||||
|
Cmd: typeof command === 'string' ? ['/bin/sh', '-c', command] : command,
|
||||||
|
AttachStdin: options?.attachStdin !== false,
|
||||||
|
AttachStdout: options?.attachStdout !== false,
|
||||||
|
AttachStderr: options?.attachStderr !== false,
|
||||||
|
Tty: options?.tty || false,
|
||||||
|
Env: options?.env || [],
|
||||||
|
WorkingDir: options?.workingDir,
|
||||||
|
User: options?.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
const execId = createResponse.body.Id;
|
||||||
|
|
||||||
|
// Step 2: Start exec instance with streaming response
|
||||||
|
const startResponse = await this.dockerHost.requestStreaming(
|
||||||
|
'POST',
|
||||||
|
`/exec/${execId}/start`,
|
||||||
|
undefined, // no stream input
|
||||||
|
{
|
||||||
|
Detach: false,
|
||||||
|
Tty: options?.tty || false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeStream = startResponse as plugins.smartstream.stream.Readable;
|
||||||
|
|
||||||
|
// Create duplex stream for bidirectional communication
|
||||||
|
const duplexStream = new plugins.smartstream.SmartDuplex({
|
||||||
|
writeFunction: async (chunk) => {
|
||||||
|
return chunk;
|
||||||
|
},
|
||||||
|
readableObjectMode: false,
|
||||||
|
writableObjectMode: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pipe exec output to duplex readable side
|
||||||
|
nodeStream.on('data', (chunk) => {
|
||||||
|
duplexStream.push(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeStream.on('end', () => {
|
||||||
|
duplexStream.push(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeStream.on('error', (error) => {
|
||||||
|
duplexStream.destroy(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = async () => {
|
||||||
|
duplexStream.end();
|
||||||
|
if (nodeStream.destroy) {
|
||||||
|
nodeStream.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
stream: duplexStream,
|
||||||
|
close,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as paths from './paths.js';
|
import * as paths from './paths.js';
|
||||||
|
import * as interfaces from './interfaces/index.js';
|
||||||
import { DockerContainer } from './classes.container.js';
|
import { DockerContainer } from './classes.container.js';
|
||||||
import { DockerNetwork } from './classes.network.js';
|
import { DockerNetwork } from './classes.network.js';
|
||||||
import { DockerService } from './classes.service.js';
|
import { DockerService } from './classes.service.js';
|
||||||
|
import { DockerSecret } from './classes.secret.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { DockerImageStore } from './classes.imagestore.js';
|
import { DockerImageStore } from './classes.imagestore.js';
|
||||||
import { DockerImage } from './classes.image.js';
|
import { DockerImage } from './classes.image.js';
|
||||||
@@ -26,7 +28,7 @@ export class DockerHost {
|
|||||||
*/
|
*/
|
||||||
public socketPath: string;
|
public socketPath: string;
|
||||||
private registryToken: string = '';
|
private registryToken: string = '';
|
||||||
public imageStore: DockerImageStore;
|
private imageStore: DockerImageStore; // Now private - use storeImage/retrieveImage instead
|
||||||
public smartBucket: plugins.smartbucket.SmartBucket;
|
public smartBucket: plugins.smartbucket.SmartBucket;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,70 +125,190 @@ export class DockerHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==============
|
// ==============
|
||||||
// NETWORKS
|
// NETWORKS - Public Factory API
|
||||||
// ==============
|
// ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gets all networks
|
* Gets all networks
|
||||||
*/
|
*/
|
||||||
public async getNetworks() {
|
public async getNetworks() {
|
||||||
return await DockerNetwork.getNetworks(this);
|
return await DockerNetwork._list(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create a network
|
* Gets a network by name
|
||||||
*/
|
|
||||||
public async createNetwork(
|
|
||||||
optionsArg: Parameters<typeof DockerNetwork.createNetwork>[1],
|
|
||||||
) {
|
|
||||||
return await DockerNetwork.createNetwork(this, optionsArg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get a network by name
|
|
||||||
*/
|
*/
|
||||||
public async getNetworkByName(networkNameArg: string) {
|
public async getNetworkByName(networkNameArg: string) {
|
||||||
return await DockerNetwork.getNetworkByName(this, networkNameArg);
|
return await DockerNetwork._fromName(this, networkNameArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a network
|
||||||
|
*/
|
||||||
|
public async createNetwork(
|
||||||
|
descriptor: interfaces.INetworkCreationDescriptor,
|
||||||
|
) {
|
||||||
|
return await DockerNetwork._create(this, descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============
|
// ==============
|
||||||
// CONTAINERS
|
// CONTAINERS - Public Factory API
|
||||||
// ==============
|
// ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gets all containers
|
* Gets all containers
|
||||||
*/
|
*/
|
||||||
public async getContainers() {
|
public async getContainers() {
|
||||||
const containerArray = await DockerContainer.getContainers(this);
|
return await DockerContainer._list(this);
|
||||||
return containerArray;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a container by ID
|
||||||
|
*/
|
||||||
|
public async getContainerById(containerId: string) {
|
||||||
|
return await DockerContainer._fromId(this, containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a container
|
||||||
|
*/
|
||||||
|
public async createContainer(
|
||||||
|
descriptor: interfaces.IContainerCreationDescriptor,
|
||||||
|
) {
|
||||||
|
return await DockerContainer._create(this, descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============
|
// ==============
|
||||||
// SERVICES
|
// SERVICES - Public Factory API
|
||||||
// ==============
|
// ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gets all services
|
* Gets all services
|
||||||
*/
|
*/
|
||||||
public async getServices() {
|
public async getServices() {
|
||||||
const serviceArray = await DockerService.getServices(this);
|
return await DockerService._list(this);
|
||||||
return serviceArray;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a service by name
|
||||||
|
*/
|
||||||
|
public async getServiceByName(serviceName: string) {
|
||||||
|
return await DockerService._fromName(this, serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a service
|
||||||
|
*/
|
||||||
|
public async createService(
|
||||||
|
descriptor: interfaces.IServiceCreationDescriptor,
|
||||||
|
) {
|
||||||
|
return await DockerService._create(this, descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============
|
// ==============
|
||||||
// IMAGES
|
// IMAGES - Public Factory API
|
||||||
// ==============
|
// ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get all images
|
* Gets all images
|
||||||
*/
|
*/
|
||||||
public async getImages() {
|
public async getImages() {
|
||||||
return await DockerImage.getImages(this);
|
return await DockerImage._list(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get an image by name
|
* Gets an image by name
|
||||||
*/
|
*/
|
||||||
public async getImageByName(imageNameArg: string) {
|
public async getImageByName(imageNameArg: string) {
|
||||||
return await DockerImage.getImageByName(this, imageNameArg);
|
return await DockerImage._fromName(this, imageNameArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an image from a registry
|
||||||
|
*/
|
||||||
|
public async createImageFromRegistry(
|
||||||
|
descriptor: interfaces.IImageCreationDescriptor,
|
||||||
|
) {
|
||||||
|
return await DockerImage._createFromRegistry(this, {
|
||||||
|
creationObject: descriptor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an image from a tar stream
|
||||||
|
*/
|
||||||
|
public async createImageFromTarStream(
|
||||||
|
tarStream: plugins.smartstream.stream.Readable,
|
||||||
|
descriptor: interfaces.IImageCreationDescriptor,
|
||||||
|
) {
|
||||||
|
return await DockerImage._createFromTarStream(this, {
|
||||||
|
creationObject: descriptor,
|
||||||
|
tarStream: tarStream,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an image from a Dockerfile
|
||||||
|
*/
|
||||||
|
public async buildImage(imageTag: string) {
|
||||||
|
return await DockerImage._build(this, imageTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============
|
||||||
|
// SECRETS - Public Factory API
|
||||||
|
// ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all secrets
|
||||||
|
*/
|
||||||
|
public async getSecrets() {
|
||||||
|
return await DockerSecret._list(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a secret by name
|
||||||
|
*/
|
||||||
|
public async getSecretByName(secretName: string) {
|
||||||
|
return await DockerSecret._fromName(this, secretName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a secret by ID
|
||||||
|
*/
|
||||||
|
public async getSecretById(secretId: string) {
|
||||||
|
return await DockerSecret._fromId(this, secretId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a secret
|
||||||
|
*/
|
||||||
|
public async createSecret(
|
||||||
|
descriptor: interfaces.ISecretCreationDescriptor,
|
||||||
|
) {
|
||||||
|
return await DockerSecret._create(this, descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============
|
||||||
|
// IMAGE STORE - Public API
|
||||||
|
// ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores an image in the local image store
|
||||||
|
*/
|
||||||
|
public async storeImage(
|
||||||
|
imageName: string,
|
||||||
|
tarStream: plugins.smartstream.stream.Readable,
|
||||||
|
): Promise<void> {
|
||||||
|
return await this.imageStore.storeImage(imageName, tarStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an image from the local image store
|
||||||
|
*/
|
||||||
|
public async retrieveImage(
|
||||||
|
imageName: string,
|
||||||
|
): Promise<plugins.smartstream.stream.Readable> {
|
||||||
|
return await this.imageStore.getImage(imageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -330,6 +452,7 @@ export class DockerHost {
|
|||||||
methodArg: string,
|
methodArg: string,
|
||||||
routeArg: string,
|
routeArg: string,
|
||||||
readStream?: plugins.smartstream.stream.Readable,
|
readStream?: plugins.smartstream.stream.Readable,
|
||||||
|
jsonData?: any,
|
||||||
) {
|
) {
|
||||||
const requestUrl = `${this.socketPath}${routeArg}`;
|
const requestUrl = `${this.socketPath}${routeArg}`;
|
||||||
|
|
||||||
@@ -342,6 +465,11 @@ export class DockerHost {
|
|||||||
.timeout(30000)
|
.timeout(30000)
|
||||||
.options({ keepAlive: false, autoDrain: true }); // Disable auto-drain for streaming
|
.options({ keepAlive: false, autoDrain: true }); // Disable auto-drain for streaming
|
||||||
|
|
||||||
|
// If we have JSON data, add it to the request
|
||||||
|
if (jsonData && Object.keys(jsonData).length > 0) {
|
||||||
|
smartRequest.json(jsonData);
|
||||||
|
}
|
||||||
|
|
||||||
// If we have a readStream, use the new stream method with logging
|
// If we have a readStream, use the new stream method with logging
|
||||||
if (readStream) {
|
if (readStream) {
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as interfaces from './interfaces/index.js';
|
import * as interfaces from './interfaces/index.js';
|
||||||
import { DockerHost } from './classes.host.js';
|
import { DockerHost } from './classes.host.js';
|
||||||
|
import { DockerResource } from './classes.base.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* represents a docker image on the remote docker host
|
* represents a docker image on the remote docker host
|
||||||
*/
|
*/
|
||||||
export class DockerImage {
|
export class DockerImage extends DockerResource {
|
||||||
// STATIC
|
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||||
public static async getImages(dockerHost: DockerHost) {
|
|
||||||
|
/**
|
||||||
|
* Internal: Get all images
|
||||||
|
* Public API: Use dockerHost.getImages() instead
|
||||||
|
*/
|
||||||
|
public static async _list(dockerHost: DockerHost) {
|
||||||
const images: DockerImage[] = [];
|
const images: DockerImage[] = [];
|
||||||
const response = await dockerHost.request('GET', '/images/json');
|
const response = await dockerHost.request('GET', '/images/json');
|
||||||
for (const imageObject of response.body) {
|
for (const imageObject of response.body) {
|
||||||
@@ -17,11 +23,15 @@ export class DockerImage {
|
|||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getImageByName(
|
/**
|
||||||
|
* Internal: Get image by name
|
||||||
|
* Public API: Use dockerHost.getImageByName(name) instead
|
||||||
|
*/
|
||||||
|
public static async _fromName(
|
||||||
dockerHost: DockerHost,
|
dockerHost: DockerHost,
|
||||||
imageNameArg: string,
|
imageNameArg: string,
|
||||||
) {
|
) {
|
||||||
const images = await this.getImages(dockerHost);
|
const images = await this._list(dockerHost);
|
||||||
const result = images.find((image) => {
|
const result = images.find((image) => {
|
||||||
if (image.RepoTags) {
|
if (image.RepoTags) {
|
||||||
return image.RepoTags.includes(imageNameArg);
|
return image.RepoTags.includes(imageNameArg);
|
||||||
@@ -32,7 +42,11 @@ export class DockerImage {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async createFromRegistry(
|
/**
|
||||||
|
* Internal: Create image from registry
|
||||||
|
* Public API: Use dockerHost.createImageFromRegistry(descriptor) instead
|
||||||
|
*/
|
||||||
|
public static async _createFromRegistry(
|
||||||
dockerHostArg: DockerHost,
|
dockerHostArg: DockerHost,
|
||||||
optionsArg: {
|
optionsArg: {
|
||||||
creationObject: interfaces.IImageCreationDescriptor;
|
creationObject: interfaces.IImageCreationDescriptor;
|
||||||
@@ -76,7 +90,7 @@ export class DockerImage {
|
|||||||
'info',
|
'info',
|
||||||
`Successfully pulled image ${imageUrlObject.imageUrl} from the registry`,
|
`Successfully pulled image ${imageUrlObject.imageUrl} from the registry`,
|
||||||
);
|
);
|
||||||
const image = await DockerImage.getImageByName(
|
const image = await DockerImage._fromName(
|
||||||
dockerHostArg,
|
dockerHostArg,
|
||||||
imageUrlObject.imageOriginTag,
|
imageUrlObject.imageOriginTag,
|
||||||
);
|
);
|
||||||
@@ -87,11 +101,10 @@ export class DockerImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Internal: Create image from tar stream
|
||||||
* @param dockerHostArg
|
* Public API: Use dockerHost.createImageFromTarStream(stream, descriptor) instead
|
||||||
* @param tarStreamArg
|
|
||||||
*/
|
*/
|
||||||
public static async createFromTarStream(
|
public static async _createFromTarStream(
|
||||||
dockerHostArg: DockerHost,
|
dockerHostArg: DockerHost,
|
||||||
optionsArg: {
|
optionsArg: {
|
||||||
creationObject: interfaces.IImageCreationDescriptor;
|
creationObject: interfaces.IImageCreationDescriptor;
|
||||||
@@ -161,11 +174,11 @@ export class DockerImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now try to look up that image by the "loadedImageTag".
|
// Now try to look up that image by the "loadedImageTag".
|
||||||
// Depending on Docker’s response, it might be something like:
|
// Depending on Docker's response, it might be something like:
|
||||||
// "myrepo/myimage:latest" OR "sha256:someHash..."
|
// "myrepo/myimage:latest" OR "sha256:someHash..."
|
||||||
// If Docker gave you an ID (e.g. "sha256:..."), you may need a separate
|
// If Docker gave you an ID (e.g. "sha256:..."), you may need a separate
|
||||||
// DockerImage.getImageById method; or if you prefer, you can treat it as a name.
|
// DockerImage.getImageById method; or if you prefer, you can treat it as a name.
|
||||||
const newlyImportedImage = await DockerImage.getImageByName(
|
const newlyImportedImage = await DockerImage._fromName(
|
||||||
dockerHostArg,
|
dockerHostArg,
|
||||||
loadedImageTag,
|
loadedImageTag,
|
||||||
);
|
);
|
||||||
@@ -192,15 +205,15 @@ export class DockerImage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async buildImage(dockerHostArg: DockerHost, dockerImageTag) {
|
/**
|
||||||
|
* Internal: Build image from Dockerfile
|
||||||
|
* Public API: Use dockerHost.buildImage(tag) instead
|
||||||
|
*/
|
||||||
|
public static async _build(dockerHostArg: DockerHost, dockerImageTag) {
|
||||||
// TODO: implement building an image
|
// TODO: implement building an image
|
||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE PROPERTIES
|
||||||
// references
|
|
||||||
public dockerHost: DockerHost;
|
|
||||||
|
|
||||||
// properties
|
|
||||||
/**
|
/**
|
||||||
* the tags for an image
|
* the tags for an image
|
||||||
*/
|
*/
|
||||||
@@ -215,13 +228,28 @@ export class DockerImage {
|
|||||||
public Size: number;
|
public Size: number;
|
||||||
public VirtualSize: number;
|
public VirtualSize: number;
|
||||||
|
|
||||||
constructor(dockerHostArg, dockerImageObjectArg: any) {
|
constructor(dockerHostArg: DockerHost, dockerImageObjectArg: any) {
|
||||||
this.dockerHost = dockerHostArg;
|
super(dockerHostArg);
|
||||||
Object.keys(dockerImageObjectArg).forEach((keyArg) => {
|
Object.keys(dockerImageObjectArg).forEach((keyArg) => {
|
||||||
this[keyArg] = dockerImageObjectArg[keyArg];
|
this[keyArg] = dockerImageObjectArg[keyArg];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// INSTANCE METHODS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes this image's state from the Docker daemon
|
||||||
|
*/
|
||||||
|
public async refresh(): Promise<void> {
|
||||||
|
if (!this.RepoTags || this.RepoTags.length === 0) {
|
||||||
|
throw new Error('Cannot refresh image without RepoTags');
|
||||||
|
}
|
||||||
|
const updated = await DockerImage._fromName(this.dockerHost, this.RepoTags[0]);
|
||||||
|
if (updated) {
|
||||||
|
Object.assign(this, updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* tag an image
|
* tag an image
|
||||||
* @param newTag
|
* @param newTag
|
||||||
@@ -234,7 +262,7 @@ export class DockerImage {
|
|||||||
* pulls the latest version from the registry
|
* pulls the latest version from the registry
|
||||||
*/
|
*/
|
||||||
public async pullLatestImageFromRegistry(): Promise<boolean> {
|
public async pullLatestImageFromRegistry(): Promise<boolean> {
|
||||||
const updatedImage = await DockerImage.createFromRegistry(this.dockerHost, {
|
const updatedImage = await DockerImage._createFromRegistry(this.dockerHost, {
|
||||||
creationObject: {
|
creationObject: {
|
||||||
imageUrl: this.RepoTags[0],
|
imageUrl: this.RepoTags[0],
|
||||||
},
|
},
|
||||||
@@ -244,6 +272,25 @@ export class DockerImage {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes this image from the Docker daemon
|
||||||
|
*/
|
||||||
|
public async remove(options?: { force?: boolean; noprune?: boolean }): Promise<void> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
if (options?.force) queryParams.append('force', '1');
|
||||||
|
if (options?.noprune) queryParams.append('noprune', '1');
|
||||||
|
|
||||||
|
const queryString = queryParams.toString();
|
||||||
|
const response = await this.dockerHost.request(
|
||||||
|
'DELETE',
|
||||||
|
`/images/${encodeURIComponent(this.Id)}${queryString ? '?' + queryString : ''}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode >= 300) {
|
||||||
|
throw new Error(`Failed to remove image: ${response.statusCode}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// get stuff
|
// get stuff
|
||||||
public async getVersion() {
|
public async getVersion() {
|
||||||
if (this.Labels && this.Labels.version) {
|
if (this.Labels && this.Labels.version) {
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ import * as plugins from './plugins.js';
|
|||||||
import * as interfaces from './interfaces/index.js';
|
import * as interfaces from './interfaces/index.js';
|
||||||
|
|
||||||
import { DockerHost } from './classes.host.js';
|
import { DockerHost } from './classes.host.js';
|
||||||
|
import { DockerResource } from './classes.base.js';
|
||||||
import { DockerService } from './classes.service.js';
|
import { DockerService } from './classes.service.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
export class DockerNetwork {
|
export class DockerNetwork extends DockerResource {
|
||||||
public static async getNetworks(
|
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal: Get all networks
|
||||||
|
* Public API: Use dockerHost.getNetworks() instead
|
||||||
|
*/
|
||||||
|
public static async _list(
|
||||||
dockerHost: DockerHost,
|
dockerHost: DockerHost,
|
||||||
): Promise<DockerNetwork[]> {
|
): Promise<DockerNetwork[]> {
|
||||||
const dockerNetworks: DockerNetwork[] = [];
|
const dockerNetworks: DockerNetwork[] = [];
|
||||||
@@ -19,17 +26,25 @@ export class DockerNetwork {
|
|||||||
return dockerNetworks;
|
return dockerNetworks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getNetworkByName(
|
/**
|
||||||
|
* Internal: Get network by name
|
||||||
|
* Public API: Use dockerHost.getNetworkByName(name) instead
|
||||||
|
*/
|
||||||
|
public static async _fromName(
|
||||||
dockerHost: DockerHost,
|
dockerHost: DockerHost,
|
||||||
dockerNetworkNameArg: string,
|
dockerNetworkNameArg: string,
|
||||||
) {
|
) {
|
||||||
const networks = await DockerNetwork.getNetworks(dockerHost);
|
const networks = await DockerNetwork._list(dockerHost);
|
||||||
return networks.find(
|
return networks.find(
|
||||||
(dockerNetwork) => dockerNetwork.Name === dockerNetworkNameArg,
|
(dockerNetwork) => dockerNetwork.Name === dockerNetworkNameArg,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async createNetwork(
|
/**
|
||||||
|
* Internal: Create a network
|
||||||
|
* Public API: Use dockerHost.createNetwork(descriptor) instead
|
||||||
|
*/
|
||||||
|
public static async _create(
|
||||||
dockerHost: DockerHost,
|
dockerHost: DockerHost,
|
||||||
networkCreationDescriptor: interfaces.INetworkCreationDescriptor,
|
networkCreationDescriptor: interfaces.INetworkCreationDescriptor,
|
||||||
): Promise<DockerNetwork> {
|
): Promise<DockerNetwork> {
|
||||||
@@ -54,7 +69,7 @@ export class DockerNetwork {
|
|||||||
});
|
});
|
||||||
if (response.statusCode < 300) {
|
if (response.statusCode < 300) {
|
||||||
logger.log('info', 'Created network successfully');
|
logger.log('info', 'Created network successfully');
|
||||||
return await DockerNetwork.getNetworkByName(
|
return await DockerNetwork._fromName(
|
||||||
dockerHost,
|
dockerHost,
|
||||||
networkCreationDescriptor.Name,
|
networkCreationDescriptor.Name,
|
||||||
);
|
);
|
||||||
@@ -67,11 +82,7 @@ export class DockerNetwork {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE PROPERTIES
|
||||||
// references
|
|
||||||
public dockerHost: DockerHost;
|
|
||||||
|
|
||||||
// properties
|
|
||||||
public Name: string;
|
public Name: string;
|
||||||
public Id: string;
|
public Id: string;
|
||||||
public Created: string;
|
public Created: string;
|
||||||
@@ -93,11 +104,23 @@ export class DockerNetwork {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(dockerHostArg: DockerHost) {
|
constructor(dockerHostArg: DockerHost) {
|
||||||
this.dockerHost = dockerHostArg;
|
super(dockerHostArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE METHODS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes this network's state from the Docker daemon
|
||||||
|
*/
|
||||||
|
public async refresh(): Promise<void> {
|
||||||
|
const updated = await DockerNetwork._fromName(this.dockerHost, this.Name);
|
||||||
|
if (updated) {
|
||||||
|
Object.assign(this, updated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* removes the network
|
* Removes the network
|
||||||
*/
|
*/
|
||||||
public async remove() {
|
public async remove() {
|
||||||
const response = await this.dockerHost.request(
|
const response = await this.dockerHost.request(
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import { DockerHost } from './classes.host.js';
|
import { DockerHost } from './classes.host.js';
|
||||||
|
import { DockerResource } from './classes.base.js';
|
||||||
|
|
||||||
// interfaces
|
// interfaces
|
||||||
import * as interfaces from './interfaces/index.js';
|
import * as interfaces from './interfaces/index.js';
|
||||||
|
|
||||||
export class DockerSecret {
|
export class DockerSecret extends DockerResource {
|
||||||
// STATIC
|
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||||
public static async getSecrets(dockerHostArg: DockerHost) {
|
|
||||||
|
/**
|
||||||
|
* Internal: Get all secrets
|
||||||
|
* Public API: Use dockerHost.getSecrets() instead
|
||||||
|
*/
|
||||||
|
public static async _list(dockerHostArg: DockerHost) {
|
||||||
const response = await dockerHostArg.request('GET', '/secrets');
|
const response = await dockerHostArg.request('GET', '/secrets');
|
||||||
const secrets: DockerSecret[] = [];
|
const secrets: DockerSecret[] = [];
|
||||||
for (const secret of response.body) {
|
for (const secret of response.body) {
|
||||||
@@ -17,20 +23,32 @@ export class DockerSecret {
|
|||||||
return secrets;
|
return secrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getSecretByID(dockerHostArg: DockerHost, idArg: string) {
|
/**
|
||||||
const secrets = await this.getSecrets(dockerHostArg);
|
* Internal: Get secret by ID
|
||||||
|
* Public API: Use dockerHost.getSecretById(id) instead
|
||||||
|
*/
|
||||||
|
public static async _fromId(dockerHostArg: DockerHost, idArg: string) {
|
||||||
|
const secrets = await this._list(dockerHostArg);
|
||||||
return secrets.find((secret) => secret.ID === idArg);
|
return secrets.find((secret) => secret.ID === idArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getSecretByName(
|
/**
|
||||||
|
* Internal: Get secret by name
|
||||||
|
* Public API: Use dockerHost.getSecretByName(name) instead
|
||||||
|
*/
|
||||||
|
public static async _fromName(
|
||||||
dockerHostArg: DockerHost,
|
dockerHostArg: DockerHost,
|
||||||
nameArg: string,
|
nameArg: string,
|
||||||
) {
|
) {
|
||||||
const secrets = await this.getSecrets(dockerHostArg);
|
const secrets = await this._list(dockerHostArg);
|
||||||
return secrets.find((secret) => secret.Spec.Name === nameArg);
|
return secrets.find((secret) => secret.Spec.Name === nameArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async createSecret(
|
/**
|
||||||
|
* Internal: Create a secret
|
||||||
|
* Public API: Use dockerHost.createSecret(descriptor) instead
|
||||||
|
*/
|
||||||
|
public static async _create(
|
||||||
dockerHostArg: DockerHost,
|
dockerHostArg: DockerHost,
|
||||||
secretDescriptor: interfaces.ISecretCreationDescriptor,
|
secretDescriptor: interfaces.ISecretCreationDescriptor,
|
||||||
) {
|
) {
|
||||||
@@ -48,12 +66,12 @@ export class DockerSecret {
|
|||||||
Object.assign(newSecretInstance, response.body);
|
Object.assign(newSecretInstance, response.body);
|
||||||
Object.assign(
|
Object.assign(
|
||||||
newSecretInstance,
|
newSecretInstance,
|
||||||
await DockerSecret.getSecretByID(dockerHostArg, newSecretInstance.ID),
|
await DockerSecret._fromId(dockerHostArg, newSecretInstance.ID),
|
||||||
);
|
);
|
||||||
return newSecretInstance;
|
return newSecretInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE PROPERTIES
|
||||||
public ID: string;
|
public ID: string;
|
||||||
public Spec: {
|
public Spec: {
|
||||||
Name: string;
|
Name: string;
|
||||||
@@ -63,13 +81,24 @@ export class DockerSecret {
|
|||||||
Index: string;
|
Index: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
public dockerHost: DockerHost;
|
|
||||||
constructor(dockerHostArg: DockerHost) {
|
constructor(dockerHostArg: DockerHost) {
|
||||||
this.dockerHost = dockerHostArg;
|
super(dockerHostArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE METHODS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes this secret's state from the Docker daemon
|
||||||
|
*/
|
||||||
|
public async refresh(): Promise<void> {
|
||||||
|
const updated = await DockerSecret._fromId(this.dockerHost, this.ID);
|
||||||
|
if (updated) {
|
||||||
|
Object.assign(this, updated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* updates a secret
|
* Updates a secret
|
||||||
*/
|
*/
|
||||||
public async update(contentArg: string) {
|
public async update(contentArg: string) {
|
||||||
const route = `/secrets/${this.ID}/update?=version=${this.Version.Index}`;
|
const route = `/secrets/${this.ID}/update?=version=${this.Version.Index}`;
|
||||||
@@ -84,11 +113,16 @@ export class DockerSecret {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes this secret from the Docker daemon
|
||||||
|
*/
|
||||||
public async remove() {
|
public async remove() {
|
||||||
await this.dockerHost.request('DELETE', `/secrets/${this.ID}`);
|
await this.dockerHost.request('DELETE', `/secrets/${this.ID}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get things
|
/**
|
||||||
|
* Gets the version label of this secret
|
||||||
|
*/
|
||||||
public async getVersion() {
|
public async getVersion() {
|
||||||
return this.Spec.Labels.version;
|
return this.Spec.Labels.version;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,19 @@ import * as plugins from './plugins.js';
|
|||||||
import * as interfaces from './interfaces/index.js';
|
import * as interfaces from './interfaces/index.js';
|
||||||
|
|
||||||
import { DockerHost } from './classes.host.js';
|
import { DockerHost } from './classes.host.js';
|
||||||
|
import { DockerResource } from './classes.base.js';
|
||||||
import { DockerImage } from './classes.image.js';
|
import { DockerImage } from './classes.image.js';
|
||||||
import { DockerSecret } from './classes.secret.js';
|
import { DockerSecret } from './classes.secret.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
export class DockerService {
|
export class DockerService extends DockerResource {
|
||||||
// STATIC
|
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||||
public static async getServices(dockerHost: DockerHost) {
|
|
||||||
|
/**
|
||||||
|
* Internal: Get all services
|
||||||
|
* Public API: Use dockerHost.getServices() instead
|
||||||
|
*/
|
||||||
|
public static async _list(dockerHost: DockerHost) {
|
||||||
const services: DockerService[] = [];
|
const services: DockerService[] = [];
|
||||||
const response = await dockerHost.request('GET', '/services');
|
const response = await dockerHost.request('GET', '/services');
|
||||||
for (const serviceObject of response.body) {
|
for (const serviceObject of response.body) {
|
||||||
@@ -19,11 +25,15 @@ export class DockerService {
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getServiceByName(
|
/**
|
||||||
|
* Internal: Get service by name
|
||||||
|
* Public API: Use dockerHost.getServiceByName(name) instead
|
||||||
|
*/
|
||||||
|
public static async _fromName(
|
||||||
dockerHost: DockerHost,
|
dockerHost: DockerHost,
|
||||||
networkName: string,
|
networkName: string,
|
||||||
): Promise<DockerService> {
|
): Promise<DockerService> {
|
||||||
const allServices = await DockerService.getServices(dockerHost);
|
const allServices = await DockerService._list(dockerHost);
|
||||||
const wantedService = allServices.find((service) => {
|
const wantedService = allServices.find((service) => {
|
||||||
return service.Spec.Name === networkName;
|
return service.Spec.Name === networkName;
|
||||||
});
|
});
|
||||||
@@ -31,20 +41,30 @@ export class DockerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creates a service
|
* Internal: Create a service
|
||||||
|
* Public API: Use dockerHost.createService(descriptor) instead
|
||||||
*/
|
*/
|
||||||
public static async createService(
|
public static async _create(
|
||||||
dockerHost: DockerHost,
|
dockerHost: DockerHost,
|
||||||
serviceCreationDescriptor: interfaces.IServiceCreationDescriptor,
|
serviceCreationDescriptor: interfaces.IServiceCreationDescriptor,
|
||||||
): Promise<DockerService> {
|
): Promise<DockerService> {
|
||||||
// lets get the image
|
|
||||||
logger.log(
|
logger.log(
|
||||||
'info',
|
'info',
|
||||||
`now creating service ${serviceCreationDescriptor.name}`,
|
`now creating service ${serviceCreationDescriptor.name}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// await serviceCreationDescriptor.image.pullLatestImageFromRegistry();
|
// Resolve image (support both string and DockerImage instance)
|
||||||
const serviceVersion = await serviceCreationDescriptor.image.getVersion();
|
let imageInstance: DockerImage;
|
||||||
|
if (typeof serviceCreationDescriptor.image === 'string') {
|
||||||
|
imageInstance = await DockerImage._fromName(dockerHost, serviceCreationDescriptor.image);
|
||||||
|
if (!imageInstance) {
|
||||||
|
throw new Error(`Image not found: ${serviceCreationDescriptor.image}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imageInstance = serviceCreationDescriptor.image;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceVersion = await imageInstance.getVersion();
|
||||||
|
|
||||||
const labels: interfaces.TLabels = {
|
const labels: interfaces.TLabels = {
|
||||||
...serviceCreationDescriptor.labels,
|
...serviceCreationDescriptor.labels,
|
||||||
@@ -90,6 +110,7 @@ export class DockerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve networks (support both string[] and DockerNetwork[])
|
||||||
const networkArray: Array<{
|
const networkArray: Array<{
|
||||||
Target: string;
|
Target: string;
|
||||||
Aliases: string[];
|
Aliases: string[];
|
||||||
@@ -101,8 +122,11 @@ export class DockerService {
|
|||||||
logger.log('warn', 'Skipping null network in service creation');
|
logger.log('warn', 'Skipping null network in service creation');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve network name
|
||||||
|
const networkName = typeof network === 'string' ? network : network.Name;
|
||||||
networkArray.push({
|
networkArray.push({
|
||||||
Target: network.Name,
|
Target: networkName,
|
||||||
Aliases: [serviceCreationDescriptor.networkAlias],
|
Aliases: [serviceCreationDescriptor.networkAlias],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -119,9 +143,20 @@ export class DockerService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// lets configure secrets
|
// Resolve secrets (support both string[] and DockerSecret[])
|
||||||
const secretArray: any[] = [];
|
const secretArray: any[] = [];
|
||||||
for (const secret of serviceCreationDescriptor.secrets) {
|
for (const secret of serviceCreationDescriptor.secrets) {
|
||||||
|
// Resolve secret instance
|
||||||
|
let secretInstance: DockerSecret;
|
||||||
|
if (typeof secret === 'string') {
|
||||||
|
secretInstance = await DockerSecret._fromName(dockerHost, secret);
|
||||||
|
if (!secretInstance) {
|
||||||
|
throw new Error(`Secret not found: ${secret}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
secretInstance = secret;
|
||||||
|
}
|
||||||
|
|
||||||
secretArray.push({
|
secretArray.push({
|
||||||
File: {
|
File: {
|
||||||
Name: 'secret.json', // TODO: make sure that works with multiple secrets
|
Name: 'secret.json', // TODO: make sure that works with multiple secrets
|
||||||
@@ -129,8 +164,8 @@ export class DockerService {
|
|||||||
GID: '33',
|
GID: '33',
|
||||||
Mode: 384,
|
Mode: 384,
|
||||||
},
|
},
|
||||||
SecretID: secret.ID,
|
SecretID: secretInstance.ID,
|
||||||
SecretName: secret.Spec.Name,
|
SecretName: secretInstance.Spec.Name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +190,7 @@ export class DockerService {
|
|||||||
Name: serviceCreationDescriptor.name,
|
Name: serviceCreationDescriptor.name,
|
||||||
TaskTemplate: {
|
TaskTemplate: {
|
||||||
ContainerSpec: {
|
ContainerSpec: {
|
||||||
Image: serviceCreationDescriptor.image.RepoTags[0],
|
Image: imageInstance.RepoTags[0],
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
Secrets: secretArray,
|
Secrets: secretArray,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
@@ -189,15 +224,15 @@ export class DockerService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdService = await DockerService.getServiceByName(
|
const createdService = await DockerService._fromName(
|
||||||
dockerHost,
|
dockerHost,
|
||||||
serviceCreationDescriptor.name,
|
serviceCreationDescriptor.name,
|
||||||
);
|
);
|
||||||
return createdService;
|
return createdService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE PROPERTIES
|
||||||
public dockerHostRef: DockerHost;
|
// Note: dockerHost (not dockerHostRef) for consistency with base class
|
||||||
|
|
||||||
public ID: string;
|
public ID: string;
|
||||||
public Version: { Index: number };
|
public Version: { Index: number };
|
||||||
@@ -229,27 +264,49 @@ export class DockerService {
|
|||||||
public Endpoint: { Spec: {}; VirtualIPs: [any[]] };
|
public Endpoint: { Spec: {}; VirtualIPs: [any[]] };
|
||||||
|
|
||||||
constructor(dockerHostArg: DockerHost) {
|
constructor(dockerHostArg: DockerHost) {
|
||||||
this.dockerHostRef = dockerHostArg;
|
super(dockerHostArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// INSTANCE METHODS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes this service's state from the Docker daemon
|
||||||
|
*/
|
||||||
|
public async refresh(): Promise<void> {
|
||||||
|
const updated = await DockerService._fromName(this.dockerHost, this.Spec.Name);
|
||||||
|
if (updated) {
|
||||||
|
Object.assign(this, updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes this service from the Docker daemon
|
||||||
|
*/
|
||||||
public async remove() {
|
public async remove() {
|
||||||
await this.dockerHostRef.request('DELETE', `/services/${this.ID}`);
|
await this.dockerHost.request('DELETE', `/services/${this.ID}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-reads service data from Docker engine
|
||||||
|
* @deprecated Use refresh() instead
|
||||||
|
*/
|
||||||
public async reReadFromDockerEngine() {
|
public async reReadFromDockerEngine() {
|
||||||
const dockerData = await this.dockerHostRef.request(
|
const dockerData = await this.dockerHost.request(
|
||||||
'GET',
|
'GET',
|
||||||
`/services/${this.ID}`,
|
`/services/${this.ID}`,
|
||||||
);
|
);
|
||||||
// TODO: Better assign: Object.assign(this, dockerData);
|
// TODO: Better assign: Object.assign(this, dockerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this service needs an update based on image version
|
||||||
|
*/
|
||||||
public async needsUpdate(): Promise<boolean> {
|
public async needsUpdate(): Promise<boolean> {
|
||||||
// TODO: implement digest based update recognition
|
// TODO: implement digest based update recognition
|
||||||
|
|
||||||
await this.reReadFromDockerEngine();
|
await this.reReadFromDockerEngine();
|
||||||
const dockerImage = await DockerImage.createFromRegistry(
|
const dockerImage = await DockerImage._createFromRegistry(
|
||||||
this.dockerHostRef,
|
this.dockerHost,
|
||||||
{
|
{
|
||||||
creationObject: {
|
creationObject: {
|
||||||
imageUrl: this.Spec.TaskTemplate.ContainerSpec.Image,
|
imageUrl: this.Spec.TaskTemplate.ContainerSpec.Image,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './classes.base.js';
|
||||||
export * from './classes.host.js';
|
export * from './classes.host.js';
|
||||||
export * from './classes.container.js';
|
export * from './classes.container.js';
|
||||||
export * from './classes.image.js';
|
export * from './classes.image.js';
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { DockerNetwork } from '../classes.network.js';
|
import { DockerNetwork } from '../classes.network.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container creation descriptor supporting both string references and class instances.
|
||||||
|
* Strings will be resolved to resources internally.
|
||||||
|
*/
|
||||||
export interface IContainerCreationDescriptor {
|
export interface IContainerCreationDescriptor {
|
||||||
Hostname: string;
|
Hostname: string;
|
||||||
Domainname: string;
|
Domainname: string;
|
||||||
networks?: DockerNetwork[];
|
/** Network names (strings) or DockerNetwork instances */
|
||||||
|
networks?: (string | DockerNetwork)[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,20 @@ import { DockerNetwork } from '../classes.network.js';
|
|||||||
import { DockerSecret } from '../classes.secret.js';
|
import { DockerSecret } from '../classes.secret.js';
|
||||||
import { DockerImage } from '../classes.image.js';
|
import { DockerImage } from '../classes.image.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service creation descriptor supporting both string references and class instances.
|
||||||
|
* Strings will be resolved to resources internally.
|
||||||
|
*/
|
||||||
export interface IServiceCreationDescriptor {
|
export interface IServiceCreationDescriptor {
|
||||||
name: string;
|
name: string;
|
||||||
image: DockerImage;
|
/** Image tag (string) or DockerImage instance */
|
||||||
|
image: string | DockerImage;
|
||||||
labels: interfaces.TLabels;
|
labels: interfaces.TLabels;
|
||||||
networks: DockerNetwork[];
|
/** Network names (strings) or DockerNetwork instances */
|
||||||
|
networks: (string | DockerNetwork)[];
|
||||||
networkAlias: string;
|
networkAlias: string;
|
||||||
secrets: DockerSecret[];
|
/** Secret names (strings) or DockerSecret instances */
|
||||||
|
secrets: (string | DockerSecret)[];
|
||||||
ports: string[];
|
ports: string[];
|
||||||
accessHostDockerSock?: boolean;
|
accessHostDockerSock?: boolean;
|
||||||
resources?: {
|
resources?: {
|
||||||
|
|||||||
Reference in New Issue
Block a user