Skip to content

Commit b9e1a87

Browse files
authored
Merge pull request #567 from crazy-max/docker-install-undock
docker(install): use undock to extract image
2 parents 68633e7 + ad06f2a commit b9e1a87

File tree

4 files changed

+127
-238
lines changed

4 files changed

+127
-238
lines changed

__tests__/docker/install.test.itg.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,31 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {describe, test, expect} from '@jest/globals';
17+
import {beforeAll, describe, test, expect} from '@jest/globals';
1818
import fs from 'fs';
1919
import os from 'os';
2020
import path from 'path';
2121

2222
import {Install, InstallSource, InstallSourceArchive, InstallSourceImage} from '../../src/docker/install';
2323
import {Docker} from '../../src/docker/docker';
24+
import {Regctl} from '../../src/regclient/regctl';
25+
import {Install as RegclientInstall} from '../../src/regclient/install';
26+
import {Undock} from '../../src/undock/undock';
27+
import {Install as UndockInstall} from '../../src/undock/install';
2428
import {Exec} from '../../src/exec';
2529

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

32+
beforeAll(async () => {
33+
const undockInstall = new UndockInstall();
34+
const undockBinPath = await undockInstall.download('v0.10.0', true);
35+
await undockInstall.install(undockBinPath);
36+
37+
const regclientInstall = new RegclientInstall();
38+
const regclientBinPath = await regclientInstall.download('v0.8.2', true);
39+
await regclientInstall.install(regclientBinPath);
40+
}, 100000);
41+
2842
describe('root', () => {
2943
// prettier-ignore
3044
test.each(getSources(true))(
@@ -34,7 +48,9 @@ describe('root', () => {
3448
source: source,
3549
runDir: tmpDir(),
3650
contextName: 'foo',
37-
daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}`
51+
daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}`,
52+
regctl: new Regctl(),
53+
undock: new Undock()
3854
});
3955
await expect(tryInstall(install)).resolves.not.toThrow();
4056
}, 30 * 60 * 1000);
@@ -54,7 +70,9 @@ describe('rootless', () => {
5470
runDir: tmpDir(),
5571
contextName: 'foo',
5672
daemonConfig: `{"debug":true}`,
57-
rootless: true
73+
rootless: true,
74+
regctl: new Regctl(),
75+
undock: new Undock()
5876
});
5977
await expect(
6078
tryInstall(install, async () => {
@@ -79,7 +97,9 @@ describe('tcp', () => {
7997
runDir: tmpDir(),
8098
contextName: 'foo',
8199
daemonConfig: `{"debug":true}`,
82-
localTCPPort: 2378
100+
localTCPPort: 2378,
101+
regctl: new Regctl(),
102+
undock: new Undock()
83103
});
84104
await expect(
85105
tryInstall(install, async () => {

__tests__/docker/install.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import * as rimraf from 'rimraf';
2222
import osm = require('os');
2323

2424
import {Install, InstallSourceArchive, InstallSourceImage} from '../../src/docker/install';
25+
import {Regctl} from '../../src/regclient/regctl';
26+
import {Undock} from '../../src/undock/undock';
2527

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

@@ -64,6 +66,8 @@ describe('download', () => {
6466
const install = new Install({
6567
source: source,
6668
runDir: tmpDir,
69+
regctl: new Regctl(),
70+
undock: new Undock()
6771
});
6872
const toolPath = await install.download();
6973
expect(fs.existsSync(toolPath)).toBe(true);

src/docker/install.ts

Lines changed: 99 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ import * as tc from '@actions/tool-cache';
2828

2929
import {Context} from '../context';
3030
import {Docker} from './docker';
31+
import {Regctl} from '../regclient/regctl';
32+
import {Undock} from '../undock/undock';
3133
import {Exec} from '../exec';
3234
import {Util} from '../util';
3335
import {limaYamlData, dockerServiceLogsPs1, setupDockerWinPs1} from './assets';
36+
3437
import {GitHubRelease} from '../types/github';
35-
import {HubRepository} from '../hubRepository';
3638
import {Image} from '../types/oci/config';
3739

3840
export interface InstallSourceImage {
@@ -57,6 +59,9 @@ export interface InstallOpts {
5759
daemonConfig?: string;
5860
rootless?: boolean;
5961
localTCPPort?: number;
62+
63+
regctl: Regctl;
64+
undock: Undock;
6065
}
6166

6267
interface LimaImage {
@@ -72,6 +77,8 @@ export class Install {
7277
private readonly daemonConfig?: string;
7378
private readonly rootless: boolean;
7479
private readonly localTCPPort?: number;
80+
private readonly regctl: Regctl;
81+
private readonly undock: Undock;
7582

7683
private _version: string | undefined;
7784
private _toolDir: string | undefined;
@@ -91,76 +98,24 @@ export class Install {
9198
this.daemonConfig = opts.daemonConfig;
9299
this.rootless = opts.rootless || false;
93100
this.localTCPPort = opts.localTCPPort;
101+
this.regctl = opts.regctl;
102+
this.undock = opts.undock;
94103
}
95104

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

100-
async downloadStaticArchive(component: 'docker' | 'docker-rootless-extras', src: InstallSourceArchive): Promise<string> {
101-
const release: GitHubRelease = await Install.getRelease(src.version);
102-
this._version = release.tag_name.replace(/^v+|v+$/g, '');
103-
core.debug(`docker.Install.download version: ${this._version}`);
104-
105-
const downloadURL = this.downloadURL(component, this._version, src.channel);
106-
core.info(`Downloading ${downloadURL}`);
107-
108-
const downloadPath = await tc.downloadTool(downloadURL);
109-
core.debug(`docker.Install.download downloadPath: ${downloadPath}`);
110-
111-
let extractFolder;
112-
if (os.platform() == 'win32') {
113-
extractFolder = await tc.extractZip(downloadPath, extractFolder);
114-
} else {
115-
extractFolder = await tc.extractTar(downloadPath, extractFolder);
116-
}
117-
if (Util.isDirectory(path.join(extractFolder, component))) {
118-
extractFolder = path.join(extractFolder, component);
119-
}
120-
core.debug(`docker.Install.download extractFolder: ${extractFolder}`);
121-
return extractFolder;
122-
}
123-
124109
public async download(): Promise<string> {
125110
let extractFolder: string;
126111
let cacheKey: string;
127112
const platform = os.platform();
128113

129114
switch (this.source.type) {
130115
case 'image': {
131-
const tag = this.source.tag;
132-
this._version = tag;
116+
this._version = this.source.tag;
133117
cacheKey = `docker-image`;
134-
135-
core.info(`Downloading docker cli from dockereng/cli-bin:${tag}`);
136-
const cli = await HubRepository.build('dockereng/cli-bin');
137-
extractFolder = await cli.extractImage(tag);
138-
139-
const moby = await HubRepository.build('moby/moby-bin');
140-
if (['win32', 'linux'].includes(platform)) {
141-
core.info(`Downloading dockerd from moby/moby-bin:${tag}`);
142-
await moby.extractImage(tag, extractFolder);
143-
} else if (platform == 'darwin') {
144-
// On macOS, the docker daemon binary will be downloaded inside the lima VM.
145-
// However, we will get the exact git revision from the image config
146-
// to get the matching systemd unit files.
147-
core.info(`Getting git revision from moby/moby-bin:${tag}`);
148-
149-
// There's no macOS image for moby/moby-bin - a linux daemon is run inside lima.
150-
const manifest = await moby.getPlatformManifest(tag, 'linux');
151-
152-
const config = await moby.getJSONBlob<Image>(manifest.config.digest);
153-
core.debug(`Config ${JSON.stringify(config.config)}`);
154-
155-
this.gitCommit = config.config?.Labels?.['org.opencontainers.image.revision'];
156-
if (!this.gitCommit) {
157-
core.warning(`No git revision can be determined from the image. Will use master.`);
158-
this.gitCommit = 'master';
159-
}
160-
core.info(`Git revision is ${this.gitCommit}`);
161-
} else {
162-
core.warning(`dockerd not supported on ${platform}, only the Docker cli will be available`);
163-
}
118+
extractFolder = await this.downloadSourceImage(platform);
164119
break;
165120
}
166121
case 'archive': {
@@ -170,10 +125,10 @@ export class Install {
170125
this._version = version;
171126

172127
core.info(`Downloading Docker ${version} from ${this.source.channel} at download.docker.com`);
173-
extractFolder = await this.downloadStaticArchive('docker', this.source);
128+
extractFolder = await this.downloadSourceArchive('docker', this.source);
174129
if (this.rootless) {
175130
core.info(`Downloading Docker rootless extras ${version} from ${this.source.channel} at download.docker.com`);
176-
const extrasFolder = await this.downloadStaticArchive('docker-rootless-extras', this.source);
131+
const extrasFolder = await this.downloadSourceArchive('docker-rootless-extras', this.source);
177132
fs.readdirSync(extrasFolder).forEach(file => {
178133
const src = path.join(extrasFolder, file);
179134
const dest = path.join(extractFolder, file);
@@ -191,7 +146,9 @@ export class Install {
191146
}
192147
// eslint-disable-next-line @typescript-eslint/no-unused-vars
193148
files.forEach(function (file, index) {
194-
fs.chmodSync(path.join(extractFolder, file), '0755');
149+
if (!Util.isDirectory(path.join(extractFolder, file))) {
150+
fs.chmodSync(path.join(extractFolder, file), '0755');
151+
}
195152
});
196153
});
197154

@@ -203,6 +160,72 @@ export class Install {
203160
return tooldir;
204161
}
205162

163+
private async downloadSourceImage(platform: string): Promise<string> {
164+
const dest = path.join(Context.tmpDir(), 'docker-install-image');
165+
const cliImage = `dockereng/cli-bin:${this._version}`;
166+
const engineImage = `moby/moby-bin:${this._version}`;
167+
168+
core.info(`Downloading Docker CLI from ${cliImage}`);
169+
await this.undock.run({
170+
source: cliImage,
171+
dist: dest
172+
});
173+
174+
if (['win32', 'linux'].includes(platform)) {
175+
core.info(`Downloading Docker engine from ${engineImage}`);
176+
await this.undock.run({
177+
source: engineImage,
178+
dist: dest
179+
});
180+
} else if (platform == 'darwin') {
181+
// On macOS, the docker daemon binary will be downloaded inside the lima VM.
182+
// However, we will get the exact git revision from the image config
183+
// to get the matching systemd unit files. There's no macOS image for
184+
// moby/moby-bin - a linux daemon is run inside lima.
185+
try {
186+
const engineImageConfig = await this.imageConfig(engineImage, 'linux/arm64');
187+
core.debug(`docker.Install.downloadSourceImage engineImageConfig: ${JSON.stringify(engineImageConfig)}`);
188+
this.gitCommit = engineImageConfig.config?.Labels?.['org.opencontainers.image.revision'];
189+
if (!this.gitCommit) {
190+
throw new Error(`No git revision can be determined from the image`);
191+
}
192+
} catch (e) {
193+
core.warning(e);
194+
this.gitCommit = 'master';
195+
}
196+
197+
core.debug(`docker.Install.downloadSourceImage gitCommit: ${this.gitCommit}`);
198+
} else {
199+
core.warning(`Docker engine not supported on ${platform}, only the Docker cli will be available`);
200+
}
201+
202+
return dest;
203+
}
204+
205+
private async downloadSourceArchive(component: 'docker' | 'docker-rootless-extras', src: InstallSourceArchive): Promise<string> {
206+
const release: GitHubRelease = await Install.getRelease(src.version);
207+
this._version = release.tag_name.replace(/^v+|v+$/g, '');
208+
core.debug(`docker.Install.downloadSourceArchive version: ${this._version}`);
209+
210+
const downloadURL = this.downloadURL(component, this._version, src.channel);
211+
core.info(`Downloading ${downloadURL}`);
212+
213+
const downloadPath = await tc.downloadTool(downloadURL);
214+
core.debug(`docker.Install.downloadSourceArchive downloadPath: ${downloadPath}`);
215+
216+
let extractFolder;
217+
if (os.platform() == 'win32') {
218+
extractFolder = await tc.extractZip(downloadPath, extractFolder);
219+
} else {
220+
extractFolder = await tc.extractTar(downloadPath, extractFolder);
221+
}
222+
if (Util.isDirectory(path.join(extractFolder, component))) {
223+
extractFolder = path.join(extractFolder, component);
224+
}
225+
core.debug(`docker.Install.downloadSourceArchive extractFolder: ${extractFolder}`);
226+
return extractFolder;
227+
}
228+
206229
public async install(): Promise<string> {
207230
if (!this.toolDir) {
208231
throw new Error('toolDir must be set. Run download first.');
@@ -709,4 +732,20 @@ EOF`,
709732
}
710733
return res;
711734
}
735+
736+
private async imageConfig(image: string, platform?: string): Promise<Image> {
737+
const manifest = await this.regctl.manifestGet({
738+
image: image,
739+
platform: platform
740+
});
741+
const configDigest = manifest?.config?.digest;
742+
if (!configDigest) {
743+
throw new Error(`No config digest found for image ${image}`);
744+
}
745+
const blob = await this.regctl.blobGet({
746+
repository: image,
747+
digest: configDigest
748+
});
749+
return <Image>JSON.parse(blob);
750+
}
712751
}

0 commit comments

Comments
 (0)