diff --git a/.changeset/lucky-bats-reflect.md b/.changeset/lucky-bats-reflect.md new file mode 100644 index 0000000000..ad3171382e --- /dev/null +++ b/.changeset/lucky-bats-reflect.md @@ -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. diff --git a/.changeset/short-schools-heal.md b/.changeset/short-schools-heal.md new file mode 100644 index 0000000000..3976ea3a0c --- /dev/null +++ b/.changeset/short-schools-heal.md @@ -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`. diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index d0fbf6d8ae..6b18cfbe1a 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -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, diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index fcaa39b285..00623dc917 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -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; } @@ -423,6 +422,15 @@ export interface ScmIntegrationsGroup { list(): T[]; } +// @public +export class SingleInstanceGithubCredentialsProvider + implements GithubCredentialsProvider +{ + // (undocumented) + static create: (config: GitHubIntegrationConfig) => GithubCredentialsProvider; + getCredentials(opts: { url: string }): Promise; +} + // 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 diff --git a/packages/integration/src/github/GithubCredentialsProvider.test.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts similarity index 94% rename from packages/integration/src/github/GithubCredentialsProvider.test.ts rename to packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts index 0f51db85be..6bea9647a1 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.test.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts @@ -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', }); diff --git a/packages/integration/src/github/GithubCredentialsProvider.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts similarity index 94% rename from packages/integration/src/github/GithubCredentialsProvider.ts rename to packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts index ece692fecc..408c5b1348 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts @@ -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, diff --git a/packages/integration/src/github/core.test.ts b/packages/integration/src/github/core.test.ts index 53733b40d0..c5d04f5b4f 100644 --- a/packages/integration/src/github/core.test.ts +++ b/packages/integration/src/github/core.test.ts @@ -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 = { diff --git a/packages/integration/src/github/core.ts b/packages/integration/src/github/core.ts index 76e54fd544..5b630d5182 100644 --- a/packages/integration/src/github/core.ts +++ b/packages/integration/src/github/core.ts @@ -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 diff --git a/packages/integration/src/github/index.ts b/packages/integration/src/github/index.ts index 9c13f13135..429a2b1c76 100644 --- a/packages/integration/src/github/index.ts +++ b/packages/integration/src/github/index.ts @@ -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'; diff --git a/packages/integration/src/github/types.ts b/packages/integration/src/github/types.ts new file mode 100644 index 0000000000..15f5375570 --- /dev/null +++ b/packages/integration/src/github/types.ts @@ -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; +} diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts index 9bd6cc775e..046455c484 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts @@ -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 }); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts index 00b173777d..5ad735ceeb 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts @@ -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 diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts index 87a910ee06..9124637e52 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts @@ -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, diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts index 244dbd8104..95db344818 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts @@ -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, diff --git a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts b/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts index 75edea6909..6fde975a2b 100644 --- a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts +++ b/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts @@ -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', diff --git a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts b/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts index ecd0d153d1..b8059a783c 100644 --- a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts +++ b/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts @@ -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, ); } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts index bea027a91b..9fdc6ced17 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts @@ -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]; }), ); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 3a9dd01686..299eb07f2e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -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(