(feat): New kubernetes backend endpoints (#12393)
* feat: add new kubernetes backend endpoints Signed-off-by: Matthew Clarke <mclarke@spotify.com> * add more tests Signed-off-by: Matthew Clarke <mclarke@spotify.com> * add eslint exceptions Signed-off-by: Matthew Clarke <mclarke@spotify.com> * register new endpoints Signed-off-by: Matthew Clarke <mclarke@spotify.com> * add changeset Signed-off-by: Matthew Clarke <mclarke@spotify.com> * add breaking change ot changeset Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix tsc in test Signed-off-by: Matthew Clarke <mclarke@spotify.com> * changeset typo Signed-off-by: Matthew Clarke <mclarke@spotify.com> * api report update Signed-off-by: Matthew Clarke <mclarke@spotify.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': minor
|
||||
---
|
||||
|
||||
Add new endpoints to Kubernetes backend plugin
|
||||
|
||||
BREAKING: Kubernetes backend plugin now depends on CatalogApi
|
||||
|
||||
```typescript
|
||||
// Create new CatalogClient
|
||||
const catalogApi = new CatalogClient({ discoveryApi: env.discovery });
|
||||
const { router } = await KubernetesBuilder.createBuilder({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
// Inject it into createBuilder params
|
||||
catalogApi,
|
||||
}).build();
|
||||
```
|
||||
@@ -17,13 +17,16 @@
|
||||
import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const catalogApi = new CatalogClient({ discoveryApi: env.discovery });
|
||||
const { router } = await KubernetesBuilder.createBuilder({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
catalogApi,
|
||||
}).build();
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Duration } from 'luxon';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
@@ -108,6 +109,7 @@ export class KubernetesBuilder {
|
||||
protected buildRouter(
|
||||
objectsProvider: KubernetesObjectsProvider,
|
||||
clusterSupplier: KubernetesClustersSupplier,
|
||||
catalogApi: CatalogApi,
|
||||
): express.Router;
|
||||
// (undocumented)
|
||||
protected buildServiceLocator(
|
||||
@@ -155,6 +157,8 @@ export interface KubernetesClustersSupplier {
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface KubernetesEnvironment {
|
||||
// (undocumented)
|
||||
catalogApi: CatalogApi;
|
||||
// (undocumented)
|
||||
config: Config;
|
||||
// (undocumented)
|
||||
@@ -268,6 +272,8 @@ export interface ObjectToFetch {
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface RouterOptions {
|
||||
// (undocumented)
|
||||
catalogApi: CatalogApi;
|
||||
// (undocumented)
|
||||
clusterSupplier?: KubernetesClustersSupplier;
|
||||
// (undocumented)
|
||||
|
||||
@@ -37,10 +37,12 @@
|
||||
"dependencies": {
|
||||
"@azure/identity": "^2.0.4",
|
||||
"@backstage/backend-common": "^0.14.1-next.2",
|
||||
"@backstage/catalog-client": "^1.0.4-next.1",
|
||||
"@backstage/catalog-model": "^1.1.0-next.2",
|
||||
"@backstage/config": "^1.0.1",
|
||||
"@backstage/errors": "^1.1.0-next.0",
|
||||
"@backstage/plugin-kubernetes-common": "^0.4.0-next.1",
|
||||
"@backstage/plugin-auth-node": "^0.2.3-next.1",
|
||||
"@backstage/plugin-kubernetes-common": "^0.4.0-next.0",
|
||||
"@google-cloud/container": "^4.0.0",
|
||||
"@kubernetes/client-node": "^0.16.0",
|
||||
"@types/express": "^4.17.6",
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
/*
|
||||
* Copyright 2022 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 { errorHandler } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import Router from 'express-promise-router';
|
||||
import { addResourceRoutesToRouter } from './resourcesRoutes';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
describe('resourcesRoutes', () => {
|
||||
let app: express.Express;
|
||||
|
||||
beforeAll(() => {
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
const router = Router();
|
||||
addResourceRoutesToRouter(
|
||||
router,
|
||||
{
|
||||
getEntityByRef: jest.fn().mockImplementation(entityRef => {
|
||||
if (entityRef.name === 'noentity') {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
return Promise.resolve({
|
||||
kind: entityRef.kind,
|
||||
metadata: {
|
||||
name: entityRef.name,
|
||||
namespace: entityRef.namespace,
|
||||
},
|
||||
} as Entity);
|
||||
}),
|
||||
} as any,
|
||||
{
|
||||
getKubernetesObjectsByEntity: jest.fn().mockImplementation(args => {
|
||||
if (args.entity.metadata.name === 'inject500') {
|
||||
return Promise.reject(new Error('some internal error'));
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
items: [
|
||||
{
|
||||
clusterOne: {
|
||||
pods: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
getCustomResourcesByEntity: jest.fn().mockImplementation(args => {
|
||||
if (args.entity.metadata.name === 'inject500') {
|
||||
return Promise.reject(new Error('some internal error'));
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
items: [
|
||||
{
|
||||
clusterOne: {
|
||||
pods: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
} as any,
|
||||
);
|
||||
app.use('/', router);
|
||||
app.use(errorHandler());
|
||||
});
|
||||
|
||||
describe('POST /resources/workloads/query', () => {
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('200 happy path', async () => {
|
||||
await request(app)
|
||||
.post('/resources/workloads/query')
|
||||
.send({
|
||||
entityRef: 'kind:namespacec/someComponent',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(200, {
|
||||
items: [
|
||||
{
|
||||
clusterOne: {
|
||||
pods: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('400 when missing entity ref', async () => {
|
||||
await request(app)
|
||||
.post('/resources/workloads/query')
|
||||
.send({
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(400, {
|
||||
error: { name: 'InputError', message: 'entity is a required field' },
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/workloads/query',
|
||||
},
|
||||
response: { statusCode: 400 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('400 when bad entity ref', async () => {
|
||||
await request(app)
|
||||
.post('/resources/workloads/query')
|
||||
.send({
|
||||
entityRef: 'ffff',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(400, {
|
||||
error: {
|
||||
name: 'InputError',
|
||||
message:
|
||||
'Invalid entity ref, Error: Entity reference "ffff" had missing or empty kind (e.g. did not start with "component:" or similar)',
|
||||
},
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/workloads/query',
|
||||
},
|
||||
response: { statusCode: 400 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('400 when no entity in catalog', async () => {
|
||||
await request(app)
|
||||
.post('/resources/workloads/query')
|
||||
.send({
|
||||
entityRef: 'noentity:noentity',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(400, {
|
||||
error: {
|
||||
name: 'InputError',
|
||||
message: 'Entity ref missing, noentity:default/noentity',
|
||||
},
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/workloads/query',
|
||||
},
|
||||
response: { statusCode: 400 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('401 when no Auth header', async () => {
|
||||
await request(app)
|
||||
.post('/resources/workloads/query')
|
||||
.send({
|
||||
entityRef: 'component:someComponent',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(401, {
|
||||
error: { name: 'AuthenticationError', message: 'No Backstage token' },
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/workloads/query',
|
||||
},
|
||||
response: { statusCode: 401 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('401 when invalid Auth header', async () => {
|
||||
await request(app)
|
||||
.post('/resources/workloads/query')
|
||||
.send({
|
||||
entityRef: 'component:someComponent',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'ffffff')
|
||||
.expect(401, {
|
||||
error: { name: 'AuthenticationError', message: 'No Backstage token' },
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/workloads/query',
|
||||
},
|
||||
response: { statusCode: 401 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('500 handle gracefully', async () => {
|
||||
await request(app)
|
||||
.post('/resources/workloads/query')
|
||||
.send({
|
||||
entityRef: 'inject500:inject500/inject500',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(500, {
|
||||
error: {
|
||||
name: 'Error',
|
||||
message: 'some internal error',
|
||||
level: 'error',
|
||||
service: 'backstage',
|
||||
},
|
||||
request: { method: 'POST', url: '/resources/workloads/query' },
|
||||
response: { statusCode: 500 },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('POST /resources/custom/query', () => {
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('200 happy path', async () => {
|
||||
await request(app)
|
||||
.post('/resources/custom/query')
|
||||
.send({
|
||||
entityRef: 'component:someComponent',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
customResources: [
|
||||
{
|
||||
group: 'someGroup',
|
||||
apiVersion: 'someApiVersion',
|
||||
plural: 'somePlural',
|
||||
},
|
||||
],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(200, {
|
||||
items: [
|
||||
{
|
||||
clusterOne: {
|
||||
pods: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('400 when missing custom resources', async () => {
|
||||
await request(app)
|
||||
.post('/resources/custom/query')
|
||||
.send({
|
||||
entityRef: 'component:someComponent',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(400, {
|
||||
error: {
|
||||
name: 'InputError',
|
||||
message: 'customResources is a required field',
|
||||
},
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/custom/query',
|
||||
},
|
||||
response: { statusCode: 400 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('400 when custom resources not array', async () => {
|
||||
await request(app)
|
||||
.post('/resources/custom/query')
|
||||
.send({
|
||||
entityRef: 'component:someComponent',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
customResources: 'somestring',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(400, {
|
||||
error: {
|
||||
name: 'InputError',
|
||||
message: 'customResources must be an array',
|
||||
},
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/custom/query',
|
||||
},
|
||||
response: { statusCode: 400 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('400 when custom resources empty', async () => {
|
||||
await request(app)
|
||||
.post('/resources/custom/query')
|
||||
.send({
|
||||
entityRef: 'component:someComponent',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
customResources: [],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(400, {
|
||||
error: {
|
||||
name: 'InputError',
|
||||
message: 'at least 1 customResource is required',
|
||||
},
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/custom/query',
|
||||
},
|
||||
response: { statusCode: 400 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('400 when missing entity ref', async () => {
|
||||
await request(app)
|
||||
.post('/resources/custom/query')
|
||||
.send({
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
customResources: [
|
||||
{
|
||||
group: 'someGroup',
|
||||
apiVersion: 'someApiVersion',
|
||||
plural: 'somePlural',
|
||||
},
|
||||
],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(400, {
|
||||
error: { name: 'InputError', message: 'entity is a required field' },
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/custom/query',
|
||||
},
|
||||
response: { statusCode: 400 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('400 when bad entity ref', async () => {
|
||||
await request(app)
|
||||
.post('/resources/custom/query')
|
||||
.send({
|
||||
entityRef: 'ffff',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
customResources: [
|
||||
{
|
||||
group: 'someGroup',
|
||||
apiVersion: 'someApiVersion',
|
||||
plural: 'somePlural',
|
||||
},
|
||||
],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(400, {
|
||||
error: {
|
||||
name: 'InputError',
|
||||
message:
|
||||
'Invalid entity ref, Error: Entity reference "ffff" had missing or empty kind (e.g. did not start with "component:" or similar)',
|
||||
},
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/custom/query',
|
||||
},
|
||||
response: { statusCode: 400 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('400 when no entity in catalog', async () => {
|
||||
await request(app)
|
||||
.post('/resources/custom/query')
|
||||
.send({
|
||||
entityRef: 'noentity:noentity',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
customResources: [
|
||||
{
|
||||
group: 'someGroup',
|
||||
apiVersion: 'someApiVersion',
|
||||
plural: 'somePlural',
|
||||
},
|
||||
],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(400, {
|
||||
error: {
|
||||
name: 'InputError',
|
||||
message: 'Entity ref missing, noentity:default/noentity',
|
||||
},
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/custom/query',
|
||||
},
|
||||
response: { statusCode: 400 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('401 when no Auth header', async () => {
|
||||
await request(app)
|
||||
.post('/resources/custom/query')
|
||||
.send({
|
||||
entityRef: 'component:someComponent',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
customResources: [
|
||||
{
|
||||
group: 'someGroup',
|
||||
apiVersion: 'someApiVersion',
|
||||
plural: 'somePlural',
|
||||
},
|
||||
],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(401, {
|
||||
error: { name: 'AuthenticationError', message: 'No Backstage token' },
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/custom/query',
|
||||
},
|
||||
response: { statusCode: 401 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('401 when invalid Auth header', async () => {
|
||||
await request(app)
|
||||
.post('/resources/custom/query')
|
||||
.send({
|
||||
entityRef: 'component:someComponent',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
customResources: [
|
||||
{
|
||||
group: 'someGroup',
|
||||
apiVersion: 'someApiVersion',
|
||||
plural: 'somePlural',
|
||||
},
|
||||
],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'ffffff')
|
||||
.expect(401, {
|
||||
error: { name: 'AuthenticationError', message: 'No Backstage token' },
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/resources/custom/query',
|
||||
},
|
||||
response: { statusCode: 401 },
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('500 handle gracefully', async () => {
|
||||
await request(app)
|
||||
.post('/resources/custom/query')
|
||||
.send({
|
||||
entityRef: 'inject500:inject500/inject500',
|
||||
auth: {
|
||||
google: 'something',
|
||||
},
|
||||
customResources: [
|
||||
{
|
||||
group: 'someGroup',
|
||||
apiVersion: 'someApiVersion',
|
||||
plural: 'somePlural',
|
||||
},
|
||||
],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', 'Bearer Zm9vYmFy')
|
||||
.expect(500, {
|
||||
error: {
|
||||
name: 'Error',
|
||||
message: 'some internal error',
|
||||
level: 'error',
|
||||
service: 'backstage',
|
||||
},
|
||||
request: { method: 'POST', url: '/resources/custom/query' },
|
||||
response: { statusCode: 500 },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
CompoundEntityRef,
|
||||
parseEntityRef,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { InputError, AuthenticationError } from '@backstage/errors';
|
||||
import express, { Request } from 'express';
|
||||
import { KubernetesObjectsProvider } from '../types/types';
|
||||
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
|
||||
|
||||
export const addResourceRoutesToRouter = (
|
||||
router: express.Router,
|
||||
catalogApi: CatalogApi,
|
||||
objectsProvider: KubernetesObjectsProvider,
|
||||
) => {
|
||||
const getEntityByReq = async (req: Request<any>) => {
|
||||
const rawEntityRef = req.body.entityRef;
|
||||
if (rawEntityRef && typeof rawEntityRef !== 'string') {
|
||||
throw new InputError(`entity query must be a string`);
|
||||
} else if (!rawEntityRef) {
|
||||
throw new InputError('entity is a required field');
|
||||
}
|
||||
let entityRef: CompoundEntityRef | undefined = undefined;
|
||||
|
||||
try {
|
||||
entityRef = parseEntityRef(rawEntityRef);
|
||||
} catch (error) {
|
||||
throw new InputError(`Invalid entity ref, ${error}`);
|
||||
}
|
||||
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.headers.authorization,
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError('No Backstage token');
|
||||
}
|
||||
|
||||
const entity = await catalogApi.getEntityByRef(entityRef, {
|
||||
token: token,
|
||||
});
|
||||
|
||||
if (!entity) {
|
||||
throw new InputError(
|
||||
`Entity ref missing, ${stringifyEntityRef(entityRef)}`,
|
||||
);
|
||||
}
|
||||
return entity;
|
||||
};
|
||||
|
||||
router.post('/resources/workloads/query', async (req, res) => {
|
||||
const entity = await getEntityByReq(req);
|
||||
const response = await objectsProvider.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: req.body.auth,
|
||||
});
|
||||
res.json(response);
|
||||
});
|
||||
|
||||
router.post('/resources/custom/query', async (req, res) => {
|
||||
const entity = await getEntityByReq(req);
|
||||
|
||||
if (!req.body.customResources) {
|
||||
throw new InputError('customResources is a required field');
|
||||
} else if (!Array.isArray(req.body.customResources)) {
|
||||
throw new InputError('customResources must be an array');
|
||||
} else if (req.body.customResources.length === 0) {
|
||||
throw new InputError('at least 1 customResource is required');
|
||||
}
|
||||
|
||||
const response = await objectsProvider.getCustomResourcesByEntity({
|
||||
entity,
|
||||
customResources: req.body.customResources,
|
||||
auth: req.body.auth,
|
||||
});
|
||||
res.json(response);
|
||||
});
|
||||
};
|
||||
@@ -31,11 +31,13 @@ import {
|
||||
import { KubernetesBuilder } from './KubernetesBuilder';
|
||||
import { KubernetesFanOutHandler } from './KubernetesFanOutHandler';
|
||||
import { PodStatus } from '@kubernetes/client-node';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
|
||||
describe('KubernetesBuilder', () => {
|
||||
let app: express.Express;
|
||||
let kubernetesFanOutHandler: jest.Mocked<KubernetesFanOutHandler>;
|
||||
let config: Config;
|
||||
let catalogApi: CatalogApi;
|
||||
|
||||
beforeAll(async () => {
|
||||
const logger = getVoidLogger();
|
||||
@@ -69,7 +71,13 @@ describe('KubernetesBuilder', () => {
|
||||
getKubernetesObjectsByEntity: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const { router } = await KubernetesBuilder.createBuilder({ config, logger })
|
||||
catalogApi = {} as CatalogApi;
|
||||
|
||||
const { router } = await KubernetesBuilder.createBuilder({
|
||||
config,
|
||||
logger,
|
||||
catalogApi,
|
||||
})
|
||||
.setObjectsProvider(kubernetesFanOutHandler)
|
||||
.setClusterSupplier(clusterSupplier)
|
||||
.build();
|
||||
@@ -240,6 +248,7 @@ describe('KubernetesBuilder', () => {
|
||||
const { router } = await KubernetesBuilder.createBuilder({
|
||||
logger,
|
||||
config,
|
||||
catalogApi,
|
||||
})
|
||||
.setClusterSupplier(clusterSupplier)
|
||||
.setServiceLocator(serviceLocator)
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
KubernetesFanOutHandler,
|
||||
} from './KubernetesFanOutHandler';
|
||||
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
import { addResourceRoutesToRouter } from '../routes/resourcesRoutes';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -45,6 +47,7 @@ import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
export interface KubernetesEnvironment {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
catalogApi: CatalogApi;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +122,11 @@ export class KubernetesBuilder {
|
||||
objectTypesToFetch: this.getObjectTypesToFetch(),
|
||||
});
|
||||
|
||||
const router = this.buildRouter(objectsProvider, clusterSupplier);
|
||||
const router = this.buildRouter(
|
||||
objectsProvider,
|
||||
clusterSupplier,
|
||||
this.env.catalogApi,
|
||||
);
|
||||
|
||||
return {
|
||||
clusterSupplier,
|
||||
@@ -226,11 +233,13 @@ export class KubernetesBuilder {
|
||||
protected buildRouter(
|
||||
objectsProvider: KubernetesObjectsProvider,
|
||||
clusterSupplier: KubernetesClustersSupplier,
|
||||
catalogApi: CatalogApi,
|
||||
): express.Router {
|
||||
const logger = this.env.logger;
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
// @deprecated
|
||||
router.post('/services/:serviceId', async (req, res) => {
|
||||
const serviceId = req.params.serviceId;
|
||||
const requestBody: ObjectsByEntityRequest = req.body;
|
||||
@@ -259,6 +268,9 @@ export class KubernetesBuilder {
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
addResourceRoutesToRouter(router, catalogApi, objectsProvider);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Logger } from 'winston';
|
||||
import { KubernetesClustersSupplier } from '../types/types';
|
||||
import express from 'express';
|
||||
import { KubernetesBuilder } from './KubernetesBuilder';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -27,6 +28,7 @@ import { KubernetesBuilder } from './KubernetesBuilder';
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
catalogApi: CatalogApi;
|
||||
clusterSupplier?: KubernetesClustersSupplier;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
errorHandler,
|
||||
notFoundHandler,
|
||||
requestLoggingHandler,
|
||||
SingleHostDiscovery,
|
||||
} from '@backstage/backend-common';
|
||||
import compression from 'compression';
|
||||
import cors from 'cors';
|
||||
@@ -26,6 +27,7 @@ import helmet from 'helmet';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
|
||||
export interface ApplicationOptions {
|
||||
enableCors: boolean;
|
||||
@@ -39,6 +41,10 @@ export async function createStandaloneApplication(
|
||||
const config = new ConfigReader({});
|
||||
const app = express();
|
||||
|
||||
const catalogApi = new CatalogClient({
|
||||
discoveryApi: SingleHostDiscovery.fromConfig(config),
|
||||
});
|
||||
|
||||
app.use(helmet());
|
||||
if (enableCors) {
|
||||
app.use(cors());
|
||||
@@ -46,7 +52,7 @@ export async function createStandaloneApplication(
|
||||
app.use(compression());
|
||||
app.use(express.json());
|
||||
app.use(requestLoggingHandler());
|
||||
app.use('/', await createRouter({ logger, config }));
|
||||
app.use('/', await createRouter({ logger, config, catalogApi }));
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user