Skip to content

Commit 38d95ac

Browse files
committed
docker/install: Support rootless
Signed-off-by: Paweł Gronowski <[email protected]>
1 parent 61c10b2 commit 38d95ac

File tree

2 files changed

+85
-20
lines changed

2 files changed

+85
-20
lines changed

__tests__/docker/install.test.itg.ts

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {Exec} from '../../src/exec';
2525

2626
const tmpDir = fs.mkdtempSync(path.join(process.env.TEMP || os.tmpdir(), 'docker-install-itg-'));
2727

28+
/*
2829
describe('install', () => {
2930
const originalEnv = process.env;
3031
beforeEach(() => {
@@ -65,18 +66,52 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g
6566
contextName: 'foo',
6667
daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}`
6768
});
68-
await expect((async () => {
69-
try {
70-
await install.download();
71-
await install.install();
72-
await Docker.printVersion();
73-
await Docker.printInfo();
74-
} catch (error) {
75-
console.error(error);
76-
throw error;
77-
} finally {
78-
await install.tearDown();
79-
}
80-
})()).resolves.not.toThrow();
69+
await expect(tryInstall(install)).resolves.not.toThrow();
8170
}, 30 * 60 * 1000);
8271
});
72+
*/
73+
74+
describe('rootless', () => {
75+
if (os.platform() != 'linux') {
76+
return;
77+
}
78+
test(
79+
'install',
80+
async () => {
81+
if (process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) {
82+
// Remove containerd first on ubuntu runners to make sure it takes
83+
// ones packaged with docker
84+
await Exec.exec('sudo', ['apt-get', 'remove', '-y', 'containerd.io'], {
85+
env: Object.assign({}, process.env, {
86+
DEBIAN_FRONTEND: 'noninteractive'
87+
}) as {
88+
[key: string]: string;
89+
}
90+
});
91+
}
92+
const install = new Install({
93+
source: {type: 'image', tag: 'latest'},
94+
runDir: tmpDir,
95+
contextName: 'foo',
96+
daemonConfig: `{"debug":true}`,
97+
rootless: true
98+
});
99+
await expect(tryInstall(install)).resolves.not.toThrow();
100+
},
101+
30 * 60 * 1000
102+
);
103+
});
104+
105+
async function tryInstall(install: Install) {
106+
try {
107+
await install.download();
108+
await install.install();
109+
await Docker.printVersion();
110+
await Docker.printInfo();
111+
} catch (error) {
112+
console.error(error);
113+
throw error;
114+
} finally {
115+
await install.tearDown();
116+
}
117+
}

src/docker/install.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {limaYamlData, dockerServiceLogsPs1, setupDockerWinPs1} from './assets';
3535
import {GitHubRelease} from '../types/github';
3636
import {HubRepository} from '../hubRepository';
3737
import {Image} from '../types/oci/config';
38+
import {exec} from "child_process";
3839

3940
export interface InstallSourceImage {
4041
type: 'image';
@@ -56,6 +57,7 @@ export interface InstallOpts {
5657
runDir: string;
5758
contextName?: string;
5859
daemonConfig?: string;
60+
rootless?: boolean;
5961
}
6062

6163
interface LimaImage {
@@ -65,19 +67,21 @@ interface LimaImage {
6567
}
6668

6769
export class Install {
68-
private readonly runDir: string;
70+
private runDir: string;
6971
private readonly source: InstallSource;
7072
private readonly contextName: string;
7173
private readonly daemonConfig?: string;
7274
private _version: string | undefined;
7375
private _toolDir: string | undefined;
76+
private rootless: boolean;
7477

7578
private gitCommit: string | undefined;
7679

7780
private readonly limaInstanceName = 'docker-actions-toolkit';
7881

7982
constructor(opts: InstallOpts) {
8083
this.runDir = opts.runDir;
84+
this.rootless = opts.rootless || false;
8185
this.source = opts.source || {
8286
type: 'archive',
8387
version: 'latest',
@@ -195,7 +199,13 @@ export class Install {
195199
if (!this.runDir) {
196200
throw new Error('runDir must be set');
197201
}
198-
switch (os.platform()) {
202+
203+
const platform = os.platform();
204+
if (this.rootless && platform != 'linux') {
205+
// TODO: Support on macOS (via lima)
206+
throw new Error(`rootless is only supported on linux`);
207+
}
208+
switch (platform) {
199209
case 'darwin': {
200210
return await this.installDarwin();
201211
}
@@ -312,6 +322,9 @@ export class Install {
312322
}
313323

314324
private async installLinux(): Promise<string> {
325+
if (this.rootless) {
326+
this.runDir = os.homedir() + '/' + this.runDir;
327+
}
315328
const dockerHost = `unix://${path.join(this.runDir, 'docker.sock')}`;
316329
await io.mkdirP(this.runDir);
317330

@@ -339,15 +352,27 @@ export class Install {
339352
}
340353

341354
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.runDir
343357
}) as {
344358
[key: string]: string;
345359
};
346360

347361
await core.group('Start Docker daemon', async () => {
348362
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+
let cmd = `${dockerPath} --host="${dockerHost}" --config-file="${daemonConfigPath}" --exec-root="${this.runDir}/execroot" --data-root="${this.runDir}/data" --pidfile="${this.runDir}/docker.pid"`;
350372
core.info(`[command] ${cmd}`); // https://github.com/actions/toolkit/blob/3d652d3133965f63309e4b2e1c8852cdbdcb3833/packages/exec/src/toolrunner.ts#L47
373+
if (this.rootless) {
374+
cmd = `sudo -u #1000`;
375+
}
351376
const proc = await child_process.spawn(
352377
// We can't use Exec.exec here because we need to detach the process to
353378
// avoid killing it when the action finishes running. Even if detached,
@@ -359,19 +384,23 @@ EOF`,
359384
[],
360385
{
361386
env: envs,
362-
detached: true,
387+
//detached: true,
363388
shell: true,
364389
stdio: ['ignore', process.stdout, process.stderr]
365390
}
366391
);
392+
core.info(`unref`);
367393
proc.unref();
368-
await Util.sleep(3);
394+
395+
core.info(`sleep`);
396+
await Util.sleep(5);
369397
const retries = 10;
398+
core.info(`Waiting for Docker daemon to start (up to ${retries} retries)...`);
370399
await retry(
371400
async bail => {
372401
try {
373402
await Exec.getExecOutput(`docker version`, undefined, {
374-
silent: true,
403+
silent: false,
375404
env: Object.assign({}, envs, {
376405
DOCKER_HOST: dockerHost,
377406
DOCKER_CONTENT_TRUST: 'false'
@@ -380,6 +409,7 @@ EOF`,
380409
}
381410
});
382411
} catch (e) {
412+
core.error(e);
383413
bail(e);
384414
}
385415
},

0 commit comments

Comments
 (0)