Merge pull request #8533 from RoadieHQ/gh-creds-interface

create an interface for gh creds provider
This commit is contained in:
Patrik Oldsberg
2021-12-23 14:29:17 +01:00
committed by GitHub
18 changed files with 136 additions and 59 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/backend-common': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
---
Uptake changes to the GitHub Credentials Provider interface.
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/integration': minor
---
Create an interface for the GitHub credentials provider in order to support providing implementations.
We have changed the name of the `GithubCredentialsProvider` to `SingleInstanceGithubCredentialsProvider`.
`GithubCredentialsProvider` is now an interface that maybe implemented to provide a custom mechanism to retrieve GitHub credentials.
In a later release we will support configuring URL readers, scaffolder tasks, and processors with customer GitHub credentials providers.
If you want to uptake this release, you will need to replace all references to `GithubCredentialsProvider.create` with `SingleInstanceGithubCredentialsProvider.create`.
@@ -16,6 +16,7 @@
import {
getGitHubFileFetchUrl,
SingleInstanceGithubCredentialsProvider,
GithubCredentialsProvider,
GitHubIntegration,
ScmIntegrations,
@@ -58,9 +59,8 @@ export class GithubUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
const integrations = ScmIntegrations.fromConfig(config);
return integrations.github.list().map(integration => {
const credentialsProvider = GithubCredentialsProvider.create(
integration.config,
);
const credentialsProvider =
SingleInstanceGithubCredentialsProvider.create(integration.config);
const reader = new GithubUrlReader(integration, {
treeResponseFactory,
credentialsProvider,
+10 -2
View File
@@ -198,9 +198,8 @@ export type GithubCredentials = {
};
// @public
export class GithubCredentialsProvider {
export interface GithubCredentialsProvider {
// (undocumented)
static create(config: GitHubIntegrationConfig): GithubCredentialsProvider;
getCredentials(opts: { url: string }): Promise<GithubCredentials>;
}
@@ -423,6 +422,15 @@ export interface ScmIntegrationsGroup<T extends ScmIntegration> {
list(): T[];
}
// @public
export class SingleInstanceGithubCredentialsProvider
implements GithubCredentialsProvider
{
// (undocumented)
static create: (config: GitHubIntegrationConfig) => GithubCredentialsProvider;
getCredentials(opts: { url: string }): Promise<GithubCredentials>;
}
// Warnings were encountered during analysis:
//
// src/gitlab/config.d.ts:29:68 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
@@ -31,11 +31,11 @@ jest.doMock('@octokit/rest', () => {
return { Octokit };
});
import { GithubCredentialsProvider } from './GithubCredentialsProvider';
import { SingleInstanceGithubCredentialsProvider } from './SingleInstanceGithubCredentialsProvider';
import { RestEndpointMethodTypes } from '@octokit/rest';
import { DateTime } from 'luxon';
const github = GithubCredentialsProvider.create({
const github = SingleInstanceGithubCredentialsProvider.create({
host: 'github.com',
apps: [
{
@@ -49,7 +49,7 @@ const github = GithubCredentialsProvider.create({
token: 'hardcoded_token',
});
describe('GithubCredentialsProvider tests', () => {
describe('DefaultGithubCredentialsProvider tests', () => {
beforeEach(() => {
jest.resetAllMocks();
});
@@ -204,7 +204,7 @@ describe('GithubCredentialsProvider tests', () => {
});
it('should return the default token if no app is configured', async () => {
const githubProvider = GithubCredentialsProvider.create({
const githubProvider = SingleInstanceGithubCredentialsProvider.create({
host: 'github.com',
apps: [],
token: 'fallback_token',
@@ -218,7 +218,7 @@ describe('GithubCredentialsProvider tests', () => {
});
it('should return the configured token if there are no installations', async () => {
const githubProvider = GithubCredentialsProvider.create({
const githubProvider = SingleInstanceGithubCredentialsProvider.create({
host: 'github.com',
apps: [
{
@@ -243,7 +243,7 @@ describe('GithubCredentialsProvider tests', () => {
});
it('should return undefined if no token or apps are configured', async () => {
const githubProvider = GithubCredentialsProvider.create({
const githubProvider = SingleInstanceGithubCredentialsProvider.create({
host: 'github.com',
});
@@ -19,6 +19,11 @@ import { GithubAppConfig, GitHubIntegrationConfig } from './config';
import { createAppAuth } from '@octokit/auth-app';
import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
import { DateTime } from 'luxon';
import {
GithubCredentials,
GithubCredentialsProvider,
GithubCredentialType,
} from './types';
type InstallationData = {
installationId: number;
@@ -215,24 +220,6 @@ export class GithubAppCredentialsMux {
}
}
/**
* The type of credentials produced by the credential provider.
*
* @public
*/
export type GithubCredentialType = 'app' | 'token';
/**
* A set of credentials information for a GitHub integration.
*
* @public
*/
export type GithubCredentials = {
headers?: { [name: string]: string };
token?: string;
type: GithubCredentialType;
};
/**
* Handles the creation and caching of credentials for GitHub integrations.
*
@@ -241,13 +228,17 @@ export type GithubCredentials = {
*
* TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake
*/
export class GithubCredentialsProvider {
static create(config: GitHubIntegrationConfig): GithubCredentialsProvider {
return new GithubCredentialsProvider(
export class SingleInstanceGithubCredentialsProvider
implements GithubCredentialsProvider
{
static create: (
config: GitHubIntegrationConfig,
) => GithubCredentialsProvider = config => {
return new SingleInstanceGithubCredentialsProvider(
new GithubAppCredentialsMux(config),
config.token,
);
}
};
private constructor(
private readonly githubAppCredentialsMux: GithubAppCredentialsMux,
+1 -1
View File
@@ -16,7 +16,7 @@
import { GitHubIntegrationConfig } from './config';
import { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core';
import { GithubCredentials } from './GithubCredentialsProvider';
import { GithubCredentials } from './types';
describe('github core', () => {
const appCredentials: GithubCredentials = {
+1 -1
View File
@@ -16,7 +16,7 @@
import parseGitUrl from 'git-url-parse';
import { GitHubIntegrationConfig } from './config';
import { GithubCredentials } from './GithubCredentialsProvider';
import { GithubCredentials } from './types';
/**
* Given a URL pointing to a file on a provider, returns a URL that is suitable
+4 -3
View File
@@ -22,10 +22,11 @@ export type { GithubAppConfig, GitHubIntegrationConfig } from './config';
export { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core';
export {
GithubAppCredentialsMux,
GithubCredentialsProvider,
} from './GithubCredentialsProvider';
SingleInstanceGithubCredentialsProvider,
} from './SingleInstanceGithubCredentialsProvider';
export type {
GithubCredentials,
GithubCredentialsProvider,
GithubCredentialType,
} from './GithubCredentialsProvider';
} from './types';
export { GitHubIntegration, replaceGitHubUrlType } from './GitHubIntegration';
+43
View File
@@ -0,0 +1,43 @@
/*
* Copyright 2021 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.
*/
/**
* The type of credentials produced by the credential provider.
*
* @public
*/
export type GithubCredentialType = 'app' | 'token';
/**
* A set of credentials information for a GitHub integration.
*
* @public
*/
export type GithubCredentials = {
headers?: { [name: string]: string };
token?: string;
type: GithubCredentialType;
};
/**
* This allows implementations to be provided to retrieve GitHub credentials.
*
* @public
*
*/
export interface GithubCredentialsProvider {
getCredentials(opts: { url: string }): Promise<GithubCredentials>;
}
@@ -17,7 +17,7 @@
import { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
GithubCredentialsProvider,
SingleInstanceGithubCredentialsProvider,
ScmIntegrations,
} from '@backstage/integration';
import { graphql } from '@octokit/graphql';
@@ -84,7 +84,7 @@ export class GithubDiscoveryProcessor implements CatalogProcessor {
// about how to handle the wild card which is special for this processor.
const orgUrl = `https://${host}/${org}`;
const { headers } = await GithubCredentialsProvider.create(
const { headers } = await SingleInstanceGithubCredentialsProvider.create(
gitHubConfig,
).getCredentials({ url: orgUrl });
@@ -18,7 +18,7 @@ import { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
GithubAppCredentialsMux,
GithubCredentialsProvider,
SingleInstanceGithubCredentialsProvider,
GitHubIntegrationConfig,
ScmIntegrations,
} from '@backstage/integration';
@@ -86,7 +86,8 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
const allUsersMap = new Map();
const baseUrl = new URL(location.target).origin;
const credentialsProvider = GithubCredentialsProvider.create(gitHubConfig);
const credentialsProvider =
SingleInstanceGithubCredentialsProvider.create(gitHubConfig);
const orgsToProcess = this.orgs.length
? this.orgs
@@ -17,7 +17,7 @@ import { getVoidLogger } from '@backstage/backend-common';
import { LocationSpec } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import {
GithubCredentialsProvider,
SingleInstanceGithubCredentialsProvider,
ScmIntegrations,
} from '@backstage/integration';
import { graphql } from '@octokit/graphql';
@@ -87,9 +87,11 @@ describe('GithubOrgReaderProcessor', () => {
(graphql.defaults as jest.Mock).mockReturnValue(mockClient);
jest.spyOn(GithubCredentialsProvider, 'create').mockReturnValue({
getCredentials: mockGetCredentials,
} as any);
jest
.spyOn(SingleInstanceGithubCredentialsProvider, 'create')
.mockReturnValue({
getCredentials: mockGetCredentials,
} as any);
const processor = new GithubOrgReaderProcessor({
integrations,
@@ -135,9 +137,11 @@ describe('GithubOrgReaderProcessor', () => {
(graphql.defaults as jest.Mock).mockReturnValue(mockClient);
jest.spyOn(GithubCredentialsProvider, 'create').mockReturnValue({
getCredentials: mockGetCredentials,
} as any);
jest
.spyOn(SingleInstanceGithubCredentialsProvider, 'create')
.mockReturnValue({
getCredentials: mockGetCredentials,
} as any);
const processor = new GithubOrgReaderProcessor({
integrations,
@@ -17,7 +17,7 @@
import { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
GithubCredentialsProvider,
SingleInstanceGithubCredentialsProvider,
GithubCredentialType,
ScmIntegrations,
} from '@backstage/integration';
@@ -107,7 +107,8 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
);
}
const credentialsProvider = GithubCredentialsProvider.create(gitHubConfig);
const credentialsProvider =
SingleInstanceGithubCredentialsProvider.create(gitHubConfig);
const { headers, type: tokenType } =
await credentialsProvider.getCredentials({
url: orgUrl,
@@ -17,7 +17,7 @@
import { getVoidLogger } from '@backstage/backend-common';
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import {
GithubCredentialsProvider,
SingleInstanceGithubCredentialsProvider,
GitHubIntegrationConfig,
} from '@backstage/integration';
import { GitHubOrgEntityProvider } from '.';
@@ -93,9 +93,11 @@ describe('GitHubOrgEntityProvider', () => {
type: 'app',
});
jest.spyOn(GithubCredentialsProvider, 'create').mockReturnValue({
getCredentials: mockGetCredentials,
} as any);
jest
.spyOn(SingleInstanceGithubCredentialsProvider, 'create')
.mockReturnValue({
getCredentials: mockGetCredentials,
} as any);
const entityProvider = new GitHubOrgEntityProvider({
id: 'my-id',
@@ -20,6 +20,7 @@ import {
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
SingleInstanceGithubCredentialsProvider,
GithubCredentialsProvider,
GitHubIntegrationConfig,
ScmIntegrations,
@@ -77,7 +78,7 @@ export class GitHubOrgEntityProvider implements EntityProvider {
logger: Logger;
},
) {
this.credentialsProvider = GithubCredentialsProvider.create(
this.credentialsProvider = SingleInstanceGithubCredentialsProvider.create(
options.gitHubConfig,
);
}
@@ -16,6 +16,7 @@
import { InputError } from '@backstage/errors';
import {
SingleInstanceGithubCredentialsProvider,
GithubCredentialsProvider,
ScmIntegrationRegistry,
} from '@backstage/integration';
@@ -40,7 +41,9 @@ export class OctokitProvider {
this.integrations = integrations;
this.credentialsProviders = new Map(
integrations.github.list().map(integration => {
const provider = GithubCredentialsProvider.create(integration.config);
const provider = SingleInstanceGithubCredentialsProvider.create(
integration.config,
);
return [integration.config.host, provider];
}),
);
@@ -18,7 +18,7 @@ import fs from 'fs-extra';
import { parseRepoUrl, isExecutable } from './util';
import {
GithubCredentialsProvider,
SingleInstanceGithubCredentialsProvider,
ScmIntegrationRegistry,
} from '@backstage/integration';
import { zipObject } from 'lodash';
@@ -76,7 +76,7 @@ export const defaultClientFactory = async ({
}
const credentialsProvider =
GithubCredentialsProvider.create(integrationConfig);
SingleInstanceGithubCredentialsProvider.create(integrationConfig);
if (!credentialsProvider) {
throw new InputError(