@@ -22,6 +22,7 @@ import {
22
22
} from './docker-commands' ;
23
23
import { injectIntoBuildStream , getBuildOutputPipeline } from './docker-build-injection' ;
24
24
import { ensureDockerServicesRunning , isDockerAvailable } from './docker-interception-services' ;
25
+ import { transformComposeResponseLabels } from './docker-compose' ;
25
26
26
27
export const getDockerPipePath = ( proxyPort : number , targetPlatform : NodeJS . Platform = process . platform ) => {
27
28
if ( targetPlatform === 'win32' ) {
@@ -47,6 +48,7 @@ const BUILD_IMAGE_MATCHER = /^\/[^\/]+\/build$/;
47
48
const EVENTS_MATCHER = / ^ \/ [ ^ \/ ] + \/ e v e n t s $ / ;
48
49
const ATTACH_CONTAINER_MATCHER = / ^ \/ [ ^ \/ ] + \/ c o n t a i n e r s \/ ( [ ^ \/ ] + ) \/ a t t a c h / ;
49
50
const 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 / ;
50
52
51
53
const DOCKER_PROXY_MAP : { [ mockServerPort : number ] : Promise < DestroyableServer > | undefined } = { } ;
52
54
@@ -144,41 +146,6 @@ async function createDockerProxy(proxyPort: number, httpsConfig: { certPath: str
144
146
}
145
147
) ;
146
148
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
- }
182
149
}
183
150
184
151
// Intercept container creation (e.g. docker start):
@@ -193,70 +160,6 @@ async function createDockerProxy(proxyPort: number, httpsConfig: { certPath: str
193
160
"The container must be recreated here first - try `docker run <image>` instead."
194
161
) ;
195
162
}
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
- }
260
163
}
261
164
262
165
let extraDockerCommandCount : Promise < number > | undefined ;
@@ -304,23 +207,73 @@ async function createDockerProxy(proxyPort: number, httpsConfig: { certPath: str
304
207
} ) ;
305
208
306
209
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
+
307
228
res . writeHead (
308
229
dockerRes . statusCode ! ,
309
230
dockerRes . statusMessage ,
310
231
dockerRes . headers
311
232
) ;
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!
316
234
317
235
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:
318
238
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
+ }
319
274
} else {
320
275
dockerRes . pipe ( res ) ;
321
276
}
322
-
323
- res . flushHeaders ( ) ; // Required, or blocking responses (/wait) don't work!
324
277
} ) ;
325
278
} ) ;
326
279
0 commit comments