Merge pull request #17152 from UsainBloot/config-driven-discovery
[backend-common, core-app-api] Add HostDiscovery to read plugin baseUrls from config
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Added `FrontendHostDiscovery` for config driven discovery implementation
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Added `HostDiscovery` to supersede deprecated `SingleHostDiscovery` (deprecated due to name)
|
||||
@@ -472,6 +472,20 @@ export class GitlabUrlReader implements UrlReader {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class HostDiscovery implements PluginEndpointDiscovery {
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options?: {
|
||||
basePath?: string;
|
||||
},
|
||||
): HostDiscovery;
|
||||
// (undocumented)
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
// (undocumented)
|
||||
getExternalBaseUrl(pluginId: string): Promise<string>;
|
||||
}
|
||||
|
||||
export { isChildPath };
|
||||
|
||||
// @public
|
||||
@@ -728,19 +742,8 @@ export type ServiceBuilder = {
|
||||
// @public
|
||||
export function setRootLogger(newLogger: winston.Logger): void;
|
||||
|
||||
// @public
|
||||
export class SingleHostDiscovery implements PluginEndpointDiscovery {
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options?: {
|
||||
basePath?: string;
|
||||
},
|
||||
): SingleHostDiscovery;
|
||||
// (undocumented)
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
// (undocumented)
|
||||
getExternalBaseUrl(pluginId: string): Promise<string>;
|
||||
}
|
||||
// @public @deprecated
|
||||
export const SingleHostDiscovery: typeof HostDiscovery;
|
||||
|
||||
// @public
|
||||
export type StatusCheck = () => Promise<any>;
|
||||
|
||||
Vendored
+20
@@ -204,4 +204,24 @@ export interface Config {
|
||||
*/
|
||||
csp?: { [policyId: string]: string[] | false };
|
||||
};
|
||||
|
||||
/** Discovery options. */
|
||||
discovery?: {
|
||||
/**
|
||||
* Endpoints
|
||||
*
|
||||
* A list of target baseUrls and the associated plugins.
|
||||
*/
|
||||
endpoints: {
|
||||
/**
|
||||
* The target baseUrl to use for the plugin
|
||||
*
|
||||
* Can be either a string or an object with internal and external keys.
|
||||
* Targets with `{{pluginId}}` or `{{ pluginId }} in the url will be replaced with the pluginId.
|
||||
*/
|
||||
target: string | { internal: string; external: string };
|
||||
/** Array of plugins which use the target baseUrl. */
|
||||
plugins: string[];
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright 2020 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 { ConfigReader } from '@backstage/config';
|
||||
import { HostDiscovery } from './HostDiscovery';
|
||||
|
||||
describe('HostDiscovery', () => {
|
||||
it('is created from config', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:80/api/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:40/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('can configure the base path', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
}),
|
||||
{ basePath: '/service' },
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:80/service/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:40/service/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ listen: ':80' }, 'http://localhost:80'],
|
||||
[{ listen: ':40', https: true }, 'https://localhost:40'],
|
||||
[{ listen: '127.0.0.1:80' }, 'http://127.0.0.1:80'],
|
||||
[{ listen: '127.0.0.1:80', https: true }, 'https://127.0.0.1:80'],
|
||||
[{ listen: '0.0.0.0:40' }, 'http://127.0.0.1:40'],
|
||||
[{ listen: { port: 80 } }, 'http://localhost:80'],
|
||||
[{ listen: { port: 8000 } }, 'http://localhost:8000'],
|
||||
[{ listen: { port: 80, host: '0.0.0.0' } }, 'http://127.0.0.1:80'],
|
||||
[{ listen: { port: 80, host: '::' } }, 'http://localhost:80'],
|
||||
[{ listen: { port: 80, host: '::1' } }, 'http://[::1]:80'],
|
||||
[{ listen: { port: 90, host: '::2' }, https: true }, 'https://[::2]:90'],
|
||||
])('resolves internal baseUrl for %j as %s', async (config, expected) => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
...config,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
`${expected}/api/catalog`,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses plugin specific targets from config if provided', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: {
|
||||
internal: 'http://catalog-backend-internal:8080/api/catalog',
|
||||
external: 'http://catalog-backend-external:8080/api/catalog',
|
||||
},
|
||||
plugins: ['catalog'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://catalog-backend-internal:8080/api/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://catalog-backend-external:8080/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses a single target for internal and external for a plugin', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://catalog-backend:8080/api/catalog',
|
||||
plugins: ['catalog'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://catalog-backend:8080/api/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://catalog-backend:8080/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults to the backend baseUrl when there is not an endpoint for a plugin', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://catalog-backend:8080/api/catalog',
|
||||
plugins: ['catalog'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe(
|
||||
'http://localhost:80/api/scaffolder',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe(
|
||||
'http://localhost:40/api/scaffolder',
|
||||
);
|
||||
});
|
||||
|
||||
it('replaces {{pluginId}} or {{ pluginId }} in the target', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://common-backend:8080/api/{{pluginId}}',
|
||||
plugins: ['catalog', 'docs'],
|
||||
},
|
||||
{
|
||||
target: {
|
||||
internal: 'http://scaffolder-internal:8080/api/{{ pluginId }}',
|
||||
external: 'http://scaffolder-external:8080/api/{{ pluginId }}',
|
||||
},
|
||||
plugins: ['scaffolder'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://common-backend:8080/api/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://common-backend:8080/api/catalog',
|
||||
);
|
||||
await expect(discovery.getBaseUrl('docs')).resolves.toBe(
|
||||
'http://common-backend:8080/api/docs',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('docs')).resolves.toBe(
|
||||
'http://common-backend:8080/api/docs',
|
||||
);
|
||||
await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe(
|
||||
'http://scaffolder-internal:8080/api/scaffolder',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe(
|
||||
'http://scaffolder-external:8080/api/scaffolder',
|
||||
);
|
||||
});
|
||||
|
||||
it('encodes the pluginId', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://common-backend:8080/api/{{pluginId}}',
|
||||
plugins: ['plugin/beta'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('plugin/beta')).resolves.toBe(
|
||||
'http://common-backend:8080/api/plugin%2Fbeta',
|
||||
);
|
||||
await expect(discovery.getBaseUrl('plugin/alpha')).resolves.toBe(
|
||||
'http://localhost:80/api/plugin%2Falpha',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('plugin/alpha')).resolves.toBe(
|
||||
'http://localhost:40/api/plugin%2Falpha',
|
||||
);
|
||||
});
|
||||
});
|
||||
+66
-7
@@ -18,9 +18,11 @@ import { Config } from '@backstage/config';
|
||||
import { PluginEndpointDiscovery } from './types';
|
||||
import { readHttpServerOptions } from '@backstage/backend-app-api';
|
||||
|
||||
type Target = string | { internal: string; external: string };
|
||||
|
||||
/**
|
||||
* SingleHostDiscovery is a basic PluginEndpointDiscovery implementation
|
||||
* that assumes that all plugins are hosted in a single deployment.
|
||||
* HostDiscovery is a basic PluginEndpointDiscovery implementation
|
||||
* that can handle plugins that are hosted in a single or multiple deployments.
|
||||
*
|
||||
* The deployment may be scaled horizontally, as long as the external URL
|
||||
* is the same for all instances. However, internal URLs will always be
|
||||
@@ -28,13 +30,28 @@ import { readHttpServerOptions } from '@backstage/backend-app-api';
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class SingleHostDiscovery implements PluginEndpointDiscovery {
|
||||
export class HostDiscovery implements PluginEndpointDiscovery {
|
||||
/**
|
||||
* Creates a new SingleHostDiscovery discovery instance by reading
|
||||
* Creates a new HostDiscovery discovery instance by reading
|
||||
* from the `backend` config section, specifically the `.baseUrl` for
|
||||
* discovering the external URL, and the `.listen` and `.https` config
|
||||
* for the internal one.
|
||||
*
|
||||
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
|
||||
* 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]
|
||||
* ```
|
||||
*
|
||||
* The basePath defaults to `/api`, meaning the default full internal
|
||||
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
||||
*/
|
||||
@@ -63,22 +80,64 @@ export class SingleHostDiscovery implements PluginEndpointDiscovery {
|
||||
|
||||
const internalBaseUrl = `${protocol}://${host}:${listenPort}`;
|
||||
|
||||
return new SingleHostDiscovery(
|
||||
return new HostDiscovery(
|
||||
internalBaseUrl + basePath,
|
||||
externalBaseUrl + basePath,
|
||||
config.getOptionalConfig('discovery'),
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly internalBaseUrl: string,
|
||||
private readonly externalBaseUrl: string,
|
||||
private readonly discoveryConfig: Config | undefined,
|
||||
) {}
|
||||
|
||||
private getTargetFromConfig(pluginId: string, type: 'internal' | 'external') {
|
||||
const endpoints = this.discoveryConfig?.getOptionalConfigArray('endpoints');
|
||||
|
||||
const target = endpoints
|
||||
?.find(endpoint => endpoint.getStringArray('plugins').includes(pluginId))
|
||||
?.get<Target>('target');
|
||||
|
||||
if (!target) {
|
||||
const baseUrl =
|
||||
type === 'external' ? this.externalBaseUrl : this.internalBaseUrl;
|
||||
|
||||
return `${baseUrl}/${encodeURIComponent(pluginId)}`;
|
||||
}
|
||||
|
||||
if (typeof target === 'string') {
|
||||
return target.replace(
|
||||
/\{\{\s*pluginId\s*\}\}/g,
|
||||
encodeURIComponent(pluginId),
|
||||
);
|
||||
}
|
||||
|
||||
return target[type].replace(
|
||||
/\{\{\s*pluginId\s*\}\}/g,
|
||||
encodeURIComponent(pluginId),
|
||||
);
|
||||
}
|
||||
|
||||
async getBaseUrl(pluginId: string): Promise<string> {
|
||||
return `${this.internalBaseUrl}/${pluginId}`;
|
||||
return this.getTargetFromConfig(pluginId, 'internal');
|
||||
}
|
||||
|
||||
async getExternalBaseUrl(pluginId: string): Promise<string> {
|
||||
return `${this.externalBaseUrl}/${pluginId}`;
|
||||
return this.getTargetFromConfig(pluginId, 'external');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SingleHostDiscovery is a basic PluginEndpointDiscovery implementation
|
||||
* that assumes that all plugins are hosted in a single deployment.
|
||||
*
|
||||
* The deployment may be scaled horizontally, as long as the external URL
|
||||
* is the same for all instances. However, internal URLs will always be
|
||||
* resolved to the same host, so there won't be any balancing of internal traffic.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link HostDiscovery} instead
|
||||
*/
|
||||
export const SingleHostDiscovery = HostDiscovery;
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 { ConfigReader } from '@backstage/config';
|
||||
import { SingleHostDiscovery } from './SingleHostDiscovery';
|
||||
|
||||
describe('SingleHostDiscovery', () => {
|
||||
it('is created from config', async () => {
|
||||
const discovery = SingleHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:80/api/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:40/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('can configure the base path', async () => {
|
||||
const discovery = SingleHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
}),
|
||||
{ basePath: '/service' },
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:80/service/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:40/service/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ listen: ':80' }, 'http://localhost:80'],
|
||||
[{ listen: ':40', https: true }, 'https://localhost:40'],
|
||||
[{ listen: '127.0.0.1:80' }, 'http://127.0.0.1:80'],
|
||||
[{ listen: '127.0.0.1:80', https: true }, 'https://127.0.0.1:80'],
|
||||
[{ listen: '0.0.0.0:40' }, 'http://127.0.0.1:40'],
|
||||
[{ listen: { port: 80 } }, 'http://localhost:80'],
|
||||
[{ listen: { port: 8000 } }, 'http://localhost:8000'],
|
||||
[{ listen: { port: 80, host: '0.0.0.0' } }, 'http://127.0.0.1:80'],
|
||||
[{ listen: { port: 80, host: '::' } }, 'http://localhost:80'],
|
||||
[{ listen: { port: 80, host: '::1' } }, 'http://[::1]:80'],
|
||||
[{ listen: { port: 90, host: '::2' }, https: true }, 'https://[::2]:90'],
|
||||
])('resolves internal baseUrl for %j as %s', async (config, expected) => {
|
||||
const discovery = SingleHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
...config,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
`${expected}/api/catalog`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { SingleHostDiscovery } from './SingleHostDiscovery';
|
||||
export { HostDiscovery, SingleHostDiscovery } from './HostDiscovery';
|
||||
export type { PluginEndpointDiscovery } from './types';
|
||||
|
||||
@@ -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)
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2020 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 { ConfigReader } from '@backstage/config';
|
||||
import { FrontendHostDiscovery } from './FrontendHostDiscovery';
|
||||
|
||||
describe('FrontendHostDiscovery', () => {
|
||||
it('is created from config', async () => {
|
||||
const discovery = FrontendHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:40/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('can configure the base path', async () => {
|
||||
const discovery = FrontendHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
},
|
||||
}),
|
||||
{ pathPattern: '/service/{{pluginId}}' },
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:40/service/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses plugin specific targets from config if provided', async () => {
|
||||
const discovery = FrontendHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: {
|
||||
internal: 'http://catalog-backend-internal:8080/api/catalog',
|
||||
external: 'http://catalog-backend-external:8080/api/catalog',
|
||||
},
|
||||
plugins: ['catalog'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://catalog-backend-external:8080/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses a single target for internal and external for a plugin', async () => {
|
||||
const discovery = FrontendHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://catalog-backend:8080/api/catalog',
|
||||
plugins: ['catalog'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://catalog-backend:8080/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults to the backend baseUrl when there is not an endpoint for a plugin', async () => {
|
||||
const discovery = FrontendHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://catalog-backend:8080/api/catalog',
|
||||
plugins: ['catalog'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe(
|
||||
'http://localhost:40/api/scaffolder',
|
||||
);
|
||||
});
|
||||
|
||||
it('replaces {{pluginId}} or {{ pluginId }} in the target', async () => {
|
||||
const discovery = FrontendHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://common-backend:8080/api/{{pluginId}}',
|
||||
plugins: ['catalog', 'docs'],
|
||||
},
|
||||
{
|
||||
target: {
|
||||
internal: 'http://scaffolder-internal:8080/api/{{ pluginId }}',
|
||||
external: 'http://scaffolder-external:8080/api/{{ pluginId }}',
|
||||
},
|
||||
plugins: ['scaffolder'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://common-backend:8080/api/catalog',
|
||||
);
|
||||
await expect(discovery.getBaseUrl('docs')).resolves.toBe(
|
||||
'http://common-backend:8080/api/docs',
|
||||
);
|
||||
await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe(
|
||||
'http://scaffolder-external:8080/api/scaffolder',
|
||||
);
|
||||
});
|
||||
|
||||
it('encodes the pluginId', async () => {
|
||||
const discovery = FrontendHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://common-backend:8080/api/{{pluginId}}',
|
||||
plugins: ['plugin/beta'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('plugin/beta')).resolves.toBe(
|
||||
'http://common-backend:8080/api/plugin%2Fbeta',
|
||||
);
|
||||
await expect(discovery.getBaseUrl('plugin/alpha')).resolves.toBe(
|
||||
'http://localhost:40/api/plugin%2Falpha',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { UrlPatternDiscovery } from './UrlPatternDiscovery';
|
||||
export { FrontendHostDiscovery } from './FrontendHostDiscovery';
|
||||
|
||||
Reference in New Issue
Block a user