Compare commits

...

15 Commits

Author SHA1 Message Date
2ecd4e9d7c v5.0.0 2025-11-24 13:27:10 +00:00
08dbad47bc BREAKING CHANGE(DockerHost): Rename array-returning get* methods to list* on DockerHost and related resource classes; update docs, tests and changelog 2025-11-24 13:27:10 +00:00
15e5dedae4 v3.0.2 2025-11-24 13:09:00 +00:00
5834721da8 fix(readme): Update README to document 3.0.0+ changes: architecture refactor, streaming improvements, health check and circular dependency fixes 2025-11-24 13:09:00 +00:00
2f31e14cbe v3.0.1 2025-11-24 13:06:44 +00:00
5691e5fb78 fix(classes.base): Use type-only import for DockerHost in classes.base to avoid runtime side-effects 2025-11-24 13:06:44 +00:00
8d043d20a8 v3.0.0 2025-11-24 12:20:30 +00:00
6fe70e0a1d BREAKING CHANGE(DockerHost): Refactor public API to DockerHost facade; introduce DockerResource base; make resource static methods internal; support flexible descriptors and stream compatibility 2025-11-24 12:20:30 +00:00
cc9c20882e v2.1.0 2025-11-18 17:30:04 +00:00
08af9fec14 feat(DockerHost): Add DockerHost.ping() to check Docker daemon availability and document health-check usage 2025-11-18 17:30:04 +00:00
b8a26bf3bd v2.0.0 2025-11-18 13:34:09 +00:00
e6432b4ea9 BREAKING CHANGE(DockerHost): Rename DockerHost constructor option dockerSockPath to socketPath and update internal socket path handling 2025-11-18 13:34:09 +00:00
e9975ba7b8 v1.3.6 2025-11-17 15:08:00 +00:00
396ce29d7a fix(streaming): Convert smartrequest v5 web ReadableStreams to Node.js streams and update deps for streaming compatibility 2025-11-17 15:08:00 +00:00
7c0935d585 update 2025-11-16 22:57:25 +00:00
22 changed files with 10487 additions and 1718 deletions

View File

@@ -1,68 +0,0 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "docker"

View File

@@ -1,5 +1,87 @@
# Changelog
## 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 - 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
- Breaking: constructor option renamed from 'dockerSockPath' to 'socketPath' — callers must update their code.
- Constructor now reads the provided 'socketPath' option first, then falls back to DOCKER_HOST, CI, and finally the default unix socket.
- README examples and documentation updated to use 'socketPath'.
## 2025-11-17 - 1.3.6 - fix(streaming)
Convert smartrequest v5 web ReadableStreams to Node.js streams and update deps for streaming compatibility
- Upgrade @push.rocks/smartrequest to ^5.0.1 and bump @git.zone dev tooling (@git.zone/tsbuild, tsrun, tstest).
- requestStreaming now uses response.stream() (web ReadableStream) and converts it to a Node.js Readable via plugins.smartstream.nodewebhelpers.convertWebReadableToNodeReadable for backward compatibility.
- Updated consumers of streaming responses (DockerHost.getEventObservable, DockerImage.createFromTarStream, DockerImage.exportToTarStream) to work with the converted Node.js stream and preserve event/backpressure semantics (.on, .pause, .resume).
- Added readme.hints.md documenting the smartrequest v5 migration, conversion approach, modified files, and test/build status (type errors resolved and Node.js tests passing).
- Removed project metadata file (.serena/project.yml) from the repository.
## 2025-08-19 - 1.3.5 - fix(core)
Stabilize CI/workflows and runtime: update CI images/metadata, improve streaming requests and image handling, and fix tests & package metadata

7241
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{
"name": "@apiclient.xyz/docker",
"version": "1.3.5",
"version": "5.0.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"
},
@@ -37,23 +37,23 @@
"@push.rocks/smartarchive": "^4.2.2",
"@push.rocks/smartbucket": "^3.3.10",
"@push.rocks/smartfile": "^11.2.7",
"@push.rocks/smartjson": "^5.0.20",
"@push.rocks/smartlog": "^3.1.8",
"@push.rocks/smartnetwork": "^4.1.2",
"@push.rocks/smartjson": "^5.2.0",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^4.3.1",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smartstring": "^4.1.0",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartversion": "^3.0.5",
"@tsclass/tsclass": "^9.2.0",
"@tsclass/tsclass": "^9.3.0",
"rxjs": "^7.8.2"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.6.7",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.5",
"@git.zone/tsbuild": "^3.1.0",
"@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^2.8.2",
"@push.rocks/qenv": "^6.1.3",
"@types/node": "22.7.5"
},

2274
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

898
readme.md

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,309 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { Qenv } from '@push.rocks/qenv';
const testQenv = new Qenv('./', './.nogit/');
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
import * as docker from '../ts/index.js';
let testDockerHost: docker.DockerHost;
tap.test('should create a new Dockersock instance', async () => {
testDockerHost = new docker.DockerHost({});
await testDockerHost.start();
return expect(testDockerHost).toBeInstanceOf(docker.DockerHost);
});
tap.test('should create a docker swarm', async () => {
await testDockerHost.activateSwarm();
});
// Containers
tap.test('should list containers', async () => {
const containers = await testDockerHost.listContainers();
console.log(containers);
});
// Networks
tap.test('should list networks', async () => {
const networks = await testDockerHost.listNetworks();
console.log(networks);
});
tap.test('should create a network', async () => {
const newNetwork = await testDockerHost.createNetwork({
Name: 'webgateway',
});
expect(newNetwork).toBeInstanceOf(docker.DockerNetwork);
expect(newNetwork.Name).toEqual('webgateway');
});
tap.test('should remove a network', async () => {
const webgateway = await testDockerHost.getNetworkByName('webgateway');
await webgateway.remove();
});
// Images
tap.test('should pull an image from imagetag', async () => {
const image = await testDockerHost.createImageFromRegistry({
imageUrl: 'hosttoday/ht-docker-node',
imageTag: 'alpine',
});
expect(image).toBeInstanceOf(docker.DockerImage);
console.log(image);
});
tap.test('should return a change Observable', async (tools) => {
const testObservable = await testDockerHost.getEventObservable();
const subscription = testObservable.subscribe((changeObject) => {
console.log(changeObject);
});
await tools.delayFor(2000);
subscription.unsubscribe();
});
// SECRETS
tap.test('should create a secret', async () => {
const mySecret = await testDockerHost.createSecret({
name: 'testSecret',
version: '1.0.3',
contentArg: `{ "hi": "wow"}`,
labels: {},
});
console.log(mySecret);
});
tap.test('should remove a secret by name', async () => {
const mySecret = await testDockerHost.getSecretByName('testSecret');
await mySecret.remove();
});
// SERVICES
tap.test('should activate swarm mode', async () => {
await testDockerHost.activateSwarm();
});
tap.test('should list all services', async (tools) => {
const services = await testDockerHost.listServices();
console.log(services);
});
tap.test('should create a service', async () => {
const testNetwork = await testDockerHost.createNetwork({
Name: 'testNetwork',
});
const testSecret = await testDockerHost.createSecret({
name: 'testSecret',
version: '0.0.1',
labels: {},
contentArg: '{"hi": "wow"}',
});
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',
networks: [testNetwork],
networkAlias: 'testService',
secrets: [testSecret],
ports: ['3000:80'],
});
await testService.remove();
await testNetwork.remove();
await testSecret.remove();
});
tap.test('should export images', async (toolsArg) => {
const done = toolsArg.defer();
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'),
);
const exportStream = await testImage.exportToTarStream();
exportStream.pipe(fsWriteStream).on('finish', () => {
done.resolve();
});
await done.promise;
});
tap.test('should import images', async () => {
const fsReadStream = plugins.smartfile.fsStream.createReadStream(
plugins.path.join(paths.nogitDir, 'testimage.tar'),
);
const importedImage = await testDockerHost.createImageFromTarStream(
fsReadStream,
{
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
},
);
expect(importedImage).toBeInstanceOf(docker.DockerImage);
});
tap.test('should expose a working DockerImageStore', async () => {
// lets first add am s3 target
const s3Descriptor = {
endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'),
accessKey: await testQenv.getEnvVarOnDemand('S3_ACCESSKEY'),
accessSecret: await testQenv.getEnvVarOnDemand('S3_ACCESSSECRET'),
bucketName: await testQenv.getEnvVarOnDemand('S3_BUCKET'),
};
await testDockerHost.addS3Storage(s3Descriptor);
// 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'),
),
);
});
// 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');
});
tap.test('cleanup', async () => {
await testDockerHost.stop();
});
export default tap.start();

View File

@@ -1,193 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { Qenv } from '@push.rocks/qenv';
const testQenv = new Qenv('./', './.nogit/');
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
import * as docker from '../ts/index.js';
let testDockerHost: docker.DockerHost;
tap.test('should create a new Dockersock instance', async () => {
testDockerHost = new docker.DockerHost({});
await testDockerHost.start();
return expect(testDockerHost).toBeInstanceOf(docker.DockerHost);
});
tap.test('should create a docker swarm', async () => {
await testDockerHost.activateSwarm();
});
// Containers
tap.test('should list containers', async () => {
const containers = await testDockerHost.getContainers();
console.log(containers);
});
// Networks
tap.test('should list networks', async () => {
const networks = await testDockerHost.getNetworks();
console.log(networks);
});
tap.test('should create a network', async () => {
const newNetwork = await docker.DockerNetwork.createNetwork(testDockerHost, {
Name: 'webgateway',
});
expect(newNetwork).toBeInstanceOf(docker.DockerNetwork);
expect(newNetwork.Name).toEqual('webgateway');
});
tap.test('should remove a network', async () => {
const webgateway = await docker.DockerNetwork.getNetworkByName(
testDockerHost,
'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',
},
});
expect(image).toBeInstanceOf(docker.DockerImage);
console.log(image);
});
tap.test('should return a change Observable', async (tools) => {
const testObservable = await testDockerHost.getEventObservable();
const subscription = testObservable.subscribe((changeObject) => {
console.log(changeObject);
});
await tools.delayFor(2000);
subscription.unsubscribe();
});
// SECRETS
tap.test('should create a secret', async () => {
const mySecret = await docker.DockerSecret.createSecret(testDockerHost, {
name: 'testSecret',
version: '1.0.3',
contentArg: `{ "hi": "wow"}`,
labels: {},
});
console.log(mySecret);
});
tap.test('should remove a secret by name', async () => {
const mySecret = await docker.DockerSecret.getSecretByName(
testDockerHost,
'testSecret',
);
await mySecret.remove();
});
// SERVICES
tap.test('should activate swarm mode', async () => {
await testDockerHost.activateSwarm();
});
tap.test('should list all services', async (tools) => {
const services = await testDockerHost.getServices();
console.log(services);
});
tap.test('should create a service', async () => {
const testNetwork = await docker.DockerNetwork.createNetwork(testDockerHost, {
Name: 'testNetwork',
});
const testSecret = await docker.DockerSecret.createSecret(testDockerHost, {
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, {
image: testImage,
labels: {},
name: 'testService',
networks: [testNetwork],
networkAlias: 'testService',
secrets: [testSecret],
ports: ['3000:80'],
});
await testService.remove();
await testNetwork.remove();
await testSecret.remove();
});
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 fsWriteStream = plugins.smartfile.fsStream.createWriteStream(
plugins.path.join(paths.nogitDir, 'testimage.tar'),
);
const exportStream = await testImage.exportToTarStream();
exportStream.pipe(fsWriteStream).on('finish', () => {
done.resolve();
});
await done.promise;
});
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,
{
tarStream: fsReadStream,
creationObject: {
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
},
},
);
expect(importedImage).toBeInstanceOf(docker.DockerImage);
});
tap.test('should expose a working DockerImageStore', async () => {
// lets first add am s3 target
const s3Descriptor = {
endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'),
accessKey: await testQenv.getEnvVarOnDemand('S3_ACCESSKEY'),
accessSecret: await testQenv.getEnvVarOnDemand('S3_ACCESSSECRET'),
bucketName: await testQenv.getEnvVarOnDemand('S3_BUCKET'),
};
await testDockerHost.addS3Storage(s3Descriptor);
//
await testDockerHost.imageStore.storeImage(
'hello2',
plugins.smartfile.fsStream.createReadStream(
plugins.path.join(paths.nogitDir, 'testimage.tar'),
),
);
});
tap.test('cleanup', async () => {
await testDockerHost.stop();
});
export default tap.start();

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@apiclient.xyz/docker',
version: '1.3.5',
version: '5.0.0',
description: 'Provides easy communication with Docker remote API from Node.js, with TypeScript support.'
}

27
ts/classes.base.ts Normal file
View 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>;
}

View File

@@ -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,49 @@ export class DockerContainer {
}
/**
* gets an container by Id
* @param containerId
* Internal: Get a container by ID
* Public API: Use dockerHost.getContainerById(id) instead
*/
public static async getContainerById(containerId: string) {
// TODO: implement get container by id
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);
}
/**
* 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 +100,294 @@ 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>;
}> {
// Step 1: Create exec instance
const createResponse = await this.dockerHost.request('POST', `/containers/${this.Id}/exec`, {
Cmd: typeof command === 'string' ? ['/bin/sh', '-c', command] : command,
AttachStdin: options?.attachStdin !== false,
AttachStdout: options?.attachStdout !== false,
AttachStderr: options?.attachStderr !== false,
Tty: options?.tty || false,
Env: options?.env || [],
WorkingDir: options?.workingDir,
User: options?.user,
});
const execId = createResponse.body.Id;
// Step 2: Start exec instance with streaming response
const startResponse = await this.dockerHost.requestStreaming(
'POST',
`/exec/${execId}/start`,
undefined, // no stream input
{
Detach: false,
Tty: options?.tty || false,
}
);
const nodeStream = startResponse as plugins.smartstream.stream.Readable;
// Create duplex stream for bidirectional communication
const duplexStream = new plugins.smartstream.SmartDuplex({
writeFunction: async (chunk) => {
return chunk;
},
readableObjectMode: false,
writableObjectMode: false,
});
// Pipe exec output to duplex readable side
nodeStream.on('data', (chunk) => {
duplexStream.push(chunk);
});
nodeStream.on('end', () => {
duplexStream.push(null);
});
nodeStream.on('error', (error) => {
duplexStream.destroy(error);
});
const close = async () => {
duplexStream.end();
if (nodeStream.destroy) {
nodeStream.destroy();
}
};
return {
stream: duplexStream,
close,
};
}
}

View File

@@ -1,10 +1,11 @@
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 path from 'path';
import { DockerImageStore } from './classes.imagestore.js';
import { DockerImage } from './classes.image.js';
@@ -15,7 +16,7 @@ export interface IAuthData {
}
export interface IDockerHostConstructorOptions {
dockerSockPath?: string;
socketPath?: string;
imageStoreDir?: string;
}
@@ -27,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;
/**
@@ -45,8 +46,8 @@ export class DockerHost {
...optionsArg,
};
let pathToUse: string;
if (optionsArg.dockerSockPath) {
pathToUse = optionsArg.dockerSockPath;
if (optionsArg.socketPath) {
pathToUse = optionsArg.socketPath;
} else if (process.env.DOCKER_HOST) {
pathToUse = process.env.DOCKER_HOST;
} else if (process.env.CI) {
@@ -75,6 +76,18 @@ 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}`);
}
}
/**
* authenticate against a registry
* @param userArg
@@ -112,70 +125,190 @@ 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
*/
public async getImages() {
return await DockerImage.getImages(this);
public async getContainerById(containerId: string) {
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,
});
}
/**
* 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);
}
/**
@@ -183,8 +316,12 @@ export class DockerHost {
*/
public async getEventObservable(): Promise<plugins.rxjs.Observable<any>> {
const response = await this.requestStreaming('GET', '/events');
// requestStreaming now returns Node.js stream, not web stream
const nodeStream = response as plugins.smartstream.stream.Readable;
return plugins.rxjs.Observable.create((observer) => {
response.on('data', (data) => {
nodeStream.on('data', (data) => {
const eventString = data.toString();
try {
const eventObject = JSON.parse(eventString);
@@ -194,7 +331,7 @@ export class DockerHost {
}
});
return () => {
response.emit('end');
nodeStream.emit('end');
};
});
}
@@ -315,6 +452,7 @@ export class DockerHost {
methodArg: string,
routeArg: string,
readStream?: plugins.smartstream.stream.Readable,
jsonData?: any,
) {
const requestUrl = `${this.socketPath}${routeArg}`;
@@ -327,6 +465,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;
@@ -348,7 +491,7 @@ export class DockerHost {
}
// Execute the request based on method
let response;
let response: plugins.smartrequest.ICoreResponse;
switch (methodArg.toUpperCase()) {
case 'GET':
response = await smartRequest.get();
@@ -368,10 +511,10 @@ export class DockerHost {
console.log(response.status);
// For streaming responses, get the Node.js stream
const nodeStream = response.streamNode();
// For streaming responses, get the web stream
const webStream = response.stream();
if (!nodeStream) {
if (!webStream) {
// If no stream is available, consume the body as text
const body = await response.text();
console.log(body);
@@ -384,7 +527,10 @@ export class DockerHost {
};
}
// For streaming responses, return the stream with added properties
// Convert web ReadableStream to Node.js stream for backward compatibility
const nodeStream = plugins.smartstream.nodewebhelpers.convertWebReadableToNodeReadable(webStream);
// Add properties for compatibility
(nodeStream as any).statusCode = response.status;
(nodeStream as any).body = ''; // For compatibility

View File

@@ -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;
@@ -105,6 +118,9 @@ export class DockerImage {
optionsArg.tarStream,
);
// requestStreaming now returns Node.js stream
const nodeStream = response as plugins.smartstream.stream.Readable;
/**
* Docker typically returns lines like:
* {"stream":"Loaded image: myrepo/myimage:latest"}
@@ -112,16 +128,16 @@ export class DockerImage {
* So we will collect those lines and parse out the final image name.
*/
let rawOutput = '';
response.on('data', (chunk) => {
nodeStream.on('data', (chunk) => {
rawOutput += chunk.toString();
});
// Wrap the end event in a Promise for easier async/await usage
await new Promise<void>((resolve, reject) => {
response.on('end', () => {
nodeStream.on('end', () => {
resolve();
});
response.on('error', (err) => {
nodeStream.on('error', (err) => {
reject(err);
});
});
@@ -158,11 +174,11 @@ export class DockerImage {
}
// Now try to look up that image by the "loadedImageTag".
// Depending on Dockers 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,
);
@@ -189,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
*/
@@ -212,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
@@ -231,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],
},
@@ -241,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) {
@@ -260,10 +310,8 @@ export class DockerImage {
`/images/${encodeURIComponent(this.RepoTags[0])}/get`,
);
// Check if response is a Node.js stream
if (!response || typeof response.on !== 'function') {
throw new Error('Failed to get streaming response for image export');
}
// requestStreaming now returns Node.js stream
const nodeStream = response as plugins.smartstream.stream.Readable;
let counter = 0;
const webduplexStream = new plugins.smartstream.SmartDuplex({
@@ -274,20 +322,20 @@ export class DockerImage {
},
});
response.on('data', (chunk) => {
nodeStream.on('data', (chunk) => {
if (!webduplexStream.write(chunk)) {
response.pause();
nodeStream.pause();
webduplexStream.once('drain', () => {
response.resume();
nodeStream.resume();
});
}
});
response.on('end', () => {
nodeStream.on('end', () => {
webduplexStream.end();
});
response.on('error', (error) => {
nodeStream.on('error', (error) => {
logger.log('error', `Error during image export: ${error.message}`);
webduplexStream.destroy(error);
});

View File

@@ -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,17 +26,25 @@ 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> {
@@ -54,7 +69,7 @@ export class DockerNetwork {
});
if (response.statusCode < 300) {
logger.log('info', 'Created network successfully');
return await DockerNetwork.getNetworkByName(
return await DockerNetwork._fromName(
dockerHost,
networkCreationDescriptor.Name,
);
@@ -67,11 +82,7 @@ export class DockerNetwork {
}
}
// INSTANCE
// references
public dockerHost: DockerHost;
// properties
// INSTANCE PROPERTIES
public Name: string;
public Id: string;
public Created: string;
@@ -93,11 +104,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 +129,7 @@ export class DockerNetwork {
);
}
public async getContainersOnNetwork(): Promise<
public async listContainersOnNetwork(): Promise<
Array<{
Name: string;
EndpointID: string;
@@ -128,7 +151,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);
});

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -1,3 +1,4 @@
export * from './classes.base.js';
export * from './classes.host.js';
export * from './classes.container.js';
export * from './classes.image.js';

View File

@@ -1,7 +1,12 @@
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)[];
}

View File

@@ -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?: {

View File

@@ -1,5 +1,5 @@
// node native path
import * as path from 'path';
import * as path from 'node:path';
export { path };