diff --git a/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl b/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl index c2f387e..c4186c2 100644 Binary files a/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl and b/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl differ diff --git a/test-stream.js b/test-stream.js new file mode 100644 index 0000000..db1e8ed --- /dev/null +++ b/test-stream.js @@ -0,0 +1,40 @@ +const { SmartRequest } = require('@push.rocks/smartrequest'); + +async function test() { + try { + const response = await SmartRequest.create() + .url('http://unix:/run/user/1000/docker.sock:/images/hello-world:latest/get') + .header('Host', 'docker.sock') + .get(); + + console.log('Response status:', response.status); + console.log('Response type:', typeof response); + + const stream = response.streamNode(); + console.log('Stream type:', typeof stream); + console.log('Has on method:', typeof stream.on); + + if (stream) { + let chunks = 0; + stream.on('data', (chunk) => { + chunks++; + if (chunks <= 3) console.log('Got chunk', chunks, chunk.length); + }); + stream.on('end', () => { + console.log('Stream ended, total chunks:', chunks); + process.exit(0); + }); + stream.on('error', (err) => { + console.error('Stream error:', err); + process.exit(1); + }); + } else { + console.log('No stream available'); + } + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +} + +test(); diff --git a/test-stream.mjs b/test-stream.mjs new file mode 100644 index 0000000..c5ece52 --- /dev/null +++ b/test-stream.mjs @@ -0,0 +1,46 @@ +import { SmartRequest } from '@push.rocks/smartrequest'; + +async function test() { + try { + const response = await SmartRequest.create() + .url('http://unix:/run/user/1000/docker.sock:/images/hello-world:latest/get') + .header('Host', 'docker.sock') + .get(); + + console.log('Response status:', response.status); + console.log('Response type:', typeof response); + + const stream = response.streamNode(); + console.log('Stream type:', typeof stream); + console.log('Has on method:', typeof stream.on); + + if (stream) { + let chunks = 0; + stream.on('data', (chunk) => { + chunks++; + if (chunks <= 3) console.log('Got chunk', chunks, chunk.length); + }); + stream.on('end', () => { + console.log('Stream ended, total chunks:', chunks); + process.exit(0); + }); + stream.on('error', (err) => { + console.error('Stream error:', err); + process.exit(1); + }); + + // Set a timeout in case stream doesn't end + setTimeout(() => { + console.log('Timeout after 5 seconds'); + process.exit(1); + }, 5000); + } else { + console.log('No stream available'); + } + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +} + +test(); diff --git a/test/test.nonci.node.ts b/test/test.nonci.node.ts index a5e5e1a..49238d7 100644 --- a/test/test.nonci.node.ts +++ b/test/test.nonci.node.ts @@ -139,17 +139,17 @@ tap.test('should export images', async (toolsArg) => { await done.promise; }); -tap.test('should import images', async (toolsArg) => { - const done = toolsArg.defer(); +tap.test('should import images', async () => { const fsReadStream = plugins.smartfile.fsStream.createReadStream( plugins.path.join(paths.nogitDir, 'testimage.tar') ); - await docker.DockerImage.createFromTarStream(testDockerHost, { + 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 () => { diff --git a/ts/classes.host.ts b/ts/classes.host.ts index b137c86..9f329a7 100644 --- a/ts/classes.host.ts +++ b/ts/classes.host.ts @@ -262,12 +262,19 @@ export class DockerHost { // Parse the response body based on content type let body; const contentType = response.headers['content-type'] || ''; - if (contentType.includes('application/json')) { + + // Docker's streaming endpoints (like /images/create) return newline-delimited JSON + // which can't be parsed as a single JSON object + const isStreamingEndpoint = routeArg.includes('/images/create') || + routeArg.includes('/images/load') || + routeArg.includes('/build'); + + if (contentType.includes('application/json') && !isStreamingEndpoint) { body = await response.json(); } else { body = await response.text(); - // Try to parse as JSON if it looks like JSON - if (body && (body.startsWith('{') || body.startsWith('['))) { + // Try to parse as JSON if it looks like JSON and is not a streaming response + if (!isStreamingEndpoint && body && (body.startsWith('{') || body.startsWith('['))) { try { body = JSON.parse(body); } catch { @@ -299,7 +306,8 @@ export class DockerHost { .header('Content-Type', 'application/json') .header('X-Registry-Auth', this.registryToken) .header('Host', 'docker.sock') - .options({ keepAlive: false }); + .timeout(600000) // Set 10 minute timeout for streaming operations + .options({ keepAlive: false, autoDrain: false }); // Disable auto-drain for streaming // If we have a readStream, use the new stream method with logging if (readStream) { diff --git a/ts/classes.image.ts b/ts/classes.image.ts index 5859319..3dce81e 100644 --- a/ts/classes.image.ts +++ b/ts/classes.image.ts @@ -250,6 +250,12 @@ export class DockerImage { public async exportToTarStream(): Promise { logger.log('info', `Exporting image ${this.RepoTags[0]} to tar stream.`); const response = await this.dockerHost.requestStreaming('GET', `/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'); + } + let counter = 0; const webduplexStream = new plugins.smartstream.SmartDuplex({ writeFunction: async (chunk, tools) => { @@ -259,17 +265,25 @@ export class DockerImage { return chunk; } }); + response.on('data', (chunk) => { if (!webduplexStream.write(chunk)) { response.pause(); webduplexStream.once('drain', () => { response.resume(); - }) - }; + }); + } }); + response.on('end', () => { webduplexStream.end(); - }) + }); + + response.on('error', (error) => { + logger.log('error', `Error during image export: ${error.message}`); + webduplexStream.destroy(error); + }); + return webduplexStream; } } diff --git a/ts/classes.service.ts b/ts/classes.service.ts index 2f5e3d7..3078225 100644 --- a/ts/classes.service.ts +++ b/ts/classes.service.ts @@ -89,6 +89,11 @@ export class DockerService { }> = []; for (const network of serviceCreationDescriptor.networks) { + // Skip null networks (can happen if network creation fails) + if (!network) { + logger.log('warn', 'Skipping null network in service creation'); + continue; + } networkArray.push({ Target: network.Name, Aliases: [serviceCreationDescriptor.networkAlias],