Techdocs: add Azure DevOps prepare support (#2748)
* Update to latest git-url-parse * Add Azure DevOps preparer to techdocs * Upgraded git-url-parse in backend-common * Switch to AZURE_TOKEN Co-authored-by: Mattias Frinnström <mattias.frinnstrom@husqvarnagroup.com>
This commit is contained in:
committed by
GitHub
parent
03e0eef930
commit
b46863b88b
@@ -61,6 +61,16 @@ function getGitlabApiUrl(url: string): URL {
|
||||
);
|
||||
}
|
||||
|
||||
function getAzureApiUrl(url: string): URL {
|
||||
const { protocol, resource, organization, owner, name } = parseGitUrl(url);
|
||||
const apiRepoPath = '_apis/git/repositories';
|
||||
const apiVersion = 'api-version=6.0';
|
||||
|
||||
return new URL(
|
||||
`${protocol}://${resource}/${organization}/${owner}/${apiRepoPath}/${name}?${apiVersion}`,
|
||||
);
|
||||
}
|
||||
|
||||
function getGithubRequestOptions(config: Config): RequestInit {
|
||||
const headers: HeadersInit = {
|
||||
Accept: 'application/vnd.github.v3.raw',
|
||||
@@ -99,6 +109,26 @@ function getGitlabRequestOptions(config: Config): RequestInit {
|
||||
};
|
||||
}
|
||||
|
||||
function getAzureRequestOptions(config: Config): RequestInit {
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
const token =
|
||||
config.getOptionalString('catalog.processors.azureApi.privateToken') ??
|
||||
process.env.AZURE_TOKEN;
|
||||
|
||||
if (token !== '') {
|
||||
headers.Authorization = `Basic ${Buffer.from(`:${token}`, 'utf8').toString(
|
||||
'base64',
|
||||
)}`;
|
||||
}
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
headers,
|
||||
};
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
async function getGithubDefaultBranch(
|
||||
repositoryUrl: string,
|
||||
config: Config,
|
||||
@@ -159,6 +189,42 @@ async function getGitlabDefaultBranch(
|
||||
}
|
||||
}
|
||||
|
||||
async function getAzureDefaultBranch(
|
||||
repositoryUrl: string,
|
||||
config: Config,
|
||||
): Promise<string> {
|
||||
const path = getAzureApiUrl(repositoryUrl).toString();
|
||||
|
||||
const options = getAzureRequestOptions(config);
|
||||
|
||||
try {
|
||||
const urlResponse = await fetch(path, options);
|
||||
if (!urlResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to load url: ${urlResponse.status} ${urlResponse.statusText}. Make sure you have permission to repository: ${repositoryUrl}`,
|
||||
);
|
||||
}
|
||||
const urlResult = await urlResponse.json();
|
||||
|
||||
const idResponse = await fetch(urlResult.url, options);
|
||||
if (!idResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to load url: ${idResponse.status} ${idResponse.statusText}. Make sure you have permission to repository: ${urlResult.repository.url}`,
|
||||
);
|
||||
}
|
||||
const idResult = await idResponse.json();
|
||||
const name = idResult.defaultBranch;
|
||||
|
||||
if (!name) {
|
||||
throw new Error('Not found Azure DevOps default branch');
|
||||
}
|
||||
|
||||
return name;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get Azure DevOps default branch: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const getDefaultBranch = async (
|
||||
repositoryUrl: string,
|
||||
): Promise<string> => {
|
||||
@@ -166,6 +232,7 @@ export const getDefaultBranch = async (
|
||||
const typeMapping = [
|
||||
{ url: /github*/g, type: 'github' },
|
||||
{ url: /gitlab*/g, type: 'gitlab' },
|
||||
{ url: /azure*/g, type: 'azure/api' },
|
||||
];
|
||||
|
||||
const type = typeMapping.filter(item => item.url.test(repositoryUrl))[0]
|
||||
@@ -177,6 +244,8 @@ export const getDefaultBranch = async (
|
||||
return await getGithubDefaultBranch(repositoryUrl, config);
|
||||
case 'gitlab':
|
||||
return await getGitlabDefaultBranch(repositoryUrl, config);
|
||||
case 'azure/api':
|
||||
return await getAzureDefaultBranch(repositoryUrl, config);
|
||||
|
||||
default:
|
||||
throw new Error('Failed to get repository type');
|
||||
|
||||
@@ -76,6 +76,7 @@ export const getLocationForEntity = (
|
||||
switch (type) {
|
||||
case 'github':
|
||||
case 'gitlab':
|
||||
case 'azure/api':
|
||||
return { type, target };
|
||||
case 'dir':
|
||||
if (path.isAbsolute(target)) return { type, target };
|
||||
@@ -124,9 +125,13 @@ export const checkoutGitRepository = async (
|
||||
const user =
|
||||
process.env.GITHUB_PRIVATE_TOKEN_USER ||
|
||||
process.env.GITLAB_PRIVATE_TOKEN_USER ||
|
||||
process.env.AZURE_PRIVATE_TOKEN_USER ||
|
||||
'';
|
||||
const token =
|
||||
process.env.GITHUB_TOKEN || process.env.GITLAB_PRIVATE_TOKEN_USER || '';
|
||||
process.env.GITHUB_TOKEN ||
|
||||
process.env.GITLAB_PRIVATE_TOKEN_USER ||
|
||||
process.env.AZURE_TOKEN ||
|
||||
'';
|
||||
|
||||
if (fs.existsSync(repositoryTmpPath)) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { getVoidLogger } from '@backstage/backend-common';
|
||||
import { AzurePreparer } from './azure';
|
||||
import { checkoutGitRepository } from '../../../helpers';
|
||||
|
||||
function normalizePath(path: string) {
|
||||
return path
|
||||
.replace(/^[a-z]:/i, '')
|
||||
.split('\\')
|
||||
.join('/');
|
||||
}
|
||||
|
||||
jest.mock('../../../helpers', () => ({
|
||||
...jest.requireActual<{}>('../../../helpers'),
|
||||
checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch'),
|
||||
}));
|
||||
|
||||
const createMockEntity = (annotations = {}) => {
|
||||
return {
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'test-component-name',
|
||||
annotations: {
|
||||
...annotations,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
describe('Azure DevOps preparer', () => {
|
||||
it('should prepare temp docs path from Azure DevOps repo', async () => {
|
||||
const preparer = new AzurePreparer(logger);
|
||||
|
||||
const mockEntity = createMockEntity({
|
||||
'backstage.io/techdocs-ref':
|
||||
'azure/api:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml',
|
||||
});
|
||||
|
||||
const tempDocsPath = await preparer.prepare(mockEntity);
|
||||
expect(checkoutGitRepository).toHaveBeenCalledTimes(1);
|
||||
expect(normalizePath(tempDocsPath)).toEqual(
|
||||
'/tmp/backstage-repo/org/name/branch/template.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 path from 'path';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { PreparerBase } from './types';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import {
|
||||
parseReferenceAnnotation,
|
||||
checkoutGitRepository,
|
||||
} from '../../../helpers';
|
||||
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export class AzurePreparer implements PreparerBase {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(logger: Logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async prepare(entity: Entity): Promise<string> {
|
||||
const { type, target } = parseReferenceAnnotation(
|
||||
'backstage.io/techdocs-ref',
|
||||
entity,
|
||||
);
|
||||
|
||||
if (type !== 'azure/api') {
|
||||
throw new InputError(`Wrong target type: ${type}, should be 'azure/api'`);
|
||||
}
|
||||
|
||||
try {
|
||||
const repoPath = await checkoutGitRepository(target, this.logger);
|
||||
const parsedGitLocation = parseGitUrl(target);
|
||||
|
||||
return path.join(repoPath, parsedGitLocation.filepath);
|
||||
} catch (error) {
|
||||
this.logger.debug(`Repo checkout failed with error ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,8 @@ export class DirectoryPreparer implements PreparerBase {
|
||||
);
|
||||
switch (type) {
|
||||
case 'github':
|
||||
case 'gitlab': {
|
||||
case 'gitlab':
|
||||
case 'azure/api': {
|
||||
const parsedGitLocation = parseGitUrl(target);
|
||||
const repoLocation = await checkoutGitRepository(target, this.logger);
|
||||
|
||||
|
||||
@@ -16,5 +16,6 @@
|
||||
export { DirectoryPreparer } from './dir';
|
||||
export { GithubPreparer } from './github';
|
||||
export { GitlabPreparer } from './gitlab';
|
||||
export { AzurePreparer } from './azure';
|
||||
export { Preparers } from './preparers';
|
||||
export type { PreparerBuilder, PreparerBase } from './types';
|
||||
|
||||
@@ -30,4 +30,4 @@ export type PreparerBuilder = {
|
||||
get(entity: Entity): PreparerBase;
|
||||
};
|
||||
|
||||
export type RemoteProtocol = 'dir' | 'github' | 'gitlab' | 'file';
|
||||
export type RemoteProtocol = 'dir' | 'github' | 'gitlab' | 'file' | 'azure/api';
|
||||
|
||||
Reference in New Issue
Block a user