Merge branch 'master' of github.com:spotify/backstage into migrate-to-msw

* 'master' of github.com:spotify/backstage: (139 commits)
  Cleanup
  Update PinButton.test.tsx
  feat: update github insights plugin version (#2973)
  Ignore IntelliJ *.iml files (#2971)
  chore(deps): bump rollup-plugin-dts from 1.4.11 to 1.4.13
  fix the plugin card on plugins page
  align 'Add to Marketplace' button on plugins page
  fix the PluginGrid on mobiles sizes
  use getBy query instead of queryBy when asserting for elements present in document (#2951)
  Update PinButton.tsx
  Add test case for Progress component (#2953)
  fix the styling of footer copy on mobile
  add changeset
  handle the case where no entities are available to show
  core-api: work around issue with ApiRef export const declarations
  core-api: move utility api system implementation into apis/system
  Update docs regarding npm config ignore-scripts flag
  Another try
  Fix Core Features configuration id (#2948)
  Fix test?
  ...
This commit is contained in:
blam
2020-10-19 23:57:40 +02:00
332 changed files with 4838 additions and 2569 deletions
+17 -5
View File
@@ -8,21 +8,33 @@ It responds to Kubernetes requests from the frontend.
## Configuration
### clusterLocatorMethod
### serviceLocatorMethod
This configures how to determine which clusters a component is running in.
Currently, the only valid locator method is:
Currently, the only valid serviceLocatorMethod is:
#### configMultiTenant
#### multiTenant
This configuration assumes that all components run on all the provided clusters.
### clusterLocatorMethods
This is used to determine where to retrieve cluster configuration from.
Currently, the only valid serviceLocatorMethod is:
#### config
This clusterLocatorMethod will read cluster information in from config
Example:
```yaml
kubernetes:
clusterLocatorMethod: 'configMultiTenant'
serviceLocatorMethod: 'multiTenant'
clusterLocatorMethods:
- 'config'
clusters:
- url: http://127.0.0.1:9999
name: minikube
@@ -35,7 +47,7 @@ kubernetes:
##### clusters
Used by the `configMultiTenant` `clusterLocatorMethod` to construct Kubernetes clients.
Used by the `config` `clusterLocatorMethods` to construct Kubernetes clients.
###### url
@@ -27,7 +27,9 @@ Update `app-config.development.yaml` as follows.
```yaml
kubernetes:
clusterLocatorMethod: 'configMultiTenant'
serviceLocatorMethod: 'multiTenant'
clusterLocatorMethods:
- 'config'
clusters:
- url: <KUBERNETES MASTER BASE URL FROM STEP 2>
name: minikube
@@ -15,10 +15,10 @@
*/
import '@backstage/backend-common';
import { MultiTenantConfigClusterLocator } from './MultiTenantConfigClusterLocator';
import { ConfigReader, Config } from '@backstage/config';
import { ConfigClusterLocator } from './ConfigClusterLocator';
describe('MultiTenantConfigClusterLocator', () => {
describe('ConfigClusterLocator', () => {
it('empty clusters returns empty cluster details', async () => {
const config: Config = new ConfigReader(
{
@@ -27,11 +27,11 @@ describe('MultiTenantConfigClusterLocator', () => {
'ctx',
);
const sut = MultiTenantConfigClusterLocator.fromConfig(
const sut = ConfigClusterLocator.fromConfig(
config.getConfigArray('clusters'),
);
const result = await sut.getClusterByServiceId('ignored');
const result = await sut.getClusters();
expect(result).toStrictEqual([]);
});
@@ -50,11 +50,11 @@ describe('MultiTenantConfigClusterLocator', () => {
'ctx',
);
const sut = MultiTenantConfigClusterLocator.fromConfig(
const sut = ConfigClusterLocator.fromConfig(
config.getConfigArray('clusters'),
);
const result = await sut.getClusterByServiceId('ignored');
const result = await sut.getClusters();
expect(result).toStrictEqual([
{
@@ -86,11 +86,11 @@ describe('MultiTenantConfigClusterLocator', () => {
'ctx',
);
const sut = MultiTenantConfigClusterLocator.fromConfig(
const sut = ConfigClusterLocator.fromConfig(
config.getConfigArray('clusters'),
);
const result = await sut.getClusterByServiceId('ignored');
const result = await sut.getClusters();
expect(result).toStrictEqual([
{
@@ -14,23 +14,20 @@
* limitations under the License.
*/
import { ClusterDetails, KubernetesClustersSupplier } from '..';
import { Config } from '@backstage/config';
import { ClusterDetails, KubernetesClusterLocator } from '..';
// This cluster locator assumes that every service is located on every cluster
// Therefore it will always return all clusters in an app configuration file
export class MultiTenantConfigClusterLocator
implements KubernetesClusterLocator {
export class ConfigClusterLocator implements KubernetesClustersSupplier {
private readonly clusterDetails: ClusterDetails[];
constructor(clusterDetails: ClusterDetails[]) {
this.clusterDetails = clusterDetails;
}
static fromConfig(config: Config[]): MultiTenantConfigClusterLocator {
static fromConfig(config: Config[]): ConfigClusterLocator {
// TODO: Add validation that authProvider is required and serviceAccountToken
// is required if authProvider is serviceAccount
return new MultiTenantConfigClusterLocator(
return new ConfigClusterLocator(
config.map(c => {
return {
name: c.getString('name'),
@@ -42,9 +39,7 @@ export class MultiTenantConfigClusterLocator
);
}
// As this implementation always returns all clusters serviceId is ignored here
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getClusterByServiceId(_serviceId: string): Promise<ClusterDetails[]> {
async getClusters(): Promise<ClusterDetails[]> {
return this.clusterDetails;
}
}
@@ -0,0 +1,68 @@
/*
* 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, ConfigReader } from '../../../../packages/config/src';
import { getCombinedClusterDetails } from './index';
describe('getCombinedClusterDetails', () => {
it('should retrieve cluster details from config', async () => {
const config: Config = new ConfigReader(
{
kubernetes: {
clusters: [
{
name: 'cluster1',
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'google',
},
],
},
},
'ctx',
);
const result = await getCombinedClusterDetails(['config'], config);
expect(result).toStrictEqual([
{
name: 'cluster1',
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
{
name: 'cluster2',
serviceAccountToken: undefined,
url: 'http://localhost:8081',
authProvider: 'google',
},
]);
});
it('throws an error when using an unsupported cluster locator', async () => {
await expect(
getCombinedClusterDetails(['magic' as any], new ConfigReader({}, 'ctx')),
).rejects.toStrictEqual(
new Error('Unsupported kubernetes.clusterLocatorMethods: "magic"'),
);
});
});
@@ -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 { ClusterDetails, ClusterLocatorMethod } from '..';
import { Config } from '../../../../packages/config/src';
import { ConfigClusterLocator } from './ConfigClusterLocator';
export { ConfigClusterLocator } from './ConfigClusterLocator';
export const getCombinedClusterDetails = async (
clusterLocatorMethods: ClusterLocatorMethod[],
rootConfig: Config,
): Promise<ClusterDetails[]> => {
return Promise.all(
clusterLocatorMethods.map(clusterLocatorMethod => {
switch (clusterLocatorMethod) {
case 'config':
return ConfigClusterLocator.fromConfig(
rootConfig.getConfigArray('kubernetes.clusters'),
).getClusters();
default:
throw new Error(
`Unsupported kubernetes.clusterLocatorMethods: "${clusterLocatorMethod}"`,
);
}
}),
)
.then(res => {
return res.flat();
})
.catch(e => {
throw e;
});
};
@@ -1,18 +0,0 @@
/*
* 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 type ClusterLocatorMethod = 'configMultiTenant' | 'http';
export type AuthProviderType = 'google' | 'serviceAccount';
@@ -0,0 +1,82 @@
/*
* 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 '@backstage/backend-common';
import { MultiTenantServiceLocator } from './MultiTenantServiceLocator';
describe('MultiTenantConfigClusterLocator', () => {
it('empty clusters returns empty cluster details', async () => {
const sut = new MultiTenantServiceLocator([]);
const result = await sut.getClustersByServiceId('ignored');
expect(result).toStrictEqual([]);
});
it('one clusters returns one cluster details', async () => {
const sut = new MultiTenantServiceLocator([
{
name: 'cluster1',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
serviceAccountToken: '12345',
},
]);
const result = await sut.getClustersByServiceId('ignored');
expect(result).toStrictEqual([
{
name: 'cluster1',
serviceAccountToken: '12345',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
]);
});
it('two clusters returns two cluster details', async () => {
const sut = new MultiTenantServiceLocator([
{
name: 'cluster1',
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'google',
},
]);
const result = await sut.getClustersByServiceId('ignored');
expect(result).toStrictEqual([
{
name: 'cluster1',
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'google',
},
]);
});
});
@@ -0,0 +1,33 @@
/*
* 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 { ClusterDetails, KubernetesServiceLocator } from '..';
// This locator assumes that every service is located on every cluster
// Therefore it will always return all clusters provided
export class MultiTenantServiceLocator implements KubernetesServiceLocator {
private readonly clusterDetails: ClusterDetails[];
constructor(clusterDetails: ClusterDetails[]) {
this.clusterDetails = clusterDetails;
}
// As this implementation always returns all clusters serviceId is ignored here
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getClustersByServiceId(_serviceId: string): Promise<ClusterDetails[]> {
return this.clusterDetails;
}
}
@@ -22,7 +22,7 @@ const TEST_SERVICE_ID = 'my-service';
const fetchObjectsByServiceId = jest.fn();
const getClusterByServiceId = jest.fn();
const getClustersByServiceId = jest.fn();
const mockFetch = (mock: jest.Mock) => {
mock.mockImplementation((serviceId: string, clusterDetails: ClusterDetails) =>
@@ -70,7 +70,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
});
it('retrieve objects for one cluster', async () => {
getClusterByServiceId.mockImplementation(() =>
getClustersByServiceId.mockImplementation(() =>
Promise.resolve([
{
name: 'test-cluster',
@@ -87,13 +87,13 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
fetchObjectsByServiceId,
},
{
getClusterByServiceId,
getClustersByServiceId,
},
getVoidLogger(),
{},
);
expect(getClusterByServiceId.mock.calls.length).toBe(1);
expect(getClustersByServiceId.mock.calls.length).toBe(1);
expect(fetchObjectsByServiceId.mock.calls.length).toBe(1);
expect(result).toStrictEqual({
items: [
@@ -140,7 +140,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
});
it('retrieve objects for two clusters', async () => {
getClusterByServiceId.mockImplementation(() =>
getClustersByServiceId.mockImplementation(() =>
Promise.resolve([
{
name: 'test-cluster',
@@ -161,7 +161,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
fetchObjectsByServiceId,
},
{
getClusterByServiceId,
getClustersByServiceId,
},
getVoidLogger(),
{
@@ -171,7 +171,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
},
);
expect(getClusterByServiceId.mock.calls.length).toBe(1);
expect(getClustersByServiceId.mock.calls.length).toBe(1);
expect(fetchObjectsByServiceId.mock.calls.length).toBe(2);
expect(result).toStrictEqual({
items: [
@@ -18,7 +18,7 @@ import { Logger } from 'winston';
import {
AuthRequestBody,
ClusterDetails,
KubernetesClusterLocator,
KubernetesServiceLocator,
KubernetesFetcher,
KubernetesObjectTypes,
ObjectsByServiceIdResponse,
@@ -29,7 +29,7 @@ import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator
export type GetKubernetesObjectsByServiceIdHandler = (
serviceId: string,
fetcher: KubernetesFetcher,
clusterLocator: KubernetesClusterLocator,
serviceLocator: KubernetesServiceLocator,
logger: Logger,
requestBody: AuthRequestBody,
objectsToFetch?: Set<KubernetesObjectTypes>,
@@ -49,12 +49,12 @@ const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServiceIdHandler = async (
serviceId,
fetcher,
clusterLocator,
serviceLocator,
logger,
requestBody,
objectsToFetch = DEFAULT_OBJECTS,
) => {
const clusterDetails: ClusterDetails[] = await clusterLocator.getClusterByServiceId(
const clusterDetails: ClusterDetails[] = await serviceLocator.getClustersByServiceId(
serviceId,
);
@@ -19,7 +19,7 @@ import express from 'express';
import request from 'supertest';
import { makeRouter } from './router';
import {
KubernetesClusterLocator,
KubernetesServiceLocator,
KubernetesFetcher,
ObjectsByServiceIdResponse,
} from '..';
@@ -27,7 +27,7 @@ import {
describe('router', () => {
let app: express.Express;
let kubernetesFetcher: jest.Mocked<KubernetesFetcher>;
let kubernetesClusterLocator: jest.Mocked<KubernetesClusterLocator>;
let kubernetesServiceLocator: jest.Mocked<KubernetesServiceLocator>;
let handleGetByServiceId: jest.Mock<Promise<ObjectsByServiceIdResponse>>;
beforeAll(async () => {
@@ -35,8 +35,8 @@ describe('router', () => {
fetchObjectsByServiceId: jest.fn(),
};
kubernetesClusterLocator = {
getClusterByServiceId: jest.fn(),
kubernetesServiceLocator = {
getClustersByServiceId: jest.fn(),
};
handleGetByServiceId = jest.fn();
@@ -44,7 +44,7 @@ describe('router', () => {
const router = makeRouter(
getVoidLogger(),
kubernetesFetcher,
kubernetesClusterLocator,
kubernetesServiceLocator,
handleGetByServiceId as any,
);
app = express().use(router);
@@ -18,8 +18,7 @@ import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import { ClusterLocatorMethod } from '../cluster-locator/types';
import { MultiTenantConfigClusterLocator } from '../cluster-locator/MultiTenantConfigClusterLocator';
import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator';
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
import { KubernetesClientProvider } from './KubernetesClientProvider';
import {
@@ -28,30 +27,35 @@ import {
} from './getKubernetesObjectsByServiceIdHandler';
import {
AuthRequestBody,
KubernetesClusterLocator,
KubernetesServiceLocator,
KubernetesFetcher,
} from '../types/types';
ServiceLocatorMethod,
ClusterLocatorMethod,
ClusterDetails,
} from '..';
import { getCombinedClusterDetails } from '../cluster-locator';
export interface RouterOptions {
logger: Logger;
config: Config;
}
const getClusterLocator = (config: Config): KubernetesClusterLocator => {
const clusterLocatorMethod = config.getString(
'kubernetes.clusterLocatorMethod',
) as ClusterLocatorMethod;
const getServiceLocator = (
config: Config,
clusterDetails: ClusterDetails[],
): KubernetesServiceLocator => {
const serviceLocatorMethod = config.getString(
'kubernetes.serviceLocatorMethod',
) as ServiceLocatorMethod;
switch (clusterLocatorMethod) {
case 'configMultiTenant':
return MultiTenantConfigClusterLocator.fromConfig(
config.getConfigArray('kubernetes.clusters'),
);
switch (serviceLocatorMethod) {
case 'multiTenant':
return new MultiTenantServiceLocator(clusterDetails);
case 'http':
throw new Error('not implemented');
default:
throw new Error(
`Unsupported kubernetes.clusterLocatorMethod "${clusterLocatorMethod}"`,
`Unsupported kubernetes.clusterLocatorMethod "${serviceLocatorMethod}"`,
);
}
};
@@ -59,13 +63,12 @@ const getClusterLocator = (config: Config): KubernetesClusterLocator => {
export const makeRouter = (
logger: Logger,
fetcher: KubernetesFetcher,
clusterLocator: KubernetesClusterLocator,
serviceLocator: KubernetesServiceLocator,
handleGetByServiceId: GetKubernetesObjectsByServiceIdHandler,
): express.Router => {
const router = Router();
router.use(express.json());
// TODO error handling
router.post('/services/:serviceId', async (req, res) => {
const serviceId = req.params.serviceId;
const requestBody: AuthRequestBody = req.body;
@@ -73,7 +76,7 @@ export const makeRouter = (
const response = await handleGetByServiceId(
serviceId,
fetcher,
clusterLocator,
serviceLocator,
logger,
requestBody,
);
@@ -96,17 +99,26 @@ export async function createRouter(
logger.info('Initializing Kubernetes backend');
const clusterLocator = getClusterLocator(options.config);
const fetcher = new KubernetesClientBasedFetcher({
kubernetesClientProvider: new KubernetesClientProvider(),
logger,
});
const clusterLocatorMethods = options.config.getStringArray(
'kubernetes.clusterLocatorMethods',
) as ClusterLocatorMethod[];
const clusterDetails = await getCombinedClusterDetails(
clusterLocatorMethods,
options.config,
);
const serviceLocator = getServiceLocator(options.config, clusterDetails);
return makeRouter(
logger,
fetcher,
clusterLocator,
serviceLocator,
handleGetKubernetesObjectsByServiceId,
);
}
+11 -2
View File
@@ -118,8 +118,13 @@ export interface KubernetesFetcher {
}
// Used to locate which cluster(s) a service is running on
export interface KubernetesClusterLocator {
getClusterByServiceId(serviceId: string): Promise<ClusterDetails[]>;
export interface KubernetesServiceLocator {
getClustersByServiceId(serviceId: string): Promise<ClusterDetails[]>;
}
// Used to load cluster details from different sources
export interface KubernetesClustersSupplier {
getClusters(): Promise<ClusterDetails[]>;
}
export type KubernetesErrorTypes =
@@ -132,3 +137,7 @@ export interface KubernetesFetchError {
statusCode?: number;
resourcePath?: string;
}
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
export type ClusterLocatorMethod = 'config';
export type AuthProviderType = 'google' | 'serviceAccount';