core-api: add DiscoveryApi + UrlPatternDiscovery implementation

This commit is contained in:
Patrik Oldsberg
2020-08-20 14:38:33 +02:00
parent 95596b8129
commit 96f1f522b9
6 changed files with 212 additions and 0 deletions
@@ -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';
@@ -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';