diff --git a/__tests__/docker/install.test.itg.ts b/__tests__/docker/install.test.itg.ts index 01c20eb..f25a547 100644 --- a/__tests__/docker/install.test.itg.ts +++ b/__tests__/docker/install.test.itg.ts @@ -36,13 +36,14 @@ describe('install', () => { process.env = originalEnv; }); // prettier-ignore - test.each(['v24.0.5'])( + test.each(['v24.0.4'])( 'install docker %s', async (version) => { await expect((async () => { const install = new Install({ version: version, runDir: tmpDir, - contextName: 'foo' + contextName: 'foo', + daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}` }); await install.download(); await install.install(); diff --git a/package.json b/package.json index 5f6eec6..8cfbc38 100644 --- a/package.json +++ b/package.json @@ -56,12 +56,14 @@ "async-retry": "^1.3.3", "csv-parse": "^5.4.0", "handlebars": "^4.7.8", + "js-yaml": "^4.1.0", "jwt-decode": "^3.1.2", "semver": "^7.5.4", "tmp": "^0.2.1" }, "devDependencies": { "@types/csv-parse": "^1.2.2", + "@types/js-yaml": "^4.0.5", "@types/node": "^16.18.21", "@types/semver": "^7.5.0", "@types/tmp": "^0.2.3", diff --git a/src/docker/assets.ts b/src/docker/assets.ts index 40fe128..75f1ff5 100644 --- a/src/docker/assets.ts +++ b/src/docker/assets.ts @@ -17,10 +17,6 @@ import fs from 'fs'; import {Context} from '../context'; -export const setupDockerLinuxSh = (): string => { - return get('docker-setup-linux.sh', setupDockerLinuxShData, '0755'); -}; - export const setupDockerWinPs1 = (): string => { return get('docker-setup-win.ps1', setupDockerWinPs1Data); }; @@ -45,43 +41,6 @@ const get = (filename: string, data: string, mode?: string): string => { return assetPath; }; -export const setupDockerLinuxShData = ` -#!/usr/bin/env bash - -set -eu - -: "\${TOOLDIR=}" -: "\${RUNDIR=}" -: "\${DOCKER_HOST=}" - -export PATH="$TOOLDIR::$PATH" - -if [ -z "$DOCKER_HOST" ]; then - echo >&2 'error: DOCKER_HOST required' - false -fi - -if ! command -v dockerd &> /dev/null; then - echo >&2 'error: dockerd missing from PATH' - false -fi - -mkdir -p "$RUNDIR" - -( - echo "Starting dockerd" - set -x - exec dockerd \\ - --debug \\ - --host="$DOCKER_HOST" \\ - --exec-root="$RUNDIR/execroot" \\ - --data-root="$RUNDIR/data" \\ - --pidfile="$RUNDIR/docker.pid" \\ - --userland-proxy=false \\ - 2>&1 | tee "$RUNDIR/dockerd.log" -) & -`; - export const setupDockerWinPs1Data = ` [CmdletBinding()] param( @@ -92,7 +51,10 @@ param( [string]$RunDir, [Parameter(Mandatory = $true)] - [string]$DockerHost) + [string]$DockerHost, + + [Parameter(Mandatory = $false)] + [string]$DaemonConfig) $pwver = (Get-ItemProperty -Path HKLM:\\SOFTWARE\\Microsoft\\PowerShell\\3\\PowerShellEngine -Name 'PowerShellVersion').PowerShellVersion Write-Host "PowerShell version: $pwver" @@ -120,6 +82,12 @@ if (Get-Service docker -ErrorAction SilentlyContinue) { $env:DOCKER_HOST = $DockerHost Write-Host "DOCKER_HOST: $env:DOCKER_HOST" +if ($DaemonConfig) { + Write-Host "Writing Docker daemon config" + New-Item -ItemType Directory -Force -Path "$env:ProgramData\\Docker\\config" + $DaemonConfig | Out-File -FilePath "$env:ProgramData\\Docker\\config\\daemon.json" +} + Write-Host "Creating service" New-Item -ItemType Directory "$RunDir\\moby-root" -ErrorAction SilentlyContinue | Out-Null New-Item -ItemType Directory "$RunDir\\moby-exec" -ErrorAction SilentlyContinue | Out-Null @@ -246,7 +214,7 @@ forwardAgent: false # # Colima default behaviour: buildkit enabled # Default: {} -docker: {} +{{daemonConfig}} # Virtual Machine type (qemu, vz) # NOTE: this is macOS 13 only. For Linux and macOS <13.0, qemu is always used. diff --git a/src/docker/install.ts b/src/docker/install.ts index bd6c819..03c0d3a 100644 --- a/src/docker/install.ts +++ b/src/docker/install.ts @@ -19,6 +19,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import retry from 'async-retry'; +import yaml from 'js-yaml'; import * as handlebars from 'handlebars'; import * as util from 'util'; import * as core from '@actions/core'; @@ -29,7 +30,7 @@ import * as tc from '@actions/tool-cache'; import {Context} from '../context'; import {Exec} from '../exec'; import {Util} from '../util'; -import {colimaYamlData, dockerServiceLogsPs1, qemuEntitlements, setupDockerLinuxSh, setupDockerWinPs1} from './assets'; +import {colimaYamlData, dockerServiceLogsPs1, qemuEntitlements, setupDockerWinPs1} from './assets'; import {GitHubRelease} from '../types/github'; export interface InstallOpts { @@ -37,6 +38,7 @@ export interface InstallOpts { channel?: string; runDir: string; contextName?: string; + daemonConfig?: string; } export class Install { @@ -44,6 +46,7 @@ export class Install { private readonly version: string; private readonly channel: string; private readonly contextName: string; + private readonly daemonConfig?: string; private _version: string | undefined; private _toolDir: string | undefined; @@ -52,6 +55,7 @@ export class Install { this.version = opts.version || 'latest'; this.channel = opts.channel || 'stable'; this.contextName = opts.contextName || 'setup-docker-action'; + this.daemonConfig = opts.daemonConfig; } get toolDir(): string { @@ -137,10 +141,15 @@ export class Install { } await core.group('Creating colima config', async () => { + let daemonConfig = yaml.dump({docker: {}}); + if (this.daemonConfig) { + daemonConfig = yaml.dump(yaml.load(JSON.stringify({docker: JSON.parse(this.daemonConfig)}))); + } const colimaCfg = handlebars.compile(colimaYamlData)({ hostArch: Install.platformArch(), dockerVersion: this._version, - dockerChannel: this.channel + dockerChannel: this.channel, + daemonConfig: daemonConfig }); core.info(`Writing colima config to ${path.join(colimaDir, 'colima.yaml')}`); fs.writeFileSync(path.join(colimaDir, 'colima.yaml'), colimaCfg); @@ -192,44 +201,65 @@ export class Install { const dockerHost = `unix://${path.join(this.runDir, 'docker.sock')}`; await io.mkdirP(this.runDir); + const daemonConfigPath = path.join(this.runDir, 'daemon.json'); + await fs.writeFileSync(daemonConfigPath, '{}'); + + let daemonConfig = undefined; + const daemonConfigDefaultPath = '/etc/docker/daemon.json'; + if (fs.existsSync(daemonConfigDefaultPath)) { + await core.group('Default Docker daemon config found', async () => { + core.info(JSON.stringify(JSON.parse(fs.readFileSync(daemonConfigDefaultPath, {encoding: 'utf8'})), null, 2)); + }); + daemonConfig = JSON.parse(fs.readFileSync(daemonConfigDefaultPath, {encoding: 'utf8'})); + } + if (this.daemonConfig) { + daemonConfig = Object.assign(daemonConfig || {}, JSON.parse(this.daemonConfig)); + } + + if (daemonConfig) { + const daemonConfigStr = JSON.stringify(daemonConfig, null, 2); + await core.group('Writing Docker daemon config', async () => { + fs.writeFileSync(daemonConfigPath, daemonConfigStr); + core.info(daemonConfigStr); + }); + } + await core.group('Start Docker daemon', async () => { const bashPath: string = await io.which('bash', true); - const proc = await child_process.spawn(`sudo -E ${bashPath} ${setupDockerLinuxSh()}`, [], { - detached: true, - shell: true, - stdio: ['ignore', process.stdout, process.stderr], - env: Object.assign({}, process.env, { - TOOLDIR: this.toolDir, - RUNDIR: this.runDir, - DOCKER_HOST: dockerHost - }) as { - [key: string]: string; + 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`; + core.info(`[command] ${cmd}`); // https://github.com/actions/toolkit/blob/3d652d3133965f63309e4b2e1c8852cdbdcb3833/packages/exec/src/toolrunner.ts#L47 + 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 -E ${bashPath} << EOF +( ${cmd} 2>&1 | tee "${this.runDir}/dockerd.log" ) & +EOF`, + [], + { + detached: true, + shell: true, + stdio: ['ignore', process.stdout, process.stderr] } - }); + ); proc.unref(); - const retries = 20; + await Util.sleep(3); + const retries = 10; await retry( async bail => { - await Exec.getExecOutput(`docker version`, undefined, { - ignoreReturnCode: true, - silent: true, - env: Object.assign({}, process.env, { - DOCKER_HOST: dockerHost - }) as { - [key: string]: string; - } - }) - .then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { - bail(new Error(res.stderr)); - return false; + try { + await Exec.getExecOutput(`docker version`, undefined, { + silent: true, + env: Object.assign({}, process.env, { + DOCKER_HOST: dockerHost + }) as { + [key: string]: string; } - return res.exitCode == 0; - }) - .catch(error => { - bail(error); - return false; }); + } catch (e) { + bail(e); + } }, { retries: retries, @@ -251,11 +281,32 @@ export class Install { private async installWindows(): Promise { const dockerHost = 'npipe:////./pipe/setup_docker_action'; + let daemonConfig = undefined; + const daemonConfigPath = path.join(this.runDir, 'daemon.json'); + if (fs.existsSync(daemonConfigPath)) { + await core.group('Default Docker daemon config found', async () => { + core.info(JSON.stringify(JSON.parse(fs.readFileSync(daemonConfigPath, {encoding: 'utf8'})), null, 2)); + }); + daemonConfig = JSON.parse(fs.readFileSync(daemonConfigPath, {encoding: 'utf8'})); + } + if (this.daemonConfig) { + daemonConfig = Object.assign(daemonConfig || {}, JSON.parse(this.daemonConfig)); + } + + let daemonConfigStr = '{}'; + if (daemonConfig) { + daemonConfigStr = JSON.stringify(daemonConfig, null, 2); + await core.group('Docker daemon config', async () => { + core.info(daemonConfigStr); + }); + } + await core.group('Install Docker daemon service', async () => { const setupCmd = await Util.powershellCommand(setupDockerWinPs1(), { ToolDir: this.toolDir, RunDir: this.runDir, - DockerHost: dockerHost + DockerHost: dockerHost, + DaemonConfig: daemonConfigStr }); await Exec.exec(setupCmd.command, setupCmd.args); const logCmd = await Util.powershellCommand(dockerServiceLogsPs1()); diff --git a/yarn.lock b/yarn.lock index b5000ab..c5c5cab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,6 +974,7 @@ __metadata: "@actions/tool-cache": ^2.0.1 "@octokit/plugin-rest-endpoint-methods": ^7.2.3 "@types/csv-parse": ^1.2.2 + "@types/js-yaml": ^4.0.5 "@types/node": ^16.18.21 "@types/semver": ^7.5.0 "@types/tmp": ^0.2.3 @@ -990,6 +991,7 @@ __metadata: eslint-plugin-prettier: ^4.2.1 handlebars: ^4.7.8 jest: ^29.5.0 + js-yaml: ^4.1.0 jwt-decode: ^3.1.2 prettier: ^2.8.7 rimraf: ^4.4.1 @@ -1825,6 +1827,13 @@ __metadata: languageName: node linkType: hard +"@types/js-yaml@npm:^4.0.5": + version: 4.0.5 + resolution: "@types/js-yaml@npm:4.0.5" + checksum: 7dcac8c50fec31643cc9d6444b5503239a861414cdfaa7ae9a38bc22597c4d850c4b8cec3d82d73b3fbca408348ce223b0408d598b32e094470dfffc6d486b4d + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.9": version: 7.0.9 resolution: "@types/json-schema@npm:7.0.9"