Merge pull request #4058 from backstage/mob/github-app-manager

Add GithubCredentialsProvider for start of GitHub Apps support
This commit is contained in:
Johan Haals
2021-01-18 08:10:26 +01:00
committed by GitHub
11 changed files with 851 additions and 103 deletions
@@ -15,6 +15,7 @@
*/
import { ConfigReader } from '@backstage/config';
import { GithubCredentialsProvider } from '@backstage/integration';
import { msw } from '@backstage/test-utils';
import fs from 'fs';
import { rest } from 'msw';
@@ -28,6 +29,18 @@ const treeResponseFactory = ReadTreeResponseFactory.create({
});
describe('GithubUrlReader', () => {
const mockCredentialsProvider = ({
getCredentials: jest.fn().mockResolvedValue({ headers: {} }),
} as unknown) as GithubCredentialsProvider;
const worker = setupServer();
msw.setupDefaultHandlers(worker);
beforeEach(() => {
jest.clearAllMocks();
});
describe('implementation', () => {
it('rejects unknown targets', async () => {
const processor = new GithubUrlReader(
@@ -35,7 +48,7 @@ describe('GithubUrlReader', () => {
host: 'github.com',
apiBaseUrl: 'https://api.github.com',
},
{ treeResponseFactory },
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
await expect(
processor.read('https://not.github.com/apa'),
@@ -45,11 +58,52 @@ describe('GithubUrlReader', () => {
});
});
describe('read', () => {
it('should use the headers from the credentials provider to the fetch request when doing read', async () => {
expect.assertions(2);
const mockHeaders = {
Authorization: 'bearer blah',
otherheader: 'something',
};
(mockCredentialsProvider.getCredentials as jest.Mock).mockResolvedValue({
headers: mockHeaders,
});
worker.use(
rest.get(
'https://api.github.com/repos/backstage/mock/tree/contents/?ref=repo',
(req, res, ctx) => {
expect(req.headers.get('authorization')).toBe(
mockHeaders.Authorization,
);
expect(req.headers.get('otherheader')).toBe(
mockHeaders.otherheader,
);
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/x-gzip'),
ctx.body('foo'),
);
},
),
);
const processor = new GithubUrlReader(
{
host: 'ghe.github.com',
apiBaseUrl: 'https://api.github.com',
},
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
await processor.read(
'https://ghe.github.com/backstage/mock/tree/blob/repo',
);
});
});
describe('readTree', () => {
const worker = setupServer();
msw.setupDefaultHandlers(worker);
const repoBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'repo.tar.gz'),
);
@@ -74,7 +128,7 @@ describe('GithubUrlReader', () => {
host: 'github.com',
apiBaseUrl: 'https://api.github.com',
},
{ treeResponseFactory },
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
const response = await processor.readTree(
@@ -110,7 +164,7 @@ describe('GithubUrlReader', () => {
host: 'ghe.github.com',
apiBaseUrl: 'https://api.github.com',
},
{ treeResponseFactory },
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
const response = await processor.readTree(
@@ -125,13 +179,57 @@ describe('GithubUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
it('should use the headers from the credentials provider to the fetch request', async () => {
expect.assertions(2);
const mockHeaders = {
Authorization: 'bearer blah',
otherheader: 'something',
};
(mockCredentialsProvider.getCredentials as jest.Mock).mockResolvedValue({
headers: mockHeaders,
});
worker.use(
rest.get(
'https://ghe.github.com/backstage/mock/archive/repo.tar.gz',
(req, res, ctx) => {
expect(req.headers.get('authorization')).toBe(
mockHeaders.Authorization,
);
expect(req.headers.get('otherheader')).toBe(
mockHeaders.otherheader,
);
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/x-gzip'),
ctx.body(repoBuffer),
);
},
),
);
const processor = new GithubUrlReader(
{
host: 'ghe.github.com',
apiBaseUrl: 'https://api.github.com',
},
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
await processor.readTree(
'https://ghe.github.com/backstage/mock/tree/repo/docs',
);
});
it('must specify a branch', async () => {
const processor = new GithubUrlReader(
{
host: 'github.com',
apiBaseUrl: 'https://api.github.com',
},
{ treeResponseFactory },
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
await expect(
@@ -147,7 +245,7 @@ describe('GithubUrlReader', () => {
host: 'github.com',
apiBaseUrl: 'https://api.github.com',
},
{ treeResponseFactory },
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
const response = await processor.readTree(
@@ -18,7 +18,7 @@ import {
GitHubIntegrationConfig,
readGitHubIntegrationConfigs,
getGitHubFileFetchUrl,
getGitHubRequestOptions,
GithubCredentialsProvider,
} from '@backstage/integration';
import fetch from 'cross-fetch';
import parseGitUri from 'git-url-parse';
@@ -42,7 +42,11 @@ export class GithubUrlReader implements UrlReader {
config.getOptionalConfigArray('integrations.github') ?? [],
);
return configs.map(provider => {
const reader = new GithubUrlReader(provider, { treeResponseFactory });
const credentialsProvider = GithubCredentialsProvider.create(provider);
const reader = new GithubUrlReader(provider, {
treeResponseFactory,
credentialsProvider,
});
const predicate = (url: URL) => url.host === provider.host;
return { reader, predicate };
});
@@ -50,7 +54,10 @@ export class GithubUrlReader implements UrlReader {
constructor(
private readonly config: GitHubIntegrationConfig,
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
private readonly deps: {
treeResponseFactory: ReadTreeResponseFactory;
credentialsProvider: GithubCredentialsProvider;
},
) {
if (!config.apiBaseUrl && !config.rawBaseUrl) {
throw new Error(
@@ -61,11 +68,17 @@ export class GithubUrlReader implements UrlReader {
async read(url: string): Promise<Buffer> {
const ghUrl = getGitHubFileFetchUrl(url, this.config);
const options = getGitHubRequestOptions(this.config);
const { headers } = await this.deps.credentialsProvider.getCredentials({
url,
});
let response: Response;
try {
response = await fetch(ghUrl.toString(), options);
response = await fetch(ghUrl.toString(), {
headers: {
...headers,
Accept: 'application/vnd.github.v3.raw',
},
});
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
}
@@ -101,12 +114,19 @@ export class GithubUrlReader implements UrlReader {
);
}
const { headers } = await this.deps.credentialsProvider.getCredentials({
url,
});
// TODO(Rugvip): use API to fetch URL instead
const response = await fetch(
new URL(
`${protocol}://${resource}/${full_name}/archive/${ref}.tar.gz`,
).toString(),
getGitHubRequestOptions(this.config),
{
headers: {
...headers,
},
},
);
if (!response.ok) {
const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`;