Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e28a35791a | |||
| b86601c939 | |||
| 889b017d4f | |||
| 35e8eff092 | |||
| 2ecd4e9d7c | |||
| 08dbad47bc | |||
| 15e5dedae4 | |||
| 5834721da8 | |||
| 2f31e14cbe | |||
| 5691e5fb78 | |||
| 8d043d20a8 | |||
| 6fe70e0a1d | |||
| cc9c20882e | |||
| 08af9fec14 |
104
changelog.md
104
changelog.md
@@ -1,5 +1,109 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-11-25 - 5.1.0 - feat(host)
|
||||
Add DockerHost version & image-prune APIs, extend network creation options, return exec inspect info, and improve image import/store and streaming
|
||||
|
||||
- Add DockerHost.getVersion() to retrieve Docker daemon version and build info
|
||||
- Add DockerHost.pruneImages() with dangling and filters support (calls /images/prune)
|
||||
- Extend INetworkCreationDescriptor and DockerNetwork._create() to accept Driver, IPAM, EnableIPv6, Attachable and Labels
|
||||
- Enhance DockerContainer.exec() to return an inspect() helper and introduce IExecInspectInfo to expose exec state and exit code
|
||||
- Improve DockerImage._createFromTarStream() parsing of docker-load output and error messages when loaded image cannot be determined
|
||||
- Implement DockerImageStore.storeImage() to persist, repackage and upload images (local processing and s3 support)
|
||||
- Various streaming/request improvements for compatibility with Node streams and better handling of streaming endpoints
|
||||
- Update tests to cover new features (network creation, exec inspect, etc.)
|
||||
|
||||
## 2025-11-24 - 5.0.2 - fix(DockerContainer)
|
||||
Fix getContainerById to return undefined for non-existent containers
|
||||
|
||||
- Prevented creation of an invalid DockerContainer from Docker API error responses when a container does not exist.
|
||||
- Changed DockerContainer._fromId to use the list+find pattern and return Promise<DockerContainer | undefined>.
|
||||
- Updated DockerHost.getContainerById to return Promise<DockerContainer | undefined> for type safety and consistent behavior.
|
||||
- Added tests to verify undefined is returned for non-existent container IDs and that valid IDs return DockerContainer instances.
|
||||
- Bumped package version to 5.0.1 and updated changelog and readme hints to document the fix.
|
||||
|
||||
## 2025-11-24 - 5.0.0 - BREAKING CHANGE(DockerHost)
|
||||
Rename array-returning get* methods to list* on DockerHost and related resource classes; update docs, tests and changelog
|
||||
|
||||
- Renamed public DockerHost methods: getContainers → listContainers, getNetworks → listNetworks, getServices → listServices, getImages → listImages, getSecrets → listSecrets.
|
||||
- Renamed DockerNetwork.getContainersOnNetwork → DockerNetwork.listContainersOnNetwork and updated usages (e.g. getContainersOnNetworkForService).
|
||||
- Updated internal/static method docs/comments to recommend dockerHost.list*() usage and adjusted implementations accordingly.
|
||||
- Updated README, readme.hints.md, tests (test.nonci.node+deno.ts) and changelog to reflect the new list* method names.
|
||||
- Bumped package version to 4.0.0.
|
||||
- Migration note: replace calls to get*() with list*() for methods that return multiple items (arrays). Single-item getters such as getContainerById or getNetworkByName remain unchanged.
|
||||
|
||||
## 2025-11-24 - 5.0.1 - fix(DockerContainer)
|
||||
Fix getContainerById() to return undefined instead of invalid container object when container doesn't exist
|
||||
|
||||
**Bug Fixed:**
|
||||
- `getContainerById()` was creating a DockerContainer object from error responses when a container didn't exist
|
||||
- The error object `{ message: "No such container: ..." }` was being passed to the constructor
|
||||
- Calling `.logs()` on this invalid container returned "[object Object]" instead of logs
|
||||
|
||||
**Solution:**
|
||||
- Changed `DockerContainer._fromId()` to use the list+filter pattern (consistent with all other resource getters)
|
||||
- Now returns `undefined` when container is not found (matches DockerImage, DockerNetwork, DockerService, DockerSecret behavior)
|
||||
- Updated return type to `Promise<DockerContainer | undefined>` for type safety
|
||||
- Added tests to verify undefined is returned for non-existent containers
|
||||
|
||||
**Migration:**
|
||||
No breaking changes - users should already be checking for undefined/null based on TypeScript types and documentation.
|
||||
|
||||
## 2025-11-24 - 4.0.0 - BREAKING CHANGE: Rename list methods for consistency
|
||||
|
||||
**Breaking Changes:**
|
||||
- Renamed all "get*" methods that return arrays to "list*" methods for better clarity:
|
||||
- `getContainers()` → `listContainers()`
|
||||
- `getNetworks()` → `listNetworks()`
|
||||
- `getServices()` → `listServices()`
|
||||
- `getImages()` → `listImages()`
|
||||
- `getSecrets()` → `listSecrets()`
|
||||
- `getContainersOnNetwork()` → `listContainersOnNetwork()` (on DockerNetwork class)
|
||||
|
||||
**Migration Guide:**
|
||||
Update all method calls from `get*()` to `list*()` where the method returns an array of resources. Single-item getters like `getContainerById()`, `getNetworkByName()`, etc. remain unchanged.
|
||||
|
||||
**Rationale:**
|
||||
The `list*` naming convention more clearly indicates that these methods return multiple items (arrays), while `get*` methods are reserved for retrieving single items by ID or name. This follows standard API design patterns and improves code readability.
|
||||
|
||||
## 2025-11-24 - 3.0.2 - fix(readme)
|
||||
Update README to document 3.0.0+ changes: architecture refactor, streaming improvements, health check and circular dependency fixes
|
||||
|
||||
- Documented major refactor to a Clean OOP / Facade pattern with DockerHost as the single entry point
|
||||
- Added/clarified real-time container streaming APIs: streamLogs(), attach(), exec()
|
||||
- Clarified support for flexible descriptors (accept both string references and class instances)
|
||||
- Documented complete container lifecycle API (start, stop, remove, logs, inspect, stats)
|
||||
- Documented new ping() health check method to verify Docker daemon availability
|
||||
- Noted fix for circular dependency issues in Node.js by using type-only imports
|
||||
- Mentioned improved TypeScript definitions and expanded examples, migration guides, and real-world use cases
|
||||
|
||||
## 2025-11-24 - 3.0.1 - fix(classes.base)
|
||||
Use type-only import for DockerHost in classes.base to avoid runtime side-effects
|
||||
|
||||
- Changed the import in ts/classes.base.ts to a type-only import: import type { DockerHost } from './classes.host.js';
|
||||
- Prevents a runtime import of classes.host when only the type is needed, reducing risk of circular dependencies and unintended side-effects during module initialization.
|
||||
- No behavior changes to the public API — TypeScript-only change; intended to improve bundling and runtime stability.
|
||||
|
||||
## 2025-11-24 - 3.0.0 - BREAKING CHANGE(DockerHost)
|
||||
Refactor public API to DockerHost facade; introduce DockerResource base; make resource static methods internal; support flexible descriptors and stream compatibility
|
||||
|
||||
- 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/listNetworks/getNetworkByName, createSecret/listSecrets/getSecretByName/getSecretById).
|
||||
- Tests and docs updated: migration guide and examples added (readme.hints.md, README); test timeout reduced from 600s to 300s in package.json.
|
||||
- BREAKING: Public API changes require consumers to migrate away from direct resource static calls and direct imageStore access to the new DockerHost-based factory methods and storeImage/retrieveImage APIs.
|
||||
|
||||
## 2025-11-18 - 2.1.0 - feat(DockerHost)
|
||||
Add DockerHost.ping() to check Docker daemon availability and document health-check usage
|
||||
|
||||
- Add DockerHost.ping() method that issues a GET to /_ping and throws an error if the response status is not 200
|
||||
- Update README: show ping() in Quick Start, add health check examples (isDockerHealthy, waitForDocker) and mention Health Checks in Key Concepts
|
||||
|
||||
## 2025-11-18 - 2.0.0 - BREAKING CHANGE(DockerHost)
|
||||
Rename DockerHost constructor option 'dockerSockPath' to 'socketPath' and update internal socket path handling
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@apiclient.xyz/docker",
|
||||
"version": "2.0.0",
|
||||
"version": "5.1.0",
|
||||
"description": "Provides easy communication with Docker remote API from Node.js, with TypeScript support.",
|
||||
"private": false,
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 600)",
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 300)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
|
||||
361
readme.hints.md
361
readme.hints.md
@@ -1,5 +1,366 @@
|
||||
# Docker Module - Development Hints
|
||||
|
||||
## New Features (2025-11-25 - v5.1.0)
|
||||
|
||||
### 1. Enhanced Network Creation with Full Configuration Support
|
||||
|
||||
**Problem:** Users were unable to create non-overlay networks or customize network configuration. The `INetworkCreationDescriptor` interface only had a `Name` property, and `DockerNetwork._create()` hardcoded `Driver: 'overlay'`.
|
||||
|
||||
**Solution:** Expanded the interface and implementation to support all Docker network configuration options:
|
||||
|
||||
```typescript
|
||||
// New interface properties:
|
||||
export interface INetworkCreationDescriptor {
|
||||
Name: string;
|
||||
Driver?: 'bridge' | 'overlay' | 'host' | 'none' | 'macvlan'; // NEW
|
||||
Attachable?: boolean; // NEW
|
||||
Labels?: Record<string, string>; // NEW
|
||||
IPAM?: { // NEW - IP Address Management
|
||||
Driver?: string;
|
||||
Config?: Array<{
|
||||
Subnet?: string;
|
||||
Gateway?: string;
|
||||
IPRange?: string;
|
||||
AuxiliaryAddresses?: Record<string, string>;
|
||||
}>;
|
||||
};
|
||||
Internal?: boolean; // NEW
|
||||
EnableIPv6?: boolean; // NEW
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```typescript
|
||||
// Create bridge network with custom IPAM
|
||||
const network = await docker.createNetwork({
|
||||
Name: 'custom-bridge',
|
||||
Driver: 'bridge',
|
||||
IPAM: {
|
||||
Config: [{
|
||||
Subnet: '172.20.0.0/16',
|
||||
Gateway: '172.20.0.1',
|
||||
}]
|
||||
},
|
||||
Labels: { environment: 'production' },
|
||||
});
|
||||
```
|
||||
|
||||
**Files Modified:**
|
||||
- `ts/interfaces/network.ts` - Added all missing properties to interface
|
||||
- `ts/classes.network.ts` - Updated `_create()` to pass through descriptor properties instead of hardcoding
|
||||
|
||||
### 2. Docker Daemon Version Information
|
||||
|
||||
**Added:** `dockerHost.getVersion()` method to retrieve Docker daemon version information.
|
||||
|
||||
**Purpose:** Essential for API compatibility checking, debugging, and ensuring minimum Docker version requirements.
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
Version: string; // e.g., "20.10.21"
|
||||
ApiVersion: string; // e.g., "1.41"
|
||||
MinAPIVersion?: string; // Minimum supported API version
|
||||
GitCommit: string;
|
||||
GoVersion: string;
|
||||
Os: string; // e.g., "linux"
|
||||
Arch: string; // e.g., "amd64"
|
||||
KernelVersion: string;
|
||||
BuildTime?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```typescript
|
||||
const version = await docker.getVersion();
|
||||
console.log(`Docker ${version.Version} (API ${version.ApiVersion})`);
|
||||
console.log(`Platform: ${version.Os}/${version.Arch}`);
|
||||
```
|
||||
|
||||
**Files Modified:**
|
||||
- `ts/classes.host.ts` - Added `getVersion()` method after `ping()`
|
||||
|
||||
### 3. Image Pruning for Disk Space Management
|
||||
|
||||
**Added:** `dockerHost.pruneImages(options?)` method to clean up unused images.
|
||||
|
||||
**Purpose:** Automated disk space management, CI/CD cleanup, scheduled maintenance tasks.
|
||||
|
||||
**Options:**
|
||||
```typescript
|
||||
{
|
||||
dangling?: boolean; // Remove untagged images
|
||||
filters?: Record<string, string[]>; // Custom filters (until, label, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
ImagesDeleted: Array<{ Untagged?: string; Deleted?: string }>;
|
||||
SpaceReclaimed: number; // Bytes freed
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```typescript
|
||||
// Remove dangling images
|
||||
const result = await docker.pruneImages({ dangling: true });
|
||||
console.log(`Reclaimed: ${(result.SpaceReclaimed / 1024 / 1024).toFixed(2)} MB`);
|
||||
|
||||
// Remove old images (older than 7 days)
|
||||
await docker.pruneImages({
|
||||
filters: {
|
||||
until: ['168h']
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Files Modified:**
|
||||
- `ts/classes.host.ts` - Added `pruneImages()` method with filter support
|
||||
|
||||
### 4. Exec Command Exit Codes and Inspection
|
||||
|
||||
**Problem:** Users could not determine if exec commands succeeded or failed. The `container.exec()` method returned a stream but provided no way to access exit codes, which are essential for:
|
||||
- Health checks (e.g., `pg_isready` exit code)
|
||||
- Test automation (npm test success/failure)
|
||||
- Deployment validation (migration checks)
|
||||
- Container readiness probes
|
||||
|
||||
**Solution:** Added `inspect()` method to `exec()` return value that provides comprehensive execution information.
|
||||
|
||||
**New Return Type:**
|
||||
```typescript
|
||||
{
|
||||
stream: Duplex;
|
||||
close: () => Promise<void>;
|
||||
inspect: () => Promise<IExecInspectInfo>; // NEW
|
||||
}
|
||||
```
|
||||
|
||||
**IExecInspectInfo Interface:**
|
||||
```typescript
|
||||
export interface IExecInspectInfo {
|
||||
ExitCode: number; // 0 = success, non-zero = failure
|
||||
Running: boolean; // Whether exec is still running
|
||||
Pid: number; // Process ID
|
||||
ContainerID: string; // Container where exec ran
|
||||
ID: string; // Exec instance ID
|
||||
OpenStderr: boolean;
|
||||
OpenStdin: boolean;
|
||||
OpenStdout: boolean;
|
||||
CanRemove: boolean;
|
||||
DetachKeys: string;
|
||||
ProcessConfig: {
|
||||
tty: boolean;
|
||||
entrypoint: string;
|
||||
arguments: string[];
|
||||
privileged: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Example:**
|
||||
```typescript
|
||||
// Health check with exit code
|
||||
const { stream, close, inspect } = await container.exec('pg_isready -U postgres');
|
||||
|
||||
stream.on('end', async () => {
|
||||
const info = await inspect();
|
||||
|
||||
if (info.ExitCode === 0) {
|
||||
console.log('✅ Database is ready');
|
||||
} else {
|
||||
console.log(`❌ Database check failed (exit code ${info.ExitCode})`);
|
||||
}
|
||||
|
||||
await close();
|
||||
});
|
||||
```
|
||||
|
||||
**Real-World Use Cases Enabled:**
|
||||
- Health checks: Verify service readiness with proper exit code handling
|
||||
- Test automation: Run tests in container and determine pass/fail
|
||||
- Deployment validation: Execute migration checks and verify success
|
||||
- CI/CD pipelines: Run build/test commands and get accurate results
|
||||
|
||||
**Files Modified:**
|
||||
- `ts/interfaces/container.ts` - Added `IExecInspectInfo` interface
|
||||
- `ts/classes.container.ts` - Updated `exec()` return type and added `inspect()` implementation
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
All changes are non-breaking additions that enhance existing functionality:
|
||||
- Network creation: New optional properties with sensible defaults
|
||||
- getVersion(): New method, no changes to existing APIs
|
||||
- pruneImages(): New method, no changes to existing APIs
|
||||
- exec() inspect(): Added to return value, existing stream/close properties unchanged
|
||||
|
||||
## getContainerById() Bug Fix (2025-11-24 - v5.0.1)
|
||||
|
||||
### Problem
|
||||
The `getContainerById()` method had a critical bug where it would create a DockerContainer object from Docker API error responses when a container didn't exist.
|
||||
|
||||
**Symptoms:**
|
||||
- Calling `docker.getContainerById('invalid-id')` returned a DockerContainer object with `{ message: "No such container: invalid-id" }`
|
||||
- Calling `.logs()` on this invalid container returned "[object Object]" instead of logs or throwing an error
|
||||
- No way to detect the error state without checking for a `.message` property
|
||||
|
||||
**Root Cause:**
|
||||
The `DockerContainer._fromId()` method made a direct API call to `/containers/{id}/json` and blindly passed `response.body` to the constructor, even when the API returned a 404 error response.
|
||||
|
||||
### Solution
|
||||
Changed `DockerContainer._fromId()` to use the **list+filter pattern**, matching the behavior of all other resource getter methods (DockerImage, DockerNetwork, DockerService, DockerSecret):
|
||||
|
||||
```typescript
|
||||
// Before (buggy):
|
||||
public static async _fromId(dockerHostArg: DockerHost, containerId: string): Promise<DockerContainer> {
|
||||
const response = await dockerHostArg.request('GET', `/containers/${containerId}/json`);
|
||||
return new DockerContainer(dockerHostArg, response.body); // Creates invalid object from error!
|
||||
}
|
||||
|
||||
// After (fixed):
|
||||
public static async _fromId(dockerHostArg: DockerHost, containerId: string): Promise<DockerContainer | undefined> {
|
||||
const containers = await this._list(dockerHostArg);
|
||||
return containers.find((container) => container.Id === containerId); // Returns undefined if not found
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- 100% consistent with all other resource classes
|
||||
- Type-safe return signature: `Promise<DockerContainer | undefined>`
|
||||
- Cannot create invalid objects - `.find()` naturally returns undefined
|
||||
- Users can now properly check for non-existent containers
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
const container = await docker.getContainerById('abc123');
|
||||
if (container) {
|
||||
const logs = await container.logs();
|
||||
console.log(logs);
|
||||
} else {
|
||||
console.log('Container not found');
|
||||
}
|
||||
```
|
||||
|
||||
## OOP Refactoring - Clean Architecture (2025-11-24)
|
||||
|
||||
### Architecture Changes
|
||||
The module has been restructured to follow a clean OOP Facade pattern:
|
||||
- **DockerHost** is now the single entry point for all Docker operations
|
||||
- All resource classes extend abstract `DockerResource` base class
|
||||
- Static methods are prefixed with `_` to indicate internal use
|
||||
- Public API is exclusively through DockerHost methods
|
||||
|
||||
### Key Changes
|
||||
|
||||
**1. Factory Pattern**
|
||||
- All resource creation/retrieval goes through DockerHost:
|
||||
```typescript
|
||||
// Old (deprecated):
|
||||
const container = await DockerContainer.getContainers(dockerHost);
|
||||
const network = await DockerNetwork.createNetwork(dockerHost, descriptor);
|
||||
|
||||
// New (clean API):
|
||||
const containers = await dockerHost.listContainers();
|
||||
const network = await dockerHost.createNetwork(descriptor);
|
||||
```
|
||||
|
||||
**2. Container Management Methods Added**
|
||||
The DockerContainer class now has full CRUD and streaming operations:
|
||||
|
||||
**Lifecycle:**
|
||||
- `container.start()` - Start container
|
||||
- `container.stop(options?)` - Stop container
|
||||
- `container.remove(options?)` - Remove container
|
||||
- `container.refresh()` - Reload state
|
||||
|
||||
**Information:**
|
||||
- `container.inspect()` - Get detailed info
|
||||
- `container.logs(options)` - Get logs as string (one-shot)
|
||||
- `container.stats(options)` - Get stats
|
||||
|
||||
**Streaming & Interactive:**
|
||||
- `container.streamLogs(options)` - Stream logs continuously (follow mode)
|
||||
- `container.attach(options)` - Attach to main process (PID 1) with bidirectional stream
|
||||
- `container.exec(command, options)` - Execute commands in container interactively
|
||||
|
||||
**Example - Stream Logs:**
|
||||
```typescript
|
||||
const container = await dockerHost.getContainerById('abc123');
|
||||
const logStream = await container.streamLogs({ timestamps: true });
|
||||
|
||||
logStream.on('data', (chunk) => {
|
||||
console.log(chunk.toString());
|
||||
});
|
||||
```
|
||||
|
||||
**Example - Attach to Container:**
|
||||
```typescript
|
||||
const { stream, close } = await container.attach({
|
||||
stdin: true,
|
||||
stdout: true,
|
||||
stderr: true
|
||||
});
|
||||
|
||||
// Pipe to/from process
|
||||
process.stdin.pipe(stream);
|
||||
stream.pipe(process.stdout);
|
||||
|
||||
// Later: detach
|
||||
await close();
|
||||
```
|
||||
|
||||
**Example - Execute Command:**
|
||||
```typescript
|
||||
const { stream, close } = await container.exec('ls -la /app', {
|
||||
tty: true
|
||||
});
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
console.log(chunk.toString());
|
||||
});
|
||||
|
||||
stream.on('end', async () => {
|
||||
await close();
|
||||
});
|
||||
```
|
||||
|
||||
**3. DockerResource Base Class**
|
||||
All resource classes now extend `DockerResource`:
|
||||
- Consistent `dockerHost` property (not `dockerHostRef`)
|
||||
- Required `refresh()` method
|
||||
- Standardized constructor pattern
|
||||
|
||||
**4. ImageStore Encapsulation**
|
||||
- `dockerHost.imageStore` is now private
|
||||
- Use `dockerHost.storeImage(name, stream)` instead
|
||||
- Use `dockerHost.retrieveImage(name)` instead
|
||||
|
||||
**5. Creation Descriptors Support Both Primitives and Instances**
|
||||
Interfaces now accept both strings and class instances:
|
||||
```typescript
|
||||
// Both work:
|
||||
await dockerHost.createService({
|
||||
image: 'nginx:latest', // String
|
||||
networks: ['my-network'], // String array
|
||||
secrets: ['my-secret'] // String array
|
||||
});
|
||||
|
||||
await dockerHost.createService({
|
||||
image: imageInstance, // DockerImage instance
|
||||
networks: [networkInstance], // DockerNetwork array
|
||||
secrets: [secretInstance] // DockerSecret array
|
||||
});
|
||||
```
|
||||
|
||||
### Migration Guide
|
||||
Replace all static method calls with dockerHost methods:
|
||||
- `DockerContainer.getContainers(host)` → `dockerHost.listContainers()`
|
||||
- `DockerImage.createFromRegistry(host, opts)` → `dockerHost.createImageFromRegistry(opts)`
|
||||
- `DockerService.createService(host, desc)` → `dockerHost.createService(desc)`
|
||||
- `dockerHost.imageStore.storeImage(...)` → `dockerHost.storeImage(...)`
|
||||
|
||||
## smartrequest v5+ Migration (2025-11-17)
|
||||
|
||||
### Breaking Change
|
||||
|
||||
@@ -22,18 +22,18 @@ tap.test('should create a docker swarm', async () => {
|
||||
|
||||
// Containers
|
||||
tap.test('should list containers', async () => {
|
||||
const containers = await testDockerHost.getContainers();
|
||||
const containers = await testDockerHost.listContainers();
|
||||
console.log(containers);
|
||||
});
|
||||
|
||||
// Networks
|
||||
tap.test('should list networks', async () => {
|
||||
const networks = await testDockerHost.getNetworks();
|
||||
const networks = await testDockerHost.listNetworks();
|
||||
console.log(networks);
|
||||
});
|
||||
|
||||
tap.test('should create a network', async () => {
|
||||
const newNetwork = await docker.DockerNetwork.createNetwork(testDockerHost, {
|
||||
const newNetwork = await testDockerHost.createNetwork({
|
||||
Name: 'webgateway',
|
||||
});
|
||||
expect(newNetwork).toBeInstanceOf(docker.DockerNetwork);
|
||||
@@ -41,20 +41,15 @@ tap.test('should create a network', async () => {
|
||||
});
|
||||
|
||||
tap.test('should remove a network', async () => {
|
||||
const webgateway = await docker.DockerNetwork.getNetworkByName(
|
||||
testDockerHost,
|
||||
'webgateway',
|
||||
);
|
||||
const webgateway = await testDockerHost.getNetworkByName('webgateway');
|
||||
await webgateway.remove();
|
||||
});
|
||||
|
||||
// Images
|
||||
tap.test('should pull an image from imagetag', async () => {
|
||||
const image = await docker.DockerImage.createFromRegistry(testDockerHost, {
|
||||
creationObject: {
|
||||
imageUrl: 'hosttoday/ht-docker-node',
|
||||
imageTag: 'alpine',
|
||||
},
|
||||
const image = await testDockerHost.createImageFromRegistry({
|
||||
imageUrl: 'hosttoday/ht-docker-node',
|
||||
imageTag: 'alpine',
|
||||
});
|
||||
expect(image).toBeInstanceOf(docker.DockerImage);
|
||||
console.log(image);
|
||||
@@ -71,7 +66,7 @@ tap.test('should return a change Observable', async (tools) => {
|
||||
|
||||
// SECRETS
|
||||
tap.test('should create a secret', async () => {
|
||||
const mySecret = await docker.DockerSecret.createSecret(testDockerHost, {
|
||||
const mySecret = await testDockerHost.createSecret({
|
||||
name: 'testSecret',
|
||||
version: '1.0.3',
|
||||
contentArg: `{ "hi": "wow"}`,
|
||||
@@ -81,10 +76,7 @@ tap.test('should create a secret', async () => {
|
||||
});
|
||||
|
||||
tap.test('should remove a secret by name', async () => {
|
||||
const mySecret = await docker.DockerSecret.getSecretByName(
|
||||
testDockerHost,
|
||||
'testSecret',
|
||||
);
|
||||
const mySecret = await testDockerHost.getSecretByName('testSecret');
|
||||
await mySecret.remove();
|
||||
});
|
||||
|
||||
@@ -94,29 +86,24 @@ tap.test('should activate swarm mode', async () => {
|
||||
});
|
||||
|
||||
tap.test('should list all services', async (tools) => {
|
||||
const services = await testDockerHost.getServices();
|
||||
const services = await testDockerHost.listServices();
|
||||
console.log(services);
|
||||
});
|
||||
|
||||
tap.test('should create a service', async () => {
|
||||
const testNetwork = await docker.DockerNetwork.createNetwork(testDockerHost, {
|
||||
const testNetwork = await testDockerHost.createNetwork({
|
||||
Name: 'testNetwork',
|
||||
});
|
||||
const testSecret = await docker.DockerSecret.createSecret(testDockerHost, {
|
||||
const testSecret = await testDockerHost.createSecret({
|
||||
name: 'testSecret',
|
||||
version: '0.0.1',
|
||||
labels: {},
|
||||
contentArg: '{"hi": "wow"}',
|
||||
});
|
||||
const testImage = await docker.DockerImage.createFromRegistry(
|
||||
testDockerHost,
|
||||
{
|
||||
creationObject: {
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
},
|
||||
},
|
||||
);
|
||||
const testService = await docker.DockerService.createService(testDockerHost, {
|
||||
const testImage = await testDockerHost.createImageFromRegistry({
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
});
|
||||
const testService = await testDockerHost.createService({
|
||||
image: testImage,
|
||||
labels: {},
|
||||
name: 'testService',
|
||||
@@ -133,14 +120,9 @@ tap.test('should create a service', async () => {
|
||||
|
||||
tap.test('should export images', async (toolsArg) => {
|
||||
const done = toolsArg.defer();
|
||||
const testImage = await docker.DockerImage.createFromRegistry(
|
||||
testDockerHost,
|
||||
{
|
||||
creationObject: {
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
},
|
||||
},
|
||||
);
|
||||
const testImage = await testDockerHost.createImageFromRegistry({
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
});
|
||||
const fsWriteStream = plugins.smartfile.fsStream.createWriteStream(
|
||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||
);
|
||||
@@ -155,13 +137,10 @@ tap.test('should import images', async () => {
|
||||
const fsReadStream = plugins.smartfile.fsStream.createReadStream(
|
||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||
);
|
||||
const importedImage = await docker.DockerImage.createFromTarStream(
|
||||
testDockerHost,
|
||||
const importedImage = await testDockerHost.createImageFromTarStream(
|
||||
fsReadStream,
|
||||
{
|
||||
tarStream: fsReadStream,
|
||||
creationObject: {
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
},
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
},
|
||||
);
|
||||
expect(importedImage).toBeInstanceOf(docker.DockerImage);
|
||||
@@ -177,8 +156,8 @@ tap.test('should expose a working DockerImageStore', async () => {
|
||||
};
|
||||
await testDockerHost.addS3Storage(s3Descriptor);
|
||||
|
||||
//
|
||||
await testDockerHost.imageStore.storeImage(
|
||||
// Use the new public API instead of direct imageStore access
|
||||
await testDockerHost.storeImage(
|
||||
'hello2',
|
||||
plugins.smartfile.fsStream.createReadStream(
|
||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||
@@ -186,6 +165,272 @@ tap.test('should expose a working DockerImageStore', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
// CONTAINER GETTERS
|
||||
tap.test('should return undefined for non-existent container', async () => {
|
||||
const container = await testDockerHost.getContainerById('invalid-container-id-12345');
|
||||
expect(container).toEqual(undefined);
|
||||
});
|
||||
|
||||
tap.test('should return container for valid container ID', async () => {
|
||||
const containers = await testDockerHost.listContainers();
|
||||
if (containers.length > 0) {
|
||||
const validId = containers[0].Id;
|
||||
const container = await testDockerHost.getContainerById(validId);
|
||||
expect(container).toBeInstanceOf(docker.DockerContainer);
|
||||
expect(container?.Id).toEqual(validId);
|
||||
}
|
||||
});
|
||||
|
||||
// CONTAINER STREAMING FEATURES
|
||||
let testContainer: docker.DockerContainer;
|
||||
|
||||
tap.test('should get an existing container for streaming tests', async () => {
|
||||
const containers = await testDockerHost.listContainers();
|
||||
|
||||
// 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');
|
||||
});
|
||||
|
||||
// NEW FEATURES TESTS (v5.1.0)
|
||||
|
||||
// Test 1: Network creation with custom driver and IPAM
|
||||
tap.test('should create bridge network with custom IPAM config', async () => {
|
||||
const network = await testDockerHost.createNetwork({
|
||||
Name: 'test-bridge-network',
|
||||
Driver: 'bridge',
|
||||
IPAM: {
|
||||
Config: [{
|
||||
Subnet: '172.20.0.0/16',
|
||||
Gateway: '172.20.0.1',
|
||||
}]
|
||||
},
|
||||
Labels: { testLabel: 'v5.1.0' },
|
||||
});
|
||||
expect(network).toBeInstanceOf(docker.DockerNetwork);
|
||||
expect(network.Name).toEqual('test-bridge-network');
|
||||
expect(network.Driver).toEqual('bridge');
|
||||
console.log('Created bridge network:', network.Name, 'with driver:', network.Driver);
|
||||
await network.remove();
|
||||
});
|
||||
|
||||
// Test 2: getVersion() returns proper Docker daemon info
|
||||
tap.test('should get Docker daemon version information', async () => {
|
||||
const version = await testDockerHost.getVersion();
|
||||
expect(version).toBeInstanceOf(Object);
|
||||
expect(typeof version.Version).toEqual('string');
|
||||
expect(typeof version.ApiVersion).toEqual('string');
|
||||
expect(typeof version.Os).toEqual('string');
|
||||
expect(typeof version.Arch).toEqual('string');
|
||||
console.log('Docker version:', version.Version, 'API version:', version.ApiVersion);
|
||||
});
|
||||
|
||||
// Test 3: pruneImages() functionality
|
||||
tap.test('should prune dangling images', async () => {
|
||||
const result = await testDockerHost.pruneImages({ dangling: true });
|
||||
expect(result).toBeInstanceOf(Object);
|
||||
expect(result).toHaveProperty('ImagesDeleted');
|
||||
expect(result).toHaveProperty('SpaceReclaimed');
|
||||
expect(Array.isArray(result.ImagesDeleted)).toEqual(true);
|
||||
expect(typeof result.SpaceReclaimed).toEqual('number');
|
||||
console.log('Pruned images. Space reclaimed:', result.SpaceReclaimed, 'bytes');
|
||||
});
|
||||
|
||||
// Test 4: exec() inspect() returns exit codes
|
||||
tap.test('should get exit code from exec command', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Execute a successful command (exit code 0)
|
||||
const { stream, close, inspect } = await testContainer.exec('echo "test successful"', {
|
||||
tty: false,
|
||||
attachStdout: true,
|
||||
attachStderr: true,
|
||||
});
|
||||
|
||||
stream.on('end', async () => {
|
||||
// Give Docker a moment to finalize the exec state
|
||||
await tools.delayFor(500);
|
||||
|
||||
const info = await inspect();
|
||||
expect(info).toBeInstanceOf(Object);
|
||||
expect(typeof info.ExitCode).toEqual('number');
|
||||
expect(info.ExitCode).toEqual(0); // Success
|
||||
expect(typeof info.Running).toEqual('boolean');
|
||||
expect(info.Running).toEqual(false); // Should be done
|
||||
expect(typeof info.ContainerID).toEqual('string');
|
||||
console.log('Exec inspect - ExitCode:', info.ExitCode, 'Running:', info.Running);
|
||||
|
||||
await close();
|
||||
done.resolve();
|
||||
});
|
||||
|
||||
stream.on('error', async (error) => {
|
||||
console.error('Exec error:', error);
|
||||
await close();
|
||||
done.resolve();
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('should get non-zero exit code from failed exec command', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Execute a command that fails (exit code 1)
|
||||
const { stream, close, inspect } = await testContainer.exec('exit 1', {
|
||||
tty: false,
|
||||
attachStdout: true,
|
||||
attachStderr: true,
|
||||
});
|
||||
|
||||
stream.on('end', async () => {
|
||||
// Give Docker a moment to finalize the exec state
|
||||
await tools.delayFor(500);
|
||||
|
||||
const info = await inspect();
|
||||
expect(info.ExitCode).toEqual(1); // Failure
|
||||
expect(info.Running).toEqual(false);
|
||||
console.log('Exec inspect (failed command) - ExitCode:', info.ExitCode);
|
||||
|
||||
await close();
|
||||
done.resolve();
|
||||
});
|
||||
|
||||
stream.on('error', async (error) => {
|
||||
console.error('Exec error:', error);
|
||||
await close();
|
||||
done.resolve();
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('cleanup', async () => {
|
||||
await testDockerHost.stop();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@apiclient.xyz/docker',
|
||||
version: '2.0.0',
|
||||
version: '5.1.0',
|
||||
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 type { 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 { DockerHost } from './classes.host.js';
|
||||
import { DockerResource } from './classes.base.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
export class DockerContainer {
|
||||
// STATIC
|
||||
export class DockerContainer extends DockerResource {
|
||||
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||
|
||||
/**
|
||||
* get all containers
|
||||
* Internal: Get all containers
|
||||
* Public API: Use dockerHost.listContainers() instead
|
||||
*/
|
||||
public static async getContainers(
|
||||
public static async _list(
|
||||
dockerHostArg: DockerHost,
|
||||
): Promise<DockerContainer[]> {
|
||||
const result: DockerContainer[] = [];
|
||||
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) {
|
||||
result.push(new DockerContainer(dockerHostArg, containerResult));
|
||||
}
|
||||
@@ -24,46 +26,50 @@ export class DockerContainer {
|
||||
}
|
||||
|
||||
/**
|
||||
* gets an container by Id
|
||||
* @param containerId
|
||||
* Internal: Get a container by ID
|
||||
* Public API: Use dockerHost.getContainerById(id) instead
|
||||
* Returns undefined if container does not exist
|
||||
*/
|
||||
public static async getContainerById(containerId: string) {
|
||||
// TODO: implement get container by id
|
||||
public static async _fromId(
|
||||
dockerHostArg: DockerHost,
|
||||
containerId: string,
|
||||
): Promise<DockerContainer | undefined> {
|
||||
const containers = await this._list(dockerHostArg);
|
||||
return containers.find((container) => container.Id === containerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a container
|
||||
* Internal: Create a container
|
||||
* Public API: Use dockerHost.createContainer(descriptor) instead
|
||||
*/
|
||||
public static async create(
|
||||
public static async _create(
|
||||
dockerHost: DockerHost,
|
||||
containerCreationDescriptor: interfaces.IContainerCreationDescriptor,
|
||||
) {
|
||||
// check for unique hostname
|
||||
const existingContainers = await DockerContainer.getContainers(dockerHost);
|
||||
): Promise<DockerContainer> {
|
||||
// Check for unique hostname
|
||||
const existingContainers = await DockerContainer._list(dockerHost);
|
||||
const sameHostNameContainer = existingContainers.find((container) => {
|
||||
// TODO implement HostName Detection;
|
||||
return false;
|
||||
});
|
||||
|
||||
const response = await dockerHost.request('POST', '/containers/create', {
|
||||
Hostname: containerCreationDescriptor.Hostname,
|
||||
Domainname: containerCreationDescriptor.Domainname,
|
||||
User: 'root',
|
||||
});
|
||||
|
||||
if (response.statusCode < 300) {
|
||||
logger.log('info', 'Container created successfully');
|
||||
// Return the created container instance
|
||||
return await DockerContainer._fromId(dockerHost, response.body.Id);
|
||||
} else {
|
||||
logger.log(
|
||||
'error',
|
||||
'There has been a problem when creating the container',
|
||||
);
|
||||
logger.log('error', 'There has been a problem when creating the container');
|
||||
throw new Error(`Failed to create container: ${response.statusCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
// references
|
||||
public dockerHost: DockerHost;
|
||||
|
||||
// properties
|
||||
// INSTANCE PROPERTIES
|
||||
public Id: string;
|
||||
public Names: string[];
|
||||
public Image: string;
|
||||
@@ -95,10 +101,301 @@ export class DockerContainer {
|
||||
};
|
||||
};
|
||||
public Mounts: any;
|
||||
|
||||
constructor(dockerHostArg: DockerHost, dockerContainerObjectArg: any) {
|
||||
this.dockerHost = dockerHostArg;
|
||||
super(dockerHostArg);
|
||||
Object.keys(dockerContainerObjectArg).forEach((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>;
|
||||
inspect: () => Promise<interfaces.IExecInspectInfo>;
|
||||
}> {
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
|
||||
const inspect = async (): Promise<interfaces.IExecInspectInfo> => {
|
||||
const inspectResponse = await this.dockerHost.request('GET', `/exec/${execId}/json`);
|
||||
return inspectResponse.body;
|
||||
};
|
||||
|
||||
return {
|
||||
stream: duplexStream,
|
||||
close,
|
||||
inspect,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import { DockerContainer } from './classes.container.js';
|
||||
import { DockerNetwork } from './classes.network.js';
|
||||
import { DockerService } from './classes.service.js';
|
||||
import { DockerSecret } from './classes.secret.js';
|
||||
import { logger } from './logger.js';
|
||||
import { DockerImageStore } from './classes.imagestore.js';
|
||||
import { DockerImage } from './classes.image.js';
|
||||
@@ -26,7 +28,7 @@ export class DockerHost {
|
||||
*/
|
||||
public socketPath: string;
|
||||
private registryToken: string = '';
|
||||
public imageStore: DockerImageStore;
|
||||
private imageStore: DockerImageStore; // Now private - use storeImage/retrieveImage instead
|
||||
public smartBucket: plugins.smartbucket.SmartBucket;
|
||||
|
||||
/**
|
||||
@@ -74,6 +76,37 @@ export class DockerHost {
|
||||
await this.imageStore.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping the Docker daemon to check if it's running and accessible
|
||||
* @returns Promise that resolves if Docker is available, rejects otherwise
|
||||
* @throws Error if Docker ping fails
|
||||
*/
|
||||
public async ping(): Promise<void> {
|
||||
const response = await this.request('GET', '/_ping');
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`Docker ping failed with status ${response.statusCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Docker daemon version information
|
||||
* @returns Version info including Docker version, API version, OS, architecture, etc.
|
||||
*/
|
||||
public async getVersion(): Promise<{
|
||||
Version: string;
|
||||
ApiVersion: string;
|
||||
MinAPIVersion?: string;
|
||||
GitCommit: string;
|
||||
GoVersion: string;
|
||||
Os: string;
|
||||
Arch: string;
|
||||
KernelVersion: string;
|
||||
BuildTime?: string;
|
||||
}> {
|
||||
const response = await this.request('GET', '/version');
|
||||
return response.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* authenticate against a registry
|
||||
* @param userArg
|
||||
@@ -111,70 +144,220 @@ export class DockerHost {
|
||||
}
|
||||
|
||||
// ==============
|
||||
// NETWORKS
|
||||
// NETWORKS - Public Factory API
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* gets all networks
|
||||
* Lists all networks
|
||||
*/
|
||||
public async getNetworks() {
|
||||
return await DockerNetwork.getNetworks(this);
|
||||
public async listNetworks() {
|
||||
return await DockerNetwork._list(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a network
|
||||
*/
|
||||
public async createNetwork(
|
||||
optionsArg: Parameters<typeof DockerNetwork.createNetwork>[1],
|
||||
) {
|
||||
return await DockerNetwork.createNetwork(this, optionsArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* get a network by name
|
||||
* Gets a network by name
|
||||
*/
|
||||
public async getNetworkByName(networkNameArg: string) {
|
||||
return await DockerNetwork.getNetworkByName(this, networkNameArg);
|
||||
return await DockerNetwork._fromName(this, networkNameArg);
|
||||
}
|
||||
|
||||
// ==============
|
||||
// CONTAINERS
|
||||
// ==============
|
||||
/**
|
||||
* gets all containers
|
||||
* Creates a network
|
||||
*/
|
||||
public async getContainers() {
|
||||
const containerArray = await DockerContainer.getContainers(this);
|
||||
return containerArray;
|
||||
public async createNetwork(
|
||||
descriptor: interfaces.INetworkCreationDescriptor,
|
||||
) {
|
||||
return await DockerNetwork._create(this, descriptor);
|
||||
}
|
||||
|
||||
// ==============
|
||||
// SERVICES
|
||||
// CONTAINERS - Public Factory API
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* gets all services
|
||||
* Lists all containers
|
||||
*/
|
||||
public async getServices() {
|
||||
const serviceArray = await DockerService.getServices(this);
|
||||
return serviceArray;
|
||||
public async listContainers() {
|
||||
return await DockerContainer._list(this);
|
||||
}
|
||||
|
||||
// ==============
|
||||
// IMAGES
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* get all images
|
||||
* Gets a container by ID
|
||||
* Returns undefined if container does not exist
|
||||
*/
|
||||
public async getImages() {
|
||||
return await DockerImage.getImages(this);
|
||||
public async getContainerById(containerId: string): Promise<DockerContainer | undefined> {
|
||||
return await DockerContainer._fromId(this, containerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* get an image by name
|
||||
* Creates a container
|
||||
*/
|
||||
public async createContainer(
|
||||
descriptor: interfaces.IContainerCreationDescriptor,
|
||||
) {
|
||||
return await DockerContainer._create(this, descriptor);
|
||||
}
|
||||
|
||||
// ==============
|
||||
// SERVICES - Public Factory API
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* Lists all services
|
||||
*/
|
||||
public async listServices() {
|
||||
return await DockerService._list(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 - Public Factory API
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* Lists all images
|
||||
*/
|
||||
public async listImages() {
|
||||
return await DockerImage._list(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an image by name
|
||||
*/
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune unused images
|
||||
* @param options Optional filters (dangling, until, label)
|
||||
* @returns Object with deleted images and space reclaimed
|
||||
*/
|
||||
public async pruneImages(options?: {
|
||||
dangling?: boolean;
|
||||
filters?: Record<string, string[]>;
|
||||
}): Promise<{
|
||||
ImagesDeleted: Array<{ Untagged?: string; Deleted?: string }>;
|
||||
SpaceReclaimed: number;
|
||||
}> {
|
||||
const filters: Record<string, string[]> = options?.filters || {};
|
||||
|
||||
// Add dangling filter if specified
|
||||
if (options?.dangling !== undefined) {
|
||||
filters.dangling = [options.dangling.toString()];
|
||||
}
|
||||
|
||||
let route = '/images/prune';
|
||||
if (filters && Object.keys(filters).length > 0) {
|
||||
route += `?filters=${encodeURIComponent(JSON.stringify(filters))}`;
|
||||
}
|
||||
|
||||
const response = await this.request('POST', route);
|
||||
|
||||
return response.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an image from a Dockerfile
|
||||
*/
|
||||
public async buildImage(imageTag: string) {
|
||||
return await DockerImage._build(this, imageTag);
|
||||
}
|
||||
|
||||
// ==============
|
||||
// SECRETS - Public Factory API
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* Lists all secrets
|
||||
*/
|
||||
public async listSecrets() {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,6 +501,7 @@ export class DockerHost {
|
||||
methodArg: string,
|
||||
routeArg: string,
|
||||
readStream?: plugins.smartstream.stream.Readable,
|
||||
jsonData?: any,
|
||||
) {
|
||||
const requestUrl = `${this.socketPath}${routeArg}`;
|
||||
|
||||
@@ -330,6 +514,11 @@ export class DockerHost {
|
||||
.timeout(30000)
|
||||
.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 (readStream) {
|
||||
let counter = 0;
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import { DockerHost } from './classes.host.js';
|
||||
import { DockerResource } from './classes.base.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* represents a docker image on the remote docker host
|
||||
*/
|
||||
export class DockerImage {
|
||||
// STATIC
|
||||
public static async getImages(dockerHost: DockerHost) {
|
||||
export class DockerImage extends DockerResource {
|
||||
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||
|
||||
/**
|
||||
* Internal: Get all images
|
||||
* Public API: Use dockerHost.listImages() instead
|
||||
*/
|
||||
public static async _list(dockerHost: DockerHost) {
|
||||
const images: DockerImage[] = [];
|
||||
const response = await dockerHost.request('GET', '/images/json');
|
||||
for (const imageObject of response.body) {
|
||||
@@ -17,11 +23,15 @@ export class DockerImage {
|
||||
return images;
|
||||
}
|
||||
|
||||
public static async getImageByName(
|
||||
/**
|
||||
* Internal: Get image by name
|
||||
* Public API: Use dockerHost.getImageByName(name) instead
|
||||
*/
|
||||
public static async _fromName(
|
||||
dockerHost: DockerHost,
|
||||
imageNameArg: string,
|
||||
) {
|
||||
const images = await this.getImages(dockerHost);
|
||||
const images = await this._list(dockerHost);
|
||||
const result = images.find((image) => {
|
||||
if (image.RepoTags) {
|
||||
return image.RepoTags.includes(imageNameArg);
|
||||
@@ -32,7 +42,11 @@ export class DockerImage {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async createFromRegistry(
|
||||
/**
|
||||
* Internal: Create image from registry
|
||||
* Public API: Use dockerHost.createImageFromRegistry(descriptor) instead
|
||||
*/
|
||||
public static async _createFromRegistry(
|
||||
dockerHostArg: DockerHost,
|
||||
optionsArg: {
|
||||
creationObject: interfaces.IImageCreationDescriptor;
|
||||
@@ -76,7 +90,7 @@ export class DockerImage {
|
||||
'info',
|
||||
`Successfully pulled image ${imageUrlObject.imageUrl} from the registry`,
|
||||
);
|
||||
const image = await DockerImage.getImageByName(
|
||||
const image = await DockerImage._fromName(
|
||||
dockerHostArg,
|
||||
imageUrlObject.imageOriginTag,
|
||||
);
|
||||
@@ -87,11 +101,10 @@ export class DockerImage {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param dockerHostArg
|
||||
* @param tarStreamArg
|
||||
* Internal: Create image from tar stream
|
||||
* Public API: Use dockerHost.createImageFromTarStream(stream, descriptor) instead
|
||||
*/
|
||||
public static async createFromTarStream(
|
||||
public static async _createFromTarStream(
|
||||
dockerHostArg: DockerHost,
|
||||
optionsArg: {
|
||||
creationObject: interfaces.IImageCreationDescriptor;
|
||||
@@ -161,11 +174,11 @@ export class DockerImage {
|
||||
}
|
||||
|
||||
// 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..."
|
||||
// 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.
|
||||
const newlyImportedImage = await DockerImage.getImageByName(
|
||||
const newlyImportedImage = await DockerImage._fromName(
|
||||
dockerHostArg,
|
||||
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
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
// references
|
||||
public dockerHost: DockerHost;
|
||||
|
||||
// properties
|
||||
// INSTANCE PROPERTIES
|
||||
/**
|
||||
* the tags for an image
|
||||
*/
|
||||
@@ -215,13 +228,28 @@ export class DockerImage {
|
||||
public Size: number;
|
||||
public VirtualSize: number;
|
||||
|
||||
constructor(dockerHostArg, dockerImageObjectArg: any) {
|
||||
this.dockerHost = dockerHostArg;
|
||||
constructor(dockerHostArg: DockerHost, dockerImageObjectArg: any) {
|
||||
super(dockerHostArg);
|
||||
Object.keys(dockerImageObjectArg).forEach((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
|
||||
* @param newTag
|
||||
@@ -234,7 +262,7 @@ export class DockerImage {
|
||||
* pulls the latest version from the registry
|
||||
*/
|
||||
public async pullLatestImageFromRegistry(): Promise<boolean> {
|
||||
const updatedImage = await DockerImage.createFromRegistry(this.dockerHost, {
|
||||
const updatedImage = await DockerImage._createFromRegistry(this.dockerHost, {
|
||||
creationObject: {
|
||||
imageUrl: this.RepoTags[0],
|
||||
},
|
||||
@@ -244,6 +272,25 @@ export class DockerImage {
|
||||
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
|
||||
public async getVersion() {
|
||||
if (this.Labels && this.Labels.version) {
|
||||
|
||||
@@ -2,11 +2,18 @@ import * as plugins from './plugins.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import { DockerHost } from './classes.host.js';
|
||||
import { DockerResource } from './classes.base.js';
|
||||
import { DockerService } from './classes.service.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
export class DockerNetwork {
|
||||
public static async getNetworks(
|
||||
export class DockerNetwork extends DockerResource {
|
||||
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||
|
||||
/**
|
||||
* Internal: Get all networks
|
||||
* Public API: Use dockerHost.getNetworks() instead
|
||||
*/
|
||||
public static async _list(
|
||||
dockerHost: DockerHost,
|
||||
): Promise<DockerNetwork[]> {
|
||||
const dockerNetworks: DockerNetwork[] = [];
|
||||
@@ -19,42 +26,42 @@ export class DockerNetwork {
|
||||
return dockerNetworks;
|
||||
}
|
||||
|
||||
public static async getNetworkByName(
|
||||
/**
|
||||
* Internal: Get network by name
|
||||
* Public API: Use dockerHost.getNetworkByName(name) instead
|
||||
*/
|
||||
public static async _fromName(
|
||||
dockerHost: DockerHost,
|
||||
dockerNetworkNameArg: string,
|
||||
) {
|
||||
const networks = await DockerNetwork.getNetworks(dockerHost);
|
||||
const networks = await DockerNetwork._list(dockerHost);
|
||||
return networks.find(
|
||||
(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,
|
||||
networkCreationDescriptor: interfaces.INetworkCreationDescriptor,
|
||||
): Promise<DockerNetwork> {
|
||||
const response = await dockerHost.request('POST', '/networks/create', {
|
||||
Name: networkCreationDescriptor.Name,
|
||||
CheckDuplicate: true,
|
||||
Driver: 'overlay',
|
||||
EnableIPv6: false,
|
||||
/* IPAM: {
|
||||
Driver: 'default',
|
||||
Config: [
|
||||
{
|
||||
Subnet: `172.20.${networkCreationDescriptor.NetworkNumber}.0/16`,
|
||||
IPRange: `172.20.${networkCreationDescriptor.NetworkNumber}.0/24`,
|
||||
Gateway: `172.20.${networkCreationDescriptor.NetworkNumber}.11`
|
||||
}
|
||||
]
|
||||
}, */
|
||||
Internal: false,
|
||||
Attachable: true,
|
||||
Driver: networkCreationDescriptor.Driver || 'overlay',
|
||||
EnableIPv6: networkCreationDescriptor.EnableIPv6 || false,
|
||||
IPAM: networkCreationDescriptor.IPAM,
|
||||
Internal: networkCreationDescriptor.Internal || false,
|
||||
Attachable: networkCreationDescriptor.Attachable !== undefined ? networkCreationDescriptor.Attachable : true,
|
||||
Labels: networkCreationDescriptor.Labels,
|
||||
Ingress: false,
|
||||
});
|
||||
if (response.statusCode < 300) {
|
||||
logger.log('info', 'Created network successfully');
|
||||
return await DockerNetwork.getNetworkByName(
|
||||
return await DockerNetwork._fromName(
|
||||
dockerHost,
|
||||
networkCreationDescriptor.Name,
|
||||
);
|
||||
@@ -67,11 +74,7 @@ export class DockerNetwork {
|
||||
}
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
// references
|
||||
public dockerHost: DockerHost;
|
||||
|
||||
// properties
|
||||
// INSTANCE PROPERTIES
|
||||
public Name: string;
|
||||
public Id: string;
|
||||
public Created: string;
|
||||
@@ -93,11 +96,23 @@ export class DockerNetwork {
|
||||
};
|
||||
|
||||
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() {
|
||||
const response = await this.dockerHost.request(
|
||||
@@ -106,7 +121,7 @@ export class DockerNetwork {
|
||||
);
|
||||
}
|
||||
|
||||
public async getContainersOnNetwork(): Promise<
|
||||
public async listContainersOnNetwork(): Promise<
|
||||
Array<{
|
||||
Name: string;
|
||||
EndpointID: string;
|
||||
@@ -128,7 +143,7 @@ export class DockerNetwork {
|
||||
}
|
||||
|
||||
public async getContainersOnNetworkForService(serviceArg: DockerService) {
|
||||
const containersOnNetwork = await this.getContainersOnNetwork();
|
||||
const containersOnNetwork = await this.listContainersOnNetwork();
|
||||
const containersOfService = containersOnNetwork.filter((container) => {
|
||||
return container.Name.startsWith(serviceArg.Spec.Name);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { DockerHost } from './classes.host.js';
|
||||
import { DockerResource } from './classes.base.js';
|
||||
|
||||
// interfaces
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
export class DockerSecret {
|
||||
// STATIC
|
||||
public static async getSecrets(dockerHostArg: DockerHost) {
|
||||
export class DockerSecret extends DockerResource {
|
||||
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||
|
||||
/**
|
||||
* Internal: Get all secrets
|
||||
* Public API: Use dockerHost.listSecrets() instead
|
||||
*/
|
||||
public static async _list(dockerHostArg: DockerHost) {
|
||||
const response = await dockerHostArg.request('GET', '/secrets');
|
||||
const secrets: DockerSecret[] = [];
|
||||
for (const secret of response.body) {
|
||||
@@ -17,20 +23,32 @@ export class DockerSecret {
|
||||
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);
|
||||
}
|
||||
|
||||
public static async getSecretByName(
|
||||
/**
|
||||
* Internal: Get secret by name
|
||||
* Public API: Use dockerHost.getSecretByName(name) instead
|
||||
*/
|
||||
public static async _fromName(
|
||||
dockerHostArg: DockerHost,
|
||||
nameArg: string,
|
||||
) {
|
||||
const secrets = await this.getSecrets(dockerHostArg);
|
||||
const secrets = await this._list(dockerHostArg);
|
||||
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,
|
||||
secretDescriptor: interfaces.ISecretCreationDescriptor,
|
||||
) {
|
||||
@@ -48,12 +66,12 @@ export class DockerSecret {
|
||||
Object.assign(newSecretInstance, response.body);
|
||||
Object.assign(
|
||||
newSecretInstance,
|
||||
await DockerSecret.getSecretByID(dockerHostArg, newSecretInstance.ID),
|
||||
await DockerSecret._fromId(dockerHostArg, newSecretInstance.ID),
|
||||
);
|
||||
return newSecretInstance;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
// INSTANCE PROPERTIES
|
||||
public ID: string;
|
||||
public Spec: {
|
||||
Name: string;
|
||||
@@ -63,13 +81,24 @@ export class DockerSecret {
|
||||
Index: string;
|
||||
};
|
||||
|
||||
public dockerHost: 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) {
|
||||
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() {
|
||||
await this.dockerHost.request('DELETE', `/secrets/${this.ID}`);
|
||||
}
|
||||
|
||||
// get things
|
||||
/**
|
||||
* Gets the version label of this secret
|
||||
*/
|
||||
public async getVersion() {
|
||||
return this.Spec.Labels.version;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,19 @@ import * as plugins from './plugins.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import { DockerHost } from './classes.host.js';
|
||||
import { DockerResource } from './classes.base.js';
|
||||
import { DockerImage } from './classes.image.js';
|
||||
import { DockerSecret } from './classes.secret.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
export class DockerService {
|
||||
// STATIC
|
||||
public static async getServices(dockerHost: DockerHost) {
|
||||
export class DockerService extends DockerResource {
|
||||
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||
|
||||
/**
|
||||
* Internal: Get all services
|
||||
* Public API: Use dockerHost.listServices() instead
|
||||
*/
|
||||
public static async _list(dockerHost: DockerHost) {
|
||||
const services: DockerService[] = [];
|
||||
const response = await dockerHost.request('GET', '/services');
|
||||
for (const serviceObject of response.body) {
|
||||
@@ -19,11 +25,15 @@ export class DockerService {
|
||||
return services;
|
||||
}
|
||||
|
||||
public static async getServiceByName(
|
||||
/**
|
||||
* Internal: Get service by name
|
||||
* Public API: Use dockerHost.getServiceByName(name) instead
|
||||
*/
|
||||
public static async _fromName(
|
||||
dockerHost: DockerHost,
|
||||
networkName: string,
|
||||
): Promise<DockerService> {
|
||||
const allServices = await DockerService.getServices(dockerHost);
|
||||
const allServices = await DockerService._list(dockerHost);
|
||||
const wantedService = allServices.find((service) => {
|
||||
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,
|
||||
serviceCreationDescriptor: interfaces.IServiceCreationDescriptor,
|
||||
): Promise<DockerService> {
|
||||
// lets get the image
|
||||
logger.log(
|
||||
'info',
|
||||
`now creating service ${serviceCreationDescriptor.name}`,
|
||||
);
|
||||
|
||||
// await serviceCreationDescriptor.image.pullLatestImageFromRegistry();
|
||||
const serviceVersion = await serviceCreationDescriptor.image.getVersion();
|
||||
// Resolve image (support both string and DockerImage instance)
|
||||
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 = {
|
||||
...serviceCreationDescriptor.labels,
|
||||
@@ -90,6 +110,7 @@ export class DockerService {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve networks (support both string[] and DockerNetwork[])
|
||||
const networkArray: Array<{
|
||||
Target: string;
|
||||
Aliases: string[];
|
||||
@@ -101,8 +122,11 @@ export class DockerService {
|
||||
logger.log('warn', 'Skipping null network in service creation');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve network name
|
||||
const networkName = typeof network === 'string' ? network : network.Name;
|
||||
networkArray.push({
|
||||
Target: network.Name,
|
||||
Target: networkName,
|
||||
Aliases: [serviceCreationDescriptor.networkAlias],
|
||||
});
|
||||
}
|
||||
@@ -119,9 +143,20 @@ export class DockerService {
|
||||
});
|
||||
}
|
||||
|
||||
// lets configure secrets
|
||||
// Resolve secrets (support both string[] and DockerSecret[])
|
||||
const secretArray: any[] = [];
|
||||
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({
|
||||
File: {
|
||||
Name: 'secret.json', // TODO: make sure that works with multiple secrets
|
||||
@@ -129,8 +164,8 @@ export class DockerService {
|
||||
GID: '33',
|
||||
Mode: 384,
|
||||
},
|
||||
SecretID: secret.ID,
|
||||
SecretName: secret.Spec.Name,
|
||||
SecretID: secretInstance.ID,
|
||||
SecretName: secretInstance.Spec.Name,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,7 +190,7 @@ export class DockerService {
|
||||
Name: serviceCreationDescriptor.name,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: serviceCreationDescriptor.image.RepoTags[0],
|
||||
Image: imageInstance.RepoTags[0],
|
||||
Labels: labels,
|
||||
Secrets: secretArray,
|
||||
Mounts: mounts,
|
||||
@@ -189,15 +224,15 @@ export class DockerService {
|
||||
},
|
||||
});
|
||||
|
||||
const createdService = await DockerService.getServiceByName(
|
||||
const createdService = await DockerService._fromName(
|
||||
dockerHost,
|
||||
serviceCreationDescriptor.name,
|
||||
);
|
||||
return createdService;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
public dockerHostRef: DockerHost;
|
||||
// INSTANCE PROPERTIES
|
||||
// Note: dockerHost (not dockerHostRef) for consistency with base class
|
||||
|
||||
public ID: string;
|
||||
public Version: { Index: number };
|
||||
@@ -229,27 +264,49 @@ export class DockerService {
|
||||
public Endpoint: { Spec: {}; VirtualIPs: [any[]] };
|
||||
|
||||
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() {
|
||||
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() {
|
||||
const dockerData = await this.dockerHostRef.request(
|
||||
const dockerData = await this.dockerHost.request(
|
||||
'GET',
|
||||
`/services/${this.ID}`,
|
||||
);
|
||||
// TODO: Better assign: Object.assign(this, dockerData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this service needs an update based on image version
|
||||
*/
|
||||
public async needsUpdate(): Promise<boolean> {
|
||||
// TODO: implement digest based update recognition
|
||||
|
||||
await this.reReadFromDockerEngine();
|
||||
const dockerImage = await DockerImage.createFromRegistry(
|
||||
this.dockerHostRef,
|
||||
const dockerImage = await DockerImage._createFromRegistry(
|
||||
this.dockerHost,
|
||||
{
|
||||
creationObject: {
|
||||
imageUrl: this.Spec.TaskTemplate.ContainerSpec.Image,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './classes.base.js';
|
||||
export * from './classes.host.js';
|
||||
export * from './classes.container.js';
|
||||
export * from './classes.image.js';
|
||||
|
||||
@@ -1,7 +1,50 @@
|
||||
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 {
|
||||
Hostname: string;
|
||||
Domainname: string;
|
||||
networks?: DockerNetwork[];
|
||||
/** Network names (strings) or DockerNetwork instances */
|
||||
networks?: (string | DockerNetwork)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about an exec instance, including exit code and running state.
|
||||
* Retrieved via container.exec().inspect()
|
||||
*/
|
||||
export interface IExecInspectInfo {
|
||||
/** Exit code of the exec command (0 = success) */
|
||||
ExitCode: number;
|
||||
/** Whether the exec is currently running */
|
||||
Running: boolean;
|
||||
/** Process ID */
|
||||
Pid: number;
|
||||
/** Container ID where exec runs */
|
||||
ContainerID: string;
|
||||
/** Exec instance ID */
|
||||
ID: string;
|
||||
/** Whether stderr is open */
|
||||
OpenStderr: boolean;
|
||||
/** Whether stdin is open */
|
||||
OpenStdin: boolean;
|
||||
/** Whether stdout is open */
|
||||
OpenStdout: boolean;
|
||||
/** Whether exec can be removed */
|
||||
CanRemove: boolean;
|
||||
/** Detach keys */
|
||||
DetachKeys: string;
|
||||
/** Process configuration */
|
||||
ProcessConfig: {
|
||||
/** Whether TTY is allocated */
|
||||
tty: boolean;
|
||||
/** Entrypoint */
|
||||
entrypoint: string;
|
||||
/** Command arguments */
|
||||
arguments: string[];
|
||||
/** Whether running in privileged mode */
|
||||
privileged: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,4 +3,18 @@
|
||||
*/
|
||||
export interface INetworkCreationDescriptor {
|
||||
Name: string;
|
||||
Driver?: 'bridge' | 'overlay' | 'host' | 'none' | 'macvlan';
|
||||
Attachable?: boolean;
|
||||
Labels?: Record<string, string>;
|
||||
IPAM?: {
|
||||
Driver?: string;
|
||||
Config?: Array<{
|
||||
Subnet?: string;
|
||||
Gateway?: string;
|
||||
IPRange?: string;
|
||||
AuxiliaryAddresses?: Record<string, string>;
|
||||
}>;
|
||||
};
|
||||
Internal?: boolean;
|
||||
EnableIPv6?: boolean;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,20 @@ import { DockerNetwork } from '../classes.network.js';
|
||||
import { DockerSecret } from '../classes.secret.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 {
|
||||
name: string;
|
||||
image: DockerImage;
|
||||
/** Image tag (string) or DockerImage instance */
|
||||
image: string | DockerImage;
|
||||
labels: interfaces.TLabels;
|
||||
networks: DockerNetwork[];
|
||||
/** Network names (strings) or DockerNetwork instances */
|
||||
networks: (string | DockerNetwork)[];
|
||||
networkAlias: string;
|
||||
secrets: DockerSecret[];
|
||||
/** Secret names (strings) or DockerSecret instances */
|
||||
secrets: (string | DockerSecret)[];
|
||||
ports: string[];
|
||||
accessHostDockerSock?: boolean;
|
||||
resources?: {
|
||||
|
||||
Reference in New Issue
Block a user