@@ -21,7 +21,6 @@ import os from 'os';
21
21
import path from 'path' ;
22
22
import retry from 'async-retry' ;
23
23
import * as handlebars from 'handlebars' ;
24
- import * as util from 'util' ;
25
24
import * as core from '@actions/core' ;
26
25
import * as httpm from '@actions/http-client' ;
27
26
import * as io from '@actions/io' ;
@@ -56,6 +55,7 @@ export interface InstallOpts {
56
55
runDir : string ;
57
56
contextName ?: string ;
58
57
daemonConfig ?: string ;
58
+ rootless ?: boolean ;
59
59
}
60
60
61
61
interface LimaImage {
@@ -65,19 +65,21 @@ interface LimaImage {
65
65
}
66
66
67
67
export class Install {
68
- private readonly runDir : string ;
68
+ private runDir : string ;
69
69
private readonly source : InstallSource ;
70
70
private readonly contextName : string ;
71
71
private readonly daemonConfig ?: string ;
72
72
private _version : string | undefined ;
73
73
private _toolDir : string | undefined ;
74
+ private rootless : boolean ;
74
75
75
76
private gitCommit : string | undefined ;
76
77
77
78
private readonly limaInstanceName = 'docker-actions-toolkit' ;
78
79
79
80
constructor ( opts : InstallOpts ) {
80
81
this . runDir = opts . runDir ;
82
+ this . rootless = opts . rootless || false ;
81
83
this . source = opts . source || {
82
84
type : 'archive' ,
83
85
version : 'latest' ,
@@ -91,25 +93,25 @@ export class Install {
91
93
return this . _toolDir || Context . tmpDir ( ) ;
92
94
}
93
95
94
- async downloadStaticArchive ( src : InstallSourceArchive ) : Promise < string > {
96
+ async downloadStaticArchive ( component : 'docker' | 'docker-rootless-extras' , src : InstallSourceArchive ) : Promise < string > {
95
97
const release : GitHubRelease = await Install . getRelease ( src . version ) ;
96
98
this . _version = release . tag_name . replace ( / ^ v + | v + $ / g, '' ) ;
97
99
core . debug ( `docker.Install.download version: ${ this . _version } ` ) ;
98
100
99
- const downloadURL = this . downloadURL ( this . _version , src . channel ) ;
101
+ const downloadURL = this . downloadURL ( component , this . _version , src . channel ) ;
100
102
core . info ( `Downloading ${ downloadURL } ` ) ;
101
103
102
104
const downloadPath = await tc . downloadTool ( downloadURL ) ;
103
105
core . debug ( `docker.Install.download downloadPath: ${ downloadPath } ` ) ;
104
106
105
- let extractFolder : string ;
107
+ let extractFolder ;
106
108
if ( os . platform ( ) == 'win32' ) {
107
- extractFolder = await tc . extractZip ( downloadPath ) ;
109
+ extractFolder = await tc . extractZip ( downloadPath , extractFolder ) ;
108
110
} else {
109
- extractFolder = await tc . extractTar ( downloadPath ) ;
111
+ extractFolder = await tc . extractTar ( downloadPath , extractFolder ) ;
110
112
}
111
- if ( Util . isDirectory ( path . join ( extractFolder , 'docker' ) ) ) {
112
- extractFolder = path . join ( extractFolder , 'docker' ) ;
113
+ if ( Util . isDirectory ( path . join ( extractFolder , component ) ) ) {
114
+ extractFolder = path . join ( extractFolder , component ) ;
113
115
}
114
116
core . debug ( `docker.Install.download extractFolder: ${ extractFolder } ` ) ;
115
117
return extractFolder ;
@@ -164,7 +166,12 @@ export class Install {
164
166
this . _version = version ;
165
167
166
168
core . info ( `Downloading Docker ${ version } from ${ this . source . channel } at download.docker.com` ) ;
167
- extractFolder = await this . downloadStaticArchive ( this . source ) ;
169
+ extractFolder = await this . downloadStaticArchive ( 'docker' , this . source ) ;
170
+ if ( this . rootless ) {
171
+ core . info ( `Downloading Docker rootless extras ${ version } from ${ this . source . channel } at download.docker.com` ) ;
172
+ const extrasFolder = await this . downloadStaticArchive ( 'docker-rootless-extras' , this . source ) ;
173
+ fs . copyFileSync ( path . join ( extrasFolder , 'dockerd-rootless.sh' ) , path . join ( extractFolder , 'dockerd-rootless.sh' ) ) ;
174
+ }
168
175
break ;
169
176
}
170
177
}
@@ -195,7 +202,13 @@ export class Install {
195
202
if ( ! this . runDir ) {
196
203
throw new Error ( 'runDir must be set' ) ;
197
204
}
198
- switch ( os . platform ( ) ) {
205
+
206
+ const platform = os . platform ( ) ;
207
+ if ( this . rootless && platform != 'linux' ) {
208
+ // TODO: Support on macOS (via lima)
209
+ throw new Error ( `rootless is only supported on linux` ) ;
210
+ }
211
+ switch ( platform ) {
199
212
case 'darwin' : {
200
213
return await this . installDarwin ( ) ;
201
214
}
@@ -339,21 +352,34 @@ export class Install {
339
352
}
340
353
341
354
const envs = Object . assign ( { } , process . env , {
342
- PATH : `${ this . toolDir } :${ process . env . PATH } `
355
+ PATH : `${ this . toolDir } :${ process . env . PATH } ` ,
356
+ XDG_RUNTIME_DIR : ( this . rootless && this . runDir ) || undefined
343
357
} ) as {
344
358
[ key : string ] : string ;
345
359
} ;
346
360
347
361
await core . group ( 'Start Docker daemon' , async ( ) => {
348
362
const bashPath : string = await io . which ( 'bash' , true ) ;
349
- const cmd = `${ this . toolDir } /dockerd --host="${ dockerHost } " --config-file="${ daemonConfigPath } " --exec-root="${ this . runDir } /execroot" --data-root="${ this . runDir } /data" --pidfile="${ this . runDir } /docker.pid" --userland-proxy=false` ;
363
+ let dockerPath = `${ this . toolDir } /dockerd` ;
364
+ if ( this . rootless ) {
365
+ dockerPath = `${ this . toolDir } /dockerd-rootless.sh` ;
366
+ if ( fs . existsSync ( '/proc/sys/kernel/apparmor_restrict_unprivileged_userns' ) ) {
367
+ await Exec . exec ( 'sudo' , [ 'sh' , '-c' , 'echo 0 > /proc/sys/kernel/apparmor_restrict_unprivileged_userns' ] ) ;
368
+ }
369
+ }
370
+
371
+ const cmd = `${ dockerPath } --host="${ dockerHost } " --config-file="${ daemonConfigPath } " --exec-root="${ this . runDir } /execroot" --data-root="${ this . runDir } /data" --pidfile="${ this . runDir } /docker.pid"` ;
350
372
core . info ( `[command] ${ cmd } ` ) ; // https://github.com/actions/toolkit/blob/3d652d3133965f63309e4b2e1c8852cdbdcb3833/packages/exec/src/toolrunner.ts#L47
373
+ let sudo = 'sudo' ;
374
+ if ( this . rootless ) {
375
+ sudo += ' -u \\#1001' ;
376
+ }
351
377
const proc = await child_process . spawn (
352
378
// We can't use Exec.exec here because we need to detach the process to
353
379
// avoid killing it when the action finishes running. Even if detached,
354
380
// we also need to run dockerd in a subshell and unref the process so
355
381
// GitHub Action doesn't wait for it to finish.
356
- `sudo env "PATH=$PATH" ${ bashPath } << EOF
382
+ `${ sudo } env "PATH=$PATH" ${ bashPath } << EOF
357
383
( ${ cmd } 2>&1 | tee "${ this . runDir } /dockerd.log" ) &
358
384
EOF` ,
359
385
[ ] ,
@@ -517,11 +543,11 @@ EOF`,
517
543
} ) ;
518
544
}
519
545
520
- private downloadURL ( version : string , channel : string ) : string {
546
+ private downloadURL ( component : 'docker' | 'docker-rootless-extras' , version : string , channel : string ) : string {
521
547
const platformOS = Install . platformOS ( ) ;
522
548
const platformArch = Install . platformArch ( ) ;
523
549
const ext = platformOS === 'win' ? '.zip' : '.tgz' ;
524
- return util . format ( ' https://download.docker.com/%s /static/%s/%s/docker-%s%s' , platformOS , channel , platformArch , version , ext ) ;
550
+ return ` https://download.docker.com/${ platformOS } /static/${ channel } / ${ platformArch } / ${ component } - ${ version } ${ ext } ` ;
525
551
}
526
552
527
553
private static platformOS ( ) : string {
0 commit comments