Merge pull request #24 from crazy-max/dockerhub-api-client
dockerhub: api client to get repo details and update description
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
"error", {
|
"error", {
|
||||||
"ignore": ["csv-parse/sync"]
|
"ignore": ["csv-parse/sync"]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"jest/no-disabled-tests": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ afterEach(() => {
|
|||||||
rimraf.sync(tmpDir);
|
rimraf.sync(tmpDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generate', () => {
|
describe('resolve', () => {
|
||||||
test.each([
|
test.each([
|
||||||
['debug = true', false, 'debug = true', null],
|
['debug = true', false, 'debug = true', null],
|
||||||
[`notfound.toml`, true, '', new Error('config file notfound.toml not found')],
|
[`notfound.toml`, true, '', new Error('config file notfound.toml not found')],
|
||||||
@@ -65,9 +65,9 @@ describe('generate', () => {
|
|||||||
});
|
});
|
||||||
let config: string;
|
let config: string;
|
||||||
if (file) {
|
if (file) {
|
||||||
config = buildkit.config.generateFromFile(val);
|
config = buildkit.config.resolveFromFile(val);
|
||||||
} else {
|
} else {
|
||||||
config = buildkit.config.generateFromString(val);
|
config = buildkit.config.resolveFromString(val);
|
||||||
}
|
}
|
||||||
expect(config).toEqual(tmpName);
|
expect(config).toEqual(tmpName);
|
||||||
const configValue = fs.readFileSync(tmpName, 'utf-8');
|
const configValue = fs.readFileSync(tmpName, 'utf-8');
|
||||||
|
|||||||
71
__tests__/dockerhub.test.ts
Normal file
71
__tests__/dockerhub.test.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 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 {describe, expect, jest, it, beforeEach} from '@jest/globals';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import {DockerHub} from '../src/dockerhub';
|
||||||
|
import {RepositoryResponse} from '../src/types/dockerhub';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
import repoInfoFixture from './fixtures/dockerhub-repoinfo.json';
|
||||||
|
|
||||||
|
describe('getRepository', () => {
|
||||||
|
it('returns repo info', async () => {
|
||||||
|
jest.spyOn(DockerHub.prototype, 'getRepository').mockImplementation((): Promise<RepositoryResponse> => {
|
||||||
|
return <Promise<RepositoryResponse>>(repoInfoFixture as unknown);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
jest.spyOn(DockerHub as any, 'login').mockReturnValue('jwt_token');
|
||||||
|
const dockerhub = await DockerHub.build({
|
||||||
|
credentials: {
|
||||||
|
username: 'foo',
|
||||||
|
password: '0123456-7890-0000-1111-222222222'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const repoinfo = await dockerhub.getRepository({
|
||||||
|
namespace: 'foo',
|
||||||
|
name: 'bar'
|
||||||
|
});
|
||||||
|
expect(repoinfo.namespace).toEqual('foo');
|
||||||
|
expect(repoinfo.name).toEqual('bar');
|
||||||
|
expect(repoinfo.repository_type).toEqual('image');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateRepoDescription', () => {
|
||||||
|
it.skip('set repo description', async () => {
|
||||||
|
const dockerhub = await DockerHub.build({
|
||||||
|
credentials: {
|
||||||
|
username: 'foo',
|
||||||
|
password: 'bar'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const resp = await dockerhub.updateRepoDescription({
|
||||||
|
namespace: 'crazymax',
|
||||||
|
name: 'test-toolkit',
|
||||||
|
description: 'Hello-World',
|
||||||
|
full_description: fs.readFileSync(path.join(__dirname, '..', 'README.md'), 'utf-8')
|
||||||
|
});
|
||||||
|
expect(resp.namespace).toEqual('foo');
|
||||||
|
expect(resp.name).toEqual('bar');
|
||||||
|
expect(resp.description).toEqual('Hello-World');
|
||||||
|
});
|
||||||
|
});
|
||||||
34
__tests__/fixtures/dockerhub-repoinfo.json
Normal file
34
__tests__/fixtures/dockerhub-repoinfo.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"user": "foo",
|
||||||
|
"name": "bar",
|
||||||
|
"namespace": "foo",
|
||||||
|
"repository_type": "image",
|
||||||
|
"status": 1,
|
||||||
|
"status_description": "active",
|
||||||
|
"description": "Bar",
|
||||||
|
"is_private": false,
|
||||||
|
"is_automated": false,
|
||||||
|
"can_edit": true,
|
||||||
|
"star_count": 68,
|
||||||
|
"pull_count": 178255909,
|
||||||
|
"last_updated": "2023-02-02T14:22:31.184404Z",
|
||||||
|
"date_registered": "2019-06-04T20:08:57.306333Z",
|
||||||
|
"collaborator_count": 0,
|
||||||
|
"affiliation": "owner",
|
||||||
|
"hub_user": "foo",
|
||||||
|
"has_starred": false,
|
||||||
|
"full_description": "This is the full description of the repo.",
|
||||||
|
"permissions": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"admin": true
|
||||||
|
},
|
||||||
|
"media_types": [
|
||||||
|
"application/vnd.docker.container.image.v1+json",
|
||||||
|
"application/vnd.docker.distribution.manifest.list.v2+json",
|
||||||
|
"application/vnd.oci.image.index.v1+json"
|
||||||
|
],
|
||||||
|
"content_types": [
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ beforeEach(() => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
import repoFixture from './fixtures/repo.json';
|
import repoFixture from './fixtures/github-repo.json';
|
||||||
jest.spyOn(GitHub.prototype, 'repoData').mockImplementation((): Promise<GitHubRepo> => {
|
jest.spyOn(GitHub.prototype, 'repoData').mockImplementation((): Promise<GitHubRepo> => {
|
||||||
return <Promise<GitHubRepo>>(repoFixture as unknown);
|
return <Promise<GitHubRepo>>(repoFixture as unknown);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ export class Config {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateFromString(s: string): string {
|
public resolveFromString(s: string): string {
|
||||||
return this.generate(s, false);
|
return this.resolve(s, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateFromFile(s: string): string {
|
public resolveFromFile(s: string): string {
|
||||||
return this.generate(s, true);
|
return this.resolve(s, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generate(s: string, file: boolean): string {
|
private resolve(s: string, file: boolean): string {
|
||||||
if (file) {
|
if (file) {
|
||||||
if (!fs.existsSync(s)) {
|
if (!fs.existsSync(s)) {
|
||||||
throw new Error(`config file ${s} not found`);
|
throw new Error(`config file ${s} not found`);
|
||||||
|
|||||||
97
src/dockerhub.ts
Normal file
97
src/dockerhub.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* 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 core from '@actions/core';
|
||||||
|
import * as httpm from '@actions/http-client';
|
||||||
|
import {HttpCodes} from '@actions/http-client';
|
||||||
|
|
||||||
|
import {RepositoryRequest, RepositoryResponse, TokenRequest, TokenResponse, UpdateRepoDescriptionRequest} from './types/dockerhub';
|
||||||
|
|
||||||
|
export interface DockerHubOpts {
|
||||||
|
credentials: TokenRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiBaseURL = 'https://hub.docker.com';
|
||||||
|
const loginURL = `${apiBaseURL}/v2/users/login?refresh_token=true`;
|
||||||
|
const repositoriesURL = `${apiBaseURL}/v2/repositories/`;
|
||||||
|
|
||||||
|
export class DockerHub {
|
||||||
|
private readonly opts: DockerHubOpts;
|
||||||
|
private readonly httpc: httpm.HttpClient;
|
||||||
|
|
||||||
|
private constructor(opts: DockerHubOpts, httpc: httpm.HttpClient) {
|
||||||
|
this.opts = opts;
|
||||||
|
this.httpc = httpc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async build(opts: DockerHubOpts): Promise<DockerHub> {
|
||||||
|
return new DockerHub(
|
||||||
|
opts,
|
||||||
|
new httpm.HttpClient('docker-actions-toolkit', [], {
|
||||||
|
headers: {
|
||||||
|
Authorization: `JWT ${await DockerHub.login(opts.credentials)}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRepository(req: RepositoryRequest): Promise<RepositoryResponse> {
|
||||||
|
const resp: httpm.HttpClientResponse = await this.httpc.get(`${repositoriesURL}${req.namespace}/${req.name}`);
|
||||||
|
return <RepositoryResponse>JSON.parse(await DockerHub.handleResponse(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateRepoDescription(req: UpdateRepoDescriptionRequest): Promise<RepositoryResponse> {
|
||||||
|
const body = {
|
||||||
|
full_description: req.full_description
|
||||||
|
};
|
||||||
|
if (req.description) {
|
||||||
|
body['description'] = req.description;
|
||||||
|
}
|
||||||
|
const resp: httpm.HttpClientResponse = await this.httpc.patch(`${repositoriesURL}${req.namespace}/${req.name}`, JSON.stringify(body));
|
||||||
|
return <RepositoryResponse>JSON.parse(await DockerHub.handleResponse(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async login(req: TokenRequest): Promise<string> {
|
||||||
|
const http: httpm.HttpClient = new httpm.HttpClient('docker-actions-toolkit', [], {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const resp: httpm.HttpClientResponse = await http.post(loginURL, JSON.stringify(req));
|
||||||
|
const tokenResp = <TokenResponse>JSON.parse(await DockerHub.handleResponse(resp));
|
||||||
|
core.setSecret(`${tokenResp.token}`);
|
||||||
|
return `${tokenResp.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async handleResponse(resp: httpm.HttpClientResponse): Promise<string> {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/types/dockerhub.ts
Normal file
66
src/types/dockerhub.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 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 interface TokenRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenResponse {
|
||||||
|
token: string;
|
||||||
|
detail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepositoryRequest {
|
||||||
|
namespace: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepositoryResponse {
|
||||||
|
user: string;
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
repository_type: string;
|
||||||
|
status: number;
|
||||||
|
status_description: string;
|
||||||
|
description: string;
|
||||||
|
is_private: boolean;
|
||||||
|
is_automated: boolean;
|
||||||
|
can_edit: boolean;
|
||||||
|
star_count: number;
|
||||||
|
pull_count: number;
|
||||||
|
last_updated: string;
|
||||||
|
date_registered: string;
|
||||||
|
collaborator_count: number;
|
||||||
|
affiliation: string;
|
||||||
|
hub_user: string;
|
||||||
|
has_starred: boolean;
|
||||||
|
full_description: string;
|
||||||
|
permissions: {
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
|
admin: boolean;
|
||||||
|
};
|
||||||
|
media_types: Array<string>;
|
||||||
|
content_types: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateRepoDescriptionRequest {
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
description?: string;
|
||||||
|
full_description: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user