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:
CrazyMax
2023-02-04 03:33:00 +01:00
committed by GitHub
9 changed files with 279 additions and 10 deletions

View File

@@ -29,6 +29,7 @@
"error", {
"ignore": ["csv-parse/sync"]
}
]
],
"jest/no-disabled-tests": 0
}
}

View File

@@ -45,7 +45,7 @@ afterEach(() => {
rimraf.sync(tmpDir);
});
describe('generate', () => {
describe('resolve', () => {
test.each([
['debug = true', false, 'debug = true', null],
[`notfound.toml`, true, '', new Error('config file notfound.toml not found')],
@@ -65,9 +65,9 @@ describe('generate', () => {
});
let config: string;
if (file) {
config = buildkit.config.generateFromFile(val);
config = buildkit.config.resolveFromFile(val);
} else {
config = buildkit.config.generateFromString(val);
config = buildkit.config.resolveFromString(val);
}
expect(config).toEqual(tmpName);
const configValue = fs.readFileSync(tmpName, 'utf-8');

View 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');
});
});

View 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"
]
}

View File

@@ -26,7 +26,7 @@ beforeEach(() => {
jest.clearAllMocks();
});
import repoFixture from './fixtures/repo.json';
import repoFixture from './fixtures/github-repo.json';
jest.spyOn(GitHub.prototype, 'repoData').mockImplementation((): Promise<GitHubRepo> => {
return <Promise<GitHubRepo>>(repoFixture as unknown);
});

View File

@@ -25,15 +25,15 @@ export class Config {
this.context = context;
}
public generateFromString(s: string): string {
return this.generate(s, false);
public resolveFromString(s: string): string {
return this.resolve(s, false);
}
public generateFromFile(s: string): string {
return this.generate(s, true);
public resolveFromFile(s: string): string {
return this.resolve(s, true);
}
private generate(s: string, file: boolean): string {
private resolve(s: string, file: boolean): string {
if (file) {
if (!fs.existsSync(s)) {
throw new Error(`config file ${s} not found`);

97
src/dockerhub.ts Normal file
View 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
View 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;
}