docker/install: Support version: master

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 <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski 2024-09-06 12:23:22 +02:00
parent a59a5f8e3f
commit 1335f081af
No known key found for this signature in database
GPG Key ID: B85EFCFE26DEF92A
4 changed files with 212 additions and 11 deletions

View File

@ -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<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}`);
@ -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<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) {

View File

@ -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}`);
}
}

157
src/hubRepository.ts Normal file
View File

@ -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<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;
}
}

View File

@ -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';