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 c681d974..80dbab4d 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 62bea8d8..3bf1d59c 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 00000000..e2a88845 --- /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 00000000..d06d1e96 --- /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 | 94 ++++++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/src/docker/install.ts b/src/docker/install.ts index 80dbab4d..76649722 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; - - 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(); + let cacheKey: string; + const platform = os.platform(); + + switch (this.source.type) { + case 'image': { + const tag = this.source.tag; + 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); + + // 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 76649722..1c35589e 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 679fa854..911d758d 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 80ecc29c..28e2b5a1 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 1c35589e..e6f219f3 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 911d758d..c061afb4 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 28e2b5a1..7016c56f 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 c061afb4..822da611 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 15e39c22..00f7332f 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 e6f219f3..d2c89a36 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);