From 1335f081af255bf3cbec6aa92bdce74f6fe3ffed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Fri, 6 Sep 2024 12:23:22 +0200 Subject: [PATCH 1/6] docker/install: Support `version: master` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for installing Docker `master` packages from `moby/moby-bin` and `dockereng/cli-bin` images. This could also allow to install arbitrary version from these images but for now it's only used for `master`. Signed-off-by: Paweł Gronowski --- src/docker/install.ts | 23 ++++- src/dockerhub.ts | 24 +++--- src/hubRepository.ts | 157 ++++++++++++++++++++++++++++++++++ src/types/docker/mediatype.ts | 19 ++++ 4 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 src/hubRepository.ts create mode 100644 src/types/docker/mediatype.ts diff --git a/src/docker/install.ts b/src/docker/install.ts index c681d97..80dbab4 100644 --- a/src/docker/install.ts +++ b/src/docker/install.ts @@ -33,6 +33,7 @@ import {Exec} from '../exec'; import {Util} from '../util'; import {limaYamlData, dockerServiceLogsPs1, setupDockerWinPs1} from './assets'; import {GitHubRelease} from '../types/github'; +import {HubRepository} from '../hubRepository'; export interface InstallOpts { version?: string; @@ -71,7 +72,7 @@ export class Install { return this._toolDir || Context.tmpDir(); } - public async download(): Promise { + async downloadStaticArchive(): Promise { const release: GitHubRelease = await Install.getRelease(this.version); this._version = release.tag_name.replace(/^v+|v+$/g, ''); core.debug(`docker.Install.download version: ${this._version}`); @@ -92,6 +93,26 @@ export class Install { extractFolder = path.join(extractFolder, 'docker'); } core.debug(`docker.Install.download extractFolder: ${extractFolder}`); + return extractFolder; + } + + public async download(): Promise { + let extractFolder: string; + + core.info(`Downloading Docker ${this.version} from ${this.channel}`); + + this._version = this.version; + if (this.version == 'master') { + core.info(`Downloading from moby/moby-bin`); + const moby = await HubRepository.build('moby/moby-bin'); + const cli = await HubRepository.build('dockereng/cli-bin'); + + extractFolder = await moby.extractImage(this.version); + await cli.extractImage(this.version, extractFolder); + } else { + core.info(`Downloading from download.docker.com`); + extractFolder = await this.downloadStaticArchive(); + } core.info('Fixing perms'); fs.readdir(path.join(extractFolder), function (err, files) { diff --git a/src/dockerhub.ts b/src/dockerhub.ts index 62bea8d..3bf1d59 100644 --- a/src/dockerhub.ts +++ b/src/dockerhub.ts @@ -111,17 +111,21 @@ export class DockerHub { const body = await resp.readBody(); resp.message.statusCode = resp.message.statusCode || HttpCodes.InternalServerError; if (resp.message.statusCode < 200 || resp.message.statusCode >= 300) { - if (resp.message.statusCode == HttpCodes.Unauthorized) { - throw new Error(`Docker Hub API: operation not permitted`); - } - const errResp = >JSON.parse(body); - for (const k of ['message', 'detail', 'error']) { - if (errResp[k]) { - throw new Error(`Docker Hub API: bad status code ${resp.message.statusCode}: ${errResp[k]}`); - } - } - throw new Error(`Docker Hub API: bad status code ${resp.message.statusCode}`); + throw DockerHub.parseError(resp, body); } return body; } + + public static parseError(resp: httpm.HttpClientResponse, body: string): Error { + if (resp.message.statusCode == HttpCodes.Unauthorized) { + throw new Error(`Docker Hub API: operation not permitted`); + } + const errResp = >JSON.parse(body); + for (const k of ['message', 'detail', 'error']) { + if (errResp[k]) { + throw new Error(`Docker Hub API: bad status code ${resp.message.statusCode}: ${errResp[k]}`); + } + } + throw new Error(`Docker Hub API: bad status code ${resp.message.statusCode}`); + } } diff --git a/src/hubRepository.ts b/src/hubRepository.ts new file mode 100644 index 0000000..e2a8884 --- /dev/null +++ b/src/hubRepository.ts @@ -0,0 +1,157 @@ +/** + * 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 * as httpm from '@actions/http-client'; +import {Index} from './types/oci'; +import os from 'os'; +import * as core from '@actions/core'; +import {Manifest} from './types/oci/manifest'; +import * as tc from '@actions/tool-cache'; +import fs from 'fs'; +import {MEDIATYPE_IMAGE_INDEX_V1, MEDIATYPE_IMAGE_MANIFEST_V1} from './types/oci/mediatype'; +import {MEDIATYPE_IMAGE_MANIFEST_V2, MEDIATYPE_IMAGE_MANIFEST_LIST_V2} from './types/docker/mediatype'; +import {DockerHub} from './dockerhub'; + +export class HubRepository { + private repo: string; + private token: string; + private static readonly http: httpm.HttpClient = new httpm.HttpClient('setup-docker-action'); + + private constructor(repository: string, token: string) { + this.repo = repository; + this.token = token; + } + + public static async build(repository: string): Promise { + const token = await this.getToken(repository); + return new HubRepository(repository, token); + } + + // Unpacks the image layers and returns the path to the extracted image. + // Only OCI indexes/manifest list are supported for now. + public async extractImage(tag: string, destDir?: string): Promise { + const index = await this.getManifest(tag); + if (index.mediaType != MEDIATYPE_IMAGE_INDEX_V1 && index.mediaType != MEDIATYPE_IMAGE_MANIFEST_LIST_V2) { + throw new Error(`Unsupported image media type: ${index.mediaType}`); + } + const digest = HubRepository.getPlatformManifestDigest(index); + const manifest = await this.getManifest(digest); + + const paths = manifest.layers.map(async layer => { + const url = this.blobUrl(layer.digest); + + return await tc.downloadTool(url, undefined, undefined, { + authorization: `Bearer ${this.token}` + }); + }); + + let files = await Promise.all(paths); + let extractFolder: string; + if (!destDir) { + extractFolder = await tc.extractTar(files[0]); + files = files.slice(1); + } else { + extractFolder = destDir; + } + + await Promise.all( + files.map(async file => { + return await tc.extractTar(file, extractFolder); + }) + ); + + fs.readdirSync(extractFolder).forEach(file => { + core.info(`extractImage(${this.repo}:${tag}) file: ${file}`); + }); + + return extractFolder; + } + + private static async getToken(repo: string): Promise { + const url = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull`; + + const resp = await this.http.get(url); + const body = await resp.readBody(); + const statusCode = resp.message.statusCode || 500; + if (statusCode != 200) { + throw DockerHub.parseError(resp, body); + } + + const json = JSON.parse(body); + return json.token; + } + + private blobUrl(digest: string): string { + return `https://registry-1.docker.io/v2/${this.repo}/blobs/${digest}`; + } + + public async getManifest(tagOrDigest: string): Promise { + const url = `https://registry-1.docker.io/v2/${this.repo}/manifests/${tagOrDigest}`; + + const headers = { + Authorization: `Bearer ${this.token}`, + Accept: [MEDIATYPE_IMAGE_INDEX_V1, MEDIATYPE_IMAGE_MANIFEST_LIST_V2, MEDIATYPE_IMAGE_MANIFEST_V1, MEDIATYPE_IMAGE_MANIFEST_V2].join(', ') + }; + const resp = await HubRepository.http.get(url, headers); + const body = await resp.readBody(); + const statusCode = resp.message.statusCode || 500; + if (statusCode != 200) { + throw DockerHub.parseError(resp, body); + } + + return JSON.parse(body); + } + + private static getPlatformManifestDigest(index: Index): string { + // This doesn't handle all possible platforms normalizations, but it's good enough for now. + let pos: string = os.platform(); + if (pos == 'win32') { + pos = 'windows'; + } + let arch = os.arch(); + if (arch == 'x64') { + arch = 'amd64'; + } + let variant = ''; + if (arch == 'arm') { + variant = 'v7'; + } + + const manifest = index.manifests.find(m => { + if (!m.platform) { + return false; + } + if (m.platform.os != pos) { + core.debug(`Skipping manifest ${m.digest} because of os: ${m.platform.os} != ${pos}`); + return false; + } + if (m.platform.architecture != arch) { + core.debug(`Skipping manifest ${m.digest} because of arch: ${m.platform.architecture} != ${arch}`); + return false; + } + if ((m.platform.variant || '') != variant) { + core.debug(`Skipping manifest ${m.digest} because of variant: ${m.platform.variant} != ${variant}`); + return false; + } + + return true; + }); + if (!manifest) { + throw new Error(`Cannot find manifest for ${pos}/${arch}/${variant}`); + } + return manifest.digest; + } +} diff --git a/src/types/docker/mediatype.ts b/src/types/docker/mediatype.ts new file mode 100644 index 0000000..d06d1e9 --- /dev/null +++ b/src/types/docker/mediatype.ts @@ -0,0 +1,19 @@ +/** + * 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. + */ + +export const MEDIATYPE_IMAGE_MANIFEST_LIST_V2 = 'application/vnd.docker.distribution.manifest.list.v2+json'; + +export const MEDIATYPE_IMAGE_MANIFEST_V2 = 'application/vnd.docker.distribution.manifest.v2+json'; From 10424facafc5938c6f88e91450a0383596e196a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Mon, 9 Sep 2024 13:27:10 +0200 Subject: [PATCH 2/6] docker/install: Install source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski --- src/docker/install.ts | 88 ++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 21 deletions(-) diff --git a/src/docker/install.ts b/src/docker/install.ts index 80dbab4..7664972 100644 --- a/src/docker/install.ts +++ b/src/docker/install.ts @@ -35,9 +35,30 @@ import {limaYamlData, dockerServiceLogsPs1, setupDockerWinPs1} from './assets'; import {GitHubRelease} from '../types/github'; import {HubRepository} from '../hubRepository'; +export interface InstallSourceImage { + type: 'image'; + tag: string; +} + +export interface InstallSourceArchive { + type: 'archive'; + version: string; + channel: string; +} + +export type InstallSource = InstallSourceImage | InstallSourceArchive; + export interface InstallOpts { + source?: InstallSource; + + // @deprecated + // Use `source = InstallSourceTypeArchive{version: ..., channel: ...}` instead version?: string; + // @deprecated + // Use `source = InstallSourceTypeArchive{version: ..., channel: ...}` instead channel?: string; + + // ... runDir: string; contextName?: string; daemonConfig?: string; @@ -51,8 +72,7 @@ interface LimaImage { export class Install { private readonly runDir: string; - private readonly version: string; - private readonly channel: string; + private readonly source: InstallSource; private readonly contextName: string; private readonly daemonConfig?: string; private _version: string | undefined; @@ -62,8 +82,11 @@ export class Install { constructor(opts: InstallOpts) { this.runDir = opts.runDir; - this.version = opts.version || 'latest'; - this.channel = opts.channel || 'stable'; + this.source = opts.source || { + type: 'archive', + version: opts.version || 'latest', + channel: opts.channel || 'stable' + }; this.contextName = opts.contextName || 'setup-docker-action'; this.daemonConfig = opts.daemonConfig; } @@ -72,12 +95,12 @@ export class Install { return this._toolDir || Context.tmpDir(); } - async downloadStaticArchive(): Promise { - const release: GitHubRelease = await Install.getRelease(this.version); + async downloadStaticArchive(src: InstallSourceArchive): Promise { + 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(this._version, this.channel); + const downloadURL = this.downloadURL(this._version, src.channel); core.info(`Downloading ${downloadURL}`); const downloadPath = await tc.downloadTool(downloadURL); @@ -98,20 +121,39 @@ export class Install { public async download(): Promise { let extractFolder: string; + let cacheKey: string; + const platform = os.platform(); - core.info(`Downloading Docker ${this.version} from ${this.channel}`); + switch (this.source.type) { + case 'image': { + const tag = this.source.tag; + this._version = tag; + cacheKey = `docker-image`; - this._version = this.version; - if (this.version == 'master') { - core.info(`Downloading from moby/moby-bin`); - const moby = await HubRepository.build('moby/moby-bin'); - const cli = await HubRepository.build('dockereng/cli-bin'); + core.info(`Downloading docker cli from dockereng/cli-bin:${tag}`); + const cli = await HubRepository.build('dockereng/cli-bin'); + extractFolder = await cli.extractImage(tag); - extractFolder = await moby.extractImage(this.version); - await cli.extractImage(this.version, extractFolder); - } else { - core.info(`Downloading from download.docker.com`); - extractFolder = await this.downloadStaticArchive(); + // Daemon is only available for Windows and Linux + if (['win32', 'linux'].includes(platform)) { + core.info(`Downloading dockerd from moby/moby-bin:${tag}`); + const moby = await HubRepository.build('moby/moby-bin'); + await moby.extractImage(tag, extractFolder); + } else { + core.info(`dockerd not supported on ${platform}`); + } + break; + } + case 'archive': { + const version = this.source.version; + const channel = this.source.channel; + cacheKey = `docker-archive-${channel}`; + this._version = version; + + core.info(`Downloading Docker ${version} from ${this.source.channel} at download.docker.com`); + extractFolder = await this.downloadStaticArchive(this.source); + break; + } } core.info('Fixing perms'); @@ -125,7 +167,7 @@ export class Install { }); }); - const tooldir = await tc.cacheDir(extractFolder, `docker-${this.channel}`, this._version.replace(/(0+)([1-9]+)/, '$2')); + const tooldir = await tc.cacheDir(extractFolder, cacheKey, this._version.replace(/(0+)([1-9]+)/, '$2')); core.addPath(tooldir); core.info('Added Docker to PATH'); @@ -157,6 +199,10 @@ export class Install { } private async installDarwin(): Promise { + if (this.source.type !== 'archive') { + throw new Error('Only archive source is supported on macOS'); + } + const src = this.source as InstallSourceArchive; const limaDir = path.join(os.homedir(), '.lima', this.limaInstanceName); await io.mkdirP(limaDir); const dockerHost = `unix://${limaDir}/docker.sock`; @@ -191,8 +237,8 @@ export class Install { customImages: Install.limaCustomImages(), daemonConfig: limaDaemonConfig, dockerSock: `${limaDir}/docker.sock`, - dockerBinVersion: this._version, - dockerBinChannel: this.channel + dockerBinVersion: src.version.replace(/^v/, ''), + dockerBinChannel: src.channel }); core.info(`Writing lima config to ${path.join(limaDir, 'lima.yaml')}`); fs.writeFileSync(path.join(limaDir, 'lima.yaml'), limaCfg); From b8a96071a82c1f0235b1e9d18dde6b62b015ebed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Mon, 9 Sep 2024 14:43:37 +0200 Subject: [PATCH 3/6] docker/install: Handle missing `v` prefix when searching GH release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski --- src/docker/install.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/docker/install.ts b/src/docker/install.ts index 7664972..1c35589 100644 --- a/src/docker/install.ts +++ b/src/docker/install.ts @@ -594,7 +594,10 @@ EOF`, } const releases = >JSON.parse(body); if (!releases[version]) { - throw new Error(`Cannot find Docker release ${version} in ${url}`); + if (!releases['v' + version]) { + throw new Error(`Cannot find Docker release ${version} in ${url}`); + } + return releases['v' + version]; } return releases[version]; } From de390e08725cac0ad54c04f9c1488e9876b64314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Tue, 15 Oct 2024 12:38:25 +0200 Subject: [PATCH 4/6] docker/install: Remove deprecated version and channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use InstallSource instead Signed-off-by: Paweł Gronowski --- __tests__/docker/install.test.itg.ts | 6 +++++- __tests__/docker/install.test.ts | 6 +++++- src/docker/install.ts | 11 ++--------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/__tests__/docker/install.test.itg.ts b/__tests__/docker/install.test.itg.ts index 679fa85..911d758 100644 --- a/__tests__/docker/install.test.itg.ts +++ b/__tests__/docker/install.test.itg.ts @@ -55,7 +55,11 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g } await expect((async () => { const install = new Install({ - version: version, + source: { + type: 'archive', + version: version, + channel: 'stable', + }, runDir: tmpDir, contextName: 'foo', daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}` diff --git a/__tests__/docker/install.test.ts b/__tests__/docker/install.test.ts index 80ecc29..28e2b5a 100644 --- a/__tests__/docker/install.test.ts +++ b/__tests__/docker/install.test.ts @@ -40,7 +40,11 @@ describe('download', () => { 'acquires %p of docker (%s)', async (version, platformOS) => { jest.spyOn(osm, 'platform').mockImplementation(() => platformOS as NodeJS.Platform); const install = new Install({ - version: version, + source: { + type: 'archive', + version: version, + channel: 'stable', + }, runDir: tmpDir, }); const toolPath = await install.download(); diff --git a/src/docker/install.ts b/src/docker/install.ts index 1c35589..e6f219f 100644 --- a/src/docker/install.ts +++ b/src/docker/install.ts @@ -51,13 +51,6 @@ export type InstallSource = InstallSourceImage | InstallSourceArchive; export interface InstallOpts { source?: InstallSource; - // @deprecated - // Use `source = InstallSourceTypeArchive{version: ..., channel: ...}` instead - version?: string; - // @deprecated - // Use `source = InstallSourceTypeArchive{version: ..., channel: ...}` instead - channel?: string; - // ... runDir: string; contextName?: string; @@ -84,8 +77,8 @@ export class Install { this.runDir = opts.runDir; this.source = opts.source || { type: 'archive', - version: opts.version || 'latest', - channel: opts.channel || 'stable' + version: 'latest', + channel: 'stable' }; this.contextName = opts.contextName || 'setup-docker-action'; this.daemonConfig = opts.daemonConfig; From b143889d3e2e615bf7e4c69d499bf6904847e560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Tue, 15 Oct 2024 12:57:18 +0200 Subject: [PATCH 5/6] docker/install: Add tests for installing from image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski --- __tests__/docker/install.test.itg.ts | 15 +++++------ __tests__/docker/install.test.ts | 39 ++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/__tests__/docker/install.test.itg.ts b/__tests__/docker/install.test.itg.ts index 911d758..c061afb 100644 --- a/__tests__/docker/install.test.itg.ts +++ b/__tests__/docker/install.test.itg.ts @@ -19,7 +19,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import {Install} from '../../src/docker/install'; +import {Install, InstallSourceArchive, InstallSourceImage} from '../../src/docker/install'; import {Docker} from '../../src/docker/docker'; import {Exec} from '../../src/exec'; @@ -40,8 +40,11 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g process.env = originalEnv; }); // prettier-ignore - test.each(['v26.1.4'])( - 'install docker %s', async (version) => { + test.each([ + {type: 'archive', version: 'v26.1.4', channel: 'stable'} as InstallSourceArchive, + {type: 'image', tag: '27.3.1'} as InstallSourceImage, + ])( + 'install docker %s', async (source) => { if (process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) { // Remove containerd first on ubuntu runners to make sure it takes // ones packaged with docker @@ -55,11 +58,7 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g } await expect((async () => { const install = new Install({ - source: { - type: 'archive', - version: version, - channel: 'stable', - }, + source: source, runDir: tmpDir, contextName: 'foo', daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}` diff --git a/__tests__/docker/install.test.ts b/__tests__/docker/install.test.ts index 28e2b5a..7016c56 100644 --- a/__tests__/docker/install.test.ts +++ b/__tests__/docker/install.test.ts @@ -21,7 +21,7 @@ import path from 'path'; import * as rimraf from 'rimraf'; import osm = require('os'); -import {Install} from '../../src/docker/install'; +import {Install, InstallSourceArchive, InstallSourceImage} from '../../src/docker/install'; const tmpDir = fs.mkdtempSync(path.join(process.env.TEMP || os.tmpdir(), 'docker-install-')); @@ -29,22 +29,39 @@ afterEach(function () { rimraf.sync(tmpDir); }); +const archive = (version: string, channel: string): InstallSourceArchive => { + return { + type: 'archive', + version: version, + channel: channel + }; +}; + +const image = (tag: string): InstallSourceImage => { + return { + type: 'image', + tag: tag + }; +}; + describe('download', () => { // prettier-ignore test.each([ - ['v19.03.14', 'linux'], - ['v20.10.22', 'linux'], - ['v20.10.22', 'darwin'], - ['v20.10.22', 'win32'], + [archive('v19.03.14', 'stable'), 'linux'], + [archive('v20.10.22', 'stable'), 'linux'], + [archive('v20.10.22', 'stable'), 'darwin'], + [archive('v20.10.22', 'stable'), 'win32'], + + [image('master'), 'linux'], + [image('master'), 'win32'], + + [image('27.3.1'), 'linux'], + [image('27.3.1'), 'win32'], ])( - 'acquires %p of docker (%s)', async (version, platformOS) => { + 'acquires %p of docker (%s)', async (source, platformOS) => { jest.spyOn(osm, 'platform').mockImplementation(() => platformOS as NodeJS.Platform); const install = new Install({ - source: { - type: 'archive', - version: version, - channel: 'stable', - }, + source: source, runDir: tmpDir, }); const toolPath = await install.download(); From e3d0e4e199e204233f4a542401cf922a0225a8c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Wed, 16 Oct 2024 10:42:54 +0200 Subject: [PATCH 6/6] Support image source on darwin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use undock inside lima to pull the image content. We could mount the downloaded binaries from the host, but for some reason lima mounts are not always mounted when the provisioning script is run. Signed-off-by: Paweł Gronowski --- __tests__/docker/install.test.itg.ts | 20 +++++++------ src/docker/assets.ts | 43 ++++++++++++++++++++++++---- src/docker/install.ts | 17 +++++------ 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/__tests__/docker/install.test.itg.ts b/__tests__/docker/install.test.itg.ts index c061afb..822da61 100644 --- a/__tests__/docker/install.test.itg.ts +++ b/__tests__/docker/install.test.itg.ts @@ -41,8 +41,9 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g }); // prettier-ignore test.each([ - {type: 'archive', version: 'v26.1.4', channel: 'stable'} as InstallSourceArchive, {type: 'image', tag: '27.3.1'} as InstallSourceImage, + {type: 'image', tag: 'master'} as InstallSourceImage, + {type: 'archive', version: 'v26.1.4', channel: 'stable'} as InstallSourceArchive, ])( 'install docker %s', async (source) => { if (process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) { @@ -56,18 +57,19 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g } }); } + const install = new Install({ + source: source, + runDir: tmpDir, + contextName: 'foo', + daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}` + }); await expect((async () => { - const install = new Install({ - source: source, - runDir: tmpDir, - contextName: 'foo', - daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}` - }); await install.download(); await install.install(); await Docker.printVersion(); await Docker.printInfo(); + })().finally(async () => { await install.tearDown(); - })()).resolves.not.toThrow(); - }, 1200000); + })).resolves.not.toThrow(); + }, 30 * 60 * 1000); }); diff --git a/src/docker/assets.ts b/src/docker/assets.ts index 15e39c2..00f7332 100644 --- a/src/docker/assets.ts +++ b/src/docker/assets.ts @@ -221,16 +221,49 @@ provision: EOF fi export DEBIAN_FRONTEND=noninteractive - curl -fsSL https://get.docker.com | sh -s -- --channel {{dockerBinChannel}} --version {{dockerBinVersion}} + if [ "{{srcType}}" == "archive" ]; then + curl -fsSL https://get.docker.com | sh -s -- --channel {{srcArchiveChannel}} --version {{srcArchiveVersion}} + elif [ "{{srcType}}" == "image" ]; then + arch=$(uname -m) + case $arch in + x86_64) arch=amd64;; + aarch64) arch=arm64;; + esac + url="https://github.com/crazy-max/undock/releases/download/v0.8.0/undock_0.8.0_linux_$arch.tar.gz" + + wget "$url" -O /tmp/undock.tar.gz + tar -C /usr/local/bin -xvf /tmp/undock.tar.gz + undock --version + + HOME=/tmp undock moby/moby-bin:{{srcImageTag}} /usr/local/bin + + wget https://raw.githubusercontent.com/moby/moby/{{srcImageTag}}/contrib/init/systemd/docker.service \ + https://raw.githubusercontent.com/moby/moby/v{{srcImageTag}}/contrib/init/systemd/docker.service \ + -O /etc/systemd/system/docker.service || true + wget https://raw.githubusercontent.com/moby/moby/{{srcImageTag}}/contrib/init/systemd/docker.socket \ + https://raw.githubusercontent.com/moby/moby/v{{srcImageTag}}/contrib/init/systemd/docker.socket \ + -O /etc/systemd/system/docker.socket || true + + sed -i 's|^ExecStart=.*|ExecStart=/usr/local/bin/dockerd -H fd://|' /etc/systemd/system/docker.service + sed -i 's|containerd.service||' /etc/systemd/system/docker.service + if ! getent group docker; then + groupadd --system docker + fi + systemctl daemon-reload + fail=0 + if ! systemctl enable --now docker; then + fail=1 + fi + systemctl status docker.socket || true + systemctl status docker.service || true + exit $fail + fi probes: - script: | #!/bin/bash set -eux -o pipefail - if ! timeout 30s bash -c "until command -v docker >/dev/null 2>&1; do sleep 3; done"; then - echo >&2 "docker is not installed yet" - exit 1 - fi + # Don't check for docker CLI as it's not installed in the VM (only on the host) if ! timeout 30s bash -c "until pgrep dockerd; do sleep 3; done"; then echo >&2 "dockerd is not running" exit 1 diff --git a/src/docker/install.ts b/src/docker/install.ts index e6f219f..d2c89a3 100644 --- a/src/docker/install.ts +++ b/src/docker/install.ts @@ -127,13 +127,14 @@ export class Install { const cli = await HubRepository.build('dockereng/cli-bin'); extractFolder = await cli.extractImage(tag); - // Daemon is only available for Windows and Linux if (['win32', 'linux'].includes(platform)) { core.info(`Downloading dockerd from moby/moby-bin:${tag}`); const moby = await HubRepository.build('moby/moby-bin'); await moby.extractImage(tag, extractFolder); + } else if (platform == 'darwin') { + // On macOS, the docker daemon binary will be downloaded inside the lima VM } else { - core.info(`dockerd not supported on ${platform}`); + core.warning(`dockerd not supported on ${platform}, only the Docker cli will be available`); } break; } @@ -192,10 +193,7 @@ export class Install { } private async installDarwin(): Promise { - if (this.source.type !== 'archive') { - throw new Error('Only archive source is supported on macOS'); - } - const src = this.source as InstallSourceArchive; + const src = this.source; const limaDir = path.join(os.homedir(), '.lima', this.limaInstanceName); await io.mkdirP(limaDir); const dockerHost = `unix://${limaDir}/docker.sock`; @@ -226,12 +224,15 @@ export class Install { handlebars.registerHelper('stringify', function (obj) { return new handlebars.SafeString(JSON.stringify(obj)); }); + const srcArchive = src as InstallSourceArchive; const limaCfg = handlebars.compile(limaYamlData)({ customImages: Install.limaCustomImages(), daemonConfig: limaDaemonConfig, dockerSock: `${limaDir}/docker.sock`, - dockerBinVersion: src.version.replace(/^v/, ''), - dockerBinChannel: src.channel + srcType: src.type, + srcArchiveVersion: srcArchive.version?.replace(/^v/, ''), + srcArchiveChannel: srcArchive.channel, + srcImageTag: (src as InstallSourceImage).tag }); core.info(`Writing lima config to ${path.join(limaDir, 'lima.yaml')}`); fs.writeFileSync(path.join(limaDir, 'lima.yaml'), limaCfg);