Skip to content

Commit

Permalink
docker/install: Support version: master
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
vvoland committed Sep 6, 2024
1 parent e132497 commit 91a2ddd
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 11 deletions.
25 changes: 24 additions & 1 deletion src/docker/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import {Exec} from '../exec';
import {Util} from '../util';
import {limaYamlData, dockerServiceLogsPs1, setupDockerWinPs1} from './assets';
import {GitHubRelease} from '../types/github';
import {Index} from '../types/oci';
import {Manifest} from '../types/oci/manifest';
import {HubRepository} from '../hubRepository';

export interface InstallOpts {
version?: string;
Expand Down Expand Up @@ -71,7 +74,7 @@ export class Install {
return this._toolDir || Context.tmpDir();
}

public async download(): Promise<string> {
async downloadStaticArchive(): Promise<string> {
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}`);
Expand All @@ -92,6 +95,26 @@ export class Install {
extractFolder = path.join(extractFolder, 'docker');
}
core.debug(`docker.Install.download extractFolder: ${extractFolder}`);
return extractFolder;
}

public async download(): Promise<string> {
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) {
Expand Down
24 changes: 14 additions & 10 deletions src/dockerhub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <Record<string, string>>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 = <Record<string, string>>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}`);
}
}
141 changes: 141 additions & 0 deletions src/hubRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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<HubRepository> {
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<string> {
const index = await this.getManifest<Index>(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 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<string> {
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<T>(tagOrDigest: string): Promise<T> {
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 <T>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;
}
}
3 changes: 3 additions & 0 deletions src/types/docker/mediatype.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
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';

0 comments on commit 91a2ddd

Please sign in to comment.