diff --git a/__tests__/docker/install.test.itg.ts b/__tests__/docker/install.test.itg.ts index 4275026..74823a0 100644 --- a/__tests__/docker/install.test.itg.ts +++ b/__tests__/docker/install.test.itg.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {jest, describe, expect, test, beforeEach, afterEach} from '@jest/globals'; +import {jest, describe, test, beforeEach, afterEach, expect} from '@jest/globals'; import fs from 'fs'; import os from 'os'; import path from 'path'; @@ -43,6 +43,7 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g test.each([ {type: 'image', tag: '27.3.1'} as InstallSourceImage, {type: 'image', tag: 'master'} as InstallSourceImage, + {type: 'image', tag: 'latest'} as InstallSourceImage, {type: 'archive', version: 'v26.1.4', channel: 'stable'} as InstallSourceArchive, {type: 'archive', version: 'latest', channel: 'stable'} as InstallSourceArchive, ])( @@ -65,12 +66,17 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}` }); await expect((async () => { - await install.download(); - await install.install(); - await Docker.printVersion(); - await Docker.printInfo(); - })().finally(async () => { - await install.tearDown(); - })).resolves.not.toThrow(); + try { + await install.download(); + await install.install(); + await Docker.printVersion(); + await Docker.printInfo(); + } catch (error) { + console.error(error); + throw error; + } finally { + await install.tearDown(); + } + })()).resolves.not.toThrow(); }, 30 * 60 * 1000); }); diff --git a/src/docker/assets.ts b/src/docker/assets.ts index 00f7332..49d2563 100644 --- a/src/docker/assets.ts +++ b/src/docker/assets.ts @@ -237,12 +237,10 @@ provision: 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 + wget https://raw.githubusercontent.com/moby/moby/{{gitCommit}}/contrib/init/systemd/docker.service \ + -O /etc/systemd/system/docker.service + wget https://raw.githubusercontent.com/moby/moby/{{gitCommit}}/contrib/init/systemd/docker.socket \ + -O /etc/systemd/system/docker.socket 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 diff --git a/src/docker/install.ts b/src/docker/install.ts index a6c8ca8..cc8cb0a 100644 --- a/src/docker/install.ts +++ b/src/docker/install.ts @@ -34,6 +34,7 @@ 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 { type: 'image'; @@ -71,6 +72,8 @@ export class Install { private _version: string | undefined; private _toolDir: string | undefined; + private gitCommit: string | undefined; + private readonly limaInstanceName = 'docker-actions-toolkit'; constructor(opts: InstallOpts) { @@ -127,12 +130,28 @@ export class Install { 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}`); - 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 + // 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(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`); } @@ -193,6 +212,9 @@ export class Install { } private async installDarwin(): Promise { + if (this.source.type == 'image' && !this.gitCommit) { + throw new Error('gitCommit must be set. Run download first.'); + } const src = this.source; const limaDir = path.join(os.homedir(), '.lima', this.limaInstanceName); await io.mkdirP(limaDir); @@ -229,6 +251,7 @@ export class Install { customImages: Install.limaCustomImages(), daemonConfig: limaDaemonConfig, dockerSock: `${limaDir}/docker.sock`, + gitCommit: this.gitCommit, srcType: src.type, srcArchiveVersion: this._version, // Use the resolved version (e.g. latest -> 27.4.0) srcArchiveChannel: srcArchive.channel, diff --git a/src/hubRepository.ts b/src/hubRepository.ts index e2a8884..23d1f48 100644 --- a/src/hubRepository.ts +++ b/src/hubRepository.ts @@ -21,8 +21,8 @@ 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 {MEDIATYPE_IMAGE_CONFIG_V1, MEDIATYPE_IMAGE_INDEX_V1, MEDIATYPE_IMAGE_MANIFEST_V1} from './types/oci/mediatype'; +import {MEDIATYPE_IMAGE_CONFIG_V1 as DOCKER_MEDIATYPE_IMAGE_CONFIG_V1, MEDIATYPE_IMAGE_MANIFEST_LIST_V2, MEDIATYPE_IMAGE_MANIFEST_V2} from './types/docker/mediatype'; import {DockerHub} from './dockerhub'; export class HubRepository { @@ -40,15 +40,20 @@ export class HubRepository { return new HubRepository(repository, token); } + public async getPlatformManifest(tagOrDigest: string, os?: string): Promise { + const index = await this.getManifest(tagOrDigest); + if (index.mediaType != MEDIATYPE_IMAGE_INDEX_V1 && index.mediaType != MEDIATYPE_IMAGE_MANIFEST_LIST_V2) { + core.error(`Unsupported image media type: ${index.mediaType}`); + throw new Error(`Unsupported image media type: ${index.mediaType}`); + } + const digest = HubRepository.getPlatformManifestDigest(index, os); + return await this.getManifest(digest); + } + // 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 manifest = await this.getPlatformManifest(tag); const paths = manifest.layers.map(async layer => { const url = this.blobUrl(layer.digest); @@ -99,25 +104,35 @@ export class HubRepository { } public async getManifest(tagOrDigest: string): Promise { - const url = `https://registry-1.docker.io/v2/${this.repo}/manifests/${tagOrDigest}`; + return await this.registryGet(tagOrDigest, 'manifests', [MEDIATYPE_IMAGE_INDEX_V1, MEDIATYPE_IMAGE_MANIFEST_LIST_V2, MEDIATYPE_IMAGE_MANIFEST_V1, MEDIATYPE_IMAGE_MANIFEST_V2]); + } + + public async getJSONBlob(tagOrDigest: string): Promise { + return await this.registryGet(tagOrDigest, 'blobs', [MEDIATYPE_IMAGE_CONFIG_V1, DOCKER_MEDIATYPE_IMAGE_CONFIG_V1]); + } + + private async registryGet(tagOrDigest: string, endpoint: 'manifests' | 'blobs', accept: Array): Promise { + const url = `https://registry-1.docker.io/v2/${this.repo}/${endpoint}/${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(', ') + Accept: accept.join(', ') }; + const resp = await HubRepository.http.get(url, headers); const body = await resp.readBody(); const statusCode = resp.message.statusCode || 500; if (statusCode != 200) { + core.error(`registryGet(${this.repo}:${tagOrDigest}) failed: ${statusCode} ${body}`); throw DockerHub.parseError(resp, body); } return JSON.parse(body); } - private static getPlatformManifestDigest(index: Index): string { + private static getPlatformManifestDigest(index: Index, osOverride?: string): string { // This doesn't handle all possible platforms normalizations, but it's good enough for now. - let pos: string = os.platform(); + let pos: string = osOverride || os.platform(); if (pos == 'win32') { pos = 'windows'; } @@ -150,8 +165,10 @@ export class HubRepository { return true; }); if (!manifest) { + core.error(`Cannot find manifest for ${pos}/${arch}/${variant}`); 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 index d06d1e9..94686e6 100644 --- a/src/types/docker/mediatype.ts +++ b/src/types/docker/mediatype.ts @@ -17,3 +17,5 @@ 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'; + +export const MEDIATYPE_IMAGE_CONFIG_V1 = 'application/vnd.docker.container.image.v1+json'; diff --git a/src/types/oci/mediatype.ts b/src/types/oci/mediatype.ts index fca5c00..26c5244 100644 --- a/src/types/oci/mediatype.ts +++ b/src/types/oci/mediatype.ts @@ -23,3 +23,5 @@ export const MEDIATYPE_IMAGE_INDEX_V1 = 'application/vnd.oci.image.index.v1+json export const MEDIATYPE_IMAGE_LAYER_V1 = 'application/vnd.oci.image.layer.v1.tar'; export const MEDIATYPE_EMPTY_JSON_V1 = 'application/vnd.oci.empty.v1+json'; + +export const MEDIATYPE_IMAGE_CONFIG_V1 = 'application/vnd.oci.image.config.v1+json';