diff --git a/.changeset/poor-bats-retire.md b/.changeset/poor-bats-retire.md new file mode 100644 index 0000000000..66f10f2d6c --- /dev/null +++ b/.changeset/poor-bats-retire.md @@ -0,0 +1,32 @@ +--- +'@backstage/app-defaults': minor +--- + +**BREAKING**: The default `DiscoveryApi` implementation has been switched to use `FrontendHostDiscovery`, which adds support for the `discovery.endpoints` configuration. + +This is marked as a breaking change because it will cause any existing `discovery.endpoints` configuration to be picked up and used, which may break existing setups. + +For example, consider the following configuration: + +```yaml +app: + baseUrl: https://backstage.acme.org + +backend: + baseUrl: https://backstage.internal.acme.org + +discovery: + endpoints: + - target: https://catalog.internal.acme.org/api/{{pluginId}} + plugins: [catalog] +``` + +This will now cause requests from the frontend towards the `catalog` plugin to be routed to `https://internal-catalog.acme.org/api/catalog`, but this might not be reachable from the frontend. To fix this, you should update the `discovery.endpoints` configuration to only override the internal URL of the plugin: + +```yaml +discovery: + endpoints: + - target: + internal: https://catalog.internal.acme.org/api/{{pluginId}} + plugins: [catalog] +``` diff --git a/.changeset/rich-berries-glow.md b/.changeset/rich-berries-glow.md new file mode 100644 index 0000000000..07312591a3 --- /dev/null +++ b/.changeset/rich-berries-glow.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-app-api': minor +'@backstage/backend-defaults': patch +--- + +The `discovery.endpoints` configuration no longer requires both `internal` and `external` target when using the object form, instead falling back to the default. diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 7fdbb78293..63ace7b4d5 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -28,13 +28,13 @@ import { BitbucketServerAuth, OAuthRequestManager, WebStorage, - UrlPatternDiscovery, OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, createFetchApi, FetchMiddlewares, VMwareCloudAuth, + FrontendHostDiscovery, } from '@backstage/core-app-api'; import { @@ -68,10 +68,7 @@ export const apis = [ createApiFactory({ api: discoveryApiRef, deps: { configApi: configApiRef }, - factory: ({ configApi }) => - UrlPatternDiscovery.compile( - `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, - ), + factory: ({ configApi }) => FrontendHostDiscovery.fromConfig(configApi), }), createApiFactory({ api: alertApiRef, diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index 438790a177..ff8cf22164 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -652,7 +652,7 @@ export interface Config { * 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 plugin ID. */ - target: string | { internal: string; external: string }; + target: string | { internal?: string; external?: string }; /** * Array of plugins which use the target base URL. */ diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts index e4e30044dd..60ea9d7c3b 100644 --- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts @@ -162,6 +162,42 @@ describe('HostDiscovery', () => { ); }); + it('allows plugin overrides to only override either internal or external targets', async () => { + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40', + listen: { port: 80, host: 'localhost' }, + }, + discovery: { + endpoints: [ + { + target: { internal: 'http://catalog-backend:8080/api/catalog' }, + plugins: ['catalog'], + }, + { + target: { external: 'http://frontend/api/scaffolder' }, + plugins: ['scaffolder'], + }, + ], + }, + }), + ); + + await expect(discovery.getBaseUrl('catalog')).resolves.toBe( + 'http://catalog-backend:8080/api/catalog', + ); + await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( + 'http://localhost:40/api/catalog', + ); + await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe( + 'http://localhost:80/api/scaffolder', + ); + await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe( + 'http://frontend/api/scaffolder', + ); + }); + it('replaces {{pluginId}} or {{ pluginId }} in the target', async () => { const discovery = HostDiscovery.fromConfig( new ConfigReader({ diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts index b6c2c38d8a..214a347040 100644 --- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts @@ -102,10 +102,13 @@ export class HostDiscovery implements DiscoveryService { private getTargetFromConfig(pluginId: string, type: 'internal' | 'external') { const endpoints = this.discoveryConfig?.getOptionalConfigArray('endpoints'); - const target = endpoints + const targetOrObj = endpoints ?.find(endpoint => endpoint.getStringArray('plugins').includes(pluginId)) ?.get('target'); + const target = + typeof targetOrObj === 'string' ? targetOrObj : targetOrObj?.[type]; + if (!target) { const baseUrl = type === 'external' ? this.externalBaseUrl : this.internalBaseUrl; @@ -113,14 +116,7 @@ export class HostDiscovery implements DiscoveryService { return `${baseUrl}/${encodeURIComponent(pluginId)}`; } - if (typeof target === 'string') { - return target.replace( - /\{\{\s*pluginId\s*\}\}/g, - encodeURIComponent(pluginId), - ); - } - - return target[type].replace( + return target.replace( /\{\{\s*pluginId\s*\}\}/g, encodeURIComponent(pluginId), ); diff --git a/packages/core-app-api/config.d.ts b/packages/core-app-api/config.d.ts index 1a7ccb666f..18519bc291 100644 --- a/packages/core-app-api/config.d.ts +++ b/packages/core-app-api/config.d.ts @@ -163,7 +163,7 @@ export interface Config { /** * @visibility frontend */ - external: string; + external?: string; }; /** * Array of plugins which use the target baseUrl. diff --git a/packages/core-app-api/src/apis/implementations/DiscoveryApi/FrontendHostDiscovery.test.ts b/packages/core-app-api/src/apis/implementations/DiscoveryApi/FrontendHostDiscovery.test.ts index 0ba6457a1b..39e7ec7fab 100644 --- a/packages/core-app-api/src/apis/implementations/DiscoveryApi/FrontendHostDiscovery.test.ts +++ b/packages/core-app-api/src/apis/implementations/DiscoveryApi/FrontendHostDiscovery.test.ts @@ -72,6 +72,30 @@ describe('FrontendHostDiscovery', () => { ); }); + it('should not use internal plugin overrides', async () => { + const discovery = FrontendHostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40', + }, + discovery: { + endpoints: [ + { + target: { + internal: 'http://catalog-backend-internal:8080/api/catalog', + }, + plugins: ['catalog'], + }, + ], + }, + }), + ); + + await expect(discovery.getBaseUrl('catalog')).resolves.toBe( + 'http://localhost:40/api/catalog', + ); + }); + it('uses a single target for internal and external for a plugin', async () => { const discovery = FrontendHostDiscovery.fromConfig( new ConfigReader({ diff --git a/packages/core-app-api/src/apis/implementations/DiscoveryApi/FrontendHostDiscovery.ts b/packages/core-app-api/src/apis/implementations/DiscoveryApi/FrontendHostDiscovery.ts index 950b746684..9c10c386d6 100644 --- a/packages/core-app-api/src/apis/implementations/DiscoveryApi/FrontendHostDiscovery.ts +++ b/packages/core-app-api/src/apis/implementations/DiscoveryApi/FrontendHostDiscovery.ts @@ -54,8 +54,11 @@ export class FrontendHostDiscovery implements DiscoveryApi { ?.flatMap(e => { const target = typeof e.get('target') === 'object' - ? e.getString('target.external') + ? e.getOptionalString('target.external') : e.getString('target'); + if (!target) { + return []; + } const discovery = UrlPatternDiscovery.compile(target); return e .getStringArray('plugins')