Skip to content

Commit 37b0f81

Browse files
authored
Merge pull request #485 from vvoland/docker-install-rootless
docker/install: Support rootless
2 parents 8c97b0d + 2d2bc84 commit 37b0f81

File tree

2 files changed

+108
-40
lines changed

2 files changed

+108
-40
lines changed

__tests__/docker/install.test.itg.ts

+66-24
Original file line numberDiff line numberDiff line change
@@ -48,35 +48,77 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g
4848
{type: 'archive', version: 'latest', channel: 'stable'} as InstallSourceArchive,
4949
])(
5050
'install docker %s', async (source) => {
51-
if (process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) {
52-
// Remove containerd first on ubuntu runners to make sure it takes
53-
// ones packaged with docker
54-
await Exec.exec('sudo', ['apt-get', 'remove', '-y', 'containerd.io'], {
55-
env: Object.assign({}, process.env, {
56-
DEBIAN_FRONTEND: 'noninteractive'
57-
}) as {
58-
[key: string]: string;
59-
}
60-
});
61-
}
51+
await ensureNoSystemContainerd();
6252
const install = new Install({
6353
source: source,
6454
runDir: tmpDir,
6555
contextName: 'foo',
6656
daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}`
6757
});
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();
58+
await expect(tryInstall(install)).resolves.not.toThrow();
8159
}, 30 * 60 * 1000);
8260
});
61+
62+
describe('rootless', () => {
63+
// prettier-ignore
64+
test.each([
65+
{type: 'image', tag: 'latest'} as InstallSourceImage,
66+
{type: 'archive', version: 'latest', channel: 'stable'} as InstallSourceArchive,
67+
])(
68+
'install %s', async (source) => {
69+
// Skip on non linux
70+
if (os.platform() !== 'linux') {
71+
return;
72+
}
73+
74+
await ensureNoSystemContainerd();
75+
const install = new Install({
76+
source: source,
77+
runDir: tmpDir,
78+
contextName: 'foo',
79+
daemonConfig: `{"debug":true}`,
80+
rootless: true
81+
});
82+
await expect(
83+
tryInstall(install, async () => {
84+
const out = await Docker.getExecOutput(['info', '-f', '{{json .SecurityOptions}}']);
85+
expect(out.exitCode).toBe(0);
86+
expect(out.stderr.trim()).toBe('');
87+
expect(out.stdout.trim()).toContain('rootless');
88+
})
89+
).resolves.not.toThrow();
90+
},
91+
30 * 60 * 1000
92+
);
93+
});
94+
95+
async function tryInstall(install: Install, extraCheck?: () => Promise<void>): Promise<void> {
96+
try {
97+
await install.download();
98+
await install.install();
99+
await Docker.printVersion();
100+
await Docker.printInfo();
101+
if (extraCheck) {
102+
await extraCheck();
103+
}
104+
} catch (error) {
105+
console.error(error);
106+
throw error;
107+
} finally {
108+
await install.tearDown();
109+
}
110+
}
111+
112+
async function ensureNoSystemContainerd() {
113+
if (process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) {
114+
// Remove containerd first on ubuntu runners to make sure it takes
115+
// ones packaged with docker
116+
await Exec.exec('sudo', ['apt-get', 'remove', '-y', 'containerd.io'], {
117+
env: Object.assign({}, process.env, {
118+
DEBIAN_FRONTEND: 'noninteractive'
119+
}) as {
120+
[key: string]: string;
121+
}
122+
});
123+
}
124+
}

src/docker/install.ts

+42-16
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import os from 'os';
2121
import path from 'path';
2222
import retry from 'async-retry';
2323
import * as handlebars from 'handlebars';
24-
import * as util from 'util';
2524
import * as core from '@actions/core';
2625
import * as httpm from '@actions/http-client';
2726
import * as io from '@actions/io';
@@ -56,6 +55,7 @@ export interface InstallOpts {
5655
runDir: string;
5756
contextName?: string;
5857
daemonConfig?: string;
58+
rootless?: boolean;
5959
}
6060

6161
interface LimaImage {
@@ -65,19 +65,21 @@ interface LimaImage {
6565
}
6666

6767
export class Install {
68-
private readonly runDir: string;
68+
private runDir: string;
6969
private readonly source: InstallSource;
7070
private readonly contextName: string;
7171
private readonly daemonConfig?: string;
7272
private _version: string | undefined;
7373
private _toolDir: string | undefined;
74+
private rootless: boolean;
7475

7576
private gitCommit: string | undefined;
7677

7778
private readonly limaInstanceName = 'docker-actions-toolkit';
7879

7980
constructor(opts: InstallOpts) {
8081
this.runDir = opts.runDir;
82+
this.rootless = opts.rootless || false;
8183
this.source = opts.source || {
8284
type: 'archive',
8385
version: 'latest',
@@ -91,25 +93,25 @@ export class Install {
9193
return this._toolDir || Context.tmpDir();
9294
}
9395

94-
async downloadStaticArchive(src: InstallSourceArchive): Promise<string> {
96+
async downloadStaticArchive(component: 'docker' | 'docker-rootless-extras', src: InstallSourceArchive): Promise<string> {
9597
const release: GitHubRelease = await Install.getRelease(src.version);
9698
this._version = release.tag_name.replace(/^v+|v+$/g, '');
9799
core.debug(`docker.Install.download version: ${this._version}`);
98100

99-
const downloadURL = this.downloadURL(this._version, src.channel);
101+
const downloadURL = this.downloadURL(component, this._version, src.channel);
100102
core.info(`Downloading ${downloadURL}`);
101103

102104
const downloadPath = await tc.downloadTool(downloadURL);
103105
core.debug(`docker.Install.download downloadPath: ${downloadPath}`);
104106

105-
let extractFolder: string;
107+
let extractFolder;
106108
if (os.platform() == 'win32') {
107-
extractFolder = await tc.extractZip(downloadPath);
109+
extractFolder = await tc.extractZip(downloadPath, extractFolder);
108110
} else {
109-
extractFolder = await tc.extractTar(downloadPath);
111+
extractFolder = await tc.extractTar(downloadPath, extractFolder);
110112
}
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);
113115
}
114116
core.debug(`docker.Install.download extractFolder: ${extractFolder}`);
115117
return extractFolder;
@@ -164,7 +166,12 @@ export class Install {
164166
this._version = version;
165167

166168
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+
}
168175
break;
169176
}
170177
}
@@ -195,7 +202,13 @@ export class Install {
195202
if (!this.runDir) {
196203
throw new Error('runDir must be set');
197204
}
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) {
199212
case 'darwin': {
200213
return await this.installDarwin();
201214
}
@@ -339,21 +352,34 @@ 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.rootless && this.runDir) || undefined
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+
const 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+
let sudo = 'sudo';
374+
if (this.rootless) {
375+
sudo += ' -u \\#1001';
376+
}
351377
const proc = await child_process.spawn(
352378
// We can't use Exec.exec here because we need to detach the process to
353379
// avoid killing it when the action finishes running. Even if detached,
354380
// we also need to run dockerd in a subshell and unref the process so
355381
// GitHub Action doesn't wait for it to finish.
356-
`sudo env "PATH=$PATH" ${bashPath} << EOF
382+
`${sudo} env "PATH=$PATH" ${bashPath} << EOF
357383
( ${cmd} 2>&1 | tee "${this.runDir}/dockerd.log" ) &
358384
EOF`,
359385
[],
@@ -517,11 +543,11 @@ EOF`,
517543
});
518544
}
519545

520-
private downloadURL(version: string, channel: string): string {
546+
private downloadURL(component: 'docker' | 'docker-rootless-extras', version: string, channel: string): string {
521547
const platformOS = Install.platformOS();
522548
const platformArch = Install.platformArch();
523549
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}`;
525551
}
526552

527553
private static platformOS(): string {

0 commit comments

Comments
 (0)