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:
@@ -33,6 +33,7 @@ import {Exec} from '../exec';
|
|||||||
import {Util} from '../util';
|
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';
|
||||||
|
|
||||||
export interface InstallOpts {
|
export interface InstallOpts {
|
||||||
version?: string;
|
version?: string;
|
||||||
@@ -71,7 +72,7 @@ export class Install {
|
|||||||
return this._toolDir || Context.tmpDir();
|
return this._toolDir || Context.tmpDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async download(): Promise<string> {
|
async downloadStaticArchive(): Promise<string> {
|
||||||
const release: GitHubRelease = await Install.getRelease(this.version);
|
const release: GitHubRelease = await Install.getRelease(this.version);
|
||||||
this._version = release.tag_name.replace(/^v+|v+$/g, '');
|
this._version = release.tag_name.replace(/^v+|v+$/g, '');
|
||||||
core.debug(`docker.Install.download version: ${this._version}`);
|
core.debug(`docker.Install.download version: ${this._version}`);
|
||||||
@@ -92,6 +93,26 @@ export class Install {
|
|||||||
extractFolder = path.join(extractFolder, 'docker');
|
extractFolder = path.join(extractFolder, 'docker');
|
||||||
}
|
}
|
||||||
core.debug(`docker.Install.download extractFolder: ${extractFolder}`);
|
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');
|
core.info('Fixing perms');
|
||||||
fs.readdir(path.join(extractFolder), function (err, files) {
|
fs.readdir(path.join(extractFolder), function (err, files) {
|
||||||
|
|||||||
@@ -111,17 +111,21 @@ export class DockerHub {
|
|||||||
const body = await resp.readBody();
|
const body = await resp.readBody();
|
||||||
resp.message.statusCode = resp.message.statusCode || HttpCodes.InternalServerError;
|
resp.message.statusCode = resp.message.statusCode || HttpCodes.InternalServerError;
|
||||||
if (resp.message.statusCode < 200 || resp.message.statusCode >= 300) {
|
if (resp.message.statusCode < 200 || resp.message.statusCode >= 300) {
|
||||||
if (resp.message.statusCode == HttpCodes.Unauthorized) {
|
throw DockerHub.parseError(resp, body);
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
return 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
157
src/hubRepository.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/types/docker/mediatype.ts
Normal file
19
src/types/docker/mediatype.ts
Normal 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';
|
||||||
Reference in New Issue
Block a user