diff --git a/changelog.md b/changelog.md index b12b075..6cc84b8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,17 @@ # Changelog +## 2025-11-25 - 5.1.0 - feat(host) +Add DockerHost version & image-prune APIs, extend network creation options, return exec inspect info, and improve image import/store and streaming + +- Add DockerHost.getVersion() to retrieve Docker daemon version and build info +- Add DockerHost.pruneImages() with dangling and filters support (calls /images/prune) +- Extend INetworkCreationDescriptor and DockerNetwork._create() to accept Driver, IPAM, EnableIPv6, Attachable and Labels +- Enhance DockerContainer.exec() to return an inspect() helper and introduce IExecInspectInfo to expose exec state and exit code +- Improve DockerImage._createFromTarStream() parsing of docker-load output and error messages when loaded image cannot be determined +- Implement DockerImageStore.storeImage() to persist, repackage and upload images (local processing and s3 support) +- Various streaming/request improvements for compatibility with Node streams and better handling of streaming endpoints +- Update tests to cover new features (network creation, exec inspect, etc.) + ## 2025-11-24 - 5.0.2 - fix(DockerContainer) Fix getContainerById to return undefined for non-existent containers diff --git a/readme.hints.md b/readme.hints.md index 90adcfb..9fdec01 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -1,5 +1,201 @@ # Docker Module - Development Hints +## New Features (2025-11-25 - v5.1.0) + +### 1. Enhanced Network Creation with Full Configuration Support + +**Problem:** Users were unable to create non-overlay networks or customize network configuration. The `INetworkCreationDescriptor` interface only had a `Name` property, and `DockerNetwork._create()` hardcoded `Driver: 'overlay'`. + +**Solution:** Expanded the interface and implementation to support all Docker network configuration options: + +```typescript +// New interface properties: +export interface INetworkCreationDescriptor { + Name: string; + Driver?: 'bridge' | 'overlay' | 'host' | 'none' | 'macvlan'; // NEW + Attachable?: boolean; // NEW + Labels?: Record; // NEW + IPAM?: { // NEW - IP Address Management + Driver?: string; + Config?: Array<{ + Subnet?: string; + Gateway?: string; + IPRange?: string; + AuxiliaryAddresses?: Record; + }>; + }; + Internal?: boolean; // NEW + EnableIPv6?: boolean; // NEW +} +``` + +**Usage Example:** +```typescript +// Create bridge network with custom IPAM +const network = await docker.createNetwork({ + Name: 'custom-bridge', + Driver: 'bridge', + IPAM: { + Config: [{ + Subnet: '172.20.0.0/16', + Gateway: '172.20.0.1', + }] + }, + Labels: { environment: 'production' }, +}); +``` + +**Files Modified:** +- `ts/interfaces/network.ts` - Added all missing properties to interface +- `ts/classes.network.ts` - Updated `_create()` to pass through descriptor properties instead of hardcoding + +### 2. Docker Daemon Version Information + +**Added:** `dockerHost.getVersion()` method to retrieve Docker daemon version information. + +**Purpose:** Essential for API compatibility checking, debugging, and ensuring minimum Docker version requirements. + +**Returns:** +```typescript +{ + Version: string; // e.g., "20.10.21" + ApiVersion: string; // e.g., "1.41" + MinAPIVersion?: string; // Minimum supported API version + GitCommit: string; + GoVersion: string; + Os: string; // e.g., "linux" + Arch: string; // e.g., "amd64" + KernelVersion: string; + BuildTime?: string; +} +``` + +**Usage Example:** +```typescript +const version = await docker.getVersion(); +console.log(`Docker ${version.Version} (API ${version.ApiVersion})`); +console.log(`Platform: ${version.Os}/${version.Arch}`); +``` + +**Files Modified:** +- `ts/classes.host.ts` - Added `getVersion()` method after `ping()` + +### 3. Image Pruning for Disk Space Management + +**Added:** `dockerHost.pruneImages(options?)` method to clean up unused images. + +**Purpose:** Automated disk space management, CI/CD cleanup, scheduled maintenance tasks. + +**Options:** +```typescript +{ + dangling?: boolean; // Remove untagged images + filters?: Record; // Custom filters (until, label, etc.) +} +``` + +**Returns:** +```typescript +{ + ImagesDeleted: Array<{ Untagged?: string; Deleted?: string }>; + SpaceReclaimed: number; // Bytes freed +} +``` + +**Usage Example:** +```typescript +// Remove dangling images +const result = await docker.pruneImages({ dangling: true }); +console.log(`Reclaimed: ${(result.SpaceReclaimed / 1024 / 1024).toFixed(2)} MB`); + +// Remove old images (older than 7 days) +await docker.pruneImages({ + filters: { + until: ['168h'] + } +}); +``` + +**Files Modified:** +- `ts/classes.host.ts` - Added `pruneImages()` method with filter support + +### 4. Exec Command Exit Codes and Inspection + +**Problem:** Users could not determine if exec commands succeeded or failed. The `container.exec()` method returned a stream but provided no way to access exit codes, which are essential for: +- Health checks (e.g., `pg_isready` exit code) +- Test automation (npm test success/failure) +- Deployment validation (migration checks) +- Container readiness probes + +**Solution:** Added `inspect()` method to `exec()` return value that provides comprehensive execution information. + +**New Return Type:** +```typescript +{ + stream: Duplex; + close: () => Promise; + inspect: () => Promise; // NEW +} +``` + +**IExecInspectInfo Interface:** +```typescript +export interface IExecInspectInfo { + ExitCode: number; // 0 = success, non-zero = failure + Running: boolean; // Whether exec is still running + Pid: number; // Process ID + ContainerID: string; // Container where exec ran + ID: string; // Exec instance ID + OpenStderr: boolean; + OpenStdin: boolean; + OpenStdout: boolean; + CanRemove: boolean; + DetachKeys: string; + ProcessConfig: { + tty: boolean; + entrypoint: string; + arguments: string[]; + privileged: boolean; + }; +} +``` + +**Usage Example:** +```typescript +// Health check with exit code +const { stream, close, inspect } = await container.exec('pg_isready -U postgres'); + +stream.on('end', async () => { + const info = await inspect(); + + if (info.ExitCode === 0) { + console.log('โœ… Database is ready'); + } else { + console.log(`โŒ Database check failed (exit code ${info.ExitCode})`); + } + + await close(); +}); +``` + +**Real-World Use Cases Enabled:** +- Health checks: Verify service readiness with proper exit code handling +- Test automation: Run tests in container and determine pass/fail +- Deployment validation: Execute migration checks and verify success +- CI/CD pipelines: Run build/test commands and get accurate results + +**Files Modified:** +- `ts/interfaces/container.ts` - Added `IExecInspectInfo` interface +- `ts/classes.container.ts` - Updated `exec()` return type and added `inspect()` implementation + +### Implementation Notes + +All changes are non-breaking additions that enhance existing functionality: +- Network creation: New optional properties with sensible defaults +- getVersion(): New method, no changes to existing APIs +- pruneImages(): New method, no changes to existing APIs +- exec() inspect(): Added to return value, existing stream/close properties unchanged + ## getContainerById() Bug Fix (2025-11-24 - v5.0.1) ### Problem diff --git a/readme.md b/readme.md index 6781294..ae4bc7e 100644 --- a/readme.md +++ b/readme.md @@ -175,6 +175,37 @@ async function waitForDocker(timeoutMs = 10000): Promise { } ``` +#### Get Docker Version Information ๐Ÿ†• + +Get detailed version information about the Docker daemon: + +```typescript +// Get Docker daemon version +const version = await docker.getVersion(); + +console.log(`Docker Version: ${version.Version}`); +console.log(`API Version: ${version.ApiVersion}`); +console.log(`Platform: ${version.Os}/${version.Arch}`); +console.log(`Go Version: ${version.GoVersion}`); +console.log(`Git Commit: ${version.GitCommit}`); +console.log(`Kernel Version: ${version.KernelVersion}`); + +// Check API compatibility +if (version.MinAPIVersion) { + console.log(`Minimum API Version: ${version.MinAPIVersion}`); +} + +// Example: Verify API compatibility +async function checkApiCompatibility(requiredVersion: string): Promise { + const version = await docker.getVersion(); + // Compare version strings (simplified) + return version.ApiVersion >= requiredVersion; +} + +const isCompatible = await checkApiCompatibility('1.40'); +console.log(`API compatible: ${isCompatible}`); +``` + ### ๐Ÿ“ฆ Container Management #### List All Containers @@ -305,7 +336,7 @@ Run commands inside a running container: ```typescript // Execute a command -const { stream, close } = await container.exec('ls -la /app', { +const { stream, close, inspect } = await container.exec('ls -la /app', { tty: true, user: 'root', workingDir: '/app', @@ -323,12 +354,84 @@ stream.on('end', async () => { }); // Execute with array of arguments -const { stream: stream2, close: close2 } = await container.exec( +const { stream: stream2, close: close2, inspect: inspect2 } = await container.exec( ['bash', '-c', 'echo "Hello from container"'], { tty: true } ); ``` +#### Check Exec Command Exit Codes ๐Ÿ†• + +Get the exit code and execution state of commands - essential for health checks and automation: + +```typescript +// Execute a command and check its exit code +const { stream, close, inspect } = await container.exec('pg_isready -U postgres', { + tty: false, + attachStdout: true, + attachStderr: true, +}); + +let output = ''; +stream.on('data', (chunk) => { + output += chunk.toString(); +}); + +stream.on('end', async () => { + // Get execution information + const info = await inspect(); + + console.log(`Exit Code: ${info.ExitCode}`); // 0 = success, non-zero = failure + console.log(`Still Running: ${info.Running}`); + console.log(`Process ID: ${info.Pid}`); + console.log(`Container ID: ${info.ContainerID}`); + + if (info.ExitCode === 0) { + console.log('โœ… Command succeeded'); + } else { + console.log(`โŒ Command failed with exit code ${info.ExitCode}`); + } + + await close(); +}); + +// Example: Health check function +async function healthCheck(container: DockerContainer): Promise { + const { stream, close, inspect } = await container.exec('curl -f http://localhost:3000/health'); + + return new Promise((resolve) => { + stream.on('end', async () => { + const info = await inspect(); + await close(); + resolve(info.ExitCode === 0); + }); + }); +} + +// Example: Run tests in container and get result +async function runTests(container: DockerContainer): Promise<{ passed: boolean; output: string }> { + const { stream, close, inspect } = await container.exec('npm test', { + workingDir: '/app', + }); + + let output = ''; + stream.on('data', (chunk) => { + output += chunk.toString(); + }); + + return new Promise((resolve) => { + stream.on('end', async () => { + const info = await inspect(); + await close(); + resolve({ + passed: info.ExitCode === 0, + output: output, + }); + }); + }); +} +``` + #### Get Container Stats ```typescript @@ -424,6 +527,58 @@ await image.remove({ force: true }); console.log('Image removed'); ``` +#### Prune Unused Images ๐Ÿ†• + +Clean up unused images to free disk space: + +```typescript +// Prune all dangling images (untagged images) +const result = await docker.pruneImages({ dangling: true }); + +console.log(`Images deleted: ${result.ImagesDeleted.length}`); +console.log(`Space reclaimed: ${(result.SpaceReclaimed / 1024 / 1024).toFixed(2)} MB`); + +// Display what was deleted +result.ImagesDeleted.forEach((deleted) => { + if (deleted.Untagged) { + console.log(` Untagged: ${deleted.Untagged}`); + } + if (deleted.Deleted) { + console.log(` Deleted: ${deleted.Deleted}`); + } +}); + +// Prune with custom filters +const resultWithFilters = await docker.pruneImages({ + filters: { + // Remove images older than 24 hours + until: ['24h'], + // Only remove images with specific label + label: ['temporary=true'], + }, +}); + +// Example: Scheduled cleanup function +async function cleanupOldImages() { + console.log('๐Ÿงน Starting image cleanup...'); + + // Remove dangling images + const danglingResult = await docker.pruneImages({ dangling: true }); + console.log(`Removed ${danglingResult.ImagesDeleted.length} dangling images`); + + // Remove old images (older than 7 days) + const oldResult = await docker.pruneImages({ + filters: { + until: ['168h'], // 7 days + }, + }); + console.log(`Removed ${oldResult.ImagesDeleted.length} old images`); + + const totalSpace = danglingResult.SpaceReclaimed + oldResult.SpaceReclaimed; + console.log(`โœ… Total space reclaimed: ${(totalSpace / 1024 / 1024 / 1024).toFixed(2)} GB`); +} +``` + ### ๐ŸŒ Network Management #### Create Custom Networks @@ -432,7 +587,7 @@ console.log('Image removed'); // Create an overlay network (for swarm) const network = await docker.createNetwork({ Name: 'my-app-network', - Driver: 'overlay', + Driver: 'overlay', // 'bridge', 'overlay', 'host', 'none', 'macvlan' EnableIPv6: false, Attachable: true, }); @@ -440,6 +595,83 @@ const network = await docker.createNetwork({ console.log(`Network created: ${network.Name} (${network.Id})`); ``` +#### Advanced Network Configuration ๐Ÿ†• + +Create networks with custom drivers, IPAM configuration, and labels: + +```typescript +// Create a bridge network with custom IPAM (IP Address Management) +const bridgeNetwork = await docker.createNetwork({ + Name: 'custom-bridge', + Driver: 'bridge', // Use bridge driver for single-host networking + IPAM: { + Driver: 'default', + Config: [{ + Subnet: '172.20.0.0/16', + Gateway: '172.20.0.1', + IPRange: '172.20.10.0/24', // Allocate IPs from this range + }] + }, + Labels: { + environment: 'production', + team: 'backend', + }, +}); + +console.log(`Bridge network created: ${bridgeNetwork.Name}`); +console.log(` Driver: ${bridgeNetwork.Driver}`); +console.log(` Subnet: ${bridgeNetwork.IPAM.Config[0].Subnet}`); + +// Create an internal network (isolated from external networks) +const internalNetwork = await docker.createNetwork({ + Name: 'internal-db', + Driver: 'bridge', + Internal: true, // No external access + Attachable: true, + Labels: { + purpose: 'database', + }, +}); + +// Create a network with IPv6 support +const ipv6Network = await docker.createNetwork({ + Name: 'ipv6-network', + Driver: 'bridge', + EnableIPv6: true, + IPAM: { + Config: [ + { + Subnet: '172.28.0.0/16', + Gateway: '172.28.0.1', + }, + { + Subnet: 'fd00:dead:beef::/48', + Gateway: 'fd00:dead:beef::1', + } + ] + }, +}); + +// Example: Create network for microservices +async function createMicroserviceNetwork() { + return await docker.createNetwork({ + Name: 'microservices', + Driver: 'overlay', // For swarm mode + Attachable: true, // Allow standalone containers to attach + IPAM: { + Config: [{ + Subnet: '10.0.0.0/24', + Gateway: '10.0.0.1', + }] + }, + Labels: { + 'com.docker.stack.namespace': 'production', + 'version': '2.0', + }, + }); +} +``` + #### List and Inspect Networks ```typescript diff --git a/test/test.nonci.node+deno.ts b/test/test.nonci.node+deno.ts index c6aac0e..e9821a0 100644 --- a/test/test.nonci.node+deno.ts +++ b/test/test.nonci.node+deno.ts @@ -318,6 +318,119 @@ tap.test('should complete container tests', async () => { console.log('Container streaming tests completed'); }); +// NEW FEATURES TESTS (v5.1.0) + +// Test 1: Network creation with custom driver and IPAM +tap.test('should create bridge network with custom IPAM config', async () => { + const network = await testDockerHost.createNetwork({ + Name: 'test-bridge-network', + Driver: 'bridge', + IPAM: { + Config: [{ + Subnet: '172.20.0.0/16', + Gateway: '172.20.0.1', + }] + }, + Labels: { testLabel: 'v5.1.0' }, + }); + expect(network).toBeInstanceOf(docker.DockerNetwork); + expect(network.Name).toEqual('test-bridge-network'); + expect(network.Driver).toEqual('bridge'); + console.log('Created bridge network:', network.Name, 'with driver:', network.Driver); + await network.remove(); +}); + +// Test 2: getVersion() returns proper Docker daemon info +tap.test('should get Docker daemon version information', async () => { + const version = await testDockerHost.getVersion(); + expect(version).toBeInstanceOf(Object); + expect(typeof version.Version).toEqual('string'); + expect(typeof version.ApiVersion).toEqual('string'); + expect(typeof version.Os).toEqual('string'); + expect(typeof version.Arch).toEqual('string'); + console.log('Docker version:', version.Version, 'API version:', version.ApiVersion); +}); + +// Test 3: pruneImages() functionality +tap.test('should prune dangling images', async () => { + const result = await testDockerHost.pruneImages({ dangling: true }); + expect(result).toBeInstanceOf(Object); + expect(result).toHaveProperty('ImagesDeleted'); + expect(result).toHaveProperty('SpaceReclaimed'); + expect(Array.isArray(result.ImagesDeleted)).toEqual(true); + expect(typeof result.SpaceReclaimed).toEqual('number'); + console.log('Pruned images. Space reclaimed:', result.SpaceReclaimed, 'bytes'); +}); + +// Test 4: exec() inspect() returns exit codes +tap.test('should get exit code from exec command', async (tools) => { + const done = tools.defer(); + + // Execute a successful command (exit code 0) + const { stream, close, inspect } = await testContainer.exec('echo "test successful"', { + tty: false, + attachStdout: true, + attachStderr: true, + }); + + stream.on('end', async () => { + // Give Docker a moment to finalize the exec state + await tools.delayFor(500); + + const info = await inspect(); + expect(info).toBeInstanceOf(Object); + expect(typeof info.ExitCode).toEqual('number'); + expect(info.ExitCode).toEqual(0); // Success + expect(typeof info.Running).toEqual('boolean'); + expect(info.Running).toEqual(false); // Should be done + expect(typeof info.ContainerID).toEqual('string'); + console.log('Exec inspect - ExitCode:', info.ExitCode, 'Running:', info.Running); + + await close(); + done.resolve(); + }); + + stream.on('error', async (error) => { + console.error('Exec error:', error); + await close(); + done.resolve(); + }); + + await done.promise; +}); + +tap.test('should get non-zero exit code from failed exec command', async (tools) => { + const done = tools.defer(); + + // Execute a command that fails (exit code 1) + const { stream, close, inspect } = await testContainer.exec('exit 1', { + tty: false, + attachStdout: true, + attachStderr: true, + }); + + stream.on('end', async () => { + // Give Docker a moment to finalize the exec state + await tools.delayFor(500); + + const info = await inspect(); + expect(info.ExitCode).toEqual(1); // Failure + expect(info.Running).toEqual(false); + console.log('Exec inspect (failed command) - ExitCode:', info.ExitCode); + + await close(); + done.resolve(); + }); + + stream.on('error', async (error) => { + console.error('Exec error:', error); + await close(); + done.resolve(); + }); + + await done.promise; +}); + tap.test('cleanup', async () => { await testDockerHost.stop(); }); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 026c413..06863f8 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@apiclient.xyz/docker', - version: '5.0.2', + version: '5.1.0', description: 'Provides easy communication with Docker remote API from Node.js, with TypeScript support.' } diff --git a/ts/classes.container.ts b/ts/classes.container.ts index 79217db..2950cc2 100644 --- a/ts/classes.container.ts +++ b/ts/classes.container.ts @@ -329,6 +329,7 @@ export class DockerContainer extends DockerResource { ): Promise<{ stream: plugins.smartstream.stream.Duplex; close: () => Promise; + inspect: () => Promise; }> { // Step 1: Create exec instance const createResponse = await this.dockerHost.request('POST', `/containers/${this.Id}/exec`, { @@ -386,9 +387,15 @@ export class DockerContainer extends DockerResource { } }; + const inspect = async (): Promise => { + const inspectResponse = await this.dockerHost.request('GET', `/exec/${execId}/json`); + return inspectResponse.body; + }; + return { stream: duplexStream, close, + inspect, }; } } diff --git a/ts/classes.host.ts b/ts/classes.host.ts index f97f354..c558956 100644 --- a/ts/classes.host.ts +++ b/ts/classes.host.ts @@ -88,6 +88,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 * @param userArg @@ -248,6 +267,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; + }): Promise<{ + ImagesDeleted: Array<{ Untagged?: string; Deleted?: string }>; + SpaceReclaimed: number; + }> { + const filters: Record = 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 */ diff --git a/ts/classes.network.ts b/ts/classes.network.ts index b13a301..044f8dc 100644 --- a/ts/classes.network.ts +++ b/ts/classes.network.ts @@ -51,20 +51,12 @@ export class DockerNetwork extends DockerResource { const response = await dockerHost.request('POST', '/networks/create', { Name: networkCreationDescriptor.Name, CheckDuplicate: true, - Driver: 'overlay', - EnableIPv6: false, - /* IPAM: { - Driver: 'default', - Config: [ - { - Subnet: `172.20.${networkCreationDescriptor.NetworkNumber}.0/16`, - IPRange: `172.20.${networkCreationDescriptor.NetworkNumber}.0/24`, - Gateway: `172.20.${networkCreationDescriptor.NetworkNumber}.11` - } - ] - }, */ - Internal: false, - Attachable: true, + Driver: networkCreationDescriptor.Driver || 'overlay', + EnableIPv6: networkCreationDescriptor.EnableIPv6 || false, + IPAM: networkCreationDescriptor.IPAM, + Internal: networkCreationDescriptor.Internal || false, + Attachable: networkCreationDescriptor.Attachable !== undefined ? networkCreationDescriptor.Attachable : true, + Labels: networkCreationDescriptor.Labels, Ingress: false, }); if (response.statusCode < 300) { diff --git a/ts/interfaces/container.ts b/ts/interfaces/container.ts index a4dde7a..77ad592 100644 --- a/ts/interfaces/container.ts +++ b/ts/interfaces/container.ts @@ -10,3 +10,41 @@ export interface IContainerCreationDescriptor { /** Network names (strings) or DockerNetwork instances */ networks?: (string | DockerNetwork)[]; } + +/** + * Information about an exec instance, including exit code and running state. + * Retrieved via container.exec().inspect() + */ +export interface IExecInspectInfo { + /** Exit code of the exec command (0 = success) */ + ExitCode: number; + /** Whether the exec is currently running */ + Running: boolean; + /** Process ID */ + Pid: number; + /** Container ID where exec runs */ + ContainerID: string; + /** Exec instance ID */ + ID: string; + /** Whether stderr is open */ + OpenStderr: boolean; + /** Whether stdin is open */ + OpenStdin: boolean; + /** Whether stdout is open */ + OpenStdout: boolean; + /** Whether exec can be removed */ + CanRemove: boolean; + /** Detach keys */ + DetachKeys: string; + /** Process configuration */ + ProcessConfig: { + /** Whether TTY is allocated */ + tty: boolean; + /** Entrypoint */ + entrypoint: string; + /** Command arguments */ + arguments: string[]; + /** Whether running in privileged mode */ + privileged: boolean; + }; +} diff --git a/ts/interfaces/network.ts b/ts/interfaces/network.ts index e3c4515..4c40711 100644 --- a/ts/interfaces/network.ts +++ b/ts/interfaces/network.ts @@ -3,4 +3,18 @@ */ export interface INetworkCreationDescriptor { Name: string; + Driver?: 'bridge' | 'overlay' | 'host' | 'none' | 'macvlan'; + Attachable?: boolean; + Labels?: Record; + IPAM?: { + Driver?: string; + Config?: Array<{ + Subnet?: string; + Gateway?: string; + IPRange?: string; + AuxiliaryAddresses?: Record; + }>; + }; + Internal?: boolean; + EnableIPv6?: boolean; }