Files
docker/readme.hints.md

6.8 KiB
Raw Permalink Blame History

Docker Module - Development Hints

getContainerById() Bug Fix (2025-11-24 - v5.0.1)

Problem

The getContainerById() method had a critical bug where it would create a DockerContainer object from Docker API error responses when a container didn't exist.

Symptoms:

  • Calling docker.getContainerById('invalid-id') returned a DockerContainer object with { message: "No such container: invalid-id" }
  • Calling .logs() on this invalid container returned "[object Object]" instead of logs or throwing an error
  • No way to detect the error state without checking for a .message property

Root Cause: The DockerContainer._fromId() method made a direct API call to /containers/{id}/json and blindly passed response.body to the constructor, even when the API returned a 404 error response.

Solution

Changed DockerContainer._fromId() to use the list+filter pattern, matching the behavior of all other resource getter methods (DockerImage, DockerNetwork, DockerService, DockerSecret):

// Before (buggy):
public static async _fromId(dockerHostArg: DockerHost, containerId: string): Promise<DockerContainer> {
  const response = await dockerHostArg.request('GET', `/containers/${containerId}/json`);
  return new DockerContainer(dockerHostArg, response.body); // Creates invalid object from error!
}

// After (fixed):
public static async _fromId(dockerHostArg: DockerHost, containerId: string): Promise<DockerContainer | undefined> {
  const containers = await this._list(dockerHostArg);
  return containers.find((container) => container.Id === containerId); // Returns undefined if not found
}

Benefits:

  • 100% consistent with all other resource classes
  • Type-safe return signature: Promise<DockerContainer | undefined>
  • Cannot create invalid objects - .find() naturally returns undefined
  • Users can now properly check for non-existent containers

Usage:

const container = await docker.getContainerById('abc123');
if (container) {
  const logs = await container.logs();
  console.log(logs);
} else {
  console.log('Container not found');
}

OOP Refactoring - Clean Architecture (2025-11-24)

Architecture Changes

The module has been restructured to follow a clean OOP Facade pattern:

  • DockerHost is now the single entry point for all Docker operations
  • All resource classes extend abstract DockerResource base class
  • Static methods are prefixed with _ to indicate internal use
  • Public API is exclusively through DockerHost methods

Key Changes

1. Factory Pattern

  • All resource creation/retrieval goes through DockerHost:
    // 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:

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:

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:

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:

// 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:

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