@@ -27,7 +27,7 @@ import {
|
||||
LocationAnalyzer,
|
||||
} from './types';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { GitHubLocationAnalyzer } from './GitHubLocationAnalyzer';
|
||||
import { GitHubLocationAnalyzer } from './analyzers/GitHubLocationAnalyzer';
|
||||
|
||||
export class RepoLocationAnalyzer implements LocationAnalyzer {
|
||||
private readonly logger: Logger;
|
||||
@@ -85,8 +85,6 @@ export class RepoLocationAnalyzer implements LocationAnalyzer {
|
||||
|
||||
if (analyzer) {
|
||||
const existingEntityFiles = await analyzer.analyze(
|
||||
owner,
|
||||
name,
|
||||
request.location.target,
|
||||
);
|
||||
if (existingEntityFiles.length > 0) {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage 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.
|
||||
*/
|
||||
|
||||
const octokit = {
|
||||
search: {
|
||||
code: jest.fn(),
|
||||
},
|
||||
repos: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@octokit/rest', () => {
|
||||
class Octokit {
|
||||
constructor() {
|
||||
return octokit;
|
||||
}
|
||||
}
|
||||
return { Octokit };
|
||||
});
|
||||
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { GitHubLocationAnalyzer } from './GitHubLocationAnalyzer';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { rest } from 'msw';
|
||||
import { GitHubIntegration } from '@backstage/integration';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
describe('GitHubLocationAnalyzer', () => {
|
||||
const mockDiscoveryApi: jest.Mocked<PluginEndpointDiscovery> = {
|
||||
getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007'),
|
||||
getExternalBaseUrl: jest.fn(),
|
||||
};
|
||||
const integration = new GitHubIntegration({
|
||||
host: 'h.com',
|
||||
apiBaseUrl: 'a',
|
||||
rawBaseUrl: 'r',
|
||||
token: 't',
|
||||
});
|
||||
|
||||
setupRequestMockHandlers(server);
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.post('http://localhost:7007/locations', async (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.json({
|
||||
location: 'test',
|
||||
exists: false,
|
||||
entities: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
name: 'test-entity',
|
||||
},
|
||||
spec: {
|
||||
type: 'url',
|
||||
target: 'whatever',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
title: 'Test Entity',
|
||||
name: 'test-entity-2',
|
||||
description: 'The expected description 2',
|
||||
},
|
||||
spec: {
|
||||
type: 'some-type',
|
||||
lifecycle: 'experimental',
|
||||
owner: 'someone',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should analyze', async () => {
|
||||
octokit.search.code.mockImplementation((opts: { q: string }) => {
|
||||
if (opts.q === 'filename:catalog-info.yaml repo:foo/bar') {
|
||||
return Promise.resolve({
|
||||
data: { items: [{ path: 'catalog-info.yaml' }], total_count: 1 },
|
||||
});
|
||||
}
|
||||
return Promise.reject();
|
||||
});
|
||||
|
||||
octokit.repos.get.mockResolvedValue({
|
||||
data: { default_branch: 'my_default_branch' },
|
||||
});
|
||||
|
||||
const analyzer = new GitHubLocationAnalyzer({
|
||||
discovery: mockDiscoveryApi,
|
||||
integration,
|
||||
});
|
||||
const result = await analyzer.analyze('https://github.com/foo/bar');
|
||||
|
||||
expect(result[0].isRegistered).toBeFalsy();
|
||||
expect(result[0].location).toEqual({
|
||||
type: 'url',
|
||||
target:
|
||||
'https://github.com/foo/bar/blob/my_default_branch/catalog-info.yaml',
|
||||
});
|
||||
});
|
||||
});
|
||||
+24
-23
@@ -14,12 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
|
||||
import { GitHubIntegration } from '@backstage/integration';
|
||||
import { DiscoveryApi } from '@backstage/plugin-permission-common';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { AnalyzeLocationExistingEntity, ScmLocationAnalyzer } from './types';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { AnalyzeLocationExistingEntity, ScmLocationAnalyzer } from '../types';
|
||||
|
||||
export type GitHubLocationAnalyzerOptions = {
|
||||
integration: GitHubIntegration;
|
||||
@@ -27,45 +28,45 @@ export type GitHubLocationAnalyzerOptions = {
|
||||
discovery: DiscoveryApi;
|
||||
};
|
||||
export class GitHubLocationAnalyzer implements ScmLocationAnalyzer {
|
||||
private readonly integration: GitHubIntegration;
|
||||
private readonly catalogFilename: string;
|
||||
private readonly discovery: DiscoveryApi;
|
||||
private readonly octokitClient: Octokit;
|
||||
private readonly catalogClient: CatalogApi;
|
||||
|
||||
constructor(options: GitHubLocationAnalyzerOptions) {
|
||||
this.integration = options.integration;
|
||||
this.catalogFilename = options.catalogFilename || 'catalog-info.yaml';
|
||||
this.discovery = options.discovery;
|
||||
this.octokitClient = new Octokit({
|
||||
auth: options.integration.config.token,
|
||||
baseUrl: options.integration.config.apiBaseUrl,
|
||||
});
|
||||
this.catalogClient = new CatalogClient({ discoveryApi: this.discovery });
|
||||
}
|
||||
|
||||
async analyze(
|
||||
owner: string,
|
||||
repo: string,
|
||||
url: string,
|
||||
): Promise<AnalyzeLocationExistingEntity[]> {
|
||||
const octo = new Octokit({
|
||||
auth: this.integration.config.token,
|
||||
baseUrl: this.integration.config.apiBaseUrl,
|
||||
});
|
||||
const query = `filename:${this.catalogFilename} repo:${owner}/${repo} `;
|
||||
async analyze(url: string): Promise<AnalyzeLocationExistingEntity[]> {
|
||||
const { owner, name: repo } = parseGitUrl(url);
|
||||
const query = `filename:${this.catalogFilename} repo:${owner}/${repo}`;
|
||||
|
||||
const catalogClient = new CatalogClient({ discoveryApi: this.discovery });
|
||||
|
||||
const searchResult = await octo.search.code({ q: query }).catch(e => {
|
||||
throw new Error(`Couldn't search repository for metadata file, ${e}`);
|
||||
});
|
||||
const searchResult = await this.octokitClient.search
|
||||
.code({ q: query })
|
||||
.catch(e => {
|
||||
throw new Error(`Couldn't search repository for metadata file, ${e}`);
|
||||
});
|
||||
|
||||
const exists = searchResult.data.total_count > 0;
|
||||
if (exists) {
|
||||
const repoInformation = await octo.repos.get({ owner, repo }).catch(e => {
|
||||
throw new Error(`Couldn't fetch repo data, ${e}`);
|
||||
});
|
||||
const repoInformation = await this.octokitClient.repos
|
||||
.get({ owner, repo })
|
||||
.catch(e => {
|
||||
throw new Error(`Couldn't fetch repo data, ${e}`);
|
||||
});
|
||||
const defaultBranch = repoInformation.data.default_branch;
|
||||
|
||||
const result = await Promise.all(
|
||||
searchResult.data.items
|
||||
.map(i => `${trimEnd(url, '/')}/blob/${defaultBranch}/${i.path}`)
|
||||
.map(async target => {
|
||||
const addLocationResult = await catalogClient.addLocation({
|
||||
const addLocationResult = await this.catalogClient.addLocation({
|
||||
type: 'url',
|
||||
target,
|
||||
dryRun: true,
|
||||
Reference in New Issue
Block a user