Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a469a7e0ce | |||
| 137672a5cd | |||
| f4c3ba74d1 | |||
| 645e1fd4a9 | |||
| 1923837225 | |||
| 97b89efd84 | |||
| e28a35791a | |||
| b86601c939 |
@@ -1,15 +1,17 @@
|
|||||||
{
|
{
|
||||||
"npmdocker": {
|
|
||||||
"baseImage": "host.today/ht-docker-node:npmci",
|
|
||||||
"command": "(ls -a && rm -r node_modules && yarn global add npmts && yarn install && npmts)",
|
|
||||||
"dockerSock": true
|
|
||||||
},
|
|
||||||
"npmci": {
|
"npmci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": [],
|
||||||
"npmAccessLevel": "public",
|
"npmAccessLevel": "public",
|
||||||
"npmRegistryUrl": "registry.npmjs.org"
|
"npmRegistryUrl": "registry.npmjs.org"
|
||||||
},
|
},
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
|
},
|
||||||
"projectType": "npm",
|
"projectType": "npm",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "code.foss.global",
|
"githost": "code.foss.global",
|
||||||
@@ -17,19 +19,12 @@
|
|||||||
"gitrepo": "docker",
|
"gitrepo": "docker",
|
||||||
"description": "Provides easy communication with Docker remote API from Node.js, with TypeScript support.",
|
"description": "Provides easy communication with Docker remote API from Node.js, with TypeScript support.",
|
||||||
"npmPackagename": "@apiclient.xyz/docker",
|
"npmPackagename": "@apiclient.xyz/docker",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"keywords": [
|
},
|
||||||
"Docker",
|
"services": [
|
||||||
"API",
|
"mongodb",
|
||||||
"Node.js",
|
"minio"
|
||||||
"TypeScript",
|
|
||||||
"Containers",
|
|
||||||
"Images",
|
|
||||||
"Networks",
|
|
||||||
"Services",
|
|
||||||
"Secrets"
|
|
||||||
]
|
]
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"tsdoc": {
|
"tsdoc": {
|
||||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"json.schemas": [
|
"json.schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": ["/npmextra.json"],
|
"fileMatch": ["/.smartconfig.json"],
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1,5 +1,37 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-28 - 5.1.3 - fix(docker-service)
|
||||||
|
move swarm service networks into task template and correct service typings
|
||||||
|
|
||||||
|
- Moves the Networks configuration from the service root into TaskTemplate when creating services.
|
||||||
|
- Updates the service type definition to reflect the Networks structure expected by swarm services.
|
||||||
|
|
||||||
|
## 2026-03-28 - 5.1.2 - fix(deps)
|
||||||
|
upgrade core tooling dependencies and adapt Docker client internals for compatibility
|
||||||
|
|
||||||
|
- replace removed smartfile filesystem APIs with node:fs and SmartFileFactory usage
|
||||||
|
- update imagestore archive handling for smartarchive v5 and smartbucket v4 overwrite behavior
|
||||||
|
- improve Docker resource creation and stream handling with stricter null checks, cleanup, and timeout safeguards
|
||||||
|
- adjust tests and runtime behavior for Deno and newer dependency constraints
|
||||||
|
|
||||||
|
## 2026-03-16 - 5.1.1 - fix(paths)
|
||||||
|
use the system temp directory for nogit storage and add release metadata
|
||||||
|
|
||||||
|
- Changes nogitDir to resolve under the OS temporary directory instead of a local .nogit folder
|
||||||
|
- Adds @git.zone/cli release and module metadata to npmextra.json for npm publishing configuration
|
||||||
|
|
||||||
|
## 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)
|
## 2025-11-24 - 5.0.2 - fix(DockerContainer)
|
||||||
Fix getContainerById to return undefined for non-existent containers
|
Fix getContainerById to return undefined for non-existent containers
|
||||||
|
|
||||||
|
|||||||
+16
-16
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@apiclient.xyz/docker",
|
"name": "@apiclient.xyz/docker",
|
||||||
"version": "5.0.2",
|
"version": "5.1.3",
|
||||||
"description": "Provides easy communication with Docker remote API from Node.js, with TypeScript support.",
|
"description": "Provides easy communication with Docker remote API from Node.js, with TypeScript support.",
|
||||||
"private": false,
|
"private": false,
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --verbose --logfile --timeout 300)",
|
"test": "(tstest test/ --verbose --logfile --timeout 600)",
|
||||||
"build": "(tsbuild --web --allowimplicitany)",
|
"build": "(tsbuild --web --allowimplicitany)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
@@ -33,29 +33,29 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://code.foss.global/apiclient.xyz/docker#readme",
|
"homepage": "https://code.foss.global/apiclient.xyz/docker#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/lik": "^6.2.2",
|
"@push.rocks/lik": "^6.4.0",
|
||||||
"@push.rocks/smartarchive": "^4.2.2",
|
"@push.rocks/smartarchive": "^5.2.1",
|
||||||
"@push.rocks/smartbucket": "^3.3.10",
|
"@push.rocks/smartbucket": "^4.5.1",
|
||||||
"@push.rocks/smartfile": "^11.2.7",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
"@push.rocks/smartjson": "^5.2.0",
|
"@push.rocks/smartjson": "^6.0.0",
|
||||||
"@push.rocks/smartlog": "^3.1.10",
|
"@push.rocks/smartlog": "^3.2.1",
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.5.2",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartstream": "^3.2.5",
|
"@push.rocks/smartstream": "^3.4.0",
|
||||||
"@push.rocks/smartstring": "^4.1.0",
|
"@push.rocks/smartstring": "^4.1.0",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/smartversion": "^3.0.5",
|
"@push.rocks/smartversion": "^3.0.5",
|
||||||
"@tsclass/tsclass": "^9.3.0",
|
"@tsclass/tsclass": "^9.5.0",
|
||||||
"rxjs": "^7.8.2"
|
"rxjs": "^7.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^3.1.0",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsrun": "^2.0.0",
|
"@git.zone/tsrun": "^2.0.2",
|
||||||
"@git.zone/tstest": "^2.8.2",
|
"@git.zone/tstest": "^3.6.3",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@types/node": "22.7.5"
|
"@types/node": "^25.5.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"dist_ts_web/**/*",
|
"dist_ts_web/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
".smartconfig.json",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
Generated
+2628
-3143
File diff suppressed because it is too large
Load Diff
+54
-174
@@ -1,194 +1,74 @@
|
|||||||
# Docker Module - Development Hints
|
# Docker Module - Development Hints
|
||||||
|
|
||||||
## getContainerById() Bug Fix (2025-11-24 - v5.0.1)
|
## Dependency Upgrade Notes (2026-03-28 - v5.2.0)
|
||||||
|
|
||||||
### Problem
|
### Major Upgrades Completed
|
||||||
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:**
|
| Package | From | To | Notes |
|
||||||
- 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
|
| @push.rocks/smartfile | ^11.2.7 | ^13.1.2 | `fs.*`, `fsStream.*` removed; use `node:fs` directly or `SmartFileFactory.nodeFs()` |
|
||||||
- No way to detect the error state without checking for a `.message` property
|
| @push.rocks/smartarchive | ^4.2.2 | ^5.2.1 | `SmartArchive.fromArchiveFile()` removed; use `SmartArchive.create().file(path).extract(dir)` |
|
||||||
|
| @push.rocks/smartbucket | ^3.3.10 | ^4.5.1 | Strict-by-default: `fastPutStream` throws on existing objects instead of overwriting |
|
||||||
|
| @push.rocks/smartjson | ^5.2.0 | ^6.0.0 | No code changes needed |
|
||||||
|
| @push.rocks/smartnetwork | ^4.4.0 | ^4.5.2 | v4.5.2 uses Rust bridge for getDefaultGateway; breaks in Deno without --allow-run |
|
||||||
|
| @tsclass/tsclass | ^9.3.0 | ^9.5.0 | No code changes needed |
|
||||||
|
| @git.zone/tsbuild | ^3.1.0 | ^4.4.0 | v4.4.0 enforces strict TS checks (strictPropertyInitialization) |
|
||||||
|
| @git.zone/tstest | ^2.8.2 | ^3.6.3 | No code changes needed |
|
||||||
|
| @types/node | ^22.15.0 | ^25.5.0 | Major version bump |
|
||||||
|
|
||||||
**Root Cause:**
|
### Migration Details
|
||||||
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
|
**smartfile v13**: All `smartfile.fs.*` and `smartfile.fsStream.*` APIs were removed. Replaced with:
|
||||||
Changed `DockerContainer._fromId()` to use the **list+filter pattern**, matching the behavior of all other resource getter methods (DockerImage, DockerNetwork, DockerService, DockerSecret):
|
- `plugins.fs.createReadStream()` / `plugins.fs.createWriteStream()` (from `node:fs`)
|
||||||
|
- `plugins.fs.promises.rm()` (for file/dir removal)
|
||||||
|
- `plugins.fs.existsSync()` (for file existence checks)
|
||||||
|
- `plugins.smartfile.SmartFileFactory.nodeFs().fromFilePath()` (for reading files into SmartFile objects)
|
||||||
|
|
||||||
|
**smartarchive v5**: Uses fluent API now:
|
||||||
```typescript
|
```typescript
|
||||||
// Before (buggy):
|
// Old: SmartArchive.fromArchiveFile(path) -> archive.exportToFs(dir)
|
||||||
public static async _fromId(dockerHostArg: DockerHost, containerId: string): Promise<DockerContainer> {
|
// New: SmartArchive.create().file(path).extract(dir)
|
||||||
const response = await dockerHostArg.request('GET', `/containers/${containerId}/json`);
|
|
||||||
return new DockerContainer(dockerHostArg, response.body); // Creates invalid object from error!
|
|
||||||
}
|
|
||||||
|
|
||||||
// After (fixed):
|
// TarTools: packDirectory() now returns Uint8Array, use getDirectoryPackStream() for streams
|
||||||
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:**
|
**smartbucket v4**: `fastPutStream` now throws if object already exists. Must delete first:
|
||||||
- 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
|
```typescript
|
||||||
const container = await docker.getContainerById('abc123');
|
try { await dir.fastRemove({ path }); } catch (e) { /* may not exist */ }
|
||||||
if (container) {
|
await dir.fastPutStream({ stream, path });
|
||||||
const logs = await container.logs();
|
|
||||||
console.log(logs);
|
|
||||||
} else {
|
|
||||||
console.log('Container not found');
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## OOP Refactoring - Clean Architecture (2025-11-24)
|
**tsbuild v4.4.0**: Enforces `strictPropertyInitialization`. All class properties populated via `Object.assign()` from Docker API responses need `!` definite assignment assertions.
|
||||||
|
|
||||||
### Architecture Changes
|
**smartnetwork v4.5.2**: `getDefaultGateway()` now uses a Rust binary bridge. Fails in Deno without `--allow-run` permission. Code wraps the call in try/catch with fallback to empty string (Docker auto-detects advertise address).
|
||||||
The module has been restructured to follow a clean OOP Facade pattern:
|
|
||||||
- **DockerHost** is now the single entry point for all Docker operations
|
### Config Migration
|
||||||
|
|
||||||
|
- `npmextra.json` renamed to `.smartconfig.json`
|
||||||
|
- Removed stale `npmdocker` and duplicate `gitzone` sections
|
||||||
|
- `@push.rocks/smartfs` removed (was imported but never used)
|
||||||
|
|
||||||
|
## OCI Image Format Handling
|
||||||
|
|
||||||
|
The `DockerImageStore.storeImage()` method handles optional `repositories` file gracefully. OCI-format image tars may not include this file, so it's checked with `fs.existsSync()` before attempting to read.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **DockerHost** is the single entry point (Facade pattern)
|
||||||
- All resource classes extend abstract `DockerResource` base class
|
- All resource classes extend abstract `DockerResource` base class
|
||||||
- Static methods are prefixed with `_` to indicate internal use
|
- Static methods prefixed with `_` indicate internal use
|
||||||
- Public API is exclusively through DockerHost methods
|
- Public API exclusively through DockerHost methods
|
||||||
|
|
||||||
### Key Changes
|
### Key Patterns
|
||||||
|
|
||||||
**1. Factory Pattern**
|
- Factory pattern: All resource creation/retrieval goes through DockerHost
|
||||||
- All resource creation/retrieval goes through DockerHost:
|
- Stream handling: Web ReadableStreams from smartrequest are converted to Node.js streams via `smartstream.nodewebhelpers`
|
||||||
```typescript
|
- Container getter: Uses list+filter pattern (not direct API call) to avoid creating invalid objects from error responses
|
||||||
// Old (deprecated):
|
|
||||||
const container = await DockerContainer.getContainers(dockerHost);
|
|
||||||
const network = await DockerNetwork.createNetwork(dockerHost, descriptor);
|
|
||||||
|
|
||||||
// New (clean API):
|
## Test Notes
|
||||||
const containers = await dockerHost.listContainers();
|
|
||||||
const network = await dockerHost.createNetwork(descriptor);
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Container Management Methods Added**
|
- Tests are `nonci` (require Docker daemon)
|
||||||
The DockerContainer class now has full CRUD and streaming operations:
|
- S3 imagestore test can take 2-3 minutes depending on network
|
||||||
|
- Exec tests use 5s safety timeout due to buildkit container not always emitting stream 'end' events
|
||||||
**Lifecycle:**
|
- Test timeout is 600s to accommodate slow S3 uploads
|
||||||
- `container.start()` - Start container
|
- Deno tests crash with smartnetwork v4.5.2 due to Rust binary spawn permissions (not a code bug)
|
||||||
- `container.stop(options?)` - Stop container
|
|
||||||
- `container.remove(options?)` - Remove container
|
|
||||||
- `container.refresh()` - Reload state
|
|
||||||
|
|
||||||
**Information:**
|
|
||||||
- `container.inspect()` - Get detailed info
|
|
||||||
- `container.logs(options)` - Get logs as string (one-shot)
|
|
||||||
- `container.stats(options)` - Get stats
|
|
||||||
|
|
||||||
**Streaming & Interactive:**
|
|
||||||
- `container.streamLogs(options)` - Stream logs continuously (follow mode)
|
|
||||||
- `container.attach(options)` - Attach to main process (PID 1) with bidirectional stream
|
|
||||||
- `container.exec(command, options)` - Execute commands in container interactively
|
|
||||||
|
|
||||||
**Example - Stream Logs:**
|
|
||||||
```typescript
|
|
||||||
const container = await dockerHost.getContainerById('abc123');
|
|
||||||
const logStream = await container.streamLogs({ timestamps: true });
|
|
||||||
|
|
||||||
logStream.on('data', (chunk) => {
|
|
||||||
console.log(chunk.toString());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example - Attach to Container:**
|
|
||||||
```typescript
|
|
||||||
const { stream, close } = await container.attach({
|
|
||||||
stdin: true,
|
|
||||||
stdout: true,
|
|
||||||
stderr: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pipe to/from process
|
|
||||||
process.stdin.pipe(stream);
|
|
||||||
stream.pipe(process.stdout);
|
|
||||||
|
|
||||||
// Later: detach
|
|
||||||
await close();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example - Execute Command:**
|
|
||||||
```typescript
|
|
||||||
const { stream, close } = await container.exec('ls -la /app', {
|
|
||||||
tty: true
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('data', (chunk) => {
|
|
||||||
console.log(chunk.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('end', async () => {
|
|
||||||
await close();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. DockerResource Base Class**
|
|
||||||
All resource classes now extend `DockerResource`:
|
|
||||||
- Consistent `dockerHost` property (not `dockerHostRef`)
|
|
||||||
- Required `refresh()` method
|
|
||||||
- Standardized constructor pattern
|
|
||||||
|
|
||||||
**4. ImageStore Encapsulation**
|
|
||||||
- `dockerHost.imageStore` is now private
|
|
||||||
- Use `dockerHost.storeImage(name, stream)` instead
|
|
||||||
- Use `dockerHost.retrieveImage(name)` instead
|
|
||||||
|
|
||||||
**5. Creation Descriptors Support Both Primitives and Instances**
|
|
||||||
Interfaces now accept both strings and class instances:
|
|
||||||
```typescript
|
|
||||||
// Both work:
|
|
||||||
await dockerHost.createService({
|
|
||||||
image: 'nginx:latest', // String
|
|
||||||
networks: ['my-network'], // String array
|
|
||||||
secrets: ['my-secret'] // String array
|
|
||||||
});
|
|
||||||
|
|
||||||
await dockerHost.createService({
|
|
||||||
image: imageInstance, // DockerImage instance
|
|
||||||
networks: [networkInstance], // DockerNetwork array
|
|
||||||
secrets: [secretInstance] // DockerSecret array
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Migration Guide
|
|
||||||
Replace all static method calls with dockerHost methods:
|
|
||||||
- `DockerContainer.getContainers(host)` → `dockerHost.listContainers()`
|
|
||||||
- `DockerImage.createFromRegistry(host, opts)` → `dockerHost.createImageFromRegistry(opts)`
|
|
||||||
- `DockerService.createService(host, desc)` → `dockerHost.createService(desc)`
|
|
||||||
- `dockerHost.imageStore.storeImage(...)` → `dockerHost.storeImage(...)`
|
|
||||||
|
|
||||||
## smartrequest v5+ Migration (2025-11-17)
|
|
||||||
|
|
||||||
### Breaking Change
|
|
||||||
smartrequest v5.0.0+ returns web `ReadableStream` objects (Web Streams API) instead of Node.js streams.
|
|
||||||
|
|
||||||
### Solution Implemented
|
|
||||||
All streaming methods now convert web ReadableStreams to Node.js streams using:
|
|
||||||
```typescript
|
|
||||||
plugins.smartstream.nodewebhelpers.convertWebReadableToNodeReadable(webStream)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Files Modified
|
|
||||||
- `ts/classes.host.ts`:
|
|
||||||
- `requestStreaming()` - Converts web stream to Node.js stream before returning
|
|
||||||
- `getEventObservable()` - Works with converted Node.js stream
|
|
||||||
|
|
||||||
- `ts/classes.image.ts`:
|
|
||||||
- `createFromTarStream()` - Uses converted Node.js stream for event handling
|
|
||||||
- `exportToTarStream()` - Uses converted Node.js stream for backpressure management
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- Build: All 11 type errors resolved
|
|
||||||
- Tests: Node.js tests pass (DockerHost, DockerContainer, DockerImage, DockerImageStore)
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- The conversion maintains backward compatibility with existing code expecting Node.js stream methods (`.on()`, `.emit()`, `.pause()`, `.resume()`)
|
|
||||||
- smartstream's `nodewebhelpers` module provides bidirectional conversion utilities between web and Node.js streams
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// tstest:deno:allowAll
|
||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { Qenv } from '@push.rocks/qenv';
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
|
||||||
@@ -114,8 +115,8 @@ tap.test('should create a service', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await testService.remove();
|
await testService.remove();
|
||||||
await testNetwork.remove();
|
if (testNetwork) await testNetwork.remove();
|
||||||
await testSecret.remove();
|
if (testSecret) await testSecret.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should export images', async (toolsArg) => {
|
tap.test('should export images', async (toolsArg) => {
|
||||||
@@ -123,7 +124,7 @@ tap.test('should export images', async (toolsArg) => {
|
|||||||
const testImage = await testDockerHost.createImageFromRegistry({
|
const testImage = await testDockerHost.createImageFromRegistry({
|
||||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||||
});
|
});
|
||||||
const fsWriteStream = plugins.smartfile.fsStream.createWriteStream(
|
const fsWriteStream = plugins.fs.createWriteStream(
|
||||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||||
);
|
);
|
||||||
const exportStream = await testImage.exportToTarStream();
|
const exportStream = await testImage.exportToTarStream();
|
||||||
@@ -134,7 +135,7 @@ tap.test('should export images', async (toolsArg) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should import images', async () => {
|
tap.test('should import images', async () => {
|
||||||
const fsReadStream = plugins.smartfile.fsStream.createReadStream(
|
const fsReadStream = plugins.fs.createReadStream(
|
||||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||||
);
|
);
|
||||||
const importedImage = await testDockerHost.createImageFromTarStream(
|
const importedImage = await testDockerHost.createImageFromTarStream(
|
||||||
@@ -148,8 +149,10 @@ tap.test('should import images', async () => {
|
|||||||
|
|
||||||
tap.test('should expose a working DockerImageStore', async () => {
|
tap.test('should expose a working DockerImageStore', async () => {
|
||||||
// lets first add am s3 target
|
// lets first add am s3 target
|
||||||
const s3Descriptor = {
|
const s3Descriptor: plugins.tsclass.storage.IS3Descriptor = {
|
||||||
endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'),
|
endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'),
|
||||||
|
port: parseInt(await testQenv.getEnvVarOnDemand('S3_PORT'), 10),
|
||||||
|
useSsl: false,
|
||||||
accessKey: await testQenv.getEnvVarOnDemand('S3_ACCESSKEY'),
|
accessKey: await testQenv.getEnvVarOnDemand('S3_ACCESSKEY'),
|
||||||
accessSecret: await testQenv.getEnvVarOnDemand('S3_ACCESSSECRET'),
|
accessSecret: await testQenv.getEnvVarOnDemand('S3_ACCESSSECRET'),
|
||||||
bucketName: await testQenv.getEnvVarOnDemand('S3_BUCKET'),
|
bucketName: await testQenv.getEnvVarOnDemand('S3_BUCKET'),
|
||||||
@@ -159,7 +162,7 @@ tap.test('should expose a working DockerImageStore', async () => {
|
|||||||
// Use the new public API instead of direct imageStore access
|
// Use the new public API instead of direct imageStore access
|
||||||
await testDockerHost.storeImage(
|
await testDockerHost.storeImage(
|
||||||
'hello2',
|
'hello2',
|
||||||
plugins.smartfile.fsStream.createReadStream(
|
plugins.fs.createReadStream(
|
||||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -318,8 +321,172 @@ tap.test('should complete container tests', async () => {
|
|||||||
console.log('Container streaming tests completed');
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
let resolved = false;
|
||||||
|
const resolve = async () => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
// 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('end', resolve);
|
||||||
|
|
||||||
|
stream.on('error', async (error) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
console.error('Exec error:', error);
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Safety timeout to prevent hanging
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
console.log('Exec test timed out, checking inspect...');
|
||||||
|
try {
|
||||||
|
const info = await inspect();
|
||||||
|
console.log('Exec inspect (timeout) - ExitCode:', info.ExitCode, 'Running:', info.Running);
|
||||||
|
expect(typeof info.ExitCode).toEqual('number');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Inspect after timeout failed:', e);
|
||||||
|
}
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
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('sh -c "exit 1"', {
|
||||||
|
tty: false,
|
||||||
|
attachStdout: true,
|
||||||
|
attachStderr: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let resolved = false;
|
||||||
|
const resolve = async () => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
// 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('end', resolve);
|
||||||
|
|
||||||
|
stream.on('error', async (error) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
console.error('Exec error:', error);
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Safety timeout to prevent hanging
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
console.log('Exec failed-command test timed out, checking inspect...');
|
||||||
|
try {
|
||||||
|
const info = await inspect();
|
||||||
|
console.log('Exec inspect (timeout) - ExitCode:', info.ExitCode);
|
||||||
|
expect(typeof info.ExitCode).toEqual('number');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Inspect after timeout failed:', e);
|
||||||
|
}
|
||||||
|
await close();
|
||||||
|
done.resolve();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
await done.promise;
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('cleanup', async () => {
|
tap.test('cleanup', async () => {
|
||||||
await testDockerHost.stop();
|
await testDockerHost.stop();
|
||||||
|
// Force exit after a short delay to clean up lingering HTTP connections
|
||||||
|
// (Deno's node:http compat layer may keep Docker socket connections open)
|
||||||
|
setTimeout(() => process.exit(0), 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@apiclient.xyz/docker',
|
name: '@apiclient.xyz/docker',
|
||||||
version: '5.0.2',
|
version: '5.1.3',
|
||||||
description: 'Provides easy communication with Docker remote API from Node.js, with TypeScript support.'
|
description: 'Provides easy communication with Docker remote API from Node.js, with TypeScript support.'
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-12
@@ -62,7 +62,11 @@ export class DockerContainer extends DockerResource {
|
|||||||
if (response.statusCode < 300) {
|
if (response.statusCode < 300) {
|
||||||
logger.log('info', 'Container created successfully');
|
logger.log('info', 'Container created successfully');
|
||||||
// Return the created container instance
|
// Return the created container instance
|
||||||
return await DockerContainer._fromId(dockerHost, response.body.Id);
|
const container = await DockerContainer._fromId(dockerHost, response.body.Id);
|
||||||
|
if (!container) {
|
||||||
|
throw new Error('Container was created but could not be retrieved');
|
||||||
|
}
|
||||||
|
return container;
|
||||||
} else {
|
} 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}`);
|
throw new Error(`Failed to create container: ${response.statusCode}`);
|
||||||
@@ -70,18 +74,18 @@ export class DockerContainer extends DockerResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE PROPERTIES
|
// INSTANCE PROPERTIES
|
||||||
public Id: string;
|
public Id!: string;
|
||||||
public Names: string[];
|
public Names!: string[];
|
||||||
public Image: string;
|
public Image!: string;
|
||||||
public ImageID: string;
|
public ImageID!: string;
|
||||||
public Command: string;
|
public Command!: string;
|
||||||
public Created: number;
|
public Created!: number;
|
||||||
public Ports: interfaces.TPorts;
|
public Ports!: interfaces.TPorts;
|
||||||
public Labels: interfaces.TLabels;
|
public Labels!: interfaces.TLabels;
|
||||||
public State: string;
|
public State!: string;
|
||||||
public Status: string;
|
public Status!: string;
|
||||||
public HostConfig: any;
|
public HostConfig: any;
|
||||||
public NetworkSettings: {
|
public NetworkSettings!: {
|
||||||
Networks: {
|
Networks: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
IPAMConfig: any;
|
IPAMConfig: any;
|
||||||
@@ -329,6 +333,7 @@ export class DockerContainer extends DockerResource {
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
stream: plugins.smartstream.stream.Duplex;
|
stream: plugins.smartstream.stream.Duplex;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
|
inspect: () => Promise<interfaces.IExecInspectInfo>;
|
||||||
}> {
|
}> {
|
||||||
// Step 1: Create exec instance
|
// Step 1: Create exec instance
|
||||||
const createResponse = await this.dockerHost.request('POST', `/containers/${this.Id}/exec`, {
|
const createResponse = await this.dockerHost.request('POST', `/containers/${this.Id}/exec`, {
|
||||||
@@ -386,9 +391,15 @@ export class DockerContainer extends DockerResource {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inspect = async (): Promise<interfaces.IExecInspectInfo> => {
|
||||||
|
const inspectResponse = await this.dockerHost.request('GET', `/exec/${execId}/json`);
|
||||||
|
return inspectResponse.body;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stream: duplexStream,
|
stream: duplexStream,
|
||||||
close,
|
close,
|
||||||
|
inspect,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-7
@@ -29,7 +29,7 @@ export class DockerHost {
|
|||||||
public socketPath: string;
|
public socketPath: string;
|
||||||
private registryToken: string = '';
|
private registryToken: string = '';
|
||||||
private imageStore: DockerImageStore; // Now private - use storeImage/retrieveImage instead
|
private imageStore: DockerImageStore; // Now private - use storeImage/retrieveImage instead
|
||||||
public smartBucket: plugins.smartbucket.SmartBucket;
|
public smartBucket!: plugins.smartbucket.SmartBucket;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the constructor to instantiate a new docker sock instance
|
* the constructor to instantiate a new docker sock instance
|
||||||
@@ -64,8 +64,8 @@ export class DockerHost {
|
|||||||
console.log(`using docker sock at ${pathToUse}`);
|
console.log(`using docker sock at ${pathToUse}`);
|
||||||
this.socketPath = pathToUse;
|
this.socketPath = pathToUse;
|
||||||
this.imageStore = new DockerImageStore({
|
this.imageStore = new DockerImageStore({
|
||||||
bucketDir: null,
|
bucketDir: null!,
|
||||||
localDirPath: this.options.imageStoreDir,
|
localDirPath: this.options.imageStoreDir!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +74,9 @@ export class DockerHost {
|
|||||||
}
|
}
|
||||||
public async stop() {
|
public async stop() {
|
||||||
await this.imageStore.stop();
|
await this.imageStore.stop();
|
||||||
|
if (this.smartBucket) {
|
||||||
|
this.smartBucket.storageClient.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,6 +91,25 @@ export class DockerHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Docker daemon version information
|
||||||
|
* @returns Version info including Docker version, API version, OS, architecture, etc.
|
||||||
|
*/
|
||||||
|
public async getVersion(): Promise<{
|
||||||
|
Version: string;
|
||||||
|
ApiVersion: string;
|
||||||
|
MinAPIVersion?: string;
|
||||||
|
GitCommit: string;
|
||||||
|
GoVersion: string;
|
||||||
|
Os: string;
|
||||||
|
Arch: string;
|
||||||
|
KernelVersion: string;
|
||||||
|
BuildTime?: string;
|
||||||
|
}> {
|
||||||
|
const response = await this.request('GET', '/version');
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* authenticate against a registry
|
* authenticate against a registry
|
||||||
* @param userArg
|
* @param userArg
|
||||||
@@ -112,7 +134,7 @@ export class DockerHost {
|
|||||||
const dockerConfigPath = plugins.smartpath.get.home(
|
const dockerConfigPath = plugins.smartpath.get.home(
|
||||||
'~/.docker/config.json',
|
'~/.docker/config.json',
|
||||||
);
|
);
|
||||||
const configObject = plugins.smartfile.fs.toObjectSync(dockerConfigPath);
|
const configObject = JSON.parse(plugins.fs.readFileSync(dockerConfigPath, 'utf8'));
|
||||||
const gitlabAuthBase64 = configObject.auths[registryUrlArg].auth;
|
const gitlabAuthBase64 = configObject.auths[registryUrlArg].auth;
|
||||||
const gitlabAuth: string =
|
const gitlabAuth: string =
|
||||||
plugins.smartstring.base64.decode(gitlabAuthBase64);
|
plugins.smartstring.base64.decode(gitlabAuthBase64);
|
||||||
@@ -248,6 +270,35 @@ export class DockerHost {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prune unused images
|
||||||
|
* @param options Optional filters (dangling, until, label)
|
||||||
|
* @returns Object with deleted images and space reclaimed
|
||||||
|
*/
|
||||||
|
public async pruneImages(options?: {
|
||||||
|
dangling?: boolean;
|
||||||
|
filters?: Record<string, string[]>;
|
||||||
|
}): Promise<{
|
||||||
|
ImagesDeleted: Array<{ Untagged?: string; Deleted?: string }>;
|
||||||
|
SpaceReclaimed: number;
|
||||||
|
}> {
|
||||||
|
const filters: Record<string, string[]> = options?.filters || {};
|
||||||
|
|
||||||
|
// Add dangling filter if specified
|
||||||
|
if (options?.dangling !== undefined) {
|
||||||
|
filters.dangling = [options.dangling.toString()];
|
||||||
|
}
|
||||||
|
|
||||||
|
let route = '/images/prune';
|
||||||
|
if (filters && Object.keys(filters).length > 0) {
|
||||||
|
route += `?filters=${encodeURIComponent(JSON.stringify(filters))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.request('POST', route);
|
||||||
|
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds an image from a Dockerfile
|
* Builds an image from a Dockerfile
|
||||||
*/
|
*/
|
||||||
@@ -331,8 +382,14 @@ export class DockerHost {
|
|||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
nodeStream.on('error', (err) => {
|
||||||
|
// Connection resets are expected when the stream is destroyed
|
||||||
|
if ((err as any).code !== 'ECONNRESET') {
|
||||||
|
observer.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
return () => {
|
return () => {
|
||||||
nodeStream.emit('end');
|
nodeStream.destroy();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -342,15 +399,20 @@ export class DockerHost {
|
|||||||
*/
|
*/
|
||||||
public async activateSwarm(addvertisementIpArg?: string) {
|
public async activateSwarm(addvertisementIpArg?: string) {
|
||||||
// determine advertisement address
|
// determine advertisement address
|
||||||
let addvertisementIp: string;
|
let addvertisementIp: string = '';
|
||||||
if (addvertisementIpArg) {
|
if (addvertisementIpArg) {
|
||||||
addvertisementIp = addvertisementIpArg;
|
addvertisementIp = addvertisementIpArg;
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
|
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
|
||||||
const defaultGateway = await smartnetworkInstance.getDefaultGateway();
|
const defaultGateway = await smartnetworkInstance.getDefaultGateway();
|
||||||
if (defaultGateway) {
|
if (defaultGateway) {
|
||||||
addvertisementIp = defaultGateway.ipv4.address;
|
addvertisementIp = defaultGateway.ipv4.address;
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Failed to determine default gateway (e.g. in Deno without --allow-run)
|
||||||
|
// Docker will auto-detect the advertise address
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.request('POST', '/swarm/init', {
|
const response = await this.request('POST', '/swarm/init', {
|
||||||
@@ -454,7 +516,7 @@ export class DockerHost {
|
|||||||
routeArg: string,
|
routeArg: string,
|
||||||
readStream?: plugins.smartstream.stream.Readable,
|
readStream?: plugins.smartstream.stream.Readable,
|
||||||
jsonData?: any,
|
jsonData?: any,
|
||||||
) {
|
): Promise<plugins.smartstream.stream.Readable | { statusCode: number; body: string; headers: any }> {
|
||||||
const requestUrl = `${this.socketPath}${routeArg}`;
|
const requestUrl = `${this.socketPath}${routeArg}`;
|
||||||
|
|
||||||
// Build the request using the fluent API
|
// Build the request using the fluent API
|
||||||
@@ -531,6 +593,10 @@ export class DockerHost {
|
|||||||
// Convert web ReadableStream to Node.js stream for backward compatibility
|
// Convert web ReadableStream to Node.js stream for backward compatibility
|
||||||
const nodeStream = plugins.smartstream.nodewebhelpers.convertWebReadableToNodeReadable(webStream);
|
const nodeStream = plugins.smartstream.nodewebhelpers.convertWebReadableToNodeReadable(webStream);
|
||||||
|
|
||||||
|
// Add a default error handler to prevent unhandled 'error' events from crashing the process.
|
||||||
|
// Callers that attach their own 'error' listener will still receive the event.
|
||||||
|
nodeStream.on('error', () => {});
|
||||||
|
|
||||||
// Add properties for compatibility
|
// Add properties for compatibility
|
||||||
(nodeStream as any).statusCode = response.status;
|
(nodeStream as any).statusCode = response.status;
|
||||||
(nodeStream as any).body = ''; // For compatibility
|
(nodeStream as any).body = ''; // For compatibility
|
||||||
|
|||||||
+28
-13
@@ -59,8 +59,8 @@ export class DockerImage extends DockerResource {
|
|||||||
imageOriginTag: string;
|
imageOriginTag: string;
|
||||||
} = {
|
} = {
|
||||||
imageUrl: optionsArg.creationObject.imageUrl,
|
imageUrl: optionsArg.creationObject.imageUrl,
|
||||||
imageTag: optionsArg.creationObject.imageTag,
|
imageTag: optionsArg.creationObject.imageTag ?? '',
|
||||||
imageOriginTag: null,
|
imageOriginTag: '',
|
||||||
};
|
};
|
||||||
if (imageUrlObject.imageUrl.includes(':')) {
|
if (imageUrlObject.imageUrl.includes(':')) {
|
||||||
const imageUrl = imageUrlObject.imageUrl.split(':')[0];
|
const imageUrl = imageUrlObject.imageUrl.split(':')[0];
|
||||||
@@ -94,9 +94,24 @@ export class DockerImage extends DockerResource {
|
|||||||
dockerHostArg,
|
dockerHostArg,
|
||||||
imageUrlObject.imageOriginTag,
|
imageUrlObject.imageOriginTag,
|
||||||
);
|
);
|
||||||
|
if (!image) {
|
||||||
|
throw new Error(`Image ${imageUrlObject.imageOriginTag} not found after pull`);
|
||||||
|
}
|
||||||
return image;
|
return image;
|
||||||
} else {
|
} else {
|
||||||
logger.log('error', `Failed at the attempt of creating a new image`);
|
// Pull failed — check if the image already exists locally
|
||||||
|
const existingImage = await DockerImage._fromName(
|
||||||
|
dockerHostArg,
|
||||||
|
imageUrlObject.imageOriginTag,
|
||||||
|
);
|
||||||
|
if (existingImage) {
|
||||||
|
logger.log(
|
||||||
|
'warn',
|
||||||
|
`Pull failed for ${imageUrlObject.imageUrl}, using locally cached image`,
|
||||||
|
);
|
||||||
|
return existingImage;
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to pull image ${imageUrlObject.imageOriginTag} and no local copy exists`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,16 +232,16 @@ export class DockerImage extends DockerResource {
|
|||||||
/**
|
/**
|
||||||
* the tags for an image
|
* the tags for an image
|
||||||
*/
|
*/
|
||||||
public Containers: number;
|
public Containers!: number;
|
||||||
public Created: number;
|
public Created!: number;
|
||||||
public Id: string;
|
public Id!: string;
|
||||||
public Labels: interfaces.TLabels;
|
public Labels!: interfaces.TLabels;
|
||||||
public ParentId: string;
|
public ParentId!: string;
|
||||||
public RepoDigests: string[];
|
public RepoDigests!: string[];
|
||||||
public RepoTags: string[];
|
public RepoTags!: string[];
|
||||||
public SharedSize: number;
|
public SharedSize!: number;
|
||||||
public Size: number;
|
public Size!: number;
|
||||||
public VirtualSize: number;
|
public VirtualSize!: number;
|
||||||
|
|
||||||
constructor(dockerHostArg: DockerHost, dockerImageObjectArg: any) {
|
constructor(dockerHostArg: DockerHost, dockerImageObjectArg: any) {
|
||||||
super(dockerHostArg);
|
super(dockerHostArg);
|
||||||
|
|||||||
+58
-41
@@ -3,6 +3,8 @@ import * as paths from './paths.js';
|
|||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import type { DockerHost } from './classes.host.js';
|
import type { DockerHost } from './classes.host.js';
|
||||||
|
|
||||||
|
const smartfileFactory = plugins.smartfile.SmartFileFactory.nodeFs();
|
||||||
|
|
||||||
export interface IDockerImageStoreConstructorOptions {
|
export interface IDockerImageStoreConstructorOptions {
|
||||||
/**
|
/**
|
||||||
* used for preparing images for longer term storage
|
* used for preparing images for longer term storage
|
||||||
@@ -38,14 +40,12 @@ export class DockerImageStore {
|
|||||||
uniqueProcessingId,
|
uniqueProcessingId,
|
||||||
);
|
);
|
||||||
// Create a write stream to store the tar file
|
// Create a write stream to store the tar file
|
||||||
const writeStream = plugins.smartfile.fsStream.createWriteStream(
|
const writeStream = plugins.fs.createWriteStream(initialTarDownloadPath);
|
||||||
initialTarDownloadPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
// lets wait for the write stream to finish
|
// lets wait for the write stream to finish
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
tarStream.pipe(writeStream);
|
tarStream.pipe(writeStream);
|
||||||
writeStream.on('finish', resolve);
|
writeStream.on('finish', () => resolve());
|
||||||
writeStream.on('error', reject);
|
writeStream.on('error', reject);
|
||||||
});
|
});
|
||||||
logger.log(
|
logger.log(
|
||||||
@@ -54,44 +54,55 @@ export class DockerImageStore {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// lets process the image
|
// lets process the image
|
||||||
const tarArchive = await plugins.smartarchive.SmartArchive.fromArchiveFile(
|
await plugins.smartarchive.SmartArchive.create()
|
||||||
initialTarDownloadPath,
|
.file(initialTarDownloadPath)
|
||||||
);
|
.extract(extractionDir);
|
||||||
await tarArchive.exportToFs(extractionDir);
|
|
||||||
logger.log('info', `Image ${imageName} extracted.`);
|
logger.log('info', `Image ${imageName} extracted.`);
|
||||||
await plugins.smartfile.fs.remove(initialTarDownloadPath);
|
await plugins.fs.promises.rm(initialTarDownloadPath, { force: true });
|
||||||
logger.log('info', `deleted original tar to save space.`);
|
logger.log('info', `deleted original tar to save space.`);
|
||||||
logger.log('info', `now repackaging for s3...`);
|
logger.log('info', `now repackaging for s3...`);
|
||||||
const smartfileIndexJson = await plugins.smartfile.SmartFile.fromFilePath(
|
const smartfileIndexJson = await smartfileFactory.fromFilePath(
|
||||||
plugins.path.join(extractionDir, 'index.json'),
|
plugins.path.join(extractionDir, 'index.json'),
|
||||||
);
|
);
|
||||||
const smartfileManifestJson =
|
const smartfileManifestJson = await smartfileFactory.fromFilePath(
|
||||||
await plugins.smartfile.SmartFile.fromFilePath(
|
|
||||||
plugins.path.join(extractionDir, 'manifest.json'),
|
plugins.path.join(extractionDir, 'manifest.json'),
|
||||||
);
|
);
|
||||||
const smartfileOciLayoutJson =
|
const smartfileOciLayoutJson = await smartfileFactory.fromFilePath(
|
||||||
await plugins.smartfile.SmartFile.fromFilePath(
|
|
||||||
plugins.path.join(extractionDir, 'oci-layout'),
|
plugins.path.join(extractionDir, 'oci-layout'),
|
||||||
);
|
);
|
||||||
const smartfileRepositoriesJson =
|
|
||||||
await plugins.smartfile.SmartFile.fromFilePath(
|
// repositories file is optional in OCI image tars
|
||||||
plugins.path.join(extractionDir, 'repositories'),
|
const repositoriesPath = plugins.path.join(extractionDir, 'repositories');
|
||||||
);
|
const hasRepositories = plugins.fs.existsSync(repositoriesPath);
|
||||||
|
const smartfileRepositoriesJson = hasRepositories
|
||||||
|
? await smartfileFactory.fromFilePath(repositoriesPath)
|
||||||
|
: null;
|
||||||
|
|
||||||
const indexJson = JSON.parse(smartfileIndexJson.contents.toString());
|
const indexJson = JSON.parse(smartfileIndexJson.contents.toString());
|
||||||
const manifestJson = JSON.parse(smartfileManifestJson.contents.toString());
|
const manifestJson = JSON.parse(smartfileManifestJson.contents.toString());
|
||||||
const ociLayoutJson = JSON.parse(
|
const ociLayoutJson = JSON.parse(
|
||||||
smartfileOciLayoutJson.contents.toString(),
|
smartfileOciLayoutJson.contents.toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (indexJson.manifests?.[0]?.annotations) {
|
||||||
|
indexJson.manifests[0].annotations['io.containerd.image.name'] = imageName;
|
||||||
|
}
|
||||||
|
if (manifestJson?.[0]?.RepoTags) {
|
||||||
|
manifestJson[0].RepoTags[0] = imageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smartfileRepositoriesJson) {
|
||||||
const repositoriesJson = JSON.parse(
|
const repositoriesJson = JSON.parse(
|
||||||
smartfileRepositoriesJson.contents.toString(),
|
smartfileRepositoriesJson.contents.toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
indexJson.manifests[0].annotations['io.containerd.image.name'] = imageName;
|
|
||||||
manifestJson[0].RepoTags[0] = imageName;
|
|
||||||
const repoFirstKey = Object.keys(repositoriesJson)[0];
|
const repoFirstKey = Object.keys(repositoriesJson)[0];
|
||||||
const repoFirstValue = repositoriesJson[repoFirstKey];
|
const repoFirstValue = repositoriesJson[repoFirstKey];
|
||||||
repositoriesJson[imageName] = repoFirstValue;
|
repositoriesJson[imageName] = repoFirstValue;
|
||||||
delete repositoriesJson[repoFirstKey];
|
delete repositoriesJson[repoFirstKey];
|
||||||
|
smartfileRepositoriesJson.contents = Buffer.from(
|
||||||
|
JSON.stringify(repositoriesJson, null, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
smartfileIndexJson.contents = Buffer.from(
|
smartfileIndexJson.contents = Buffer.from(
|
||||||
JSON.stringify(indexJson, null, 2),
|
JSON.stringify(indexJson, null, 2),
|
||||||
@@ -102,45 +113,51 @@ export class DockerImageStore {
|
|||||||
smartfileOciLayoutJson.contents = Buffer.from(
|
smartfileOciLayoutJson.contents = Buffer.from(
|
||||||
JSON.stringify(ociLayoutJson, null, 2),
|
JSON.stringify(ociLayoutJson, null, 2),
|
||||||
);
|
);
|
||||||
smartfileRepositoriesJson.contents = Buffer.from(
|
|
||||||
JSON.stringify(repositoriesJson, null, 2),
|
const writePromises = [
|
||||||
);
|
|
||||||
await Promise.all([
|
|
||||||
smartfileIndexJson.write(),
|
smartfileIndexJson.write(),
|
||||||
smartfileManifestJson.write(),
|
smartfileManifestJson.write(),
|
||||||
smartfileOciLayoutJson.write(),
|
smartfileOciLayoutJson.write(),
|
||||||
smartfileRepositoriesJson.write(),
|
];
|
||||||
]);
|
if (smartfileRepositoriesJson) {
|
||||||
|
writePromises.push(smartfileRepositoriesJson.write());
|
||||||
|
}
|
||||||
|
await Promise.all(writePromises);
|
||||||
|
|
||||||
logger.log('info', 'repackaging archive for s3...');
|
logger.log('info', 'repackaging archive for s3...');
|
||||||
const tartools = new plugins.smartarchive.TarTools();
|
const tartools = new plugins.smartarchive.TarTools();
|
||||||
const newTarPack = await tartools.packDirectory(extractionDir);
|
const newTarPack = await tartools.getDirectoryPackStream(extractionDir);
|
||||||
const finalTarName = `${uniqueProcessingId}.processed.tar`;
|
const finalTarName = `${uniqueProcessingId}.processed.tar`;
|
||||||
const finalTarPath = plugins.path.join(
|
const finalTarPath = plugins.path.join(
|
||||||
this.options.localDirPath,
|
this.options.localDirPath,
|
||||||
finalTarName,
|
finalTarName,
|
||||||
);
|
);
|
||||||
const finalWriteStream =
|
const finalWriteStream = plugins.fs.createWriteStream(finalTarPath);
|
||||||
plugins.smartfile.fsStream.createWriteStream(finalTarPath);
|
await new Promise<void>((resolve, reject) => {
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
newTarPack.finalize();
|
|
||||||
newTarPack.pipe(finalWriteStream);
|
newTarPack.pipe(finalWriteStream);
|
||||||
finalWriteStream.on('finish', resolve);
|
finalWriteStream.on('finish', () => resolve());
|
||||||
finalWriteStream.on('error', reject);
|
finalWriteStream.on('error', reject);
|
||||||
});
|
});
|
||||||
logger.log('ok', `Repackaged image ${imageName} for s3.`);
|
logger.log('ok', `Repackaged image ${imageName} for s3.`);
|
||||||
await plugins.smartfile.fs.remove(extractionDir);
|
await plugins.fs.promises.rm(extractionDir, { recursive: true, force: true });
|
||||||
const finalTarReadStream =
|
// Remove existing file in bucket if it exists (smartbucket v4 no longer silently overwrites)
|
||||||
plugins.smartfile.fsStream.createReadStream(finalTarPath);
|
try {
|
||||||
|
await this.options.bucketDir.fastRemove({ path: `${imageName}.tar` });
|
||||||
|
} catch (e) {
|
||||||
|
// File may not exist, which is fine
|
||||||
|
}
|
||||||
|
const finalTarReadStream = plugins.fs.createReadStream(finalTarPath);
|
||||||
await this.options.bucketDir.fastPutStream({
|
await this.options.bucketDir.fastPutStream({
|
||||||
stream: finalTarReadStream,
|
stream: finalTarReadStream,
|
||||||
path: `${imageName}.tar`,
|
path: `${imageName}.tar`,
|
||||||
});
|
});
|
||||||
await plugins.smartfile.fs.remove(finalTarPath);
|
await plugins.fs.promises.rm(finalTarPath, { force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
await plugins.smartfile.fs.ensureEmptyDir(this.options.localDirPath);
|
// Ensure the local directory exists and is empty
|
||||||
|
await plugins.fs.promises.rm(this.options.localDirPath, { recursive: true, force: true });
|
||||||
|
await plugins.fs.promises.mkdir(this.options.localDirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop() {}
|
public async stop() {}
|
||||||
@@ -154,10 +171,10 @@ export class DockerImageStore {
|
|||||||
`${imageName}.tar`,
|
`${imageName}.tar`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!(await plugins.smartfile.fs.fileExists(imagePath))) {
|
if (!plugins.fs.existsSync(imagePath)) {
|
||||||
throw new Error(`Image ${imageName} does not exist.`);
|
throw new Error(`Image ${imageName} does not exist.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugins.smartfile.fsStream.createReadStream(imagePath);
|
return plugins.fs.createReadStream(imagePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-31
@@ -51,48 +51,40 @@ export class DockerNetwork extends DockerResource {
|
|||||||
const response = await dockerHost.request('POST', '/networks/create', {
|
const response = await dockerHost.request('POST', '/networks/create', {
|
||||||
Name: networkCreationDescriptor.Name,
|
Name: networkCreationDescriptor.Name,
|
||||||
CheckDuplicate: true,
|
CheckDuplicate: true,
|
||||||
Driver: 'overlay',
|
Driver: networkCreationDescriptor.Driver || 'overlay',
|
||||||
EnableIPv6: false,
|
EnableIPv6: networkCreationDescriptor.EnableIPv6 || false,
|
||||||
/* IPAM: {
|
IPAM: networkCreationDescriptor.IPAM,
|
||||||
Driver: 'default',
|
Internal: networkCreationDescriptor.Internal || false,
|
||||||
Config: [
|
Attachable: networkCreationDescriptor.Attachable !== undefined ? networkCreationDescriptor.Attachable : true,
|
||||||
{
|
Labels: networkCreationDescriptor.Labels,
|
||||||
Subnet: `172.20.${networkCreationDescriptor.NetworkNumber}.0/16`,
|
|
||||||
IPRange: `172.20.${networkCreationDescriptor.NetworkNumber}.0/24`,
|
|
||||||
Gateway: `172.20.${networkCreationDescriptor.NetworkNumber}.11`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, */
|
|
||||||
Internal: false,
|
|
||||||
Attachable: true,
|
|
||||||
Ingress: false,
|
Ingress: false,
|
||||||
});
|
});
|
||||||
if (response.statusCode < 300) {
|
if (response.statusCode < 300) {
|
||||||
logger.log('info', 'Created network successfully');
|
logger.log('info', 'Created network successfully');
|
||||||
return await DockerNetwork._fromName(
|
const network = await DockerNetwork._fromName(
|
||||||
dockerHost,
|
dockerHost,
|
||||||
networkCreationDescriptor.Name,
|
networkCreationDescriptor.Name,
|
||||||
);
|
);
|
||||||
|
if (!network) {
|
||||||
|
throw new Error('Network was created but could not be retrieved');
|
||||||
|
}
|
||||||
|
return network;
|
||||||
} else {
|
} else {
|
||||||
logger.log(
|
throw new Error('There has been an error creating the wanted network');
|
||||||
'error',
|
|
||||||
'There has been an error creating the wanted network',
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE PROPERTIES
|
// INSTANCE PROPERTIES
|
||||||
public Name: string;
|
public Name!: string;
|
||||||
public Id: string;
|
public Id!: string;
|
||||||
public Created: string;
|
public Created!: string;
|
||||||
public Scope: string;
|
public Scope!: string;
|
||||||
public Driver: string;
|
public Driver!: string;
|
||||||
public EnableIPv6: boolean;
|
public EnableIPv6!: boolean;
|
||||||
public Internal: boolean;
|
public Internal!: boolean;
|
||||||
public Attachable: boolean;
|
public Attachable!: boolean;
|
||||||
public Ingress: false;
|
public Ingress!: false;
|
||||||
public IPAM: {
|
public IPAM!: {
|
||||||
Driver: 'default' | 'bridge' | 'overlay';
|
Driver: 'default' | 'bridge' | 'overlay';
|
||||||
Config: [
|
Config: [
|
||||||
{
|
{
|
||||||
@@ -138,7 +130,7 @@ export class DockerNetwork extends DockerResource {
|
|||||||
IPv6Address: string;
|
IPv6Address: string;
|
||||||
}>
|
}>
|
||||||
> {
|
> {
|
||||||
const returnArray = [];
|
const returnArray: any[] = [];
|
||||||
const response = await this.dockerHost.request(
|
const response = await this.dockerHost.request(
|
||||||
'GET',
|
'GET',
|
||||||
`/networks/${this.Id}`,
|
`/networks/${this.Id}`,
|
||||||
|
|||||||
@@ -72,12 +72,12 @@ export class DockerSecret extends DockerResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE PROPERTIES
|
// INSTANCE PROPERTIES
|
||||||
public ID: string;
|
public ID!: string;
|
||||||
public Spec: {
|
public Spec!: {
|
||||||
Name: string;
|
Name: string;
|
||||||
Labels: interfaces.TLabels;
|
Labels: interfaces.TLabels;
|
||||||
};
|
};
|
||||||
public Version: {
|
public Version!: {
|
||||||
Index: string;
|
Index: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,7 +101,6 @@ export class DockerSecret extends DockerResource {
|
|||||||
* Updates a secret
|
* Updates a secret
|
||||||
*/
|
*/
|
||||||
public async update(contentArg: string) {
|
public async update(contentArg: string) {
|
||||||
const route = `/secrets/${this.ID}/update?=version=${this.Version.Index}`;
|
|
||||||
const response = await this.dockerHost.request(
|
const response = await this.dockerHost.request(
|
||||||
'POST',
|
'POST',
|
||||||
`/secrets/${this.ID}/update?version=${this.Version.Index}`,
|
`/secrets/${this.ID}/update?version=${this.Version.Index}`,
|
||||||
|
|||||||
+23
-23
@@ -37,6 +37,9 @@ export class DockerService extends DockerResource {
|
|||||||
const wantedService = allServices.find((service) => {
|
const wantedService = allServices.find((service) => {
|
||||||
return service.Spec.Name === networkName;
|
return service.Spec.Name === networkName;
|
||||||
});
|
});
|
||||||
|
if (!wantedService) {
|
||||||
|
throw new Error(`Service not found: ${networkName}`);
|
||||||
|
}
|
||||||
return wantedService;
|
return wantedService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,10 +59,11 @@ export class DockerService extends DockerResource {
|
|||||||
// Resolve image (support both string and DockerImage instance)
|
// Resolve image (support both string and DockerImage instance)
|
||||||
let imageInstance: DockerImage;
|
let imageInstance: DockerImage;
|
||||||
if (typeof serviceCreationDescriptor.image === 'string') {
|
if (typeof serviceCreationDescriptor.image === 'string') {
|
||||||
imageInstance = await DockerImage._fromName(dockerHost, serviceCreationDescriptor.image);
|
const foundImage = await DockerImage._fromName(dockerHost, serviceCreationDescriptor.image);
|
||||||
if (!imageInstance) {
|
if (!foundImage) {
|
||||||
throw new Error(`Image not found: ${serviceCreationDescriptor.image}`);
|
throw new Error(`Image not found: ${serviceCreationDescriptor.image}`);
|
||||||
}
|
}
|
||||||
|
imageInstance = foundImage;
|
||||||
} else {
|
} else {
|
||||||
imageInstance = serviceCreationDescriptor.image;
|
imageInstance = serviceCreationDescriptor.image;
|
||||||
}
|
}
|
||||||
@@ -131,7 +135,7 @@ export class DockerService extends DockerResource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ports = [];
|
const ports: Array<{ Protocol: string; PublishedPort: number; TargetPort: number }> = [];
|
||||||
for (const port of serviceCreationDescriptor.ports) {
|
for (const port of serviceCreationDescriptor.ports) {
|
||||||
const portArray = port.split(':');
|
const portArray = port.split(':');
|
||||||
const hostPort = portArray[0];
|
const hostPort = portArray[0];
|
||||||
@@ -149,10 +153,11 @@ export class DockerService extends DockerResource {
|
|||||||
// Resolve secret instance
|
// Resolve secret instance
|
||||||
let secretInstance: DockerSecret;
|
let secretInstance: DockerSecret;
|
||||||
if (typeof secret === 'string') {
|
if (typeof secret === 'string') {
|
||||||
secretInstance = await DockerSecret._fromName(dockerHost, secret);
|
const foundSecret = await DockerSecret._fromName(dockerHost, secret);
|
||||||
if (!secretInstance) {
|
if (!foundSecret) {
|
||||||
throw new Error(`Secret not found: ${secret}`);
|
throw new Error(`Secret not found: ${secret}`);
|
||||||
}
|
}
|
||||||
|
secretInstance = foundSecret;
|
||||||
} else {
|
} else {
|
||||||
secretInstance = secret;
|
secretInstance = secret;
|
||||||
}
|
}
|
||||||
@@ -171,21 +176,12 @@ export class DockerService extends DockerResource {
|
|||||||
|
|
||||||
// lets configure limits
|
// lets configure limits
|
||||||
|
|
||||||
const memoryLimitMB =
|
const memoryLimitMB = serviceCreationDescriptor.resources?.memorySizeMB ?? 1000;
|
||||||
serviceCreationDescriptor.resources &&
|
|
||||||
serviceCreationDescriptor.resources.memorySizeMB
|
|
||||||
? serviceCreationDescriptor.resources.memorySizeMB
|
|
||||||
: 1000;
|
|
||||||
|
|
||||||
const limits = {
|
const limits = {
|
||||||
MemoryBytes: memoryLimitMB * 1000000,
|
MemoryBytes: memoryLimitMB * 1000000,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (serviceCreationDescriptor.resources) {
|
|
||||||
limits.MemoryBytes =
|
|
||||||
serviceCreationDescriptor.resources.memorySizeMB * 1000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await dockerHost.request('POST', '/services/create', {
|
const response = await dockerHost.request('POST', '/services/create', {
|
||||||
Name: serviceCreationDescriptor.name,
|
Name: serviceCreationDescriptor.name,
|
||||||
TaskTemplate: {
|
TaskTemplate: {
|
||||||
@@ -209,6 +205,7 @@ export class DockerService extends DockerResource {
|
|||||||
Resources: {
|
Resources: {
|
||||||
Limits: limits,
|
Limits: limits,
|
||||||
},
|
},
|
||||||
|
Networks: networkArray,
|
||||||
LogDriver: {
|
LogDriver: {
|
||||||
Name: 'json-file',
|
Name: 'json-file',
|
||||||
Options: {
|
Options: {
|
||||||
@@ -218,7 +215,6 @@ export class DockerService extends DockerResource {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
Networks: networkArray,
|
|
||||||
EndpointSpec: {
|
EndpointSpec: {
|
||||||
Ports: ports,
|
Ports: ports,
|
||||||
},
|
},
|
||||||
@@ -234,11 +230,11 @@ export class DockerService extends DockerResource {
|
|||||||
// INSTANCE PROPERTIES
|
// INSTANCE PROPERTIES
|
||||||
// Note: dockerHost (not dockerHostRef) for consistency with base class
|
// Note: dockerHost (not dockerHostRef) for consistency with base class
|
||||||
|
|
||||||
public ID: string;
|
public ID!: string;
|
||||||
public Version: { Index: number };
|
public Version!: { Index: number };
|
||||||
public CreatedAt: string;
|
public CreatedAt!: string;
|
||||||
public UpdatedAt: string;
|
public UpdatedAt!: string;
|
||||||
public Spec: {
|
public Spec!: {
|
||||||
Name: string;
|
Name: string;
|
||||||
Labels: interfaces.TLabels;
|
Labels: interfaces.TLabels;
|
||||||
TaskTemplate: {
|
TaskTemplate: {
|
||||||
@@ -257,11 +253,14 @@ export class DockerService extends DockerResource {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
ForceUpdate: 0;
|
ForceUpdate: 0;
|
||||||
|
Networks: Array<{
|
||||||
|
Target: string;
|
||||||
|
Aliases: string[];
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
Mode: {};
|
Mode: {};
|
||||||
Networks: [any[]];
|
|
||||||
};
|
};
|
||||||
public Endpoint: { Spec: {}; VirtualIPs: [any[]] };
|
public Endpoint!: { Spec: {}; VirtualIPs: [any[]] };
|
||||||
|
|
||||||
constructor(dockerHostArg: DockerHost) {
|
constructor(dockerHostArg: DockerHost) {
|
||||||
super(dockerHostArg);
|
super(dockerHostArg);
|
||||||
@@ -325,6 +324,7 @@ export class DockerService extends DockerResource {
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
console.log(`service ${this.Spec.Name} is up to date.`);
|
console.log(`service ${this.Spec.Name} is up to date.`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,3 +10,41 @@ export interface IContainerCreationDescriptor {
|
|||||||
/** Network names (strings) or DockerNetwork instances */
|
/** Network names (strings) or DockerNetwork instances */
|
||||||
networks?: (string | DockerNetwork)[];
|
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 {
|
export interface INetworkCreationDescriptor {
|
||||||
Name: string;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
|
||||||
export const packageDir = plugins.path.resolve(
|
export const packageDir = plugins.path.resolve(
|
||||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||||
'../',
|
'../',
|
||||||
);
|
);
|
||||||
|
|
||||||
export const nogitDir = plugins.path.resolve(packageDir, '.nogit/');
|
export const nogitDir = plugins.path.resolve(tmpdir(), 'apiclient-docker');
|
||||||
plugins.smartfile.fs.ensureDir(nogitDir);
|
|
||||||
|
|||||||
+5
-2
@@ -1,13 +1,15 @@
|
|||||||
// node native path
|
// node native
|
||||||
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
|
||||||
export { path };
|
export { fs, path };
|
||||||
|
|
||||||
// @pushrocks scope
|
// @pushrocks scope
|
||||||
import * as lik from '@push.rocks/lik';
|
import * as lik from '@push.rocks/lik';
|
||||||
import * as smartarchive from '@push.rocks/smartarchive';
|
import * as smartarchive from '@push.rocks/smartarchive';
|
||||||
import * as smartbucket from '@push.rocks/smartbucket';
|
import * as smartbucket from '@push.rocks/smartbucket';
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
|
||||||
import * as smartjson from '@push.rocks/smartjson';
|
import * as smartjson from '@push.rocks/smartjson';
|
||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartnetwork from '@push.rocks/smartnetwork';
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||||
@@ -24,6 +26,7 @@ export {
|
|||||||
smartarchive,
|
smartarchive,
|
||||||
smartbucket,
|
smartbucket,
|
||||||
smartfile,
|
smartfile,
|
||||||
|
|
||||||
smartjson,
|
smartjson,
|
||||||
smartlog,
|
smartlog,
|
||||||
smartnetwork,
|
smartnetwork,
|
||||||
|
|||||||
+1
-3
@@ -6,9 +6,7 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {}
|
|
||||||
},
|
},
|
||||||
"exclude": ["dist_*/**/*.d.ts"]
|
"exclude": ["dist_*/**/*.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user