Skip to content

Commit

Permalink
docker(install): use undock to extract image
Browse files Browse the repository at this point in the history
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
  • Loading branch information
crazy-max committed Jan 19, 2025
1 parent a54d83c commit b28bf20
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 236 deletions.
19 changes: 15 additions & 4 deletions __tests__/docker/install.test.itg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,25 @@
* limitations under the License.
*/

import {describe, test, expect} from '@jest/globals';
import {beforeAll, describe, test, expect} from '@jest/globals';
import fs from 'fs';
import os from 'os';
import path from 'path';

import {Install, InstallSource, InstallSourceArchive, InstallSourceImage} from '../../src/docker/install';
import {Docker} from '../../src/docker/docker';
import {Undock} from '../../src/undock/undock';
import {Install as UndockInstall} from '../../src/undock/install';
import {Exec} from '../../src/exec';

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

beforeAll(async () => {
const undockInstall = new UndockInstall();
const binPath = await undockInstall.download('v0.9.0', true);
await undockInstall.install(binPath);
});

describe('root', () => {
// prettier-ignore
test.each(getSources(true))(
Expand All @@ -34,7 +42,8 @@ describe('root', () => {
source: source,
runDir: tmpDir(),
contextName: 'foo',
daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}`
daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}`,
undock: new Undock()
});
await expect(tryInstall(install)).resolves.not.toThrow();
}, 30 * 60 * 1000);
Expand All @@ -54,7 +63,8 @@ describe('rootless', () => {
runDir: tmpDir(),
contextName: 'foo',
daemonConfig: `{"debug":true}`,
rootless: true
rootless: true,
undock: new Undock()
});
await expect(
tryInstall(install, async () => {
Expand All @@ -79,7 +89,8 @@ describe('tcp', () => {
runDir: tmpDir(),
contextName: 'foo',
daemonConfig: `{"debug":true}`,
localTCPPort: 2378
localTCPPort: 2378,
undock: new Undock()
});
await expect(
tryInstall(install, async () => {
Expand Down
11 changes: 10 additions & 1 deletion __tests__/docker/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,25 @@
* limitations under the License.
*/

import {describe, expect, jest, test, beforeEach, afterEach, it} from '@jest/globals';
import {describe, expect, jest, test, beforeEach, afterEach, it, beforeAll} from '@jest/globals';
import fs from 'fs';
import os from 'os';
import path from 'path';
import * as rimraf from 'rimraf';
import osm = require('os');

import {Install, InstallSourceArchive, InstallSourceImage} from '../../src/docker/install';
import {Undock} from '../../src/undock/undock';
import {Install as UndockInstall} from '../../src/undock/install';

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

beforeAll(async () => {
const undockInstall = new UndockInstall();
const binPath = await undockInstall.download('v0.9.0', true);
await undockInstall.install(binPath);
});

afterEach(function () {
rimraf.sync(tmpDir);
});
Expand Down Expand Up @@ -63,6 +71,7 @@ describe('download', () => {
const install = new Install({
source: source,
runDir: tmpDir,
undock: new Undock()
});
const toolPath = await install.download();
expect(fs.existsSync(toolPath)).toBe(true);
Expand Down
134 changes: 77 additions & 57 deletions src/docker/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ import * as tc from '@actions/tool-cache';

import {Context} from '../context';
import {Docker} from './docker';
import {ImageTools} from '../buildx/imagetools';
import {Undock} from '../undock/undock';
import {Exec} from '../exec';
import {Util} from '../util';
import {limaYamlData, dockerServiceLogsPs1, setupDockerWinPs1} from './assets';

import {GitHubRelease} from '../types/github';
import {HubRepository} from '../hubRepository';
import {Image} from '../types/oci/config';

export interface InstallSourceImage {
Expand All @@ -57,6 +59,8 @@ export interface InstallOpts {
daemonConfig?: string;
rootless?: boolean;
localTCPPort?: number;

undock: Undock;
}

interface LimaImage {
Expand All @@ -72,6 +76,7 @@ export class Install {
private readonly daemonConfig?: string;
private readonly rootless: boolean;
private readonly localTCPPort?: number;
private readonly undock: Undock;

private _version: string | undefined;
private _toolDir: string | undefined;
Expand All @@ -91,36 +96,13 @@ export class Install {
this.daemonConfig = opts.daemonConfig;
this.rootless = opts.rootless || false;
this.localTCPPort = opts.localTCPPort;
this.undock = opts.undock;
}

get toolDir(): string {
return this._toolDir || Context.tmpDir();
}

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

const downloadURL = this.downloadURL(component, this._version, src.channel);
core.info(`Downloading ${downloadURL}`);

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

let extractFolder;
if (os.platform() == 'win32') {
extractFolder = await tc.extractZip(downloadPath, extractFolder);
} else {
extractFolder = await tc.extractTar(downloadPath, extractFolder);
}
if (Util.isDirectory(path.join(extractFolder, component))) {
extractFolder = path.join(extractFolder, component);
}
core.debug(`docker.Install.download extractFolder: ${extractFolder}`);
return extractFolder;
}

public async download(): Promise<string> {
let extractFolder: string;
let cacheKey: string;
Expand All @@ -132,35 +114,8 @@ export class Install {
this._version = tag;
cacheKey = `docker-image`;

core.info(`Downloading docker cli from dockereng/cli-bin:${tag}`);
const cli = await HubRepository.build('dockereng/cli-bin');
extractFolder = await cli.extractImage(tag);

const moby = await HubRepository.build('moby/moby-bin');
if (['win32', 'linux'].includes(platform)) {
core.info(`Downloading dockerd from moby/moby-bin:${tag}`);
await moby.extractImage(tag, extractFolder);
} else if (platform == 'darwin') {
// On macOS, the docker daemon binary will be downloaded inside the lima VM.
// However, we will get the exact git revision from the image config
// to get the matching systemd unit files.
core.info(`Getting git revision from moby/moby-bin:${tag}`);

// There's no macOS image for moby/moby-bin - a linux daemon is run inside lima.
const manifest = await moby.getPlatformManifest(tag, 'linux');

const config = await moby.getJSONBlob<Image>(manifest.config.digest);
core.debug(`Config ${JSON.stringify(config.config)}`);

this.gitCommit = config.config?.Labels?.['org.opencontainers.image.revision'];
if (!this.gitCommit) {
core.warning(`No git revision can be determined from the image. Will use master.`);
this.gitCommit = 'master';
}
core.info(`Git revision is ${this.gitCommit}`);
} else {
core.warning(`dockerd not supported on ${platform}, only the Docker cli will be available`);
}
core.info(`Downloading Docker ${tag} from image`);
extractFolder = await this.downloadSourceImage(platform);
break;
}
case 'archive': {
Expand All @@ -170,10 +125,10 @@ export class Install {
this._version = version;

core.info(`Downloading Docker ${version} from ${this.source.channel} at download.docker.com`);
extractFolder = await this.downloadStaticArchive('docker', this.source);
extractFolder = await this.downloadSourceArchive('docker', this.source);
if (this.rootless) {
core.info(`Downloading Docker rootless extras ${version} from ${this.source.channel} at download.docker.com`);
const extrasFolder = await this.downloadStaticArchive('docker-rootless-extras', this.source);
const extrasFolder = await this.downloadSourceArchive('docker-rootless-extras', this.source);
fs.readdirSync(extrasFolder).forEach(file => {
const src = path.join(extrasFolder, file);
const dest = path.join(extractFolder, file);
Expand All @@ -191,7 +146,9 @@ export class Install {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
files.forEach(function (file, index) {
fs.chmodSync(path.join(extractFolder, file), '0755');
if (!Util.isDirectory(path.join(extractFolder, file))) {
fs.chmodSync(path.join(extractFolder, file), '0755');
}
});
});

Expand All @@ -203,6 +160,69 @@ export class Install {
return tooldir;
}

private async downloadSourceImage(platform: string): Promise<string> {
const dest = path.join(Context.tmpDir(), 'docker-install-image');

const cliImage = `dockereng/cli-bin:${this._version}`;
core.info(`Downloading docker cli from ${cliImage}`);
await this.undock.run({
source: cliImage,
dist: dest
});

const engineImage = `moby/moby-bin:${this._version}`;
if (['win32', 'linux'].includes(platform)) {
core.info(`Downloading docker engine from ${engineImage}`);
await this.undock.run({
source: cliImage,
dist: dest
});
} else if (platform == 'darwin') {
// On macOS, the docker daemon binary will be downloaded inside the lima VM.
// However, we will get the exact git revision from the image config
// to get the matching systemd unit files. There's no macOS image for
// moby/moby-bin - a linux daemon is run inside lima.
const engineImageConfig = (await new ImageTools().inspectImage(engineImage)) as Record<string, Image>;
core.debug(`docker.Install.downloadSourceImage engineImageConfig: ${JSON.stringify(engineImageConfig)}`);

this.gitCommit = engineImageConfig['linux/arm64'].config?.Labels?.['org.opencontainers.image.revision'];
if (!this.gitCommit) {
core.warning(`No git revision can be determined from the image. Will use default branch as Git revision.`);
this.gitCommit = 'master';
}

core.debug(`docker.Install.downloadSourceImage gitCommit: ${this.gitCommit}`);
} else {
core.warning(`Docker engine not supported on ${platform}, only the Docker cli will be available`);
}

return dest;
}

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

const downloadURL = this.downloadURL(component, this._version, src.channel);
core.info(`Downloading ${downloadURL}`);

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

let extractFolder;
if (os.platform() == 'win32') {
extractFolder = await tc.extractZip(downloadPath, extractFolder);
} else {
extractFolder = await tc.extractTar(downloadPath, extractFolder);
}
if (Util.isDirectory(path.join(extractFolder, component))) {
extractFolder = path.join(extractFolder, component);
}
core.debug(`docker.Install.downloadSourceArchive extractFolder: ${extractFolder}`);
return extractFolder;
}

public async install(): Promise<string> {
if (!this.toolDir) {
throw new Error('toolDir must be set. Run download first.');
Expand Down
Loading

0 comments on commit b28bf20

Please sign in to comment.