Merge pull request #2074 from spotify/rugvip/discovery

[RFC] Add DiscoveryApi
This commit is contained in:
Raghunandan Balachandran
2020-08-27 15:07:21 +02:00
committed by GitHub
22 changed files with 373 additions and 211 deletions
+15 -33
View File
@@ -24,6 +24,8 @@ import {
ErrorAlerter,
featureFlagsApiRef,
FeatureFlags,
discoveryApiRef,
UrlPatternDiscovery,
GoogleAuth,
GithubAuth,
OAuth2,
@@ -90,6 +92,10 @@ export const apis = (config: ConfigApi) => {
const builder = ApiRegistry.builder();
const discoveryApi = builder.add(
discoveryApiRef,
UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`),
);
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
const errorApi = builder.add(
errorApiRef,
@@ -121,8 +127,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
googleAuthApiRef,
GoogleAuth.create({
backendUrl,
basePath: '/auth/',
discoveryApi,
oauthRequestApi,
}),
);
@@ -139,8 +144,7 @@ export const apis = (config: ConfigApi) => {
const githubAuthApi = builder.add(
githubAuthApiRef,
GithubAuth.create({
backendUrl,
basePath: '/auth/',
discoveryApi,
oauthRequestApi,
}),
);
@@ -148,8 +152,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
oktaAuthApiRef,
OktaAuth.create({
backendUrl,
basePath: '/auth/',
discoveryApi,
oauthRequestApi,
}),
);
@@ -157,8 +160,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
gitlabAuthApiRef,
GitlabAuth.create({
backendUrl,
basePath: '/auth/',
discoveryApi,
oauthRequestApi,
}),
);
@@ -166,8 +168,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
auth0AuthApiRef,
Auth0Auth.create({
backendUrl,
basePath: '/auth/',
discoveryApi,
oauthRequestApi,
}),
);
@@ -175,8 +176,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
oauth2ApiRef,
OAuth2.create({
backendUrl,
basePath: '/auth/',
discoveryApi,
oauthRequestApi,
}),
);
@@ -189,21 +189,9 @@ export const apis = (config: ConfigApi) => {
}),
);
builder.add(
catalogApiRef,
new CatalogClient({
apiOrigin: backendUrl,
basePath: '/catalog',
}),
);
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
builder.add(
scaffolderApiRef,
new ScaffolderApi({
apiOrigin: backendUrl,
basePath: '/scaffolder/v1',
}),
);
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008'));
@@ -224,13 +212,7 @@ export const apis = (config: ConfigApi) => {
]),
);
builder.add(
rollbarApiRef,
new RollbarClient({
apiOrigin: backendUrl,
basePath: '/rollbar',
}),
);
builder.add(rollbarApiRef, new RollbarClient({ discoveryApi }));
builder.add(
techdocsStorageApiRef,
@@ -0,0 +1,47 @@
/*
* 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 { createApiRef } from '../ApiRef';
/**
* The discovery API is used to provide a mechanism for plugins to
* discover the endpoint to use to talk to their backend counterpart.
*
* The purpose of the discovery API is to allow for many different deployment
* setups and routing methods through a central configuration, instead
* of letting each individual plugin manage that configuration.
*
* Implementations of the discovery API can be a simple as a URL pattern
* using the pluginId, but could also have overrides for individual plugins,
* or query a separate discovery service.
*/
export type DiscoveryApi = {
/**
* Returns the HTTP base backend URL for a given plugin, without a trailing slash.
*
* This method must always be called just before making a request. as opposed to
* fetching the URL when constructing an API client. That is to ensure that more
* flexible routing patterns can be supported.
*
* For example, asking for the URL for `auth` may return something
* like `https://backstage.example.com/api/auth`
*/
getBaseUrl(pluginId: string): Promise<string>;
};
export const discoveryApiRef = createApiRef<DiscoveryApi>({
id: 'core.discovery',
description: 'Provides service discovery of backend plugins',
});
@@ -27,6 +27,7 @@ export * from './AppThemeApi';
export * from './ConfigApi';
export * from './ErrorApi';
export * from './FeatureFlagsApi';
export * from './DiscoveryApi';
export * from './IdentityApi';
export * from './OAuthRequestApi';
export * from './StorageApi';
@@ -0,0 +1,84 @@
/*
* 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 { UrlPatternDiscovery } from './UrlPatternDiscovery';
describe('UrlPatternDiscovery', () => {
it('should not require interpolation', async () => {
const discoveryApi = UrlPatternDiscovery.compile('http://example.com');
await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe(
'http://example.com',
);
});
it('should use a plain pattern', async () => {
const discoveryApi = UrlPatternDiscovery.compile(
'http://localhost:7000/{{ pluginId }}',
);
await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe(
'http://localhost:7000/my-plugin',
);
});
it('should allow for multiple interpolation points', async () => {
const discoveryApi = UrlPatternDiscovery.compile(
'https://{{pluginId }}.example.com/api/{{ pluginId}}',
);
await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe(
'https://my-plugin.example.com/api/my-plugin',
);
});
it('should validate that the pattern is a valid URL', () => {
expect(() => {
UrlPatternDiscovery.compile('example.com');
}).toThrow('Invalid discovery URL pattern, Invalid URL: example.com');
expect(() => {
UrlPatternDiscovery.compile('http://');
}).toThrow('Invalid discovery URL pattern, Invalid URL: http://');
expect(() => {
UrlPatternDiscovery.compile('abc123');
}).toThrow('Invalid discovery URL pattern, Invalid URL: abc123');
expect(() => {
UrlPatternDiscovery.compile('http://example.com:{{pluginId}}');
}).toThrow(
'Invalid discovery URL pattern, Invalid URL: http://example.com:pluginId',
);
expect(() => {
UrlPatternDiscovery.compile('/{{pluginId}}');
}).toThrow('Invalid discovery URL pattern, Invalid URL: /pluginId');
expect(() => {
UrlPatternDiscovery.compile('http://localhost/{{pluginId}}?forbidden');
}).toThrow('Invalid discovery URL pattern, URL must not have a query');
expect(() => {
UrlPatternDiscovery.compile('http://localhost/{{pluginId}}#forbidden');
}).toThrow('Invalid discovery URL pattern, URL must not have a hash');
expect(() => {
UrlPatternDiscovery.compile('http://localhost/{{pluginId}}/');
}).toThrow('Invalid discovery URL pattern, URL must not end with a slash');
expect(() => {
UrlPatternDiscovery.compile('http://localhost/');
}).toThrow('Invalid discovery URL pattern, URL must not end with a slash');
});
});
@@ -0,0 +1,58 @@
/*
* 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 { DiscoveryApi } from '../../definitions/DiscoveryApi';
/**
* UrlPatternDiscovery is a lightweight DiscoveryApi implementation.
* It uses a single template string to construct URLs for each plugin.
*/
export class UrlPatternDiscovery implements DiscoveryApi {
/**
* Creates a new UrlPatternDiscovery given a template. The the only
* interpolation done for the template is to replace instances of `{{pluginId}}`
* with the ID of the plugin being requested.
*
* Example pattern: `http://localhost:7000/api/{{ pluginId }}`
*/
static compile(pattern: string): UrlPatternDiscovery {
const parts = pattern.split(/\{\{\s*pluginId\s*\}\}/);
try {
const urlStr = parts.join('pluginId');
const url = new URL(urlStr);
if (url.hash) {
throw new Error('URL must not have a hash');
}
if (url.search) {
throw new Error('URL must not have a query');
}
if (urlStr.endsWith('/')) {
throw new Error('URL must not end with a slash');
}
} catch (error) {
throw new Error(`Invalid discovery URL pattern, ${error.message}`);
}
return new UrlPatternDiscovery(parts);
}
private constructor(private readonly parts: string[]) {}
async getBaseUrl(pluginId: string): Promise<string> {
return this.parts.join(pluginId);
}
}
@@ -0,0 +1,21 @@
/*
* 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.
*/
// This folder contains implementations for all core APIs.
//
// Plugins should rely on these APIs for functionality as much as possible.
export { UrlPatternDiscovery } from './UrlPatternDiscovery';
@@ -27,16 +27,17 @@ import {
AuthRequestOptions,
BackstageIdentity,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
type CreateOptions = {
// TODO(Following the words of Rugvip): These two should be grabbed from global config when available, they're not unique to Auth0Auth
backendUrl: string;
basePath: string;
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
@@ -67,15 +68,13 @@ class Auth0Auth
BackstageIdentityApi,
SessionStateApi {
static create({
backendUrl,
basePath,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
backendUrl,
basePath,
discoveryApi,
environment,
provider,
oauthRequestApi: oauthRequestApi,
@@ -25,7 +25,11 @@ import {
BackstageIdentity,
AuthRequestOptions,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import {
AuthSessionStore,
@@ -34,10 +38,7 @@ import {
import { Observable } from '../../../../types';
type CreateOptions = {
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth
backendUrl: string;
basePath: string;
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
@@ -62,15 +63,13 @@ const DEFAULT_PROVIDER = {
class GithubAuth implements OAuthApi, SessionStateApi {
static create({
backendUrl,
basePath,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
backendUrl,
basePath,
discoveryApi,
environment,
provider,
oauthRequestApi: oauthRequestApi,
@@ -25,15 +25,17 @@ import {
BackstageIdentity,
AuthRequestOptions,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
type CreateOptions = {
backendUrl: string;
basePath: string;
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
@@ -58,15 +60,13 @@ const DEFAULT_PROVIDER = {
class GitlabAuth implements OAuthApi, SessionStateApi {
static create({
backendUrl,
basePath,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
backendUrl,
basePath,
discoveryApi,
environment,
provider,
oauthRequestApi,
@@ -28,16 +28,17 @@ import {
AuthRequestOptions,
BackstageIdentity,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
type CreateOptions = {
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth
backendUrl: string;
basePath: string;
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
@@ -71,15 +72,13 @@ class GoogleAuth
BackstageIdentityApi,
SessionStateApi {
static create({
backendUrl,
basePath,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
backendUrl,
basePath,
discoveryApi,
environment,
provider,
oauthRequestApi: oauthRequestApi,
@@ -19,7 +19,11 @@ import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { Observable } from '../../../../types';
import { AuthProvider, OAuthRequestApi } from '../../../definitions';
import {
AuthProvider,
OAuthRequestApi,
DiscoveryApi,
} from '../../../definitions';
import {
AuthRequestOptions,
BackstageIdentity,
@@ -33,9 +37,7 @@ import {
import { OAuth2Session } from './types';
type CreateOptions = {
backendUrl: string;
basePath: string;
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
@@ -64,15 +66,13 @@ const SCOPE_PREFIX = '';
class OAuth2
implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi {
static create({
backendUrl,
basePath,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
backendUrl,
basePath,
discoveryApi,
environment,
provider,
oauthRequestApi: oauthRequestApi,
@@ -28,15 +28,17 @@ import {
AuthRequestOptions,
BackstageIdentity,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
type CreateOptions = {
backendUrl: string;
basePath: string;
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
@@ -80,15 +82,13 @@ class OktaAuth
BackstageIdentityApi,
SessionStateApi {
static create({
backendUrl,
basePath,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
backendUrl,
basePath,
discoveryApi,
environment,
provider,
oauthRequestApi: oauthRequestApi,
@@ -24,5 +24,6 @@ export * from './AlertApi';
export * from './AppThemeApi';
export * from './ConfigApi';
export * from './ErrorApi';
export * from './DiscoveryApi';
export * from './OAuthRequestApi';
export * from './StorageApi';
@@ -18,11 +18,12 @@ import ProviderIcon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from './DefaultAuthConnector';
import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi';
import * as loginPopup from '../loginPopup';
import { UrlPatternDiscovery } from '../../apis';
const anyFetch = fetch as any;
const defaultOptions = {
backendUrl: 'http://my-origin',
discoveryApi: UrlPatternDiscovery.compile('http://my-host/api/{{pluginId}}'),
environment: 'production',
provider: {
id: 'my-provider',
@@ -115,7 +116,7 @@ describe('DefaultAuthConnector', () => {
expect(popupSpy).toBeCalledTimes(1);
expect(popupSpy.mock.calls[0][0]).toMatchObject({
url:
'http://my-origin/api/auth/my-provider/start?scope=a%20b&env=production',
'http://my-host/api/auth/my-provider/start?scope=a%20b&env=production',
});
await expect(sessionPromise).resolves.toEqual({
@@ -141,9 +142,9 @@ describe('DefaultAuthConnector', () => {
instantPopup: true,
});
expect(popupSpy).toBeCalledTimes(1);
await expect(sessionPromise).resolves.toBe('my-session');
expect(popupSpy).toBeCalledTimes(1);
});
it('should use join func to join scopes', async () => {
@@ -164,7 +165,7 @@ describe('DefaultAuthConnector', () => {
expect(popupSpy).toBeCalledTimes(1);
expect(popupSpy.mock.calls[0][0]).toMatchObject({
url:
'http://my-origin/api/auth/my-provider/start?scope=-ab-&env=production',
'http://my-host/api/auth/my-provider/start?scope=-ab-&env=production',
});
});
});
@@ -15,21 +15,19 @@
*/
import { AuthRequester } from '../../apis';
import { OAuthRequestApi, AuthProvider } from '../../apis/definitions';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../apis/definitions';
import { showLoginPopup } from '../loginPopup';
import { AuthConnector, CreateSessionOptions } from './types';
const DEFAULT_BASE_PATH = '/api/auth/';
type Options<AuthSession> = {
/**
* The base URL of the auth backend.
* DiscoveryApi instance used to locate the auth backend endpoint.
*/
backendUrl?: string;
/**
* Base path of the auth requests, defaults to /api/auth/
*/
basePath?: string;
discoveryApi: DiscoveryApi;
/**
* Environment hint passed on to auth backend, for example 'production' or 'development'
*/
@@ -64,8 +62,7 @@ function defaultJoinScopes(scopes: Set<string>) {
*/
export class DefaultAuthConnector<AuthSession>
implements AuthConnector<AuthSession> {
private readonly backendUrl: string;
private readonly basePath: string;
private readonly discoveryApi: DiscoveryApi;
private readonly environment: string;
private readonly provider: AuthProvider & { id: string };
private readonly joinScopesFunc: (scopes: Set<string>) => string;
@@ -74,8 +71,7 @@ export class DefaultAuthConnector<AuthSession>
constructor(options: Options<AuthSession>) {
const {
backendUrl = window.location.origin,
basePath = DEFAULT_BASE_PATH,
discoveryApi,
environment,
provider,
joinScopes = defaultJoinScopes,
@@ -88,8 +84,7 @@ export class DefaultAuthConnector<AuthSession>
onAuthRequest: scopes => this.showPopup(scopes),
});
this.backendUrl = backendUrl;
this.basePath = basePath;
this.discoveryApi = discoveryApi;
this.environment = environment;
this.provider = provider;
this.joinScopesFunc = joinScopes;
@@ -104,12 +99,15 @@ export class DefaultAuthConnector<AuthSession>
}
async refreshSession(): Promise<any> {
const res = await fetch(this.buildUrl('/refresh', { optional: true }), {
headers: {
'x-requested-with': 'XMLHttpRequest',
const res = await fetch(
await this.buildUrl('/refresh', { optional: true }),
{
headers: {
'x-requested-with': 'XMLHttpRequest',
},
credentials: 'include',
},
credentials: 'include',
}).catch(error => {
).catch(error => {
throw new Error(`Auth refresh request failed, ${error}`);
});
@@ -134,7 +132,7 @@ export class DefaultAuthConnector<AuthSession>
}
async removeSession(): Promise<void> {
const res = await fetch(this.buildUrl('/logout'), {
const res = await fetch(await this.buildUrl('/logout'), {
method: 'POST',
headers: {
'x-requested-with': 'XMLHttpRequest',
@@ -153,13 +151,12 @@ export class DefaultAuthConnector<AuthSession>
private async showPopup(scopes: Set<string>): Promise<AuthSession> {
const scope = this.joinScopesFunc(scopes);
const popupUrl = this.buildUrl('/start', { scope });
const { origin } = new URL(this.backendUrl);
const popupUrl = await this.buildUrl('/start', { scope });
const payload = await showLoginPopup({
url: popupUrl,
name: `${this.provider.title} Login`,
origin,
origin: new URL(popupUrl).origin,
width: 450,
height: 730,
});
@@ -167,16 +164,17 @@ export class DefaultAuthConnector<AuthSession>
return await this.sessionTransform(payload);
}
private buildUrl(
private async buildUrl(
path: string,
query?: { [key: string]: string | boolean | undefined },
): string {
): Promise<string> {
const baseUrl = await this.discoveryApi.getBaseUrl('auth');
const queryString = this.buildQueryString({
...query,
env: this.environment,
});
return `${this.backendUrl}${this.basePath}${this.provider.id}${path}${queryString}`;
return `${baseUrl}/${this.provider.id}${path}${queryString}`;
}
private buildQueryString(query?: {
@@ -6,6 +6,8 @@ import {
ConfigApi,
ErrorApiForwarder,
ErrorAlerter,
discoveryApiRef,
UrlPatternDiscovery,
oauthRequestApiRef,
OAuthRequestManager,
storageApiRef,
@@ -24,6 +26,10 @@ export const apis = (config: ConfigApi) => {
const builder = ApiRegistry.builder();
const discoveryApi = builder.add(
discoveryApiRef,
UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`),
);
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
const errorApi = builder.add(
errorApiRef,
@@ -33,21 +39,9 @@ export const apis = (config: ConfigApi) => {
builder.add(storageApiRef, WebStorage.create({ errorApi }));
builder.add(oauthRequestApiRef, new OAuthRequestManager());
builder.add(
catalogApiRef,
new CatalogClient({
apiOrigin: backendUrl,
basePath: '/catalog',
}),
);
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
builder.add(
scaffolderApiRef,
new ScaffolderApi({
apiOrigin: backendUrl,
basePath: '/scaffolder/v1',
}),
);
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
return builder.build();
};
+21 -16
View File
@@ -24,6 +24,8 @@ import {
AlertApiForwarder,
oauthRequestApiRef,
OAuthRequestManager,
UrlPatternDiscovery,
discoveryApiRef,
GoogleAuth,
googleAuthApiRef,
GithubAuth,
@@ -58,46 +60,49 @@ export const oauthRequestApiFactory = createApiFactory({
factory: () => new OAuthRequestManager(),
});
export const discoveryApiFactory = createApiFactory({
implements: discoveryApiRef,
deps: {},
factory: () =>
UrlPatternDiscovery.compile(`http://localhost:7000/{{ pluginId }}`),
});
export const googleAuthApiFactory = createApiFactory({
implements: googleAuthApiRef,
deps: { oauthRequestApi: oauthRequestApiRef },
factory: ({ oauthRequestApi }) =>
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
factory: ({ discoveryApi, oauthRequestApi }) =>
GoogleAuth.create({
backendUrl: 'http://localhost:7000',
basePath: '/auth/',
discoveryApi,
oauthRequestApi,
}),
});
export const githubAuthApiFactory = createApiFactory({
implements: githubAuthApiRef,
deps: { oauthRequestApi: oauthRequestApiRef },
factory: ({ oauthRequestApi }) =>
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
factory: ({ discoveryApi, oauthRequestApi }) =>
GithubAuth.create({
backendUrl: 'http://localhost:7000',
basePath: '/auth/',
discoveryApi,
oauthRequestApi,
}),
});
export const gitlabAuthApiFactory = createApiFactory({
implements: gitlabAuthApiRef,
deps: { oauthRequestApi: oauthRequestApiRef },
factory: ({ oauthRequestApi }) =>
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
factory: ({ discoveryApi, oauthRequestApi }) =>
GitlabAuth.create({
backendUrl: 'http://localhost:7000',
basePath: '/auth/',
discoveryApi,
oauthRequestApi,
}),
});
export const auth0AuthApiFactory = createApiFactory({
implements: auth0AuthApiRef,
deps: { oauthRequestApi: oauthRequestApiRef },
factory: ({ oauthRequestApi }) =>
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
factory: ({ discoveryApi, oauthRequestApi }) =>
Auth0Auth.create({
backendUrl: 'http://localhost:7000',
basePath: '/auth/',
discoveryApi,
oauthRequestApi,
}),
});
+13 -20
View File
@@ -18,25 +18,21 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { CatalogClient } from './CatalogClient';
import { Entity } from '@backstage/catalog-model';
import { UrlPatternDiscovery } from '@backstage/core';
const server = setupServer();
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
describe('CatalogClient', () => {
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
const mockApiOrigin = 'http://backstage:9191';
const mockBasePath = '/i-am-a-mock-base';
let client = new CatalogClient({
apiOrigin: mockApiOrigin,
basePath: mockBasePath,
});
let client = new CatalogClient({ discoveryApi });
beforeEach(() => {
client = new CatalogClient({
apiOrigin: mockApiOrigin,
basePath: mockBasePath,
});
client = new CatalogClient({ discoveryApi });
});
describe('getEntiies', () => {
@@ -61,7 +57,7 @@ describe('CatalogClient', () => {
beforeEach(() => {
server.use(
rest.get(`${mockApiOrigin}${mockBasePath}/entities`, (_, res, ctx) => {
rest.get(`${mockBaseUrl}/entities`, (_, res, ctx) => {
return res(ctx.json(defaultResponse));
}),
);
@@ -75,15 +71,12 @@ describe('CatalogClient', () => {
it('builds entity search filters properly', async () => {
expect.assertions(2);
server.use(
rest.get(
`${mockApiOrigin}${mockBasePath}/entities`,
(req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'a=1&b=2&b=3&%C3%B6=%3D',
);
return res(ctx.json([]));
},
),
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'a=1&b=2&b=3&%C3%B6=%3D',
);
return res(ctx.json([]));
}),
);
const entities = await client.getEntities({
+8 -15
View File
@@ -20,24 +20,17 @@ import {
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import { CatalogApi, EntityCompoundName } from './types';
import { DiscoveryApi } from '@backstage/core';
export class CatalogClient implements CatalogApi {
private apiOrigin: string;
private basePath: string;
private readonly discoveryApi: DiscoveryApi;
constructor({
apiOrigin,
basePath,
}: {
apiOrigin: string;
basePath: string;
}) {
this.apiOrigin = apiOrigin;
this.basePath = basePath;
constructor(options: { discoveryApi: DiscoveryApi }) {
this.discoveryApi = options.discoveryApi;
}
private async getRequired(path: string): Promise<any> {
const url = `${this.apiOrigin}${this.basePath}${path}`;
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
const response = await fetch(url);
if (!response.ok) {
@@ -50,7 +43,7 @@ export class CatalogClient implements CatalogApi {
}
private async getOptional(path: string): Promise<any | undefined> {
const url = `${this.apiOrigin}${this.basePath}${path}`;
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
const response = await fetch(url);
if (!response.ok) {
@@ -100,7 +93,7 @@ export class CatalogClient implements CatalogApi {
async addLocation(type: string, target: string) {
const response = await fetch(
`${this.apiOrigin}${this.basePath}/locations`,
`${await this.discoveryApi.getBaseUrl('catalog')}/locations`,
{
headers: {
'Content-Type': 'application/json',
@@ -135,7 +128,7 @@ export class CatalogClient implements CatalogApi {
async removeEntityByUid(uid: string): Promise<void> {
const response = await fetch(
`${this.apiOrigin}${this.basePath}/entities/by-uid/${uid}`,
`${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`,
{
method: 'DELETE',
},
@@ -26,6 +26,7 @@ import {
githubAuthApiRef,
GithubAuth,
OAuthRequestManager,
UrlPatternDiscovery,
} from '@backstage/core';
import { gitOpsApiRef, GitOpsRestApi } from '../../api';
@@ -37,8 +38,9 @@ describe('ProfileCatalog', () => {
[
githubAuthApiRef,
GithubAuth.create({
backendUrl: 'http://localhost:7000',
basePath: '/auth/',
discoveryApi: UrlPatternDiscovery.compile(
'http://example.com/{{pluginId}}',
),
oauthRequestApi,
}),
],
+5 -12
View File
@@ -20,20 +20,13 @@ import {
RollbarProject,
RollbarTopActiveItem,
} from './types';
import { DiscoveryApi } from '@backstage/core';
export class RollbarClient implements RollbarApi {
private apiOrigin: string;
private basePath: string;
private readonly discoveryApi: DiscoveryApi;
constructor({
apiOrigin,
basePath,
}: {
apiOrigin: string;
basePath: string;
}) {
this.apiOrigin = apiOrigin;
this.basePath = basePath;
constructor(options: { discoveryApi: DiscoveryApi }) {
this.discoveryApi = options.discoveryApi;
}
async getAllProjects(): Promise<RollbarProject[]> {
@@ -59,7 +52,7 @@ export class RollbarClient implements RollbarApi {
}
private async get(path: string): Promise<any> {
const url = `${this.apiOrigin}${this.basePath}${path}`;
const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`;
const response = await fetch(url);
if (!response.ok) {
+8 -16
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createApiRef } from '@backstage/core';
import { createApiRef, DiscoveryApi } from '@backstage/core';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
export const scaffolderApiRef = createApiRef<ScaffolderApi>({
@@ -23,18 +23,10 @@ export const scaffolderApiRef = createApiRef<ScaffolderApi>({
});
export class ScaffolderApi {
private apiOrigin: string;
private basePath: string;
private readonly discoveryApi: DiscoveryApi;
constructor({
apiOrigin,
basePath,
}: {
apiOrigin: string;
basePath: string;
}) {
this.apiOrigin = apiOrigin;
this.basePath = basePath;
constructor(options: { discoveryApi: DiscoveryApi }) {
this.discoveryApi = options.discoveryApi;
}
/**
@@ -46,7 +38,7 @@ export class ScaffolderApi {
template: TemplateEntityV1alpha1,
values: Record<string, any>,
) {
const url = `${this.apiOrigin}${this.basePath}/jobs`;
const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v1/jobs`;
const response = await fetch(url, {
method: 'POST',
headers: {
@@ -65,9 +57,9 @@ export class ScaffolderApi {
}
async getJob(jobId: string) {
const url = `${this.apiOrigin}${this.basePath}/job/${encodeURIComponent(
jobId,
)}`;
const url = `${await this.discoveryApi.getBaseUrl(
'scaffolder',
)}/v1/job/${encodeURIComponent(jobId)}`;
return fetch(url).then(x => x.json());
}
}