@@ -22,6 +22,7 @@ import {
2222} from './docker-commands' ;
2323import { injectIntoBuildStream , getBuildOutputPipeline } from './docker-build-injection' ;
2424import { ensureDockerServicesRunning , isDockerAvailable } from './docker-interception-services' ;
25+ import { transformComposeResponseLabels } from './docker-compose' ;
2526
2627export const getDockerPipePath = ( proxyPort : number , targetPlatform : NodeJS . Platform = process . platform ) => {
2728 if ( targetPlatform === 'win32' ) {
@@ -47,6 +48,7 @@ const BUILD_IMAGE_MATCHER = /^\/[^\/]+\/build$/;
4748const EVENTS_MATCHER = / ^ \/ [ ^ \/ ] + \/ e v e n t s $ / ;
4849const ATTACH_CONTAINER_MATCHER = / ^ \/ [ ^ \/ ] + \/ c o n t a i n e r s \/ ( [ ^ \/ ] + ) \/ a t t a c h / ;
4950const CONTAINER_LIST_MATCHER = / ^ \/ [ ^ \/ ] + \/ c o n t a i n e r s \/ j s o n / ;
51+ const CONTAINER_INSPECT_MATCHER = / ^ \/ [ ^ \/ ] + \/ c o n t a i n e r s \/ [ ^ \/ ] + \/ j s o n / ;
5052
5153const DOCKER_PROXY_MAP : { [ mockServerPort : number ] : Promise < DestroyableServer > | undefined } = { } ;
5254
@@ -144,41 +146,6 @@ async function createDockerProxy(proxyPort: number, httpsConfig: { certPath: str
144146 }
145147 ) ;
146148 requestBodyStream = stream . Readable . from ( JSON . stringify ( transformedConfig ) ) ;
147-
148- // If you specify an explicit name in cases that will cause conflicts (when a container already
149- // exists, or if you're using docker-compose) we try to create a separate container instead,
150- // that uses a _HTK$PORT suffix to avoid conflicts. This happens commonly, because of how
151- // docker-compose automatically generates container names.
152- const containerName = reqUrl . searchParams . get ( 'name' ) ;
153- if ( containerName ) {
154- const existingContainer = await docker . getContainer ( containerName ) . inspect ( )
155- . catch < false > ( ( ) => false ) ;
156-
157- if ( existingContainer && existingContainer . State . Running ) {
158- // If there's a duplicate running container, we could rename the new one, but instead we return
159- // an error. It's likely that this will create conflicts otherwise - e.g. two running containers
160- // using the same volumes or the same network aliases. Better to play it safe.
161- res . statusCode = 409 ;
162- res . end ( JSON . stringify ( {
163- "message" : `Conflict. The container name ${
164- containerName
165- } is already in use by a running container.\n${ ''
166- } HTTP Toolkit won't intercept this by default to avoid conflicts over shared resources. ${ ''
167- } To create & intercept this container, either stop the existing unintercepted container, or use a different container name.`
168- } ) ) ;
169- return ;
170- } else if ( existingContainer || hasDockerComposeLabels ) {
171- // If there's a naming conflict, and we can safely work around it (because the container isn't
172- // running) then we do so.
173-
174- // For Docker-Compose, we *always* rewrite names. This ensures that subsequent Docker-Compose usage
175- // outside intercepted usage doesn't run into naming conflicts (however, this still only applies
176- // after checking for running containers - we never create a duplicate parallel container ourselves)
177-
178- reqUrl . searchParams . set ( 'name' , `${ containerName } _HTK${ proxyPort } ` ) ;
179- req . url = reqUrl . toString ( ) ;
180- }
181- }
182149 }
183150
184151 // Intercept container creation (e.g. docker start):
@@ -193,70 +160,6 @@ async function createDockerProxy(proxyPort: number, httpsConfig: { certPath: str
193160 "The container must be recreated here first - try `docker run <image>` instead."
194161 ) ;
195162 }
196-
197- if ( containerData . Name . endsWith ( `_HTK${ proxyPort } ` ) ) {
198- // Trim initial slash and our HTK suffix:
199- const clonedContainerName = containerData . Name . slice ( 1 , - 1 * `_HTK${ proxyPort } ` . length ) ;
200- const clonedContainerData = await docker . getContainer ( clonedContainerName )
201- . inspect ( )
202- . catch ( ( ) => undefined ) ;
203-
204- if ( clonedContainerData && clonedContainerData . State . Running ) {
205- // If you successfully intercept a docker-compose container, stop it & start the original container(s),
206- // and then restart the already-created intercepted container, you could risk conflicts, so we warn you:
207- res . writeHead ( 409 ) . end (
208- `Conflict: an unintercepted container with the same base name is already running.\n${ ''
209- } HTTP Toolkit won't launch intercepted containers in parallel by default to avoid conflicts ${ ''
210- } over shared resources. To create & intercept this container, either stop the existing ${ ''
211- } unintercepted container, or use a different container name.`
212-
213- ) ;
214- }
215- }
216- }
217-
218- if (
219- reqPath . match ( CONTAINER_LIST_MATCHER ) ||
220- reqPath . match ( EVENTS_MATCHER )
221- ) {
222- const filterString = reqUrl . searchParams . get ( 'filters' ) ?? '{}' ;
223-
224- try {
225- const filters = JSON . parse ( filterString ) as {
226- // Docs say only string[] is allowed, but Docker CLI uses bool maps in "docker compose"
227- [ key : string ] : string [ ] | { [ key : string ] : boolean }
228- } ;
229- const labelFilters = (
230- _ . isArray ( filters . label )
231- ? filters . label
232- : Object . keys ( filters . label ?? { } )
233- . filter ( key => ! ! ( filters . label as _ . Dictionary < boolean > ) [ key ] )
234- ) ;
235- const projectFilter = labelFilters . filter ( l => l . startsWith ( "com.docker.compose.project=" ) ) [ 0 ] ;
236-
237- if ( projectFilter ) {
238- const project = projectFilter . slice ( projectFilter . indexOf ( '=' ) + 1 ) ;
239-
240- // This is a request from docker-compose, looking for containers related to a
241- // specific project. Add an extra filter so that it only finds *intercepted*
242- // containers. This ensures it'll recreate any unintercepted containers.
243- reqUrl . searchParams . set ( 'filters' , JSON . stringify ( {
244- ...filters ,
245- label : [
246- // Replace the project filter, to ensure that the intercepted & non-intercepted containers
247- // are handled separately. We need to ensure that a) this request only finds intercepted
248- // containers, and b) future non-proxied requests only find non-intercepted containers.
249- // By excluding non-intercepted containers, we force DC to recreate, so we can then
250- // intercept the container creation itself and inject what we need.
251- ...labelFilters . filter ( ( label ) => label !== projectFilter ) ,
252- `com.docker.compose.project=${ project } _HTK:${ proxyPort } `
253- ]
254- } ) ) ;
255- req . url = reqUrl . toString ( ) ;
256- }
257- } catch ( e ) {
258- console . log ( "Could not parse /containers/json filters param" , e ) ;
259- }
260163 }
261164
262165 let extraDockerCommandCount : Promise < number > | undefined ;
@@ -304,23 +207,73 @@ async function createDockerProxy(proxyPort: number, httpsConfig: { certPath: str
304207 } ) ;
305208
306209 dockerReq . on ( 'response' , async ( dockerRes ) => {
210+ res . on ( 'error' , ( e ) => {
211+ console . error ( 'Docker proxy conn error' , e ) ;
212+ dockerRes . destroy ( ) ;
213+ } ) ;
214+
215+ // In any container data responses that might be used by docker-compose, we need to remap some of the
216+ // content to ensure that intercepted containers are always used:
217+ const isContainerInspect = reqPath . match ( CONTAINER_INSPECT_MATCHER ) ;
218+ const isComposeContainerQuery = reqPath . match ( CONTAINER_LIST_MATCHER ) &&
219+ reqUrl . searchParams . get ( 'filters' ) ?. includes ( "com.docker.compose" ) ;
220+ const shouldRemapContainerData = isContainerInspect || isComposeContainerQuery ;
221+
222+ if ( shouldRemapContainerData ) {
223+ // We're going to mess with the body, so we need to ensure that the content
224+ // length isn't going to conflict along the way:
225+ delete dockerRes . headers [ 'content-length' ] ;
226+ }
227+
307228 res . writeHead (
308229 dockerRes . statusCode ! ,
309230 dockerRes . statusMessage ,
310231 dockerRes . headers
311232 ) ;
312- res . on ( 'error' , ( e ) => {
313- console . error ( 'Docker proxy conn error' , e ) ;
314- dockerRes . destroy ( ) ;
315- } ) ;
233+ res . flushHeaders ( ) ; // Required, or blocking responses (/wait) don't work!
316234
317235 if ( reqPath . match ( BUILD_IMAGE_MATCHER ) && dockerRes . statusCode === 200 ) {
236+ // We transform the build output to replace the docker build interception steps with a cleaner
237+ // & simpler HTTP Toolkit interception message:
318238 dockerRes . pipe ( getBuildOutputPipeline ( await extraDockerCommandCount ! ) ) . pipe ( res ) ;
239+ } else if ( shouldRemapContainerData ) {
240+ // We need to remap container data, to hook all docker-compose behaviour:
241+ const data = await new Promise < Buffer > ( ( resolve , reject ) => {
242+ const dataChunks : Buffer [ ] = [ ] ;
243+ dockerRes . on ( 'data' , ( d ) => dataChunks . push ( d ) ) ;
244+ dockerRes . on ( 'end' , ( ) => resolve ( Buffer . concat ( dataChunks ) ) ) ;
245+ dockerRes . on ( 'error' , reject ) ;
246+ } ) ;
247+
248+ try {
249+ if ( isComposeContainerQuery ) {
250+ const containerQueryResponse : Dockerode . ContainerInfo [ ] = JSON . parse ( data . toString ( 'utf8' ) ) ;
251+ const modifiedResponse = containerQueryResponse . map ( ( container ) => ( {
252+ ...container ,
253+ Labels : transformComposeResponseLabels ( proxyPort , container . Labels )
254+ } ) ) ;
255+
256+ res . end ( JSON . stringify ( modifiedResponse ) ) ;
257+ } else {
258+ const containerInspectResponse : Dockerode . ContainerInspectInfo = JSON . parse ( data . toString ( 'utf8' ) ) ;
259+ const modifiedResponse = {
260+ ...containerInspectResponse ,
261+ Config : {
262+ ...containerInspectResponse . Config ,
263+ Labels : transformComposeResponseLabels ( proxyPort , containerInspectResponse . Config ?. Labels )
264+ }
265+ } ;
266+
267+ res . end ( JSON . stringify ( modifiedResponse ) ) ;
268+ }
269+ } catch ( e ) {
270+ console . error ( "Failed to parse container data response" , e ) ;
271+ // Write the raw body back to the response - effectively just do nothing.
272+ res . end ( data ) ;
273+ }
319274 } else {
320275 dockerRes . pipe ( res ) ;
321276 }
322-
323- res . flushHeaders ( ) ; // Required, or blocking responses (/wait) don't work!
324277 } ) ;
325278 } ) ;
326279
0 commit comments