docker/install: Fix latest image install on lima

`latest` is not a valid git tag or revision to get the matching systemd
unit files.
Look up the exact source git commit from the
`'org.opencontainers.image.revision` image config label.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski 2024-10-30 11:11:22 +01:00
parent e2acba1767
commit 61c10b2d7d
No known key found for this signature in database
GPG Key ID: B85EFCFE26DEF92A
6 changed files with 76 additions and 28 deletions

View File

@ -14,7 +14,7 @@
* limitations under the License. * 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 fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
@ -43,6 +43,7 @@ aarch64:https://cloud.debian.org/images/cloud/bookworm/20231013-1532/debian-12-g
test.each([ test.each([
{type: 'image', tag: '27.3.1'} as InstallSourceImage, {type: 'image', tag: '27.3.1'} as InstallSourceImage,
{type: 'image', tag: 'master'} 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: 'v26.1.4', channel: 'stable'} as InstallSourceArchive,
{type: 'archive', version: 'latest', 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}}` daemonConfig: `{"debug":true,"features":{"containerd-snapshotter":true}}`
}); });
await expect((async () => { await expect((async () => {
try {
await install.download(); await install.download();
await install.install(); await install.install();
await Docker.printVersion(); await Docker.printVersion();
await Docker.printInfo(); await Docker.printInfo();
})().finally(async () => { } catch (error) {
console.error(error);
throw error;
} finally {
await install.tearDown(); await install.tearDown();
})).resolves.not.toThrow(); }
})()).resolves.not.toThrow();
}, 30 * 60 * 1000); }, 30 * 60 * 1000);
}); });

View File

@ -237,12 +237,10 @@ provision:
HOME=/tmp undock moby/moby-bin:{{srcImageTag}} /usr/local/bin HOME=/tmp undock moby/moby-bin:{{srcImageTag}} /usr/local/bin
wget https://raw.githubusercontent.com/moby/moby/{{srcImageTag}}/contrib/init/systemd/docker.service \ wget https://raw.githubusercontent.com/moby/moby/{{gitCommit}}/contrib/init/systemd/docker.service \
https://raw.githubusercontent.com/moby/moby/v{{srcImageTag}}/contrib/init/systemd/docker.service \ -O /etc/systemd/system/docker.service
-O /etc/systemd/system/docker.service || true wget https://raw.githubusercontent.com/moby/moby/{{gitCommit}}/contrib/init/systemd/docker.socket \
wget https://raw.githubusercontent.com/moby/moby/{{srcImageTag}}/contrib/init/systemd/docker.socket \ -O /etc/systemd/system/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|^ExecStart=.*|ExecStart=/usr/local/bin/dockerd -H fd://|' /etc/systemd/system/docker.service
sed -i 's|containerd.service||' /etc/systemd/system/docker.service sed -i 's|containerd.service||' /etc/systemd/system/docker.service

View File

@ -34,6 +34,7 @@ import {Util} from '../util';
import {limaYamlData, dockerServiceLogsPs1, setupDockerWinPs1} from './assets'; import {limaYamlData, dockerServiceLogsPs1, setupDockerWinPs1} from './assets';
import {GitHubRelease} from '../types/github'; import {GitHubRelease} from '../types/github';
import {HubRepository} from '../hubRepository'; import {HubRepository} from '../hubRepository';
import {Image} from '../types/oci/config';
export interface InstallSourceImage { export interface InstallSourceImage {
type: 'image'; type: 'image';
@ -71,6 +72,8 @@ export class Install {
private _version: string | undefined; private _version: string | undefined;
private _toolDir: string | undefined; private _toolDir: string | undefined;
private gitCommit: string | undefined;
private readonly limaInstanceName = 'docker-actions-toolkit'; private readonly limaInstanceName = 'docker-actions-toolkit';
constructor(opts: InstallOpts) { constructor(opts: InstallOpts) {
@ -127,12 +130,28 @@ export class Install {
const cli = await HubRepository.build('dockereng/cli-bin'); const cli = await HubRepository.build('dockereng/cli-bin');
extractFolder = await cli.extractImage(tag); extractFolder = await cli.extractImage(tag);
const moby = await HubRepository.build('moby/moby-bin');
if (['win32', 'linux'].includes(platform)) { if (['win32', 'linux'].includes(platform)) {
core.info(`Downloading dockerd from moby/moby-bin:${tag}`); core.info(`Downloading dockerd from moby/moby-bin:${tag}`);
const moby = await HubRepository.build('moby/moby-bin');
await moby.extractImage(tag, extractFolder); await moby.extractImage(tag, extractFolder);
} else if (platform == 'darwin') { } 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<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 { } else {
core.warning(`dockerd not supported on ${platform}, only the Docker cli will be available`); 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<string> { private async installDarwin(): Promise<string> {
if (this.source.type == 'image' && !this.gitCommit) {
throw new Error('gitCommit must be set. Run download first.');
}
const src = this.source; const src = this.source;
const limaDir = path.join(os.homedir(), '.lima', this.limaInstanceName); const limaDir = path.join(os.homedir(), '.lima', this.limaInstanceName);
await io.mkdirP(limaDir); await io.mkdirP(limaDir);
@ -229,6 +251,7 @@ export class Install {
customImages: Install.limaCustomImages(), customImages: Install.limaCustomImages(),
daemonConfig: limaDaemonConfig, daemonConfig: limaDaemonConfig,
dockerSock: `${limaDir}/docker.sock`, dockerSock: `${limaDir}/docker.sock`,
gitCommit: this.gitCommit,
srcType: src.type, srcType: src.type,
srcArchiveVersion: this._version, // Use the resolved version (e.g. latest -> 27.4.0) srcArchiveVersion: this._version, // Use the resolved version (e.g. latest -> 27.4.0)
srcArchiveChannel: srcArchive.channel, srcArchiveChannel: srcArchive.channel,

View File

@ -21,8 +21,8 @@ import * as core from '@actions/core';
import {Manifest} from './types/oci/manifest'; import {Manifest} from './types/oci/manifest';
import * as tc from '@actions/tool-cache'; import * as tc from '@actions/tool-cache';
import fs from 'fs'; import fs from 'fs';
import {MEDIATYPE_IMAGE_INDEX_V1, MEDIATYPE_IMAGE_MANIFEST_V1} from './types/oci/mediatype'; import {MEDIATYPE_IMAGE_CONFIG_V1, 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 as DOCKER_MEDIATYPE_IMAGE_CONFIG_V1, MEDIATYPE_IMAGE_MANIFEST_LIST_V2, MEDIATYPE_IMAGE_MANIFEST_V2} from './types/docker/mediatype';
import {DockerHub} from './dockerhub'; import {DockerHub} from './dockerhub';
export class HubRepository { export class HubRepository {
@ -40,15 +40,20 @@ export class HubRepository {
return new HubRepository(repository, token); return new HubRepository(repository, token);
} }
public async getPlatformManifest(tagOrDigest: string, os?: string): Promise<Manifest> {
const index = await this.getManifest<Index>(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<Manifest>(digest);
}
// Unpacks the image layers and returns the path to the extracted image. // Unpacks the image layers and returns the path to the extracted image.
// Only OCI indexes/manifest list are supported for now. // Only OCI indexes/manifest list are supported for now.
public async extractImage(tag: string, destDir?: string): Promise<string> { public async extractImage(tag: string, destDir?: string): Promise<string> {
const index = await this.getManifest<Index>(tag); const manifest = await this.getPlatformManifest(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<Manifest>(digest);
const paths = manifest.layers.map(async layer => { const paths = manifest.layers.map(async layer => {
const url = this.blobUrl(layer.digest); const url = this.blobUrl(layer.digest);
@ -99,25 +104,35 @@ export class HubRepository {
} }
public async getManifest<T>(tagOrDigest: string): Promise<T> { public async getManifest<T>(tagOrDigest: string): Promise<T> {
const url = `https://registry-1.docker.io/v2/${this.repo}/manifests/${tagOrDigest}`; return await this.registryGet<T>(tagOrDigest, 'manifests', [MEDIATYPE_IMAGE_INDEX_V1, MEDIATYPE_IMAGE_MANIFEST_LIST_V2, MEDIATYPE_IMAGE_MANIFEST_V1, MEDIATYPE_IMAGE_MANIFEST_V2]);
}
public async getJSONBlob<T>(tagOrDigest: string): Promise<T> {
return await this.registryGet<T>(tagOrDigest, 'blobs', [MEDIATYPE_IMAGE_CONFIG_V1, DOCKER_MEDIATYPE_IMAGE_CONFIG_V1]);
}
private async registryGet<T>(tagOrDigest: string, endpoint: 'manifests' | 'blobs', accept: Array<string>): Promise<T> {
const url = `https://registry-1.docker.io/v2/${this.repo}/${endpoint}/${tagOrDigest}`;
const headers = { const headers = {
Authorization: `Bearer ${this.token}`, 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 resp = await HubRepository.http.get(url, headers);
const body = await resp.readBody(); const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500; const statusCode = resp.message.statusCode || 500;
if (statusCode != 200) { if (statusCode != 200) {
core.error(`registryGet(${this.repo}:${tagOrDigest}) failed: ${statusCode} ${body}`);
throw DockerHub.parseError(resp, body); throw DockerHub.parseError(resp, body);
} }
return <T>JSON.parse(body); return <T>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. // 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') { if (pos == 'win32') {
pos = 'windows'; pos = 'windows';
} }
@ -150,8 +165,10 @@ export class HubRepository {
return true; return true;
}); });
if (!manifest) { if (!manifest) {
core.error(`Cannot find manifest for ${pos}/${arch}/${variant}`);
throw new Error(`Cannot find manifest for ${pos}/${arch}/${variant}`); throw new Error(`Cannot find manifest for ${pos}/${arch}/${variant}`);
} }
return manifest.digest; return manifest.digest;
} }
} }

View File

@ -17,3 +17,5 @@
export const MEDIATYPE_IMAGE_MANIFEST_LIST_V2 = 'application/vnd.docker.distribution.manifest.list.v2+json'; 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_MANIFEST_V2 = 'application/vnd.docker.distribution.manifest.v2+json';
export const MEDIATYPE_IMAGE_CONFIG_V1 = 'application/vnd.docker.container.image.v1+json';

View File

@ -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_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_EMPTY_JSON_V1 = 'application/vnd.oci.empty.v1+json';
export const MEDIATYPE_IMAGE_CONFIG_V1 = 'application/vnd.oci.image.config.v1+json';