@@ -19,16 +19,48 @@ import * as crypto from "crypto";
19
19
import * as childProcess from "child_process" ;
20
20
import * as fse from "fs-extra" ;
21
21
22
+ /**
23
+ * @param cmd - command to execute
24
+ * @param args - arguments to pass to executed command
25
+ * @param suppressOutput - whether to suppress the stdout and stderr resulting from this command.
26
+ * @return Promise which resolves to an object containing the string value of what was
27
+ * written to stdout and stderr by the executed command.
28
+ */
29
+ const exec = ( cmd : string , args : string [ ] , suppressOutput = false ) : Promise < { stdout : string ; stderr : string } > => {
30
+ return new Promise ( ( resolve , reject ) => {
31
+ if ( ! suppressOutput ) {
32
+ const log = [ "Running command:" , cmd , ...args , "\n" ] . join ( " " ) ;
33
+ // When in CI mode we combine reports from multiple runners into a single HTML report
34
+ // which has separate files for stdout and stderr, so we print the executed command to both
35
+ process . stdout . write ( log ) ;
36
+ if ( process . env . CI ) process . stderr . write ( log ) ;
37
+ }
38
+ const { stdout, stderr } = childProcess . execFile ( cmd , args , { encoding : "utf8" } , ( err , stdout , stderr ) => {
39
+ if ( err ) reject ( err ) ;
40
+ resolve ( { stdout, stderr } ) ;
41
+ if ( ! suppressOutput ) {
42
+ process . stdout . write ( "\n" ) ;
43
+ if ( process . env . CI ) process . stderr . write ( "\n" ) ;
44
+ }
45
+ } ) ;
46
+ if ( ! suppressOutput ) {
47
+ stdout . pipe ( process . stdout ) ;
48
+ stderr . pipe ( process . stderr ) ;
49
+ }
50
+ } ) ;
51
+ } ;
52
+
22
53
export class Docker {
23
54
public id : string ;
24
55
25
56
async run ( opts : { image : string ; containerName : string ; params ?: string [ ] ; cmd ?: string [ ] } ) : Promise < string > {
26
57
const userInfo = os . userInfo ( ) ;
27
58
const params = opts . params ?? [ ] ;
28
59
29
- if ( params ?. includes ( "-v" ) && userInfo . uid >= 0 ) {
60
+ const isPodman = await Docker . isPodman ( ) ;
61
+ if ( params . includes ( "-v" ) && userInfo . uid >= 0 ) {
30
62
// Run the docker container as our uid:gid to prevent problems with permissions.
31
- if ( await Docker . isPodman ( ) ) {
63
+ if ( isPodman ) {
32
64
// Note: this setup is for podman rootless containers.
33
65
34
66
// In podman, run as root in the container, which maps to the current
@@ -45,75 +77,57 @@ export class Docker {
45
77
}
46
78
}
47
79
80
+ // Make host.containers.internal work to allow the container to talk to other services via host ports.
81
+ if ( isPodman ) {
82
+ params . push ( "--network" ) ;
83
+ params . push ( "slirp4netns:allow_host_loopback=true" ) ;
84
+ } else {
85
+ // Docker for Desktop includes a host-gateway mapping on host.docker.internal but to simplify the config
86
+ // we use the Podman variant host.containers.internal in all environments.
87
+ params . push ( "--add-host" ) ;
88
+ params . push ( "host.containers.internal:host-gateway" ) ;
89
+ }
90
+
91
+ // Provided we are not running in CI, add a `--rm` parameter.
92
+ // There is no need to remove containers in CI (since they are automatically removed anyway), and
93
+ // `--rm` means that if a container crashes this means its logs are wiped out.
94
+ if ( ! process . env . CI ) params . unshift ( "--rm" ) ;
95
+
48
96
const args = [
49
97
"run" ,
50
98
"--name" ,
51
99
`${ opts . containerName } -${ crypto . randomBytes ( 4 ) . toString ( "hex" ) } ` ,
52
100
"-d" ,
53
- "--rm" ,
54
101
...params ,
55
102
opts . image ,
56
103
] ;
57
104
58
105
if ( opts . cmd ) args . push ( ...opts . cmd ) ;
59
106
60
- this . id = await new Promise < string > ( ( resolve , reject ) => {
61
- childProcess . execFile ( "docker" , args , ( err , stdout ) => {
62
- if ( err ) reject ( err ) ;
63
- resolve ( stdout . trim ( ) ) ;
64
- } ) ;
65
- } ) ;
107
+ const { stdout } = await exec ( "docker" , args ) ;
108
+ this . id = stdout . trim ( ) ;
66
109
return this . id ;
67
110
}
68
111
69
- stop ( ) : Promise < void > {
70
- return new Promise < void > ( ( resolve , reject ) => {
71
- childProcess . execFile ( "docker" , [ "stop" , this . id ] , ( err ) => {
72
- if ( err ) reject ( err ) ;
73
- resolve ( ) ;
74
- } ) ;
75
- } ) ;
76
- }
77
-
78
- exec ( params : string [ ] ) : Promise < void > {
79
- return new Promise < void > ( ( resolve , reject ) => {
80
- childProcess . execFile (
81
- "docker" ,
82
- [ "exec" , this . id , ...params ] ,
83
- { encoding : "utf8" } ,
84
- ( err , stdout , stderr ) => {
85
- if ( err ) {
86
- console . log ( stdout ) ;
87
- console . log ( stderr ) ;
88
- reject ( err ) ;
89
- return ;
90
- }
91
- resolve ( ) ;
92
- } ,
93
- ) ;
94
- } ) ;
112
+ async stop ( ) : Promise < void > {
113
+ try {
114
+ await exec ( "docker" , [ "stop" , this . id ] ) ;
115
+ } catch ( err ) {
116
+ console . error ( `Failed to stop docker container` , this . id , err ) ;
117
+ }
95
118
}
96
119
97
- rm ( ) : Promise < void > {
98
- return new Promise < void > ( ( resolve , reject ) => {
99
- childProcess . execFile ( "docker" , [ "rm" , this . id ] , ( err ) => {
100
- if ( err ) reject ( err ) ;
101
- resolve ( ) ;
102
- } ) ;
103
- } ) ;
120
+ /**
121
+ * @param params - list of parameters to pass to `docker exec`
122
+ * @param suppressOutput - whether to suppress the stdout and stderr resulting from this command.
123
+ */
124
+ async exec ( params : string [ ] , suppressOutput = true ) : Promise < void > {
125
+ await exec ( "docker" , [ "exec" , this . id , ...params ] , suppressOutput ) ;
104
126
}
105
127
106
- getContainerIp ( ) : Promise < string > {
107
- return new Promise < string > ( ( resolve , reject ) => {
108
- childProcess . execFile (
109
- "docker" ,
110
- [ "inspect" , "-f" , "{{ .NetworkSettings.IPAddress }}" , this . id ] ,
111
- ( err , stdout ) => {
112
- if ( err ) reject ( err ) ;
113
- else resolve ( stdout . trim ( ) ) ;
114
- } ,
115
- ) ;
116
- } ) ;
128
+ async getContainerIp ( ) : Promise < string > {
129
+ const { stdout } = await exec ( "docker" , [ "inspect" , "-f" , "{{ .NetworkSettings.IPAddress }}" , this . id ] ) ;
130
+ return stdout . trim ( ) ;
117
131
}
118
132
119
133
async persistLogsToFile ( args : { stdoutFile ?: string ; stderrFile ?: string } ) : Promise < void > {
@@ -134,20 +148,8 @@ export class Docker {
134
148
* Detects whether the docker command is actually podman.
135
149
* To do this, it looks for "podman" in the output of "docker --help".
136
150
*/
137
- static isPodman ( ) : Promise < boolean > {
138
- return new Promise < boolean > ( ( resolve , reject ) => {
139
- childProcess . execFile ( "docker" , [ "--help" ] , ( err , stdout ) => {
140
- if ( err ) reject ( err ) ;
141
- else resolve ( stdout . toLowerCase ( ) . includes ( "podman" ) ) ;
142
- } ) ;
143
- } ) ;
144
- }
145
-
146
- /**
147
- * Supply the right hostname to use to talk to the host machine. On Docker this
148
- * is "host.docker.internal" and on Podman this is "host.containers.internal".
149
- */
150
- static async hostnameOfHost ( ) : Promise < "host.containers.internal" | "host.docker.internal" > {
151
- return ( await Docker . isPodman ( ) ) ? "host.containers.internal" : "host.docker.internal" ;
151
+ static async isPodman ( ) : Promise < boolean > {
152
+ const { stdout } = await exec ( "docker" , [ "--help" ] , true ) ;
153
+ return stdout . toLowerCase ( ) . includes ( "podman" ) ;
152
154
}
153
155
}
0 commit comments