backend-common: add initial discovery API and simple single host implementation

This commit is contained in:
Patrik Oldsberg
2020-09-24 23:37:52 +02:00
parent eb6b2e6572
commit b0565777e9
6 changed files with 166 additions and 8 deletions
@@ -0,0 +1,80 @@
/*
* 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 { Config } from '@backstage/config';
import { PluginEndpointDiscovery } from './types';
import { readBaseOptions } from '../service/lib/config';
import { DEFAULT_PORT } from '../service/lib/ServiceBuilderImpl';
/**
* 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.
*/
export class SingleHostDiscovery implements PluginEndpointDiscovery {
/**
* Creates a new SingleHostDiscovery 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.
*
* The basePath defaults to `/api`, meaning the default full internal
* path for the `catalog` plugin will be `http://localhost:7000/api/catalog`.
*/
static fromConfig(config: Config, options?: { basePath?: string }) {
const basePath = options?.basePath ?? '/api';
const externalBaseUrl = config.getString('backend.baseUrl');
const { listenHost = '::', listenPort = DEFAULT_PORT } = readBaseOptions(
config.getConfig('backend'),
);
const protocol = config.has('backend.https') ? 'https' : 'http';
// Translate bind-all to localhost, and support IPv6
let host = listenHost;
if (host === '::') {
host = '::1';
} else if (host === '0.0.0.0') {
host = '127.0.0.1';
}
if (host.includes(':')) {
host = `[${host}]`;
}
const internalBaseUrl = `${protocol}://${host}:${listenPort}`;
return new SingleHostDiscovery(
internalBaseUrl + basePath,
externalBaseUrl + basePath,
);
}
private constructor(
private readonly internalBaseUrl: string,
private readonly externalBaseUrl: string,
) {}
async getBaseUrl(pluginId: string): Promise<string> {
return `${this.internalBaseUrl}/${pluginId}`;
}
async getExternalBaseUrl(pluginId: string): Promise<string> {
return `${this.externalBaseUrl}/${pluginId}`;
}
}
@@ -0,0 +1,18 @@
/*
* 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.
*/
export { SingleHostDiscovery } from './SingleHostDiscovery';
export type { PluginEndpointDiscovery } from './types';
@@ -0,0 +1,61 @@
/*
* 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.
*/
/**
* The PluginEndpointDiscovery is used to provide a mechanism for backend
* plugins to discover the endpoints for itself or other backend plugins.
*
* 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 as simple as a URL pattern
* using the pluginId, but could also have overrides for individual plugins,
* or query a separate discovery service.
*/
export type PluginEndpointDiscovery = {
/**
* Returns the internal HTTP base URL for a given plugin, without a trailing slash.
*
* The returned URL should point to an internal endpoint for the plugin, with
* the shortest route possible. The URL should be used for service-to-service
* communication within a Backstage backend deployment.
*
* 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 `catalog` may return something
* like `http://10.1.2.3/api/catalog`
*/
getBaseUrl(pluginId: string): Promise<string>;
/**
* Returns the external HTTP base backend URL for a given plugin, without a trailing slash.
*
* The returned URL should point to an external endpoint for the plugin, such that
* it is reachable from the Backstage frontend and other external services. The returned
* URL should be usable for example as a callback / webhook URL.
*
* The returned URL should be stable and in general not change unless other static
* or external configuration is changed. Changes should not come as a surprise
* to an operator of the Backstage backend.
*
* For example, asking for the URL for `catalog` may return something
* like `https://backstage.example.com/api/catalog`
*/
getExternalBaseUrl(pluginId: string): Promise<string>;
};
+1
View File
@@ -16,6 +16,7 @@
export * from './config';
export * from './database';
export * from './discovery';
export * from './errors';
export * from './logging';
export * from './middleware';
@@ -39,7 +39,7 @@ import {
import { createHttpServer, createHttpsServer } from './hostFactory';
import { metricsHandler } from './metrics';
const DEFAULT_PORT = 7000;
export const DEFAULT_PORT = 7000;
// '' is express default, which listens to all interfaces
const DEFAULT_HOST = '';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { Config } from '@backstage/config';
import { CorsOptions } from 'cors';
export type BaseOptions = {
@@ -71,7 +71,7 @@ export type CertificateAttributes = {
* }
* ```
*/
export function readBaseOptions(config: ConfigReader): BaseOptions {
export function readBaseOptions(config: Config): BaseOptions {
if (typeof config.get('listen') === 'string') {
// TODO(freben): Expand this to support more addresses and perhaps optional
const { host, port } = parseListenAddress(config.getString('listen'));
@@ -105,7 +105,7 @@ export function readBaseOptions(config: ConfigReader): BaseOptions {
* }
* ```
*/
export function readCorsOptions(config: ConfigReader): CorsOptions | undefined {
export function readCorsOptions(config: Config): CorsOptions | undefined {
const cc = config.getOptionalConfig('cors');
if (!cc) {
return undefined;
@@ -138,9 +138,7 @@ export function readCorsOptions(config: ConfigReader): CorsOptions | undefined {
* }
* ```
*/
export function readHttpsSettings(
config: ConfigReader,
): HttpsSettings | undefined {
export function readHttpsSettings(config: Config): HttpsSettings | undefined {
const cc = config.getOptionalConfig('https');
if (!cc) {
@@ -157,7 +155,7 @@ export function readHttpsSettings(
}
function getOptionalStringOrStrings(
config: ConfigReader,
config: Config,
key: string,
): string | string[] | undefined {
const value = config.getOptional(key);