Skip to content

Commit

Permalink
docker/install: Support rootless
Browse files Browse the repository at this point in the history
Add support for running a rootless daemon. Currently only Linux host is
supported.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
  • Loading branch information
vvoland committed Nov 6, 2024
1 parent 61c10b2 commit 409eeff
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 18 deletions.
65 changes: 52 additions & 13 deletions __tests__/docker/install.test.itg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,57 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g
contextName: 'foo',
daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}`
});
await expect((async () => {
try {
await install.download();
await install.install();
await Docker.printVersion();
await Docker.printInfo();
} catch (error) {
console.error(error);
throw error;
} finally {
await install.tearDown();
}
})()).resolves.not.toThrow();
await expect(tryInstall(install)).resolves.not.toThrow();
}, 30 * 60 * 1000);
});

describe('rootless', () => {
test(
'install',
async () => {
// Skip on non linux
if (os.platform() !== 'linux') {
return;
}
if (process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) {
// Remove containerd first on ubuntu runners to make sure it takes
// ones packaged with docker
await Exec.exec('sudo', ['apt-get', 'remove', '-y', 'containerd.io'], {
env: Object.assign({}, process.env, {
DEBIAN_FRONTEND: 'noninteractive'
}) as {
[key: string]: string;
}
});
}
const install = new Install({
source: {type: 'image', tag: 'latest'},
runDir: tmpDir,
contextName: 'foo',
daemonConfig: `{"debug":true}`,
rootless: true
});
await expect(tryInstall(install)).resolves.not.toThrow();

const out = await Docker.getExecOutput(['info', '-f', '{{json .SecurityOptions}}']);
expect(out.exitCode).toBe(0);
expect(out.stderr.trim()).toBe('');
expect(out.stdout.trim()).toContain('rootless');
},
30 * 60 * 1000
);
});

async function tryInstall(install: Install) {
try {
await install.download();
await install.install();
await Docker.printVersion();
await Docker.printInfo();
} catch (error) {
console.error(error);
throw error;
} finally {
await install.tearDown();
}
}
32 changes: 27 additions & 5 deletions src/docker/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface InstallOpts {
runDir: string;
contextName?: string;
daemonConfig?: string;
rootless?: boolean;
}

interface LimaImage {
Expand All @@ -65,19 +66,21 @@ interface LimaImage {
}

export class Install {
private readonly runDir: string;
private runDir: string;
private readonly source: InstallSource;
private readonly contextName: string;
private readonly daemonConfig?: string;
private _version: string | undefined;
private _toolDir: string | undefined;
private rootless: boolean;

private gitCommit: string | undefined;

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

constructor(opts: InstallOpts) {
this.runDir = opts.runDir;
this.rootless = opts.rootless || false;
this.source = opts.source || {
type: 'archive',
version: 'latest',
Expand Down Expand Up @@ -195,7 +198,13 @@ export class Install {
if (!this.runDir) {
throw new Error('runDir must be set');
}
switch (os.platform()) {

const platform = os.platform();
if (this.rootless && platform != 'linux') {
// TODO: Support on macOS (via lima)
throw new Error(`rootless is only supported on linux`);
}
switch (platform) {
case 'darwin': {
return await this.installDarwin();
}
Expand Down Expand Up @@ -339,21 +348,34 @@ export class Install {
}

const envs = Object.assign({}, process.env, {
PATH: `${this.toolDir}:${process.env.PATH}`
PATH: `${this.toolDir}:${process.env.PATH}`,
XDG_RUNTIME_DIR: (this.rootless && this.runDir) || undefined
}) as {
[key: string]: string;
};

await core.group('Start Docker daemon', async () => {
const bashPath: string = await io.which('bash', true);
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`;
let dockerPath = `${this.toolDir}/dockerd`;
if (this.rootless) {
dockerPath = `${this.toolDir}/dockerd-rootless.sh`;
if (fs.existsSync('/proc/sys/kernel/apparmor_restrict_unprivileged_userns')) {
await Exec.exec('sudo', ['sh', '-c', 'echo 0 > /proc/sys/kernel/apparmor_restrict_unprivileged_userns']);
}
}

const cmd = `${dockerPath} --host="${dockerHost}" --config-file="${daemonConfigPath}" --exec-root="${this.runDir}/execroot" --data-root="${this.runDir}/data" --pidfile="${this.runDir}/docker.pid"`;
core.info(`[command] ${cmd}`); // https://github.com/actions/toolkit/blob/3d652d3133965f63309e4b2e1c8852cdbdcb3833/packages/exec/src/toolrunner.ts#L47
let sudo = 'sudo';
if (this.rootless) {
sudo += ' -u \\#1001';
}
const proc = await child_process.spawn(
// We can't use Exec.exec here because we need to detach the process to
// avoid killing it when the action finishes running. Even if detached,
// we also need to run dockerd in a subshell and unref the process so
// GitHub Action doesn't wait for it to finish.
`sudo env "PATH=$PATH" ${bashPath} << EOF
`${sudo} env "PATH=$PATH" ${bashPath} << EOF
( ${cmd} 2>&1 | tee "${this.runDir}/dockerd.log" ) &
EOF`,
[],
Expand Down

0 comments on commit 409eeff

Please sign in to comment.