docker: install and download methods for macos and windows

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax 2023-02-27 09:10:57 +01:00
parent 964381b7e9
commit 4d66b2fa08
No known key found for this signature in database
GPG Key ID: 3248E46B6BB8C7F7
11 changed files with 469 additions and 3 deletions

39
.github/workflows/e2e.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: e2e
on:
workflow_dispatch:
push:
branches:
- 'main'
pull_request:
paths-ignore:
- '.github/buildx-releases.json'
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
#- ubuntu-latest
- macos-latest
- windows-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
-
name: Install
run: yarn install
-
name: Test
run: yarn test:e2e
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,6 +1,7 @@
[![Version](https://img.shields.io/npm/v/@docker/actions-toolkit?label=version&logo=npm&style=flat-square)](https://www.npmjs.com/package/@docker/actions-toolkit)
[![Downloads](https://img.shields.io/npm/dw/@docker/actions-toolkit?logo=npm&style=flat-square)](https://www.npmjs.com/package/@docker/actions-toolkit)
[![Test workflow](https://img.shields.io/github/actions/workflow/status/docker/actions-toolkit/test.yml?label=test&logo=github&style=flat-square)](https://github.com/docker/actions-toolkit/actions?workflow=test)
[![E2E workflow](https://img.shields.io/github/actions/workflow/status/docker/actions-toolkit/e2e.yml?label=e2e&logo=github&style=flat-square)](https://github.com/docker/actions-toolkit/actions?workflow=e2e)
[![Codecov](https://img.shields.io/codecov/c/github/docker/actions-toolkit?logo=codecov&style=flat-square)](https://codecov.io/gh/docker/actions-toolkit)
# Actions Toolkit

View File

@ -0,0 +1,34 @@
/**
* Copyright 2023 actions-toolkit authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {describe, expect, test} from '@jest/globals';
import {Install} from '../../src/docker/install';
import {Docker} from '../../src/docker/docker';
describe('install', () => {
// prettier-ignore
test.each(['23.0.0'])(
'install docker %s', async (version) => {
await expect((async () => {
const install = new Install();
const toolPath = await install.download(version);
await install.install(toolPath);
await Docker.printVersion();
await Docker.printInfo();
})()).resolves.not.toThrow();
});
});

View File

@ -0,0 +1,50 @@
/**
* Copyright 2023 actions-toolkit authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {describe, expect, jest, test, beforeEach, afterEach} from '@jest/globals';
import * as fs from 'fs';
import * as path from 'path';
import * as rimraf from 'rimraf';
import osm = require('os');
import {Install} from '../../src/docker/install';
// prettier-ignore
const tmpDir = path.join(process.env.TEMP || '/tmp', 'buildx-jest');
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(function () {
rimraf.sync(tmpDir);
});
describe('download', () => {
// prettier-ignore
test.each([
['19.03.6', 'linux'],
['20.10.22', 'linux'],
['20.10.22', 'darwin'],
['20.10.22', 'win32'],
])(
'acquires %p of docker (%s)', async (version, platformOS) => {
jest.spyOn(osm, 'platform').mockImplementation(() => platformOS);
const install = new Install();
const toolPath = await install.download(version);
expect(fs.existsSync(toolPath)).toBe(true);
}, 100000);
});

31
jest.config.e2e.ts Normal file
View File

@ -0,0 +1,31 @@
/**
* Copyright 2023 actions-toolkit authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = {
clearMocks: true,
testEnvironment: 'node',
moduleFileExtensions: ['js', 'ts'],
setupFiles: ['dotenv/config'],
testMatch: ['**/*.test.e2e.ts'],
testTimeout: 1800000, // 30 minutes
transform: {
'^.+\\.ts$': 'ts-jest'
},
moduleNameMapper: {
'^csv-parse/sync': '<rootDir>/node_modules/csv-parse/dist/cjs/sync.cjs'
},
verbose: false
};

View File

@ -11,7 +11,8 @@
"prettier": "prettier --check \"./**/*.ts\"",
"prettier:fix": "prettier --write \"./**/*.ts\"",
"test": "jest",
"test-coverage": "jest --coverage"
"test-coverage": "jest --coverage",
"test:e2e": "jest -c jest.config.e2e.ts"
},
"repository": {
"type": "git",
@ -35,7 +36,8 @@
"test": "__tests__"
},
"files": [
"lib"
"lib",
"scripts"
],
"publishConfig": {
"access": "public",

69
scripts/setup-docker.ps1 Normal file
View File

@ -0,0 +1,69 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ToolDir,
[Parameter(Mandatory = $false)]
[string]$TmpDir,
[Parameter(Mandatory = $true)]
[string]$DockerHost)
$pwver = (Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine -Name 'PowerShellVersion').PowerShellVersion
Write-Host "PowerShell version: $pwver"
# Create temp directory
if (!$TmpDir) { $TmpDir = $env:TEMP }
New-Item -ItemType Directory "$TmpDir" -ErrorAction SilentlyContinue | Out-Null
# Remove existing service
if (Get-Service docker -ErrorAction SilentlyContinue) {
$dockerVersion = (docker version -f "{{.Server.Version}}")
Write-Host "Current installed Docker version: $dockerVersion"
# stop service
Stop-Service -Force -Name docker
Write-Host "Service stopped"
# remove service
sc.exe delete "docker"
# removes event log entry. we could use "Remove-EventLog -LogName -Source docker"
# but this cmd is not available atm
$ErrorActionPreference = "SilentlyContinue"
& reg delete "HKLM\SYSTEM\CurrentControlSet\Services\EventLog\Application\docker" /f 2>&1 | Out-Null
$ErrorActionPreference = "Stop"
Write-Host "Service removed"
}
$env:DOCKER_HOST = $DockerHost
Write-Host "DOCKER_HOST: $env:DOCKER_HOST"
Write-Host "Creating service"
New-Item -ItemType Directory "$TmpDir\moby-root" -ErrorAction SilentlyContinue | Out-Null
New-Item -ItemType Directory "$TmpDir\moby-exec" -ErrorAction SilentlyContinue | Out-Null
Start-Process -Wait -NoNewWindow "$ToolDir\dockerd" `
-ArgumentList `
"--host=$DockerHost", `
"--data-root=$TmpDir\moby-root", `
"--exec-root=$TmpDir\moby-exec", `
"--pidfile=$TmpDir\docker.pid", `
"--register-service"
Write-Host "Starting service"
Start-Service -Name docker
Write-Host "Service started successfully!"
$tries=20
Write-Host "Waiting for Docker daemon to start..."
While ($true) {
$ErrorActionPreference = "SilentlyContinue"
& "$ToolDir\docker" version | Out-Null
$ErrorActionPreference = "Stop"
If ($LastExitCode -eq 0) {
break
}
$tries--
If ($tries -le 0) {
Throw "Failed to get a response from Docker daemon"
}
Write-Host -NoNewline "."
Start-Sleep -Seconds 1
}
Write-Host "Docker daemon started successfully!"

195
src/docker/install.ts Normal file
View File

@ -0,0 +1,195 @@
/**
* Copyright 2023 actions-toolkit authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
import * as core from '@actions/core';
import * as io from '@actions/io';
import * as tc from '@actions/tool-cache';
import * as util from 'util';
import * as scripts from '../scripts';
import {Context} from '../context';
import {Exec} from '../exec';
import {Util} from '../util';
export class Install {
public async download(version: string, channel?: string): Promise<string> {
channel = channel || 'stable';
const downloadURL = this.downloadURL(version, channel);
core.info(`Downloading ${downloadURL}`);
const downloadPath = await tc.downloadTool(downloadURL);
core.debug(`docker.Install.download downloadPath: ${downloadPath}`);
let extractFolder: string;
if (os.platform() == 'win32') {
extractFolder = await tc.extractZip(downloadPath);
} else {
extractFolder = await tc.extractTar(downloadPath, path.join(Context.tmpDir(), 'docker'));
}
if (Util.isDirectory(path.join(extractFolder, 'docker'))) {
extractFolder = path.join(extractFolder, 'docker');
}
core.debug(`docker.Install.download extractFolder: ${extractFolder}`);
core.info('Fixing perms');
fs.readdir(path.join(extractFolder), function (err, files) {
if (err) {
throw err;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
files.forEach(function (file, index) {
fs.chmodSync(path.join(extractFolder, file), '0755');
});
});
const tooldir = await tc.cacheDir(extractFolder, `docker-${channel}`, version.replace(/(0+)([1-9]+)/, '$2'));
core.addPath(tooldir);
core.info('Added Docker to PATH');
return tooldir;
}
public async install(toolDir: string): Promise<void> {
switch (os.platform()) {
case 'darwin': {
await this.installDarwin(toolDir);
break;
}
case 'linux': {
await this.installLinux(toolDir);
break;
}
case 'win32': {
await this.installWindows(toolDir);
break;
}
default: {
throw new Error(`Unsupported platform: ${os.platform()}`);
}
}
}
private async installDarwin(toolDir: string): Promise<void> {
if (!(await Install.colimaInstalled())) {
await core.group('Installing colima', async () => {
await Exec.exec('brew', ['install', 'colima']);
});
}
// colima is already started on the runner so env var added in download
// method is not expanded to the running process.
const envs = Object.assign({}, process.env, {
PATH: `${toolDir}:${process.env.PATH}`
}) as {
[key: string]: string;
};
await core.group('Starting colima', async () => {
await Exec.exec('colima', ['start', '--runtime', 'docker', '--mount-type', '9p'], {env: envs});
});
}
private async installLinux(toolDir: string): Promise<void> {
core.addPath(toolDir);
core.info('Added Docker to PATH');
}
private async installWindows(toolDir: string): Promise<void> {
const dockerHost = 'npipe:////./pipe/setup_docker_action';
const setupCmd = await Util.powershellCommand(scripts.setupDockerPowershell, {
ToolDir: toolDir,
TmpDir: Context.tmpDir(),
DockerHost: dockerHost
});
await core.group('Install Docker daemon service', async () => {
await Exec.exec(setupCmd.command, setupCmd.args);
});
await core.group('Create Docker context', async () => {
await Exec.exec('docker', ['context', 'create', 'setup-docker-action', '--docker', `host=${dockerHost}`]);
await Exec.exec('docker', ['context', 'use', 'setup-docker-action']);
});
}
private downloadURL(version: string, channel: string): string {
let platformOS, platformArch: string;
switch (os.platform()) {
case 'darwin': {
platformOS = 'mac';
break;
}
case 'linux': {
platformOS = 'linux';
break;
}
case 'win32': {
platformOS = 'win';
break;
}
default: {
platformOS = os.platform();
break;
}
}
switch (os.arch()) {
case 'x64': {
platformArch = 'x86_64';
break;
}
case 'ppc64': {
platformArch = 'ppc64le';
break;
}
case 'arm': {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const arm_version = (process.config.variables as any).arm_version;
switch (arm_version) {
case 6: {
platformArch = 'armel';
break;
}
case 7: {
platformArch = 'armhf';
break;
}
default: {
platformArch = `v${arm_version}`;
break;
}
}
break;
}
default: {
platformArch = os.arch();
break;
}
}
const ext = platformOS === 'win' ? '.zip' : '.tgz';
return util.format('https://download.docker.com/%s/static/%s/%s/docker-%s%s', platformOS, channel, platformArch, version, ext);
}
private static async colimaInstalled(): Promise<boolean> {
return await io
.which('colima', true)
.then(res => {
core.debug(`docker.Install.colimaAvailable ok: ${res}`);
return true;
})
.catch(error => {
core.debug(`docker.Install.colimaAvailable error: ${error}`);
return false;
});
}
}

21
src/scripts.ts Normal file
View File

@ -0,0 +1,21 @@
/**
* Copyright 2023 actions-toolkit authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import path from 'path';
const scriptsPath = path.join(__dirname, '..', 'scripts');
export const setupDockerPowershell = path.join(scriptsPath, 'setup-docker.ps1');

View File

@ -14,7 +14,9 @@
* limitations under the License.
*/
import fs from 'fs';
import * as core from '@actions/core';
import * as io from '@actions/io';
import {parse} from 'csv-parse/sync';
export interface InputListOpts {
@ -71,4 +73,26 @@ export class Util {
}
return true;
}
public static async powershellCommand(script: string, params: Record<string, string>) {
const powershellPath: string = await io.which('powershell', true);
const escapedScript = script.replace(/'/g, "''").replace(/"|\n|\r/g, '');
const escapedParams: string[] = [];
for (const key in params) {
escapedParams.push(`-${key} '${params[key].replace(/'/g, "''").replace(/"|\n|\r/g, '')}'`);
}
return {
command: `"${powershellPath}"`,
args: ['-NoLogo', '-Sta', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Unrestricted', '-Command', `& '${escapedScript}' ${escapedParams.join(' ')}`]
};
}
public static isDirectory(p) {
try {
return fs.lstatSync(p).isDirectory();
} catch (_) {
// noop
}
return false;
}
}

View File

@ -19,6 +19,6 @@
"./__tests__/**/*",
"./lib/**/*",
"node_modules",
"jest.config.ts"
"jest.config*.ts"
]
}