core-app-api: refactor into FrontendHostDiscovery

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-04-25 16:08:14 +02:00
parent 9a16d5b88b
commit 51329677e1
8 changed files with 109 additions and 112 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
'@backstage/core-app-api': patch
---
Added HostDiscovery for config driven discovery implementation
Added `FrontendHostDiscovery` for config driven discovery implementation
+1 -1
View File
@@ -2,4 +2,4 @@
'@backstage/backend-common': patch
---
Added HostDiscovery to supersede deprecated SingleHostDiscovery (deprecated due to name)
Added `HostDiscovery` to supersede deprecated `SingleHostDiscovery` (deprecated due to name)
+12 -12
View File
@@ -410,6 +410,18 @@ export type FlatRoutesProps = {
children: ReactNode;
};
// @public
export class FrontendHostDiscovery implements DiscoveryApi {
static fromConfig(
config: Config,
options?: {
pathPattern?: string;
},
): FrontendHostDiscovery;
// (undocumented)
getBaseUrl(pluginId: string): Promise<string>;
}
// @public
export class GithubAuth {
// (undocumented)
@@ -428,18 +440,6 @@ export class GoogleAuth {
static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T;
}
// @public
export class HostDiscovery implements DiscoveryApi {
static fromConfig(
config: Config,
options?: {
basePath?: string;
},
): HostDiscovery;
// (undocumented)
getBaseUrl(pluginId: string): Promise<string>;
}
// @public
export class LocalStorageFeatureFlags implements FeatureFlagsApi {
// (undocumented)
@@ -15,11 +15,11 @@
*/
import { ConfigReader } from '@backstage/config';
import { HostDiscovery } from './HostDiscovery';
import { FrontendHostDiscovery } from './FrontendHostDiscovery';
describe('HostDiscovery', () => {
describe('FrontendHostDiscovery', () => {
it('is created from config', async () => {
const discovery = HostDiscovery.fromConfig(
const discovery = FrontendHostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
@@ -33,13 +33,13 @@ describe('HostDiscovery', () => {
});
it('can configure the base path', async () => {
const discovery = HostDiscovery.fromConfig(
const discovery = FrontendHostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
},
}),
{ basePath: '/service' },
{ pathPattern: '/service/{{pluginId}}' },
);
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
@@ -48,7 +48,7 @@ describe('HostDiscovery', () => {
});
it('uses plugin specific targets from config if provided', async () => {
const discovery = HostDiscovery.fromConfig(
const discovery = FrontendHostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
@@ -73,7 +73,7 @@ describe('HostDiscovery', () => {
});
it('uses a single target for internal and external for a plugin', async () => {
const discovery = HostDiscovery.fromConfig(
const discovery = FrontendHostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
@@ -95,7 +95,7 @@ describe('HostDiscovery', () => {
});
it('defaults to the backend baseUrl when there is not an endpoint for a plugin', async () => {
const discovery = HostDiscovery.fromConfig(
const discovery = FrontendHostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
@@ -117,7 +117,7 @@ describe('HostDiscovery', () => {
});
it('replaces {{pluginId}} or {{ pluginId }} in the target', async () => {
const discovery = HostDiscovery.fromConfig(
const discovery = FrontendHostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
@@ -152,7 +152,7 @@ describe('HostDiscovery', () => {
});
it('encodes the pluginId', async () => {
const discovery = HostDiscovery.fromConfig(
const discovery = FrontendHostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
@@ -0,0 +1,83 @@
/*
* Copyright 2023 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.
*/
import { Config } from '@backstage/config';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { UrlPatternDiscovery } from './UrlPatternDiscovery';
/**
* FrontendHostDiscovery is a config driven DiscoveryApi implementation.
* It uses the app-config to determine the url for a plugin.
*
* @public
*/
export class FrontendHostDiscovery implements DiscoveryApi {
/**
* Creates a new FrontendHostDiscovery discovery instance by reading
* the external target URL from the `discovery.endpoints` config section.
*
* eg.
* ```yaml
* discovery:
* endpoints:
* - target: https://internal.example.com/internal-catalog
* plugins: [catalog]
* - target: https://internal.example.com/secure/api/{{pluginId}}
* plugins: [auth, permissions]
* - target:
* internal: https://internal.example.com/search
* external: https://example.com/search
* plugins: [search]
* ```
*
* If a plugin is not declared in the config, the discovery will fall back to using the baseUrl with
* the provided `pathPattern` appended. The default path pattern is `"/api/{{ pluginId }}"`.
*/
static fromConfig(config: Config, options?: { pathPattern?: string }) {
const path = options?.pathPattern ?? '/api/{{ pluginId }}';
const baseUrl = config.getString('backend.baseUrl');
const endpoints = config
.getOptionalConfigArray('discovery.endpoints')
?.flatMap(e => {
const target =
typeof e.get('target') === 'object'
? e.getString('target.external')
: e.getString('target');
const discovery = UrlPatternDiscovery.compile(target);
return e
.getStringArray('plugins')
.map(pluginId => [pluginId, discovery] as const);
});
return new FrontendHostDiscovery(
new Map(endpoints),
UrlPatternDiscovery.compile(`${baseUrl}${path}`),
);
}
private constructor(
private readonly endpoints: Map<string, DiscoveryApi>,
private readonly defaultEndpoint: DiscoveryApi,
) {}
async getBaseUrl(pluginId: string): Promise<string> {
const endpoint = this.endpoints.get(pluginId);
if (endpoint) {
return endpoint.getBaseUrl(pluginId);
}
return this.defaultEndpoint.getBaseUrl(pluginId);
}
}
@@ -1,86 +0,0 @@
/*
* Copyright 2023 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.
*/
import { Config } from '@backstage/config';
import { DiscoveryApi } from '@backstage/core-plugin-api';
type Target = string | { internal: string; external: string };
/**
* HostDiscovery is a config driven DiscoveryApi implementation.
* It uses the app-config to determine the url for a plugin.
*
* @public
*/
export class HostDiscovery implements DiscoveryApi {
/**
* Creates a new HostDiscovery discovery instance by reading
* from the `discovery.endpoints` config section. If both internal and external exist, then external is used.
*
* eg.
* ```yaml
* discovery:
* endpoints:
* - target: https://internal.example.com/internal-catalog
* plugins: [catalog]
* - target: https://internal.example.com/secure/api/{{pluginId}}
* plugins: [auth, permissions]
* - target:
* internal: https://internal.example.com/search
* external: https://example.com/search
* plugins: [search]
* ```
*
* If a plugin is not declared in the config, the baseUrl is used, defaulting to "/api" unless `basePath` is provided in the options.
*/
static fromConfig(config: Config, options?: { basePath?: string }) {
const basePath = options?.basePath ?? '/api';
const baseUrl = config.getString('backend.baseUrl');
return new HostDiscovery(
baseUrl + basePath,
config.getOptionalConfig('discovery'),
);
}
private constructor(
private readonly baseUrl: string,
private readonly discoveryConfig: Config | undefined,
) {}
async getBaseUrl(pluginId: string): Promise<string> {
const endpoints = this.discoveryConfig?.getOptionalConfigArray('endpoints');
const target = endpoints
?.find(endpoint => endpoint.getStringArray('plugins').includes(pluginId))
?.get<Target>('target');
if (!target) {
return `${this.baseUrl}/${encodeURIComponent(pluginId)}`;
}
if (typeof target === 'string') {
return target.replace(
/\{\{\s*pluginId\s*\}\}/g,
encodeURIComponent(pluginId),
);
}
return target.external.replace(
/\{\{\s*pluginId\s*\}\}/g,
encodeURIComponent(pluginId),
);
}
}
@@ -58,6 +58,6 @@ export class UrlPatternDiscovery implements DiscoveryApi {
private constructor(private readonly parts: string[]) {}
async getBaseUrl(pluginId: string): Promise<string> {
return this.parts.join(pluginId);
return this.parts.join(encodeURIComponent(pluginId));
}
}
@@ -15,4 +15,4 @@
*/
export { UrlPatternDiscovery } from './UrlPatternDiscovery';
export { HostDiscovery } from './HostDiscovery';
export { FrontendHostDiscovery } from './FrontendHostDiscovery';